Compare commits

...

48 Commits

Author SHA1 Message Date
Spring Buildmaster 1c5e9faf90 Release version 5.2.12.RELEASE 2020-12-09 06:34:31 +00:00
Rossen Stoyanchev 33476a2eae Backport fixes for discarding data buffers
Closes gh-26232
2020-12-08 22:00:28 +00:00
Brian Clozel a8091b916b Avoid closing Jackson JsonGenerator for error cases
Prior to this commit, a change introduced in gh-25910 would close the
`JsonGenerator` after it's been used for JSON serialization. This would
not only close it and recycle resources, but also flush the underlyning
buffer to the output.
In a case where the JSON serialization process would throw an exception,
the buffer would be still flushed to the response output. Before the
change introduced in gh-25910, the response body could be still empty at
that point and error handling could write an error body instead.

This commits only closes the `JsonGenerator` when serialization has been
successful.

Note that we're changing this in the spirit of backwards compatibility
in the 5.2.x line, but change this won't be merged forward on the 5.3.x
line, for several reasons:

* this behavior is not consistent. If the JSON output exceeds a
certain size, or if Jackson has been configured to flush after each
write, the response output might still contain an incomplete JSON
payload (just like before this change)

* this behavior is not consistent with the WebFlux and Messaging codecs,
which are flushing or closing the generator

* not closing the generator for error cases prevents resources from
being recycled as expected by Jackson

Fixes gh-26246
2020-12-08 20:50:26 +01:00
Juergen Hoeller 396fdf125f Remove duplicate "property" in PropertyCacheKey.toString()
Closes gh-26237
2020-12-08 15:03:33 +01:00
Stephane Nicoll 085825fbc0 Upgrade to Reactor Dysprosium-SR15
Closes gh-26175
2020-12-08 05:50:48 +01:00
Juergen Hoeller b0c6e5e322 Polishing
(cherry picked from commit c970c318f4)
2020-12-07 22:09:43 +01:00
Juergen Hoeller a7efa9659a Clarify intended advice execution behavior (includes related polishing)
Closes gh-26202

(cherry picked from commit 834032df1f)
2020-12-07 22:09:30 +01:00
Juergen Hoeller f7605eaf58 Upgrade to Hibernate ORM 5.4.25 and Checkstyle 8.38 2020-12-02 17:37:59 +01:00
Juergen Hoeller ce67b89861 Support for multi-threaded addConverter calls
Closes gh-26183

(cherry picked from commit 396fb0cd51)
2020-12-02 12:35:21 +01:00
Rossen Stoyanchev 736af46fc0 ContentCachingResponseWrapper skips contentLength for chunked responses
Closes gh-26182
2020-12-01 17:50:07 +00:00
Stephane Nicoll a495bd6679 Start building against Reactor Dysprosium-SR15 snapshots
See gh-26175
2020-11-30 07:23:19 +01:00
Juergen Hoeller 7c33c70742 Polishing 2020-11-27 21:58:55 +01:00
Juergen Hoeller 81c1b60f19 Register @Bean definitions as dependent on containing configuration class
Closes gh-26167
2020-11-27 21:57:55 +01:00
Rossen Stoyanchev 3703be5aaf MessageHeaderAccessor handle self-copy correctly
1. Revert changes in setHeader from 5.2.9 that caused regression on self-copy.
2. Update copyHeaders/IfAbsent to ensure a copy of native headers.
3. Exit if source and target are the same instance, as an optimization.

Closes gh-26155
2020-11-26 21:56:17 +00:00
Сергей Цыпанов b929edb221 Remove unused package-private class o.s.w.u.p.SubSequence
(cherry picked from commit 42216b77df)
2020-11-26 15:38:14 +01:00
Juergen Hoeller 50803ce142 Remove misleading default note on ISO.DATE_TIME
Closes gh-26134

(cherry picked from commit 86f9716fef)
2020-11-26 15:38:05 +01:00
Sam Brannen 346445ee7e Fix broken links to XSD schemas in ref docs
Closes gh-26129
2020-11-23 14:52:47 +01:00
Juergen Hoeller e238c8a87c Declare resolvedCharset as transient (restoring serializability)
Closes gh-26127
2020-11-20 18:57:30 +01:00
Juergen Hoeller 9e99fd5df2 Upgrade to Hibernate ORM 5.4.24
(cherry picked from commit 135682e073)
2020-11-19 16:03:03 +01:00
Juergen Hoeller 990c74b1e2 Upgrade to Apache HttpClient 4.5.13 (and consistent Caffeine declarations) 2020-11-19 15:19:29 +01:00
Rossen Stoyanchev 37bda566eb Allow "*" for Access-Control-Expose-Headers
Closes gh-26113
2020-11-19 09:48:41 +00:00
Juergen Hoeller 322babc04a Document that @Transactional does not propagate to new threads
Closes gh-25439
2020-11-17 16:16:24 +01:00
Juergen Hoeller 4e720e8104 Encode hash symbol in jar file path (for compatibility with JDK 11+)
Closes gh-26104

(cherry picked from commit b29723623b)
2020-11-17 16:11:07 +01:00
Juergen Hoeller fdab75a1d8 Upgrade to Reactor Dysprosium-SR14, OpenPDF 1.3.23, AssertJ 3.18.1, MockK 1.10.2, Checkstyle 8.37 2020-11-16 21:08:24 +01:00
Juergen Hoeller 240cfb1224 Polishing 2020-11-16 17:50:15 +01:00
Rossen Stoyanchev db2786264b UrlPathHelper.removeJsessionid correctly appends remainder
Closes gh-26079
2020-11-16 11:18:20 +00:00
Juergen Hoeller 763fa98bdf Early log entry for async EntityManagerFactory initialization failure
Closes gh-26093
2020-11-13 17:59:14 +01:00
Sam Brannen 25cbc263f0 Assert same instance returned for cached merged BeanDefinition 2020-11-12 14:58:58 +01:00
Juergen Hoeller 9949a91048 Fix javadoc and assertion glitches 2020-11-12 14:19:12 +01:00
Juergen Hoeller d8b7a593c5 Individually apply the SQL type from each SqlParameterSource argument
Closes gh-26071
2020-11-12 14:18:23 +01:00
Rossen Stoyanchev d3d8f1a487 LimitedDataBufferList adds or raises error
Closes gh-26060
2020-11-10 20:12:38 +00:00
Spring Buildmaster a637f6a27c Next Development Version 2020-11-10 07:09:28 +00:00
Juergen Hoeller e9416b369e Polishing 2020-11-09 18:23:45 +01:00
Juergen Hoeller cde95e1446 Polishing 2020-11-09 13:52:34 +01:00
Sam Brannen da4e37dc1d Do not create intermediate list in MergedAnnotationCollectors.toAnnotationSet()
Prior to this commit, MergedAnnotationCollectors.toAnnotationSet()
created an intermediate ArrayList for storing the results prior to
creating a LinkedHashSet in the finishing step.

Since the creation of the intermediate list is unnecessary, this commit
simplifies the implementation of toAnnotationSet() by using the
Collector.of() factory method that does not accept a `finisher` argument.
The resulting Collector internally uses a `castingIdentity()` function
as the `finisher`.

Closes gh-26031
2020-11-08 15:20:18 +01:00
Rossen Stoyanchev 010d0947c7 Refine logging in StompErrorHandler
Avoid a full stacktrace at ERROR level for a client message that could
not be sent to a MessageChannel.

See gh-26026
2020-11-05 21:51:16 +00:00
Juergen Hoeller 5b06c23a1b Consistent javadoc within SpelCompiler 2020-11-05 18:33:12 +01:00
Juergen Hoeller 7881329cf7 Polishing 2020-11-05 18:18:38 +01:00
Juergen Hoeller 58aa0659cc Suppress NotWritablePropertyException in case of ignoreUnknown=true
Closes gh-25986
2020-11-05 18:15:59 +01:00
Juergen Hoeller dbbedc6c86 Add FullyQualifiedAnnotationBeanNameGenerator.INSTANCE
Closes gh-26025
2020-11-05 18:15:29 +01:00
Juergen Hoeller 09c1e986b9 Upgrade to Hibernate ORM 5.4.23
(cherry picked from commit b815accca9)
2020-11-05 17:54:18 +01:00
Juergen Hoeller 10bff054a9 Reliably refresh metadata for dynamically changing prototype bean class
Closes gh-26019

(cherry picked from commit 412aa06d86)
2020-11-05 17:50:24 +01:00
Sam Brannen e713e0d6d5 Preserve registration order in @ActiveProfiles
With this commit, bean definition profiles declared via @ActiveProfiles
are once again stored in registration order, in order to support use
cases in Spring Boot and other frameworks that depend on the
registration order.

This effectively reverts the changes made in conjunction with gh-25973.

Closes gh-26004
2020-11-02 22:40:59 +01:00
Rossen Stoyanchev b4f8fc8177 Use static accessors in DefaultSimpUserRegistry
Closes gh-26010
2020-11-02 17:32:12 +00:00
Rossen Stoyanchev 8c3cdc6118 Remove unused import 2020-10-30 18:43:19 +00:00
Rossen Stoyanchev 01827fd8d2 Ensure response not closed by MappingJackson2HttpMessageConverter
Closes gh-25987
2020-10-30 18:42:11 +00:00
Stephane Nicoll af1d721aa3 Fix a broken Asciidoctor syntax in core-resources.adoc
Closes gh-26000
2020-10-30 09:33:36 +01:00
Arjen Poutsma 0acb1e5513 Copy default headers, cookies in WebClient builder
This commit makes copies of the default headers and cookies when a
WebClient is built, so that subsequent changes to these do not affect
previously built clients.

Closes: gh-25992
2020-10-29 13:36:26 +01:00
91 changed files with 1145 additions and 780 deletions
+8 -8
View File
@@ -8,9 +8,9 @@ plugins {
id 'org.asciidoctor.jvm.pdf' version '2.4.0'
id 'de.undercouch.download' version '4.1.1'
id "io.freefair.aspectj" version '4.1.6' apply false
id "com.github.ben-manes.versions" version '0.28.0'
id 'com.gradle.build-scan' version '3.2'
id "com.jfrog.artifactory" version '4.12.0' apply false
id "com.github.ben-manes.versions" version '0.24.0'
}
apply from: "$rootDir/gradle/build-scan-user-data.gradle"
@@ -29,7 +29,7 @@ configure(allprojects) { project ->
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.10.5"
mavenBom "io.netty:netty-bom:4.1.51.Final"
mavenBom "io.projectreactor:reactor-bom:Dysprosium-SR13"
mavenBom "io.projectreactor:reactor-bom:Dysprosium-SR15"
mavenBom "io.rsocket:rsocket-bom:1.0.3"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.31.v20200723"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.3.72"
@@ -89,7 +89,7 @@ configure(allprojects) { project ->
dependency "com.h2database:h2:1.4.200"
dependency "com.github.ben-manes.caffeine:caffeine:2.8.6"
dependency "com.github.librepdf:openpdf:1.3.22"
dependency "com.github.librepdf:openpdf:1.3.23"
dependency "com.rometools:rome:1.12.2"
dependency "commons-io:commons-io:2.5"
dependency "io.vavr:vavr:0.10.3"
@@ -116,7 +116,7 @@ configure(allprojects) { project ->
dependency "net.sf.ehcache:ehcache:2.10.6"
dependency "org.ehcache:jcache:1.0.1"
dependency "org.ehcache:ehcache:3.4.0"
dependency "org.hibernate:hibernate-core:5.4.22.Final"
dependency "org.hibernate:hibernate-core:5.4.25.Final"
dependency "org.hibernate:hibernate-validator:6.1.6.Final"
dependency "org.webjars:webjars-locator-core:0.46"
dependency "org.webjars:underscorejs:1.8.3"
@@ -147,7 +147,7 @@ configure(allprojects) { project ->
entry 'okhttp'
entry 'mockwebserver'
}
dependency("org.apache.httpcomponents:httpclient:4.5.12") {
dependency("org.apache.httpcomponents:httpclient:4.5.13") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.apache.httpcomponents:httpasyncclient:4.1.4") {
@@ -180,7 +180,7 @@ configure(allprojects) { project ->
dependency "org.testng:testng:6.14.3"
dependency "org.hamcrest:hamcrest:2.1"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.17.2"
dependency "org.assertj:assertj-core:3.18.1"
dependencySet(group: 'org.xmlunit', version: '2.6.2') {
entry 'xmlunit-assertj'
entry('xmlunit-matchers') {
@@ -193,7 +193,7 @@ configure(allprojects) { project ->
}
entry 'mockito-junit-jupiter'
}
dependency "io.mockk:mockk:1.10.0"
dependency "io.mockk:mockk:1.10.2"
dependency("net.sourceforge.htmlunit:htmlunit:2.43.0") {
exclude group: "commons-logging", name: "commons-logging"
@@ -326,7 +326,7 @@ configure([rootProject] + javaProjects) { project ->
}
checkstyle {
toolVersion = "8.36.2"
toolVersion = "8.38"
configDir = rootProject.file("src/checkstyle")
}
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.2.11.BUILD-SNAPSHOT
version=5.2.12.RELEASE
org.gradle.jvmargs=-Xmx1536M
org.gradle.caching=true
org.gradle.parallel=true
+1 -2
View File
@@ -29,12 +29,11 @@ eclipse.classpath.file.whenMerged { classpath ->
classpath.entries.removeAll { entry -> (entry.path =~ /(?!.*?repack.*\.jar).*?\/([^\/]+)\/build\/libs\/[^\/]+\.jar/) }
}
// Use separate main/test outputs (prevents WTP from packaging test classes)
eclipse.classpath.defaultOutputDir = file(project.name+"/bin/eclipse")
eclipse.classpath.file.beforeMerged { classpath ->
classpath.entries.findAll{ it instanceof SourceFolder }.each {
if(it.output.startsWith("bin/")) {
if (it.output.startsWith("bin/")) {
it.output = null
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -219,10 +219,12 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
@Override
@Nullable
public String[] getParameterNames() {
if (this.parameterNames == null) {
this.parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
String[] parameterNames = this.parameterNames;
if (parameterNames == null) {
parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
this.parameterNames = parameterNames;
}
return this.parameterNames;
return parameterNames;
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -59,7 +59,8 @@ public abstract aspect AbstractTransactionAspect extends TransactionAspectSuppor
@Override
public void destroy() {
clearTransactionManagerCache(); // An aspect is basically a singleton
// An aspect is basically a singleton -> cleanup on destruction
clearTransactionManagerCache();
}
@SuppressAjWarnings("adviceDidNotMatch")
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -422,9 +422,12 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
return;
}
else {
throw createNotWritablePropertyException(tokens.canonicalName);
if (this.suppressNotWritablePropertyException) {
// Optimization for common ignoreUnknown=true scenario since the
// exception would be caught and swallowed higher up anyway...
return;
}
throw createNotWritablePropertyException(tokens.canonicalName);
}
Object oldValue = null;
@@ -806,7 +809,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
* @param propertyPath property path, which may be nested
* @return a property accessor for the target bean
*/
@SuppressWarnings("unchecked") // avoid nested generic
protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
// Handle nested properties recursively.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -40,6 +40,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
private boolean autoGrowNestedPaths = false;
boolean suppressNotWritablePropertyException = false;
@Override
public void setExtractOldValueForEditor(boolean extractOldValueForEditor) {
@@ -89,30 +91,41 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
List<PropertyAccessException> propertyAccessExceptions = null;
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
for (PropertyValue pv : propertyValues) {
try {
// This method may throw any BeansException, which won't be caught
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = true;
}
try {
for (PropertyValue pv : propertyValues) {
// setPropertyValue may throw any BeansException, which won't be caught
// here, if there is a critical failure such as no matching field.
// We can attempt to deal only with less serious exceptions.
setPropertyValue(pv);
}
catch (NotWritablePropertyException ex) {
if (!ignoreUnknown) {
throw ex;
try {
setPropertyValue(pv);
}
// Otherwise, just ignore it and continue...
}
catch (NullValueInNestedPathException ex) {
if (!ignoreInvalid) {
throw ex;
catch (NotWritablePropertyException ex) {
if (!ignoreUnknown) {
throw ex;
}
// Otherwise, just ignore it and continue...
}
// Otherwise, just ignore it and continue...
}
catch (PropertyAccessException ex) {
if (propertyAccessExceptions == null) {
propertyAccessExceptions = new ArrayList<>();
catch (NullValueInNestedPathException ex) {
if (!ignoreInvalid) {
throw ex;
}
// Otherwise, just ignore it and continue...
}
propertyAccessExceptions.add(ex);
catch (PropertyAccessException ex) {
if (propertyAccessExceptions == null) {
propertyAccessExceptions = new ArrayList<>();
}
propertyAccessExceptions.add(ex);
}
}
}
finally {
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -92,8 +92,7 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
@Override
protected NotWritablePropertyException createNotWritablePropertyException(String propertyName) {
PropertyMatches matches = PropertyMatches.forField(propertyName, getRootClass());
throw new NotWritablePropertyException(
getRootClass(), getNestedPath() + propertyName,
throw new NotWritablePropertyException(getRootClass(), getNestedPath() + propertyName,
matches.buildErrorMessage(), matches.getPossibleMatches());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -613,7 +613,7 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean
private final boolean required;
private volatile boolean cached = false;
private volatile boolean cached;
@Nullable
private volatile Object cachedFieldValue;
@@ -644,21 +644,20 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean
}
synchronized (this) {
if (!this.cached) {
Object cachedFieldValue = null;
if (value != null || this.required) {
this.cachedFieldValue = desc;
cachedFieldValue = desc;
registerDependentBeans(beanName, autowiredBeanNames);
if (autowiredBeanNames.size() == 1) {
String autowiredBeanName = autowiredBeanNames.iterator().next();
if (beanFactory.containsBean(autowiredBeanName) &&
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
this.cachedFieldValue = new ShortcutDependencyDescriptor(
cachedFieldValue = new ShortcutDependencyDescriptor(
desc, autowiredBeanName, field.getType());
}
}
}
else {
this.cachedFieldValue = null;
}
this.cachedFieldValue = cachedFieldValue;
this.cached = true;
}
}
@@ -678,7 +677,7 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean
private final boolean required;
private volatile boolean cached = false;
private volatile boolean cached;
@Nullable
private volatile Object[] cachedMethodArguments;
@@ -26,9 +26,6 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -69,8 +66,6 @@ public class InjectionMetadata {
};
private static final Log logger = LogFactory.getLog(InjectionMetadata.class);
private final Class<?> targetClass;
private final Collection<InjectedElement> injectedElements;
@@ -110,9 +105,6 @@ public class InjectionMetadata {
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
beanDefinition.registerExternallyManagedConfigMember(member);
checkedElements.add(element);
if (logger.isTraceEnabled()) {
logger.trace("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
}
}
}
this.checkedElements = checkedElements;
@@ -124,9 +116,6 @@ public class InjectionMetadata {
(checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
if (logger.isTraceEnabled()) {
logger.trace("Processing injected element of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
@@ -152,12 +141,12 @@ public class InjectionMetadata {
* Return an {@code InjectionMetadata} instance, possibly for empty elements.
* @param elements the elements to inject (possibly empty)
* @param clazz the target class
* @return a new {@link #InjectionMetadata(Class, Collection)} instance,
* or {@link #EMPTY} in case of no elements
* @return a new {@link #InjectionMetadata(Class, Collection)} instance
* @since 5.2
*/
public static InjectionMetadata forElements(Collection<InjectedElement> elements, Class<?> clazz) {
return (elements.isEmpty() ? InjectionMetadata.EMPTY : new InjectionMetadata(clazz, elements));
return (elements.isEmpty() ? new InjectionMetadata(clazz, Collections.emptyList()) :
new InjectionMetadata(clazz, elements));
}
/**
@@ -66,6 +66,7 @@ import org.springframework.util.StringUtils;
/**
* Delegate for resolving constructors and factory methods.
*
* <p>Performs constructor resolution through argument matching.
*
* @author Juergen Hoeller
@@ -84,7 +85,7 @@ class ConstructorResolver {
private static final Object[] EMPTY_ARGS = new Object[0];
/**
* Marker for autowired arguments in a cached argument array, to be later replaced
* Marker for autowired arguments in a cached argument array, to be replaced
* by a {@linkplain #resolveAutowiredArgument resolved autowired argument}.
*/
private static final Object autowiredArgumentMarker = new Object();
@@ -148,7 +149,7 @@ class ConstructorResolver {
}
}
if (argsToResolve != null) {
argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true);
argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve);
}
}
@@ -409,6 +410,7 @@ class ConstructorResolver {
if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {
throw new ImplicitlyAppearedSingletonException();
}
this.beanFactory.registerDependentBean(factoryBeanName, beanName);
factoryClass = factoryBean.getClass();
isStatic = false;
}
@@ -443,7 +445,7 @@ class ConstructorResolver {
}
}
if (argsToResolve != null) {
argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve, true);
argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve);
}
}
@@ -815,7 +817,7 @@ class ConstructorResolver {
* Resolve the prepared arguments stored in the given bean definition.
*/
private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
Executable executable, Object[] argsToResolve, boolean fallback) {
Executable executable, Object[] argsToResolve) {
TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
TypeConverter converter = (customConverter != null ? customConverter : bw);
@@ -828,7 +830,7 @@ class ConstructorResolver {
Object argValue = argsToResolve[argIndex];
MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex);
if (argValue == autowiredArgumentMarker) {
argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, fallback);
argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, true);
}
else if (argValue instanceof BeanMetadataElement) {
argValue = valueResolver.resolveValueIfNecessary("constructor argument", argValue);
@@ -63,9 +63,8 @@ public class BeanFactoryUtilsTests {
@BeforeEach
public void setUp() {
public void setup() {
// Interesting hierarchical factory to test counts.
// Slow to read so we cache it.
DefaultListableBeanFactory grandParent = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader(grandParent).loadBeanDefinitions(ROOT_CONTEXT);
@@ -93,7 +92,7 @@ public class BeanFactoryUtilsTests {
* Check that override doesn't count as two separate beans.
*/
@Test
public void testHierarchicalCountBeansWithOverride() throws Exception {
public void testHierarchicalCountBeansWithOverride() {
// Leaf count
assertThat(this.listableBeanFactory.getBeanDefinitionCount() == 1).isTrue();
// Count minus duplicate
@@ -101,14 +100,14 @@ public class BeanFactoryUtilsTests {
}
@Test
public void testHierarchicalNamesWithNoMatch() throws Exception {
public void testHierarchicalNamesWithNoMatch() {
List<String> names = Arrays.asList(
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, NoOp.class));
assertThat(names.size()).isEqualTo(0);
}
@Test
public void testHierarchicalNamesWithMatchOnlyInRoot() throws Exception {
public void testHierarchicalNamesWithMatchOnlyInRoot() {
List<String> names = Arrays.asList(
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, IndexedTestBean.class));
assertThat(names.size()).isEqualTo(1);
@@ -118,7 +117,7 @@ public class BeanFactoryUtilsTests {
}
@Test
public void testGetBeanNamesForTypeWithOverride() throws Exception {
public void testGetBeanNamesForTypeWithOverride() {
List<String> names = Arrays.asList(
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class));
// includes 2 TestBeans from FactoryBeans (DummyFactory definitions)
@@ -236,7 +235,7 @@ public class BeanFactoryUtilsTests {
}
@Test
public void testHierarchicalResolutionWithOverride() throws Exception {
public void testHierarchicalResolutionWithOverride() {
Object test3 = this.listableBeanFactory.getBean("test3");
Object test = this.listableBeanFactory.getBean("test");
@@ -276,14 +275,14 @@ public class BeanFactoryUtilsTests {
}
@Test
public void testHierarchicalNamesForAnnotationWithNoMatch() throws Exception {
public void testHierarchicalNamesForAnnotationWithNoMatch() {
List<String> names = Arrays.asList(
BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, Override.class));
assertThat(names.size()).isEqualTo(0);
}
@Test
public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() throws Exception {
public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() {
List<String> names = Arrays.asList(
BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, TestAnnotation.class));
assertThat(names.size()).isEqualTo(1);
@@ -293,7 +292,7 @@ public class BeanFactoryUtilsTests {
}
@Test
public void testGetBeanNamesForAnnotationWithOverride() throws Exception {
public void testGetBeanNamesForAnnotationWithOverride() {
AnnotatedBean annotatedBean = new AnnotatedBean();
this.listableBeanFactory.registerSingleton("anotherAnnotatedBean", annotatedBean);
List<String> names = Arrays.asList(
@@ -433,6 +432,7 @@ public class BeanFactoryUtilsTests {
String basePackage() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@ControllerAdvice
@interface RestControllerAdvice {
@@ -444,18 +444,23 @@ public class BeanFactoryUtilsTests {
String basePackage() default "";
}
@ControllerAdvice("com.example")
static class ControllerAdviceClass {
}
@RestControllerAdvice("com.example")
static class RestControllerAdviceClass {
}
static class TestBeanSmartFactoryBean implements SmartFactoryBean<TestBean> {
private final TestBean testBean = new TestBean("enigma", 42);
private final boolean singleton;
private final boolean prototype;
TestBeanSmartFactoryBean(boolean singleton, boolean prototype) {
@@ -478,7 +483,7 @@ public class BeanFactoryUtilsTests {
return TestBean.class;
}
public TestBean getObject() throws Exception {
public TestBean getObject() {
// We don't really care if the actual instance is a singleton or prototype
// for the tests that use this factory.
return this.testBean;
@@ -796,12 +796,13 @@ class DefaultListableBeanFactoryTests {
factory.registerBeanDefinition("child", childDefinition);
factory.registerAlias("parent", "alias");
TestBean child = (TestBean) factory.getBean("child");
TestBean child = factory.getBean("child", TestBean.class);
assertThat(child.getName()).isEqualTo(EXPECTED_NAME);
assertThat(child.getAge()).isEqualTo(EXPECTED_AGE);
Object mergedBeanDefinition2 = factory.getMergedBeanDefinition("child");
BeanDefinition mergedBeanDefinition1 = factory.getMergedBeanDefinition("child");
BeanDefinition mergedBeanDefinition2 = factory.getMergedBeanDefinition("child");
assertThat(mergedBeanDefinition2).as("Use cached merged bean definition").isEqualTo(mergedBeanDefinition2);
assertThat(mergedBeanDefinition1).as("Use cached merged bean definition").isSameAs(mergedBeanDefinition2);
}
@Test
@@ -1849,8 +1850,7 @@ class DefaultListableBeanFactoryTests {
assertThat(factoryBean).as("The FactoryBean should have been registered.").isNotNull();
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) lbf.autowire(FactoryBeanDependentBean.class,
AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);
Object mergedBeanDefinition2 = bean.getFactoryBean();
assertThat(mergedBeanDefinition2).as("The FactoryBeanDependentBean should have been autowired 'by type' with the LazyInitFactory.").isEqualTo(mergedBeanDefinition2);
assertThat(bean.getFactoryBean()).as("The FactoryBeanDependentBean should have been autowired 'by type' with the LazyInitFactory.").isEqualTo(factoryBean);
}
@Test
@@ -2556,8 +2556,7 @@ class DefaultListableBeanFactoryTests {
BeanWithDestroyMethod.closeCount = 0;
lbf.preInstantiateSingletons();
lbf.destroySingletons();
Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount;
assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2);
assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1);
}
@Test
@@ -2571,8 +2570,7 @@ class DefaultListableBeanFactoryTests {
BeanWithDestroyMethod.closeCount = 0;
lbf.preInstantiateSingletons();
lbf.destroySingletons();
Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount;
assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2);
assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(2);
}
@Test
@@ -2587,8 +2585,7 @@ class DefaultListableBeanFactoryTests {
BeanWithDestroyMethod.closeCount = 0;
lbf.preInstantiateSingletons();
lbf.destroySingletons();
Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount;
assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2);
assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1);
}
@Test
@@ -2710,14 +2707,15 @@ class DefaultListableBeanFactoryTests {
factory.registerBeanDefinition("child", child);
AbstractBeanDefinition def = (AbstractBeanDefinition) factory.getBeanDefinition("child");
Object mergedBeanDefinition2 = def.getScope();
assertThat(mergedBeanDefinition2).as("Child 'scope' not overriding parent scope (it must).").isEqualTo(mergedBeanDefinition2);
assertThat(def.getScope()).as("Child 'scope' not overriding parent scope (it must).").isEqualTo(theChildScope);
}
@Test
void scopeInheritanceForChildBeanDefinitions() {
String theParentScope = "bonanza!";
RootBeanDefinition parent = new RootBeanDefinition();
parent.setScope("bonanza!");
parent.setScope(theParentScope);
AbstractBeanDefinition child = new ChildBeanDefinition("parent");
child.setBeanClass(TestBean.class);
@@ -2727,8 +2725,7 @@ class DefaultListableBeanFactoryTests {
factory.registerBeanDefinition("child", child);
BeanDefinition def = factory.getMergedBeanDefinition("child");
Object mergedBeanDefinition2 = def.getScope();
assertThat(mergedBeanDefinition2).as("Child 'scope' not inherited").isEqualTo(mergedBeanDefinition2);
assertThat(def.getScope()).as("Child 'scope' not inherited").isEqualTo(theParentScope);
}
@Test
@@ -2764,15 +2761,12 @@ class DefaultListableBeanFactoryTests {
});
lbf.preInstantiateSingletons();
TestBean tb = (TestBean) lbf.getBean("test");
Object mergedBeanDefinition2 = tb.getName();
assertThat(mergedBeanDefinition2).as("Name was set on field by IAPP").isEqualTo(mergedBeanDefinition2);
assertThat(tb.getName()).as("Name was set on field by IAPP").isEqualTo(nameSetOnField);
if (!skipPropertyPopulation) {
Object mergedBeanDefinition21 = tb.getAge();
assertThat(mergedBeanDefinition21).as("Property value still set").isEqualTo(mergedBeanDefinition21);
assertThat(tb.getAge()).as("Property value still set").isEqualTo(ageSetByPropertyValue);
}
else {
Object mergedBeanDefinition21 = tb.getAge();
assertThat(mergedBeanDefinition21).as("Property value was NOT set and still has default value").isEqualTo(mergedBeanDefinition21);
assertThat(tb.getAge()).as("Property value was NOT set and still has default value").isEqualTo(0);
}
}
@@ -43,6 +43,15 @@ import org.springframework.util.Assert;
*/
public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
/**
* A convenient constant for a default {@code FullyQualifiedAnnotationBeanNameGenerator}
* instance, as used for configuration-level import purposes.
* @since 5.2.11
*/
public static final FullyQualifiedAnnotationBeanNameGenerator INSTANCE =
new FullyQualifiedAnnotationBeanNameGenerator();
@Override
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -106,7 +106,6 @@ public @interface DateTimeFormat {
/**
* The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX},
* e.g. "2000-10-31T01:30:00.000-05:00".
* <p>This is the default if no annotation value is specified.
*/
DATE_TIME,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -36,24 +36,26 @@ import static org.assertj.core.api.Assertions.assertThat;
public class AtAspectJAfterThrowingTests {
@Test
public void testAccessThrowable() throws Exception {
public void testAccessThrowable() {
ClassPathXmlApplicationContext ctx =
new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass());
new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass());
ITestBean bean = (ITestBean) ctx.getBean("testBean");
ExceptionHandlingAspect aspect = (ExceptionHandlingAspect) ctx.getBean("aspect");
assertThat(AopUtils.isAopProxy(bean)).isTrue();
IOException exceptionThrown = null;
try {
bean.unreliableFileOperation();
}
catch (IOException e) {
//
catch (IOException ex) {
exceptionThrown = ex;
}
assertThat(aspect.handled).isEqualTo(1);
assertThat(aspect.lastException).isNotNull();
assertThat(aspect.lastException).isSameAs(exceptionThrown);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class AtAspectJAnnotationBindingTests {
private AnnotatedTestBean testBean;
private ClassPathXmlApplicationContext ctx;
@@ -70,8 +71,7 @@ public class AtAspectJAnnotationBindingTests {
class AtAspectJAnnotationBindingTestAspect {
@Around("execution(* *(..)) && @annotation(testAnn)")
public Object doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnn)
throws Throwable {
public Object doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnn) throws Throwable {
String annValue = testAnn.value();
Object result = pjp.proceed();
return (result instanceof String ? annValue + " " + result : result);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -105,7 +104,9 @@ public class ConfigurationClassPostProcessorTests {
Foo foo = beanFactory.getBean("foo", Foo.class);
Bar bar = beanFactory.getBean("bar", Bar.class);
assertThat(bar.foo).isSameAs(foo);
assertThat(Arrays.asList(beanFactory.getDependentBeans("foo")).contains("bar")).isTrue();
assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("foo"), "bar")).isTrue();
assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "foo")).isTrue();
assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "bar")).isTrue();
}
@Test
@@ -117,7 +118,9 @@ public class ConfigurationClassPostProcessorTests {
Foo foo = beanFactory.getBean("foo", Foo.class);
Bar bar = beanFactory.getBean("bar", Bar.class);
assertThat(bar.foo).isSameAs(foo);
assertThat(Arrays.asList(beanFactory.getDependentBeans("foo")).contains("bar")).isTrue();
assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("foo"), "bar")).isTrue();
assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "foo")).isTrue();
assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "bar")).isTrue();
}
@Test
@@ -307,6 +307,23 @@ public class ConfigurationClassProcessingTests {
assertThat(tb.getLawyer()).isEqualTo(ctx.getBean(NestedTestBean.class));
}
@Test // gh-26019
public void autowiringWithDynamicPrototypeBeanClass() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
ConfigWithDynamicPrototype.class, PrototypeDependency.class);
PrototypeInterface p1 = ctx.getBean(PrototypeInterface.class, 1);
assertThat(p1).isInstanceOf(PrototypeOne.class);
assertThat(((PrototypeOne) p1).prototypeDependency).isNotNull();
PrototypeInterface p2 = ctx.getBean(PrototypeInterface.class, 2);
assertThat(p2).isInstanceOf(PrototypeTwo.class);
PrototypeInterface p3 = ctx.getBean(PrototypeInterface.class, 1);
assertThat(p3).isInstanceOf(PrototypeOne.class);
assertThat(((PrototypeOne) p3).prototypeDependency).isNotNull();
}
/**
* Creates a new {@link BeanFactory}, populates it with a {@link BeanDefinition}
@@ -632,4 +649,42 @@ public class ConfigurationClassProcessingTests {
}
}
static class PrototypeDependency {
}
interface PrototypeInterface {
}
static class PrototypeOne extends AbstractPrototype {
@Autowired
PrototypeDependency prototypeDependency;
}
static class PrototypeTwo extends AbstractPrototype {
// no autowired dependency here, in contrast to above
}
static class AbstractPrototype implements PrototypeInterface {
}
@Configuration
static class ConfigWithDynamicPrototype {
@Bean
@Scope(value = "prototype")
public PrototypeInterface getDemoBean( int i) {
switch ( i) {
case 1: return new PrototypeOne();
case 2:
default:
return new PrototypeTwo();
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -66,10 +66,10 @@ internal fun isSuspendingFunction(method: Method) = method.kotlinFunction!!.isSu
* @since 5.2
*/
@Suppress("UNCHECKED_CAST")
internal fun invokeSuspendingFunction(method: Method, bean: Any, vararg args: Any?): Publisher<*> {
internal fun invokeSuspendingFunction(method: Method, target: Any, vararg args: Any?): Publisher<*> {
val function = method.kotlinFunction!!
val mono = mono(Dispatchers.Unconfined) {
function.callSuspend(bean, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it }
function.callSuspend(target, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it }
}.onErrorMap(InvocationTargetException::class.java) { it.targetException }
return if (function.returnType.classifier == Flow::class) {
mono.flatMapMany { (it as Flow<Any>).asFlux() }
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,8 +18,8 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.IntFunction;
@@ -31,10 +31,11 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Collector implementations that provide various reduction operations for
* {@link Collector} implementations that provide various reduction operations for
* {@link MergedAnnotation} instances.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2
*/
public abstract class MergedAnnotationCollectors {
@@ -52,13 +53,16 @@ public abstract class MergedAnnotationCollectors {
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link LinkedHashSet} containing {@linkplain MergedAnnotation#synthesize()
* synthesized} versions.
* <p>The collector returned by this method is effectively equivalent to
* {@code Collectors.mapping(MergedAnnotation::synthesize, Collectors.toCollection(LinkedHashSet::new))}
* but avoids the creation of a composite collector.
* @param <A> the annotation type
* @return a {@link Collector} which collects and synthesizes the
* annotations into a {@link Set}
*/
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Set<A>> toAnnotationSet() {
return Collector.of(ArrayList<A>::new, (list, annotation) -> list.add(annotation.synthesize()),
MergedAnnotationCollectors::addAll, LinkedHashSet::new);
return Collector.of(LinkedHashSet::new, (set, annotation) -> set.add(annotation.synthesize()),
MergedAnnotationCollectors::combiner);
}
/**
@@ -90,14 +94,14 @@ public abstract class MergedAnnotationCollectors {
IntFunction<R[]> generator) {
return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()),
MergedAnnotationCollectors::addAll, list -> list.toArray(generator.apply(list.size())));
MergedAnnotationCollectors::combiner, list -> list.toArray(generator.apply(list.size())));
}
/**
* Create a new {@link Collector} that accumulates merged annotations to an
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param adaptations the adaptations that should be applied to the annotation values
* @return a {@link Collector} which collects and synthesizes the
@@ -111,13 +115,13 @@ public abstract class MergedAnnotationCollectors {
}
/**
* Create a new {@link Collector} that accumulates merged annotations to an
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param adaptations the adaptations that should be applied to the annotation values
* @param finisher the finisher function for the new {@link MultiValueMap}
* @param adaptations the adaptations that should be applied to the annotation values
* @return a {@link Collector} which collects and synthesizes the
* annotations into a {@link LinkedMultiValueMap}
* @see #toMultiValueMap(MergedAnnotation.Adapt...)
@@ -130,7 +134,7 @@ public abstract class MergedAnnotationCollectors {
IDENTITY_FINISH_CHARACTERISTICS : NO_CHARACTERISTICS);
return Collector.of(LinkedMultiValueMap::new,
(map, annotation) -> annotation.asMap(adaptations).forEach(map::add),
MergedAnnotationCollectors::merge, finisher, characteristics);
MergedAnnotationCollectors::combiner, finisher, characteristics);
}
@@ -138,13 +142,22 @@ public abstract class MergedAnnotationCollectors {
return instance == candidate;
}
private static <E, L extends List<E>> L addAll(L list, L additions) {
list.addAll(additions);
return list;
/**
* {@link Collector#combiner() Combiner} for collections.
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
*/
private static <E, C extends Collection<E>> C combiner(C collection, C additions) {
collection.addAll(additions);
return collection;
}
private static <K, V> MultiValueMap<K, V> merge(MultiValueMap<K, V> map,
MultiValueMap<K, V> additions) {
/**
* {@link Collector#combiner() Combiner} for multi-value maps.
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
*/
private static <K, V> MultiValueMap<K, V> combiner(MultiValueMap<K, V> map, MultiValueMap<K, V> additions) {
map.addAll(additions);
return map;
}
@@ -19,14 +19,15 @@ package org.springframework.core.convert.support;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.ResolvableType;
@@ -499,9 +500,9 @@ public class GenericConversionService implements ConfigurableConversionService {
*/
private static class Converters {
private final Set<GenericConverter> globalConverters = new LinkedHashSet<>();
private final Set<GenericConverter> globalConverters = new CopyOnWriteArraySet<>();
private final Map<ConvertiblePair, ConvertersForPair> converters = new LinkedHashMap<>(36);
private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);
public void add(GenericConverter converter) {
Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
@@ -512,8 +513,7 @@ public class GenericConversionService implements ConfigurableConversionService {
}
else {
for (ConvertiblePair convertiblePair : convertibleTypes) {
ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair);
convertersForPair.add(converter);
getMatchableConverters(convertiblePair).add(converter);
}
}
}
@@ -651,7 +651,7 @@ public class GenericConversionService implements ConfigurableConversionService {
*/
private static class ConvertersForPair {
private final LinkedList<GenericConverter> converters = new LinkedList<>();
private final Deque<GenericConverter> converters = new ConcurrentLinkedDeque<>();
public void add(GenericConverter converter) {
this.converters.addFirst(converter);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -54,11 +54,8 @@ public class LimitedDataBufferList extends ArrayList<DataBuffer> {
@Override
public boolean add(DataBuffer buffer) {
boolean result = super.add(buffer);
if (result) {
updateCount(buffer.readableByteCount());
}
return result;
updateCount(buffer.readableByteCount());
return super.add(buffer);
}
@Override
@@ -432,6 +432,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
// Possibly "c:" drive prefix on Windows, to be upper-cased for proper duplicate detection
filePath = StringUtils.capitalize(filePath);
}
// # can appear in directories/filenames, java.net.URL should not treat it as a fragment
filePath = StringUtils.replace(filePath, "#", "%23");
// Build URL that points to the root of the jar file
UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX +
ResourceUtils.FILE_URL_PREFIX + filePath + ResourceUtils.JAR_URL_SEPARATOR);
// Potentially overlapping with URLClassLoader.getURLs() result above!
@@ -16,6 +16,8 @@
package org.springframework.util;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.BitSet;
@@ -104,7 +106,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
private final Map<String, String> parameters;
@Nullable
private Charset resolvedCharset;
private transient Charset resolvedCharset;
@Nullable
private volatile String toStringValue;
@@ -184,9 +186,9 @@ public class MimeType implements Comparable<MimeType>, Serializable {
this.subtype = subtype.toLowerCase(Locale.ENGLISH);
if (!CollectionUtils.isEmpty(parameters)) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH);
parameters.forEach((attribute, value) -> {
checkParameters(attribute, value);
map.put(attribute, value);
parameters.forEach((parameter, value) -> {
checkParameters(parameter, value);
map.put(parameter, value);
});
this.parameters = Collections.unmodifiableMap(map);
}
@@ -210,11 +212,11 @@ public class MimeType implements Comparable<MimeType>, Serializable {
}
}
protected void checkParameters(String attribute, String value) {
Assert.hasLength(attribute, "'attribute' must not be empty");
protected void checkParameters(String parameter, String value) {
Assert.hasLength(parameter, "'parameter' must not be empty");
Assert.hasLength(value, "'value' must not be empty");
checkToken(attribute);
if (PARAM_CHARSET.equals(attribute)) {
checkToken(parameter);
if (PARAM_CHARSET.equals(parameter)) {
if (this.resolvedCharset == null) {
this.resolvedCharset = Charset.forName(unquote(value));
}
@@ -569,6 +571,17 @@ public class MimeType implements Comparable<MimeType>, Serializable {
return 0;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Rely on default serialization, just initialize state after deserialization.
ois.defaultReadObject();
// Initialize transient fields.
String charsetName = getParameter(PARAM_CHARSET);
if (charsetName != null) {
this.resolvedCharset = Charset.forName(unquote(charsetName));
}
}
/**
* Parse the given String value into a {@code MimeType} object,
@@ -35,6 +35,8 @@ import java.util.List;
import java.util.concurrent.CountDownLatch;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
@@ -834,6 +836,22 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
.verifyError(DataBufferLimitException.class);
}
@Test // gh-26060
void joinWithLimitDoesNotOverRelease() {
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT);
byte[] bytes = "foo-bar-baz".getBytes(StandardCharsets.UTF_8);
NettyDataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
buffer.getNativeBuffer().retain(); // should be at 2 now
buffer.write(bytes);
Mono<DataBuffer> result = DataBufferUtils.join(Flux.just(buffer), 8);
StepVerifier.create(result).verifyError(DataBufferLimitException.class);
assertThat(buffer.getNativeBuffer().refCnt()).isEqualTo(1);
buffer.release();
}
@ParameterizedDataBufferAllocatingTest
void joinErrors(String displayName, DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
@@ -17,9 +17,11 @@ package org.springframework.core.io.buffer;
import java.nio.charset.StandardCharsets;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link LimitedDataBufferList}.
* @author Rossen Stoyanchev
@@ -32,8 +34,10 @@ public class LimitedDataBufferListTests {
@Test
void limitEnforced() {
Assertions.assertThatThrownBy(() -> new LimitedDataBufferList(5).add(toDataBuffer("123456")))
.isInstanceOf(DataBufferLimitException.class);
LimitedDataBufferList list = new LimitedDataBufferList(5);
assertThatThrownBy(() -> list.add(toDataBuffer("123456"))).isInstanceOf(DataBufferLimitException.class);
assertThat(list).isEmpty();
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,6 +26,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.testfixture.io.SerializationTestUtils;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
@@ -267,13 +268,13 @@ class MimeTypeTests {
assertThat(mimeType.getParameter("attr")).isEqualTo("'v>alue'");
}
@Test // SPR-16630
@Test // SPR-16630
void parseMimeTypeWithSpacesAroundEquals() {
MimeType mimeType = MimeTypeUtils.parseMimeType("multipart/x-mixed-replace;boundary = --myboundary");
assertThat(mimeType.getParameter("boundary")).isEqualTo("--myboundary");
}
@Test // SPR-16630
@Test // SPR-16630
void parseMimeTypeWithSpacesAroundEqualsAndQuotedValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain; foo = \" bar \" ");
assertThat(mimeType.getParameter("foo")).isEqualTo("\" bar \"");
@@ -303,14 +304,14 @@ class MimeTypeTests {
assertThat(mimeTypes.size()).as("Invalid amount of mime types").isEqualTo(0);
}
@Test // gh-23241
@Test // gh-23241
void parseMimeTypesWithTrailingComma() {
List<MimeType> mimeTypes = MimeTypeUtils.parseMimeTypes("text/plain, text/html,");
assertThat(mimeTypes).as("No mime types returned").isNotNull();
assertThat(mimeTypes.size()).as("Incorrect number of mime types").isEqualTo(2);
}
@Test // SPR-17459
@Test // SPR-17459
void parseMimeTypesWithQuotedParameters() {
testWithQuotedParameters("foo/bar;param=\",\"");
testWithQuotedParameters("foo/bar;param=\"s,a,\"");
@@ -323,8 +324,9 @@ class MimeTypeTests {
private void testWithQuotedParameters(String... mimeTypes) {
String s = String.join(",", mimeTypes);
List<MimeType> actual = MimeTypeUtils.parseMimeTypes(s);
assertThat(actual.size()).isEqualTo(mimeTypes.length);
for (int i=0; i < mimeTypes.length; i++) {
for (int i = 0; i < mimeTypes.length; i++) {
assertThat(actual.get(i).toString()).isEqualTo(mimeTypes[i]);
}
}
@@ -351,6 +353,7 @@ class MimeTypeTests {
List<MimeType> result = new ArrayList<>(expected);
Random rnd = new Random();
// shuffle & sort 10 times
for (int i = 0; i < 10; i++) {
Collections.shuffle(result, rnd);
@@ -380,11 +383,7 @@ class MimeTypeTests {
assertThat(m2.compareTo(m1) != 0).as("Invalid comparison result").isTrue();
}
/**
* SPR-13157
* @since 4.2
*/
@Test
@Test // SPR-13157
void equalsIsCaseInsensitiveForCharsets() {
MimeType m1 = new MimeType("text", "plain", singletonMap("charset", "UTF-8"));
MimeType m2 = new MimeType("text", "plain", singletonMap("charset", "utf-8"));
@@ -394,4 +393,12 @@ class MimeTypeTests {
assertThat(m2.compareTo(m1)).isEqualTo(0);
}
@Test // gh-26127
void serialize() throws Exception {
MimeType original = new MimeType("text", "plain", StandardCharsets.UTF_8);
MimeType deserialized = (MimeType) SerializationTestUtils.serializeAndDeserialize(original);
assertThat(deserialized).isEqualTo(original);
assertThat(original).isEqualTo(deserialized);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -54,10 +54,9 @@ public abstract class AstUtils {
}
else {
if (targetType != null) {
int pos = 0;
for (Class<?> clazz : targets) {
if (clazz == targetType) { // put exact matches on the front to be tried first?
specificAccessors.add(pos++, resolver);
specificAccessors.add(resolver);
}
else if (clazz.isAssignableFrom(targetType)) { // put supertype matches at the end of the
// specificAccessor list
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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,18 +63,20 @@ import org.springframework.util.StringUtils;
* <p>Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}.
*
* @author Andy Clement
* @author Juergen Hoeller
* @since 4.1
*/
public final class SpelCompiler implements Opcodes {
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
private static final int CLASSES_DEFINED_LIMIT = 100;
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
// A compiler is created for each classloader, it manages a child class loader of that
// classloader and the child is used to load the compiled expressions.
private static final Map<ClassLoader, SpelCompiler> compilers = new ConcurrentReferenceHashMap<>();
// The child ClassLoader used to load the compiled expression classes
private ChildClassLoader ccl;
@@ -90,7 +92,7 @@ public final class SpelCompiler implements Opcodes {
/**
* Attempt compilation of the supplied expression. A check is made to see
* if it is compilable before compilation proceeds. The check involves
* visiting all the nodes in the expression Ast and ensuring enough state
* visiting all the nodes in the expression AST and ensuring enough state
* is known about them that bytecode can be generated for them.
* @param expression the expression to compile
* @return an instance of the class implementing the compiled expression,
@@ -125,7 +127,7 @@ public final class SpelCompiler implements Opcodes {
/**
* Generate the class that encapsulates the compiled expression and define it.
* The generated class will be a subtype of CompiledExpression.
* The generated class will be a subtype of CompiledExpression.
* @param expressionToCompile the expression to be compiled
* @return the expression call, or {@code null} if the decision was to opt out of
* compilation during code generation
@@ -150,7 +152,7 @@ public final class SpelCompiler implements Opcodes {
// Create getValue() method
mv = cw.visitMethod(ACC_PUBLIC, "getValue",
"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,
new String[ ]{"org/springframework/expression/EvaluationException"});
new String[] {"org/springframework/expression/EvaluationException"});
mv.visitCode();
CodeFlow cf = new CodeFlow(className, cw);
@@ -187,7 +189,7 @@ public final class SpelCompiler implements Opcodes {
/**
* Load a compiled expression class. Makes sure the classloaders aren't used too much
* because they anchor compiled classes in memory and prevent GC. If you have expressions
* because they anchor compiled classes in memory and prevent GC. If you have expressions
* continually recompiling over time then by replacing the classloader periodically
* at least some of the older variants can be garbage collected.
* @param name the name of the class
@@ -202,6 +204,7 @@ public final class SpelCompiler implements Opcodes {
return (Class<? extends CompiledExpression>) this.ccl.defineClass(name, bytes);
}
/**
* Factory method for compiler instances. The returned SpelCompiler will
* attach a class loader as the child of the given class loader and this
@@ -222,10 +225,12 @@ public final class SpelCompiler implements Opcodes {
}
/**
* Request that an attempt is made to compile the specified expression. It may fail if
* components of the expression are not suitable for compilation or the data types
* involved are not suitable for compilation. Used for testing.
* @return true if the expression was successfully compiled
* Request that an attempt is made to compile the specified expression.
* It may fail if components of the expression are not suitable for compilation
* or the data types involved are not suitable for compilation. Used for testing.
* @param expression the expression to compile
* @return {@code true} if the expression was successfully compiled,
* {@code false} otherwise
*/
public static boolean compile(Expression expression) {
return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression());
@@ -256,18 +261,21 @@ public final class SpelCompiler implements Opcodes {
super(NO_URLS, classLoader);
}
int getClassesDefinedCount() {
return this.classesDefinedCount;
}
public Class<?> defineClass(String name, byte[] bytes) {
Class<?> clazz = super.defineClass(name, bytes, 0, bytes.length);
this.classesDefinedCount++;
return clazz;
}
public int getClassesDefinedCount() {
return this.classesDefinedCount;
}
}
/**
* An ASM ClassWriter extension bound to the SpelCompiler's ClassLoader.
*/
private class ExpressionClassWriter extends ClassWriter {
public ExpressionClassWriter() {
@@ -47,7 +47,7 @@ import org.springframework.util.StringUtils;
/**
* A powerful {@link PropertyAccessor} that uses reflection to access properties
* for reading and possibly also for writing.
* for reading and possibly also for writing on a target instance.
*
* <p>A property can be referenced through a public getter method (when being read)
* or a public setter method (when being written), and also as a public field.
@@ -98,8 +98,8 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
}
/**
* Create a new property accessor for reading and possibly writing.
* @param allowWrite whether to also allow for write operations
* Create a new property accessor for reading and possibly also writing.
* @param allowWrite whether to allow write operations on a target instance
* @since 4.3.15
* @see #canWrite
*/
@@ -623,8 +623,8 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
@Override
public String toString() {
return "CacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + ", " +
this.property + ", targetIsClass=" + this.targetIsClass + "]";
return "PropertyCacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property +
", targetIsClass=" + this.targetIsClass + "]";
}
@Override
@@ -76,7 +76,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
public class SpelReproTests extends AbstractExpressionTests {
@Test
public void NPE_SPR5661() {
evaluate("joinThreeStrings('a',null,'c')", "anullc", String.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,10 @@
package org.springframework.expression.spel.testresources;
///CLOVER:OFF
public class Person {
private String privateName;
Company company;
public Person(String name) {
@@ -41,4 +42,5 @@ public class Person {
public Company getCompany() {
return company;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,19 +19,25 @@ package org.springframework.expression.spel.testresources;
import java.util.List;
public class TestAddress{
private String street;
private List<String> crossStreets;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public List<String> getCrossStreets() {
return crossStreets;
}
public void setCrossStreets(List<String> crossStreets) {
this.crossStreets = crossStreets;
}
private String street;
private List<String> crossStreets;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public List<String> getCrossStreets() {
return crossStreets;
}
public void setCrossStreets(List<String> crossStreets) {
this.crossStreets = crossStreets;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,19 +17,25 @@
package org.springframework.expression.spel.testresources;
public class TestPerson {
private String name;
private TestAddress address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestAddress getAddress() {
return address;
}
public void setAddress(TestAddress address) {
this.address = address;
}
private String name;
private TestAddress address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestAddress getAddress() {
return address;
}
public void setAddress(TestAddress address) {
this.address = address;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -30,7 +30,6 @@ import java.util.Set;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Helper class that efficiently creates multiple {@link PreparedStatementCreator}
@@ -200,9 +199,8 @@ public class PreparedStatementCreatorFactory {
public PreparedStatementCreatorImpl(String actualSql, List<?> parameters) {
this.actualSql = actualSql;
Assert.notNull(parameters, "Parameters List must not be null");
this.parameters = parameters;
if (this.parameters.size() != declaredParameters.size()) {
if (parameters.size() != declaredParameters.size()) {
// Account for named parameters being used multiple times
Set<String> names = new HashSet<>();
for (int i = 0; i < parameters.size(); i++) {
@@ -637,21 +637,23 @@ public class CallMetaDataContext {
schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
}
String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName());
if (isFunction() || isReturnValueRequired()) {
callString = new StringBuilder().append("{? = call ").
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
append(procedureNameToUse).append("(");
callString = new StringBuilder("{? = call ");
parameterCount = -1;
}
else {
callString = new StringBuilder().append("{call ").
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
append(procedureNameToUse).append("(");
callString = new StringBuilder("{call ");
}
if (StringUtils.hasLength(catalogNameToUse)) {
callString.append(catalogNameToUse).append(".");
}
if (StringUtils.hasLength(schemaNameToUse)) {
callString.append(schemaNameToUse).append(".");
}
callString.append(this.metaDataProvider.procedureNameToUse(getProcedureName()));
callString.append("(");
for (SqlParameter parameter : this.callParameters) {
if (!parameter.isResultsParameter()) {
if (parameterCount > 0) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -345,9 +345,9 @@ public abstract class NamedParameterUtils {
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
try {
Object value = paramSource.getValue(paramName);
SqlParameter param = findParameter(declaredParams, paramName, i);
paramArray[i] = (param != null ? new SqlParameterValue(param, value) : value);
paramArray[i] = (param != null ? new SqlParameterValue(param, paramSource.getValue(paramName)) :
SqlParameterSourceUtils.getTypedValue(paramSource, paramName));
}
catch (IllegalArgumentException ex) {
throw new InvalidDataAccessApiUsageException(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -44,7 +44,6 @@ public abstract class SqlParameterSourceUtils {
* @see BeanPropertySqlParameterSource
* @see NamedParameterJdbcTemplate#batchUpdate(String, SqlParameterSource[])
*/
@SuppressWarnings("unchecked")
public static SqlParameterSource[] createBatch(Object... candidates) {
return createBatch(Arrays.asList(candidates));
}
@@ -93,17 +92,13 @@ public abstract class SqlParameterSourceUtils {
* @param source the source of parameter values and type information
* @param parameterName the name of the parameter
* @return the value object
* @see SqlParameterValue
*/
@Nullable
public static Object getTypedValue(SqlParameterSource source, String parameterName) {
int sqlType = source.getSqlType(parameterName);
if (sqlType != SqlParameterSource.TYPE_UNKNOWN) {
if (source.getTypeName(parameterName) != null) {
return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName));
}
else {
return new SqlParameterValue(sqlType, source.getValue(parameterName));
}
return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName));
}
else {
return source.getValue(parameterName);
@@ -526,10 +526,11 @@ public class NamedParameterJdbcTemplateTests {
@Test
public void testBatchUpdateWithSqlParameterSourcePlusTypeInfo() throws Exception {
SqlParameterSource[] ids = new SqlParameterSource[2];
ids[0] = new MapSqlParameterSource().addValue("id", 100, Types.NUMERIC);
ids[1] = new MapSqlParameterSource().addValue("id", 200, Types.NUMERIC);
final int[] rowsAffected = new int[] {1, 2};
SqlParameterSource[] ids = new SqlParameterSource[3];
ids[0] = new MapSqlParameterSource().addValue("id", null, Types.NULL);
ids[1] = new MapSqlParameterSource().addValue("id", 100, Types.NUMERIC);
ids[2] = new MapSqlParameterSource().addValue("id", 200, Types.NUMERIC);
final int[] rowsAffected = new int[] {1, 2, 3};
given(preparedStatement.executeBatch()).willReturn(rowsAffected);
given(connection.getMetaData()).willReturn(databaseMetaData);
@@ -537,13 +538,15 @@ public class NamedParameterJdbcTemplateTests {
int[] actualRowsAffected = namedParameterTemplate.batchUpdate(
"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = :id", ids);
assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
assertThat(actualRowsAffected.length == 3).as("executed 3 updates").isTrue();
assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);
assertThat(actualRowsAffected[2]).isEqualTo(rowsAffected[2]);
verify(connection).prepareStatement("UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?");
verify(preparedStatement).setNull(1, Types.NULL);
verify(preparedStatement).setObject(1, 100, Types.NUMERIC);
verify(preparedStatement).setObject(1, 200, Types.NUMERIC);
verify(preparedStatement, times(2)).addBatch();
verify(preparedStatement, times(3)).addBatch();
verify(preparedStatement, atLeastOnce()).close();
verify(connection, atLeastOnce()).close();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -233,7 +233,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
@Override
protected StompBrokerRelayMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(
getClientInboundChannel(), getClientOutboundChannel(),
brokerChannel, getDestinationPrefixes());
@@ -378,13 +378,14 @@ public class MessageHeaderAccessor {
* {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values.
*/
public void copyHeaders(@Nullable Map<String, ?> headersToCopy) {
if (headersToCopy != null) {
headersToCopy.forEach((key, value) -> {
if (!isReadOnly(key)) {
setHeader(key, value);
}
});
if (headersToCopy == null || this.headers == headersToCopy) {
return;
}
headersToCopy.forEach((key, value) -> {
if (!isReadOnly(key)) {
setHeader(key, value);
}
});
}
/**
@@ -392,13 +393,14 @@ public class MessageHeaderAccessor {
* <p>This operation will <em>not</em> overwrite any existing values.
*/
public void copyHeadersIfAbsent(@Nullable Map<String, ?> headersToCopy) {
if (headersToCopy != null) {
headersToCopy.forEach((key, value) -> {
if (!isReadOnly(key)) {
setHeaderIfAbsent(key, value);
}
});
if (headersToCopy == null || this.headers == headersToCopy) {
return;
}
headersToCopy.forEach((key, value) -> {
if (!isReadOnly(key)) {
setHeaderIfAbsent(key, value);
}
});
}
protected boolean isReadOnly(String headerName) {
@@ -16,6 +16,7 @@
package org.springframework.messaging.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -75,6 +76,8 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor {
@SuppressWarnings("unchecked")
Map<String, List<String>> map = (Map<String, List<String>>) getHeader(NATIVE_HEADERS);
if (map != null) {
// setHeader checks for equality but we need copy of native headers
setHeader(NATIVE_HEADERS, null);
setHeader(NATIVE_HEADERS, new LinkedMultiValueMap<>(map));
}
}
@@ -103,6 +106,8 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor {
if (isMutable()) {
Map<String, List<String>> map = getNativeHeaders();
if (map != null) {
// setHeader checks for equality but we need immutable wrapper
setHeader(NATIVE_HEADERS, null);
setHeader(NATIVE_HEADERS, Collections.unmodifiableMap(map));
}
super.setImmutable();
@@ -110,31 +115,34 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor {
}
@Override
public void setHeader(String name, @Nullable Object value) {
if (name.equalsIgnoreCase(NATIVE_HEADERS)) {
// Force removal since setHeader checks for equality
super.setHeader(NATIVE_HEADERS, null);
public void copyHeaders(@Nullable Map<String, ?> headersToCopy) {
if (headersToCopy == null) {
return;
}
super.setHeader(name, value);
@SuppressWarnings("unchecked")
Map<String, List<String>> map = (Map<String, List<String>>) headersToCopy.get(NATIVE_HEADERS);
if (map != null && map != getNativeHeaders()) {
map.forEach(this::setNativeHeaderValues);
}
// setHeader checks for equality, native headers should be equal by now
super.copyHeaders(headersToCopy);
}
@Override
@SuppressWarnings("unchecked")
public void copyHeaders(@Nullable Map<String, ?> headersToCopy) {
if (headersToCopy != null) {
Map<String, List<String>> nativeHeaders = getNativeHeaders();
Map<String, List<String>> map = (Map<String, List<String>>) headersToCopy.get(NATIVE_HEADERS);
if (map != null) {
if (nativeHeaders != null) {
nativeHeaders.putAll(map);
}
else {
nativeHeaders = new LinkedMultiValueMap<>(map);
}
}
super.copyHeaders(headersToCopy);
setHeader(NATIVE_HEADERS, nativeHeaders);
public void copyHeadersIfAbsent(@Nullable Map<String, ?> headersToCopy) {
if (headersToCopy == null) {
return;
}
@SuppressWarnings("unchecked")
Map<String, List<String>> map = (Map<String, List<String>>) headersToCopy.get(NATIVE_HEADERS);
if (map != null && getNativeHeaders() == null) {
map.forEach(this::setNativeHeaderValues);
}
super.copyHeadersIfAbsent(headersToCopy);
}
/**
@@ -201,6 +209,30 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor {
}
}
/**
* Variant of {@link #addNativeHeader(String, String)} for all values.
* @since 5.2.12
*/
public void setNativeHeaderValues(String name, @Nullable List<String> values) {
Assert.state(isMutable(), "Already immutable");
Map<String, List<String>> map = getNativeHeaders();
if (values == null) {
if (map != null && map.get(name) != null) {
setModified(true);
map.remove(name);
}
return;
}
if (map == null) {
map = new LinkedMultiValueMap<>(3);
setHeader(NATIVE_HEADERS, map);
}
if (!ObjectUtils.nullSafeEquals(values, getHeader(name))) {
setModified(true);
map.put(name, new ArrayList<>(values));
}
}
/**
* Add the specified native header value to existing values.
* <p>In order for this to work, the accessor must be {@link #isMutable()
@@ -226,19 +226,46 @@ public class NativeMessageHeaderAccessorTests {
@Test // gh-25821
void copyImmutableToMutable() {
NativeMessageHeaderAccessor source = new NativeMessageHeaderAccessor();
source.addNativeHeader("foo", "bar");
Message<String> message = MessageBuilder.createMessage("payload", source.getMessageHeaders());
NativeMessageHeaderAccessor sourceAccessor = new NativeMessageHeaderAccessor();
sourceAccessor.addNativeHeader("foo", "bar");
Message<String> source = MessageBuilder.createMessage("payload", sourceAccessor.getMessageHeaders());
NativeMessageHeaderAccessor target = new NativeMessageHeaderAccessor();
target.copyHeaders(message.getHeaders());
target.setLeaveMutable(true);
message = MessageBuilder.createMessage(message.getPayload(), target.getMessageHeaders());
NativeMessageHeaderAccessor targetAccessor = new NativeMessageHeaderAccessor();
targetAccessor.copyHeaders(source.getHeaders());
targetAccessor.setLeaveMutable(true);
Message<?> target = MessageBuilder.createMessage(source.getPayload(), targetAccessor.getMessageHeaders());
MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(message);
MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(target);
assertThat(accessor.isMutable());
((NativeMessageHeaderAccessor) accessor).addNativeHeader("foo", "baz");
assertThat(((NativeMessageHeaderAccessor) accessor).getNativeHeader("foo")).containsExactly("bar", "baz");
}
@Test // gh-25821
void copyIfAbsentImmutableToMutable() {
NativeMessageHeaderAccessor sourceAccessor = new NativeMessageHeaderAccessor();
sourceAccessor.addNativeHeader("foo", "bar");
Message<String> source = MessageBuilder.createMessage("payload", sourceAccessor.getMessageHeaders());
MessageHeaderAccessor targetAccessor = new NativeMessageHeaderAccessor();
targetAccessor.copyHeadersIfAbsent(source.getHeaders());
targetAccessor.setLeaveMutable(true);
Message<?> target = MessageBuilder.createMessage(source.getPayload(), targetAccessor.getMessageHeaders());
MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(target);
assertThat(accessor.isMutable());
((NativeMessageHeaderAccessor) accessor).addNativeHeader("foo", "baz");
assertThat(((NativeMessageHeaderAccessor) accessor).getNativeHeader("foo")).containsExactly("bar", "baz");
}
@Test // gh-26155
void copySelf() {
NativeMessageHeaderAccessor accessor = new NativeMessageHeaderAccessor();
accessor.addNativeHeader("foo", "bar");
accessor.setHeader("otherHeader", "otherHeaderValue");
accessor.setLeaveMutable(true);
// Does not fail with ConcurrentModificationException
accessor.copyHeaders(accessor.getMessageHeaders());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -193,8 +193,8 @@ public abstract class AbstractEntityManagerFactoryBean implements
* {@code Persistence.createEntityManagerFactory} (if any).
* <p>Can be populated with a String "value" (parsed via PropertiesEditor) or a
* "props" element in XML bean definitions.
* @see javax.persistence.Persistence#createEntityManagerFactory(String, java.util.Map)
* @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(javax.persistence.spi.PersistenceUnitInfo, java.util.Map)
* @see javax.persistence.Persistence#createEntityManagerFactory(String, Map)
* @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, Map)
*/
public void setJpaProperties(Properties jpaProperties) {
CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.jpaPropertyMap);
@@ -204,8 +204,8 @@ public abstract class AbstractEntityManagerFactoryBean implements
* Specify JPA properties as a Map, to be passed into
* {@code Persistence.createEntityManagerFactory} (if any).
* <p>Can be populated with a "map" or "props" element in XML bean definitions.
* @see javax.persistence.Persistence#createEntityManagerFactory(String, java.util.Map)
* @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(javax.persistence.spi.PersistenceUnitInfo, java.util.Map)
* @see javax.persistence.Persistence#createEntityManagerFactory(String, Map)
* @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, Map)
*/
public void setJpaPropertyMap(@Nullable Map<String, ?> jpaProperties) {
if (jpaProperties != null) {
@@ -400,10 +400,13 @@ public abstract class AbstractEntityManagerFactoryBean implements
String message = ex.getMessage();
String causeString = cause.toString();
if (!message.endsWith(causeString)) {
throw new PersistenceException(message + "; nested exception is " + causeString, cause);
ex = new PersistenceException(message + "; nested exception is " + causeString, cause);
}
}
}
if (logger.isErrorEnabled()) {
logger.error("Failed to initialize JPA EntityManagerFactory: " + ex.getMessage());
}
throw ex;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -79,7 +79,7 @@ public @interface ActiveProfiles {
* <p>The default value is {@code true}, which means that a test
* class will <em>inherit</em> bean definition profiles defined by a
* test superclass. Specifically, the bean definition profiles for a test
* class will be added to the list of bean definition profiles
* class will be appended to the list of bean definition profiles
* defined by a test superclass. Thus, subclasses have the option of
* <em>extending</em> the list of bean definition profiles.
* <p>If {@code inheritProfiles} is set to {@code false}, the bean
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,8 +19,8 @@ package org.springframework.test.context;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
@@ -533,8 +533,8 @@ public class MergedContextConfiguration implements Serializable {
return EMPTY_STRING_ARRAY;
}
// Active profiles must be unique and sorted
Set<String> profilesSet = new TreeSet<>(Arrays.asList(activeProfiles));
// Active profiles must be unique
Set<String> profilesSet = new LinkedHashSet<>(Arrays.asList(activeProfiles));
return StringUtils.toStringArray(profilesSet);
}
@@ -16,8 +16,11 @@
package org.springframework.test.context.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -67,7 +70,7 @@ abstract class ActiveProfilesUtils {
static String[] resolveActiveProfiles(Class<?> testClass) {
Assert.notNull(testClass, "Class must not be null");
Set<String> activeProfiles = new TreeSet<>();
List<String[]> profileArrays = new ArrayList<>();
Class<ActiveProfiles> annotationType = ActiveProfiles.class;
AnnotationDescriptor<ActiveProfiles> descriptor =
@@ -106,17 +109,25 @@ abstract class ActiveProfilesUtils {
String[] profiles = resolver.resolve(rootDeclaringClass);
if (!ObjectUtils.isEmpty(profiles)) {
for (String profile : profiles) {
if (StringUtils.hasText(profile)) {
activeProfiles.add(profile.trim());
}
}
profileArrays.add(profiles);
}
descriptor = (annotation.inheritProfiles() ? MetaAnnotationUtils.findAnnotationDescriptor(
rootDeclaringClass.getSuperclass(), annotationType) : null);
}
// Reverse the list so that we can traverse "down" the hierarchy.
Collections.reverse(profileArrays);
Set<String> activeProfiles = new LinkedHashSet<>();
for (String[] profiles : profileArrays) {
for (String profile : profiles) {
if (StringUtils.hasText(profile)) {
activeProfiles.add(profile.trim());
}
}
}
return StringUtils.toStringArray(activeProfiles);
}
@@ -16,9 +16,6 @@
package org.springframework.test.context.support;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -26,7 +23,6 @@ import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptor;
@@ -43,6 +39,8 @@ import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDe
*/
public class DefaultActiveProfilesResolver implements ActiveProfilesResolver {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final Log logger = LogFactory.getLog(DefaultActiveProfilesResolver.class);
@@ -58,36 +56,24 @@ public class DefaultActiveProfilesResolver implements ActiveProfilesResolver {
@Override
public String[] resolve(Class<?> testClass) {
Assert.notNull(testClass, "Class must not be null");
Set<String> activeProfiles = new TreeSet<>();
Class<ActiveProfiles> annotationType = ActiveProfiles.class;
AnnotationDescriptor<ActiveProfiles> descriptor = findAnnotationDescriptor(testClass, annotationType);
AnnotationDescriptor<ActiveProfiles> descriptor = findAnnotationDescriptor(testClass, ActiveProfiles.class);
if (descriptor == null) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
annotationType.getName(), testClass.getName()));
ActiveProfiles.class.getName(), testClass.getName()));
}
return EMPTY_STRING_ARRAY;
}
else {
Class<?> declaringClass = descriptor.getDeclaringClass();
ActiveProfiles annotation = descriptor.synthesizeAnnotation();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
declaringClass.getName()));
}
for (String profile : annotation.profiles()) {
if (StringUtils.hasText(profile)) {
activeProfiles.add(profile.trim());
}
descriptor.getDeclaringClass().getName()));
}
return annotation.profiles();
}
return StringUtils.toStringArray(activeProfiles);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -143,7 +143,7 @@ class MergedContextConfigurationTests {
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles1, loader);
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(),
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles2, loader);
assertThat(mergedConfig2).hasSameHashCodeAs(mergedConfig1);
assertThat(mergedConfig2.hashCode()).isNotEqualTo(mergedConfig1.hashCode());
}
@Test
@@ -339,7 +339,7 @@ class MergedContextConfigurationTests {
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles1, loader);
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(),
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles2, loader);
assertThat(mergedConfig2).isEqualTo(mergedConfig1);
assertThat(mergedConfig2).isNotEqualTo(mergedConfig1);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -90,8 +90,8 @@ class ContextCacheTests {
int size = 0, hit = 0, miss = 0;
loadCtxAndAssertStats(FooBarProfilesTestCase.class, ++size, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
// Profiles {foo, bar} MUST hash to the same as {bar, foo}
loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, ++hit, miss);
// Profiles {foo, bar} should not hash to the same as {bar,foo}
loadCtxAndAssertStats(BarFooProfilesTestCase.class, ++size, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, ++hit, miss);
@@ -67,12 +67,12 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
@Test
void resolveActiveProfilesWithDuplicatedProfiles() {
assertResolvedProfiles(DuplicatedProfiles.class, "bar", "baz", "foo");
assertResolvedProfiles(DuplicatedProfiles.class, "foo", "bar", "baz");
}
@Test
void resolveActiveProfilesWithLocalAndInheritedDuplicatedProfiles() {
assertResolvedProfiles(ExtendedDuplicatedProfiles.class, "bar", "baz", "cat", "dog", "foo");
assertResolvedProfiles(ExtendedDuplicatedProfiles.class, "foo", "bar", "baz", "cat", "dog");
}
@Test
@@ -92,12 +92,12 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
@Test
void resolveActiveProfilesWithLocalAndInheritedAnnotations() {
assertResolvedProfiles(LocationsBar.class, "bar", "foo");
assertResolvedProfiles(LocationsBar.class, "foo", "bar");
}
@Test
void resolveActiveProfilesWithOverriddenAnnotation() {
assertResolvedProfiles(Animals.class, "cat", "dog");
assertResolvedProfiles(Animals.class, "dog", "cat");
}
/**
@@ -129,7 +129,7 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
*/
@Test
void resolveActiveProfilesWithLocalAndInheritedMetaAnnotations() {
assertResolvedProfiles(MetaLocationsBar.class, "bar", "foo");
assertResolvedProfiles(MetaLocationsBar.class, "foo", "bar");
}
/**
@@ -137,7 +137,7 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
*/
@Test
void resolveActiveProfilesWithOverriddenMetaAnnotation() {
assertResolvedProfiles(MetaAnimals.class, "cat", "dog");
assertResolvedProfiles(MetaAnimals.class, "dog", "cat");
}
/**
@@ -161,7 +161,7 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
*/
@Test
void resolveActiveProfilesWithMergedInheritedResolver() {
assertResolvedProfiles(MergedInheritedFooActiveProfilesResolverTestCase.class, "bar", "foo");
assertResolvedProfiles(MergedInheritedFooActiveProfilesResolverTestCase.class, "foo", "bar");
}
/**
@@ -38,16 +38,25 @@ import org.springframework.transaction.TransactionDefinition;
* {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute}
* class, and in fact {@link AnnotationTransactionAttributeSource} will directly
* convert the data to the latter class, so that Spring's transaction support code
* does not have to know about annotations. If no rules are relevant to the exception,
* it will be treated like
* {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute}
* (rolling back on {@link RuntimeException} and {@link Error} but not on checked
* exceptions).
* does not have to know about annotations. If no custom rollback rules apply,
* the transaction will roll back on {@link RuntimeException} and {@link Error}
* but not on checked exceptions.
*
* <p>For specific information about the semantics of this annotation's attributes,
* consult the {@link org.springframework.transaction.TransactionDefinition} and
* {@link org.springframework.transaction.interceptor.TransactionAttribute} javadocs.
*
* <p>This annotation commonly works with thread-bound transactions managed by
* {@link org.springframework.transaction.PlatformTransactionManager}, exposing a
* transaction to all data access operations within the current execution thread.
* <b>Note: This does NOT propagate to newly started threads within the method.</b>
*
* <p>Alternatively, this annotation may demarcate a reactive transaction managed
* by {@link org.springframework.transaction.ReactiveTransactionManager} which
* uses the Reactor context instead of thread-local attributes. As a consequence,
* all participating data access operations need to execute within the same
* Reactor context in the same reactive pipeline.
*
* @author Colin Sampaleanu
* @author Juergen Hoeller
* @author Sam Brannen
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import org.springframework.lang.Nullable;
/**
* TransactionAttribute implementation that works out whether a given exception
* should cause transaction rollback by applying a number of rollback rules,
* both positive and negative. If no rules are relevant to the exception, it
* both positive and negative. If no custom rollback rules apply, this attribute
* behaves like DefaultTransactionAttribute (rolling back on runtime exceptions).
*
* <p>{@link TransactionAttributeEditor} creates objects of this class.
@@ -480,9 +480,9 @@ public class MediaType extends MimeType implements Serializable {
@Override
protected void checkParameters(String attribute, String value) {
super.checkParameters(attribute, value);
if (PARAM_QUALITY_FACTOR.equals(attribute)) {
protected void checkParameters(String parameter, String value) {
super.checkParameters(parameter, value);
if (PARAM_QUALITY_FACTOR.equals(parameter)) {
value = unquote(value);
double d = Double.parseDouble(value);
Assert.isTrue(d >= 0D && d <= 1D,
@@ -128,7 +128,8 @@ public class EncoderHttpMessageWriter<T> implements HttpMessageWriter<T> {
message.getHeaders().setContentLength(buffer.readableByteCount());
return message.writeWith(Mono.just(buffer)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release));
});
})
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
}
if (isStreamingMediaType(contentType)) {
@@ -18,6 +18,7 @@ package org.springframework.http.converter.json;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
@@ -55,6 +56,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.TypeUtils;
/**
@@ -308,7 +310,9 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding)) {
OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding);
try {
writePrefix(generator, object);
Object value = object;
@@ -343,6 +347,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
writeSuffix(generator, object);
generator.flush();
generator.close();
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
@@ -203,10 +203,28 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
// We must resolve value first however, for a chance to handle potential error.
if (body instanceof Mono) {
return ((Mono<? extends DataBuffer>) body)
.flatMap(buffer -> doCommit(() ->
writeWithInternal(Mono.fromCallable(() -> buffer)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release))))
.doOnError(t -> getHeaders().clearContentHeaders());
.flatMap(buffer -> {
AtomicReference<Boolean> subscribed = new AtomicReference<>(false);
return doCommit(
() -> {
try {
return writeWithInternal(Mono.fromCallable(() -> buffer)
.doOnSubscribe(s -> subscribed.set(true))
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release));
}
catch (Throwable ex) {
return Mono.error(ex);
}
})
.doOnError(ex -> DataBufferUtils.release(buffer))
.doOnCancel(() -> {
if (!subscribed.get()) {
DataBufferUtils.release(buffer);
}
});
})
.doOnError(t -> getHeaders().clearContentHeaders())
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
}
else {
return new ChannelSendOperator<>(body, inner -> doCommit(() -> writeWithInternal(inner)))
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -112,6 +112,8 @@ public @interface CrossOrigin {
* {@code Expires}, {@code Last-Modified}, or {@code Pragma},
* <p>Exposed headers are listed in the {@code Access-Control-Expose-Headers}
* response header of actual CORS requests.
* <p>The special value {@code "*"} allows all headers to be exposed for
* non-credentialed requests.
* <p>By default no headers are listed as exposed.
*/
String[] exposedHeaders() default {};
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -254,13 +254,11 @@ public class CorsConfiguration {
* {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
* {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an
* actual response might have and can be exposed.
* <p>Note that {@code "*"} is not a valid exposed header value.
* <p>The special value {@code "*"} allows all headers to be exposed for
* non-credentialed requests.
* <p>By default this is not set.
*/
public void setExposedHeaders(@Nullable List<String> exposedHeaders) {
if (exposedHeaders != null && exposedHeaders.contains(ALL)) {
throw new IllegalArgumentException("'*' is not a valid exposed header value");
}
this.exposedHeaders = (exposedHeaders != null ? new ArrayList<>(exposedHeaders) : null);
}
@@ -276,12 +274,10 @@ public class CorsConfiguration {
/**
* Add a response header to expose.
* <p>Note that {@code "*"} is not a valid exposed header value.
* <p>The special value {@code "*"} allows all headers to be exposed for
* non-credentialed requests.
*/
public void addExposedHeader(String exposedHeader) {
if (ALL.equals(exposedHeader)) {
throw new IllegalArgumentException("'*' is not a valid exposed header value");
}
if (this.exposedHeaders == null) {
this.exposedHeaders = new ArrayList<>(4);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,8 +42,8 @@ public class MapMethodProcessor implements HandlerMethodArgumentResolver, Handle
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Map.class.isAssignableFrom(parameter.getParameterType()) &&
parameter.getParameterAnnotations().length == 0;
return (Map.class.isAssignableFrom(parameter.getParameterType()) &&
parameter.getParameterAnnotations().length == 0);
}
@Override
@@ -70,8 +70,8 @@ public class MapMethodProcessor implements HandlerMethodArgumentResolver, Handle
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
throw new UnsupportedOperationException("Unexpected return type [" +
returnType.getParameterType().getName() + "] in method: " + returnType.getMethod());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 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.
@@ -70,8 +70,8 @@ public class ModelMethodProcessor implements HandlerMethodArgumentResolver, Hand
}
else {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
throw new UnsupportedOperationException("Unexpected return type [" +
returnType.getParameterType().getName() + "] in method: " + returnType.getMethod());
}
}
@@ -27,6 +27,7 @@ import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.util.FastByteArrayOutputStream;
@@ -209,7 +210,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
if (this.content.size() > 0) {
HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {
rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) {
rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
}
this.contentLength = null;
}
this.content.writeTo(rawResponse.getOutputStream());
@@ -578,7 +578,7 @@ public class UrlPathHelper {
return requestUri;
}
String start = requestUri.substring(0, index);
for (int i = key.length(); i < requestUri.length(); i++) {
for (int i = index + key.length(); i < requestUri.length(); i++) {
char c = requestUri.charAt(i);
if (c == ';' || c == '/') {
return start + requestUri.substring(i);
@@ -1,64 +0,0 @@
/*
* Copyright 2002-2017 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.util.pattern;
/**
* Used to represent a subsection of an array, useful when wanting to pass that subset of data
* to another method (e.g. a java regex matcher) but not wanting to create a new string object
* to hold all that data.
*
* @author Andy Clement
* @since 5.0
*/
class SubSequence implements CharSequence {
private final char[] chars;
private final int start;
private final int end;
SubSequence(char[] chars, int start, int end) {
this.chars = chars;
this.start = start;
this.end = end;
}
@Override
public int length() {
return (this.end - this.start);
}
@Override
public char charAt(int index) {
return this.chars[this.start + index];
}
@Override
public CharSequence subSequence(int start, int end) {
return new SubSequence(this.chars, this.start + start, this.start + end);
}
@Override
public String toString() {
return new String(this.chars, this.start, this.end - this.start);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.http;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -26,6 +27,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.testfixture.io.SerializationTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -160,7 +162,7 @@ public class MediaTypeTests {
assertThat(mediaTypes.size()).as("Invalid amount of media types").isEqualTo(0);
}
@Test // gh-23241
@Test // gh-23241
public void parseMediaTypesWithTrailingComma() {
List<MediaType> mediaTypes = MediaType.parseMediaTypes("text/plain, text/html, ");
assertThat(mediaTypes).as("No media types returned").isNotNull();
@@ -460,4 +462,12 @@ public class MediaTypeTests {
assertThat(new MediaType("text", "*").isConcrete()).as("text/* concrete").isFalse();
}
@Test // gh-26127
void serialize() throws Exception {
MediaType original = new MediaType("text", "plain", StandardCharsets.UTF_8);
MediaType deserialized = (MediaType) SerializationTestUtils.serializeAndDeserialize(original);
assertThat(deserialized).isEqualTo(original);
assertThat(original).isEqualTo(deserialized);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,13 @@
package org.springframework.http.converter.json;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -47,6 +49,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.api.Assertions.within;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Jackson 2.x converter tests.
@@ -133,13 +137,7 @@ public class MappingJackson2HttpMessageConverterTests {
@Test
public void write() throws IOException {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
MyBean body = new MyBean();
body.setString("Foo");
body.setNumber(42);
body.setFraction(42F);
body.setArray(new String[] {"Foo", "Bar"});
body.setBool(true);
body.setBytes(new byte[] {0x1, 0x2});
MyBean body = createSampleBean();
converter.write(body, null, outputMessage);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result.contains("\"string\":\"Foo\"")).isTrue();
@@ -149,18 +147,13 @@ public class MappingJackson2HttpMessageConverterTests {
assertThat(result.contains("\"bool\":true")).isTrue();
assertThat(result.contains("\"bytes\":\"AQI=\"")).isTrue();
assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(MediaType.APPLICATION_JSON);
verify(outputMessage.getBody(), never()).close();
}
@Test
public void writeWithBaseType() throws IOException {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
MyBean body = new MyBean();
body.setString("Foo");
body.setNumber(42);
body.setFraction(42F);
body.setArray(new String[] {"Foo", "Bar"});
body.setBool(true);
body.setBytes(new byte[] {0x1, 0x2});
MyBean body = createSampleBean();
converter.write(body, MyBase.class, null, outputMessage);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result.contains("\"string\":\"Foo\"")).isTrue();
@@ -191,6 +184,16 @@ public class MappingJackson2HttpMessageConverterTests {
converter.read(MyBean.class, inputMessage));
}
@Test // See gh-26246
public void writeInvalidJson() throws IOException {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
MyBean bean = createSampleBean();
List<Object> body = Arrays.asList(bean, new ByteArrayOutputStream());
assertThatExceptionOfType(HttpMessageConversionException.class)
.isThrownBy(() -> converter.write(body, null, outputMessage));
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8)).isEmpty();
}
@Test
public void readValidJsonWithUnknownProperty() throws IOException {
String body = "{\"string\":\"string\",\"unknownProperty\":\"value\"}";
@@ -489,6 +492,17 @@ public class MappingJackson2HttpMessageConverterTests {
assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(contentType);
}
private MyBean createSampleBean() {
MyBean body = new MyBean();
body.setString("Foo");
body.setNumber(42);
body.setFraction(42F);
body.setArray(new String[] {"Foo", "Bar"});
body.setBool(true);
body.setBytes(new byte[] {0x1, 0x2});
return body;
}
interface MyInterface {
@@ -19,7 +19,9 @@ package org.springframework.http.server.reactive;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -27,14 +29,22 @@ import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.channel.AbortedException;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
import static org.assertj.core.api.Assertions.assertThat;
@@ -176,6 +186,24 @@ public class ServerHttpResponseTests {
});
}
@Test // gh-26232
void monoResponseShouldNotLeakIfCancelled() {
LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory();
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
MockServerHttpResponse response = new MockServerHttpResponse(bufferFactory);
response.setWriteHandler(flux -> {
throw AbortedException.beforeSend();
});
HttpMessageWriter<Object> messageWriter = new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder());
Mono<Void> result = messageWriter.write(Mono.just(Collections.singletonMap("foo", "bar")),
ResolvableType.forClass(Mono.class), ResolvableType.forClass(Map.class), null,
request, response, Collections.emptyMap());
StepVerifier.create(result).expectError(AbortedException.class).verify();
bufferFactory.checkForLeaks();
}
private DefaultDataBuffer wrap(String a) {
return new DefaultDataBufferFactory().wrap(ByteBuffer.wrap(a.getBytes(StandardCharsets.UTF_8)));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,7 +25,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Unit tests for {@link CorsConfiguration}.
@@ -61,29 +60,13 @@ public class CorsConfigurationTests {
assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("*"));
config.addAllowedMethod("*");
assertThat(config.getAllowedMethods()).isEqualTo(Arrays.asList("*"));
config.addExposedHeader("header1");
config.addExposedHeader("header2");
assertThat(config.getExposedHeaders()).isEqualTo(Arrays.asList("header1", "header2"));
config.addExposedHeader("*");
config.setAllowCredentials(true);
assertThat((boolean) config.getAllowCredentials()).isTrue();
config.setMaxAge(123L);
assertThat(config.getMaxAge()).isEqualTo(new Long(123));
}
@Test
public void asteriskWildCardOnAddExposedHeader() {
CorsConfiguration config = new CorsConfiguration();
assertThatIllegalArgumentException().isThrownBy(() ->
config.addExposedHeader("*"));
}
@Test
public void asteriskWildCardOnSetExposedHeaders() {
CorsConfiguration config = new CorsConfiguration();
assertThatIllegalArgumentException().isThrownBy(() ->
config.setExposedHeaders(Arrays.asList("*")));
}
@Test
public void combineWithNull() {
CorsConfiguration config = new CorsConfiguration();
@@ -120,26 +103,34 @@ public class CorsConfigurationTests {
other.addAllowedMethod(HttpMethod.PUT.name());
CorsConfiguration combinedConfig = config.combine(other);
assertThat(combinedConfig.getAllowedOrigins()).isEqualTo(Arrays.asList("https://domain.com"));
assertThat(combinedConfig.getAllowedHeaders()).isEqualTo(Arrays.asList("header1"));
assertThat(combinedConfig.getAllowedMethods()).isEqualTo(Arrays.asList(HttpMethod.PUT.name()));
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1");
assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name());
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
combinedConfig = other.combine(config);
assertThat(combinedConfig.getAllowedOrigins()).isEqualTo(Arrays.asList("https://domain.com"));
assertThat(combinedConfig.getAllowedHeaders()).isEqualTo(Arrays.asList("header1"));
assertThat(combinedConfig.getAllowedMethods()).isEqualTo(Arrays.asList(HttpMethod.PUT.name()));
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1");
assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name());
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
combinedConfig = config.combine(new CorsConfiguration());
assertThat(config.getAllowedOrigins()).isEqualTo(Arrays.asList("*"));
assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig.getAllowedMethods()).isEqualTo(Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(),
HttpMethod.POST.name()));
assertThat(config.getAllowedOrigins()).containsExactly("*");
assertThat(config.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedMethods())
.containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
combinedConfig = new CorsConfiguration().combine(config);
assertThat(config.getAllowedOrigins()).isEqualTo(Arrays.asList("*"));
assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig.getAllowedMethods()).isEqualTo(Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(),
HttpMethod.POST.name()));
assertThat(config.getAllowedOrigins()).containsExactly("*");
assertThat(config.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedMethods())
.containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
}
@Test
@@ -147,20 +138,29 @@ public class CorsConfigurationTests {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addExposedHeader("*");
config.addAllowedMethod("*");
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOrigin("https://domain.com");
other.addAllowedHeader("header1");
other.addExposedHeader("header2");
other.addAllowedHeader("anotherHeader1");
other.addExposedHeader("anotherHeader2");
other.addAllowedMethod(HttpMethod.PUT.name());
CorsConfiguration combinedConfig = config.combine(other);
assertThat(combinedConfig.getAllowedOrigins()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig.getAllowedHeaders()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig.getAllowedMethods()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig.getExposedHeaders()).containsExactly("*");
assertThat(combinedConfig.getAllowedMethods()).containsExactly("*");
combinedConfig = other.combine(config);
assertThat(combinedConfig.getAllowedOrigins()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig.getAllowedHeaders()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig.getAllowedMethods()).isEqualTo(Arrays.asList("*"));
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig.getExposedHeaders()).containsExactly("*");
assertThat(combinedConfig.getAllowedMethods()).containsExactly("*");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*");
}
@Test // SPR-14792
@@ -0,0 +1,69 @@
/*
* Copyright 2002-2020 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.filter;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.util.ContentCachingResponseWrapper;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ContentCachingResponseWrapper}.
* @author Rossen Stoyanchev
*/
public class ContentCachingResponseWrapperTests {
@Test
void copyBodyToResponse() throws Exception {
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
MockHttpServletResponse response = new MockHttpServletResponse();
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setStatus(HttpServletResponse.SC_OK);
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
responseWrapper.copyBodyToResponse();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentLength() > 0).isTrue();
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
}
@Test
void copyBodyToResponseWithTransferEncoding() throws Exception {
byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(StandardCharsets.UTF_8);
MockHttpServletResponse response = new MockHttpServletResponse();
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setStatus(HttpServletResponse.SC_OK);
responseWrapper.setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked");
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
responseWrapper.copyBodyToResponse();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getHeader(HttpHeaders.TRANSFER_ENCODING)).isEqualTo("chunked");
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isNull();
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
}
}
@@ -53,8 +53,8 @@ class UriComponentsBuilderTests {
void plain() throws URISyntaxException {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
UriComponents result = builder.scheme("https").host("example.com")
.path("foo").queryParam("bar").fragment("baz")
.build();
.path("foo").queryParam("bar").fragment("baz").build();
assertThat(result.getScheme()).isEqualTo("https");
assertThat(result.getHost()).isEqualTo("example.com");
assertThat(result.getPath()).isEqualTo("foo");
@@ -91,10 +91,10 @@ class UriComponentsBuilderTests {
@Test
void fromPath() throws URISyntaxException {
UriComponents result = UriComponentsBuilder.fromPath("foo").queryParam("bar").fragment("baz").build();
assertThat(result.getPath()).isEqualTo("foo");
assertThat(result.getQuery()).isEqualTo("bar");
assertThat(result.getFragment()).isEqualTo("baz");
assertThat(result.toUriString()).as("Invalid result URI String").isEqualTo("foo?bar#baz");
URI expected = new URI("foo?bar#baz");
@@ -111,12 +111,12 @@ class UriComponentsBuilderTests {
void fromHierarchicalUri() throws URISyntaxException {
URI uri = new URI("https://example.com/foo?bar#baz");
UriComponents result = UriComponentsBuilder.fromUri(uri).build();
assertThat(result.getScheme()).isEqualTo("https");
assertThat(result.getHost()).isEqualTo("example.com");
assertThat(result.getPath()).isEqualTo("/foo");
assertThat(result.getQuery()).isEqualTo("bar");
assertThat(result.getFragment()).isEqualTo("baz");
assertThat(result.toUri()).as("Invalid result URI").isEqualTo(uri);
}
@@ -124,10 +124,10 @@ class UriComponentsBuilderTests {
void fromOpaqueUri() throws URISyntaxException {
URI uri = new URI("mailto:foo@bar.com#baz");
UriComponents result = UriComponentsBuilder.fromUri(uri).build();
assertThat(result.getScheme()).isEqualTo("mailto");
assertThat(result.getSchemeSpecificPart()).isEqualTo("foo@bar.com");
assertThat(result.getFragment()).isEqualTo("baz");
assertThat(result.toUri()).as("Invalid result URI").isEqualTo(uri);
}
@@ -606,7 +606,7 @@ class UriComponentsBuilderTests {
assertThat(after.getPath()).isEqualTo("/foo/");
}
@Test // gh-19890
@Test // gh-19890
void fromHttpRequestWithEmptyScheme() {
HttpRequest request = new HttpRequest() {
@Override
@@ -854,7 +854,7 @@ class UriComponentsBuilderTests {
assertThat(uriComponents.getQueryParams().get("bar").get(0)).isNull();
}
@Test // gh-24444
@Test // gh-24444
void opaqueUriDoesNotResetOnNullInput() throws URISyntaxException {
URI uri = new URI("urn:ietf:wg:oauth:2.0:oob");
UriComponents result = UriComponentsBuilder.fromUri(uri)
@@ -1114,7 +1114,7 @@ class UriComponentsBuilderTests {
assertThat(result.toUriString()).isEqualTo("https://example.com/rest/mobile/users/1");
}
@Test // gh-25737
@Test // gh-25737
void fromHttpRequestForwardedHeaderComma() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Forwarded", "for=192.0.2.0,for=192.0.2.1;proto=https;host=192.0.2.3:9090");
@@ -1153,7 +1153,7 @@ class UriComponentsBuilderTests {
assertThat(uri).isEqualTo("http://localhost:8081/{path}?sort={sort}&sort=another_value");
}
@Test // SPR-17630
@Test // SPR-17630
void toUriStringWithCurlyBraces() {
assertThat(UriComponentsBuilder.fromUriString("/path?q={asa}asa").toUriString()).isEqualTo("/path?q=%7Basa%7Dasa");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,7 +43,6 @@ public class UriComponentsTests {
@Test
public void expandAndEncode() {
UriComponents uri = UriComponentsBuilder
.fromPath("/hotel list/{city} specials").queryParam("q", "{value}").build()
.expand("Z\u00fcrich", "a+b").encode();
@@ -53,7 +52,6 @@ public class UriComponentsTests {
@Test
public void encodeAndExpand() {
UriComponents uri = UriComponentsBuilder
.fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode().build()
.expand("Z\u00fcrich", "a+b");
@@ -63,16 +61,14 @@ public class UriComponentsTests {
@Test
public void encodeAndExpandPartially() {
UriComponents uri = UriComponentsBuilder
.fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode()
.uriVariables(Collections.singletonMap("city", "Z\u00fcrich"))
.build();
.uriVariables(Collections.singletonMap("city", "Z\u00fcrich")).build();
assertThat(uri.expand("a+b").toString()).isEqualTo("/hotel%20list/Z%C3%BCrich%20specials?q=a%2Bb");
}
@Test // SPR-17168
@Test // SPR-17168
public void encodeAndExpandWithDollarSign() {
UriComponents uri = UriComponentsBuilder.fromPath("/path").queryParam("q", "{value}").encode().build();
assertThat(uri.expand("JavaClass$1.class").toString()).isEqualTo("/path?q=JavaClass%241.class");
@@ -80,71 +76,71 @@ public class UriComponentsTests {
@Test
public void toUriEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uriComponents.encode().toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uri.encode().toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
}
@Test
public void toUriNotEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uriComponents.toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z\u00fcrich"));
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uri.toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z\u00fcrich"));
}
@Test
public void toUriAlreadyEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com/hotel%20list/Z%C3%BCrich").build(true);
UriComponents encoded = uriComponents.encode();
assertThat(encoded.toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/hotel%20list/Z%C3%BCrich").build(true);
assertThat(uri.encode().toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
}
@Test
public void toUriWithIpv6HostAlreadyEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
UriComponents uri = UriComponentsBuilder.fromUriString(
"http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich").build(true);
UriComponents encoded = uriComponents.encode();
assertThat(encoded.toUri()).isEqualTo(new URI("http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich"));
assertThat(uri.encode().toUri()).isEqualTo(
new URI("http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich"));
}
@Test
public void expand() {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com").path("/{foo} {bar}").build();
uriComponents = uriComponents.expand("1 2", "3 4");
assertThat(uriComponents.getPath()).isEqualTo("/1 2 3 4");
assertThat(uriComponents.toUriString()).isEqualTo("https://example.com/1 2 3 4");
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com").path("/{foo} {bar}").build();
uri = uri.expand("1 2", "3 4");
assertThat(uri.getPath()).isEqualTo("/1 2 3 4");
assertThat(uri.toUriString()).isEqualTo("https://example.com/1 2 3 4");
}
@Test // SPR-13311
@Test // SPR-13311
public void expandWithRegexVar() {
String template = "/myurl/{name:[a-z]{1,5}}/show";
UriComponents uriComponents = UriComponentsBuilder.fromUriString(template).build();
uriComponents = uriComponents.expand(Collections.singletonMap("name", "test"));
assertThat(uriComponents.getPath()).isEqualTo("/myurl/test/show");
UriComponents uri = UriComponentsBuilder.fromUriString(template).build();
uri = uri.expand(Collections.singletonMap("name", "test"));
assertThat(uri.getPath()).isEqualTo("/myurl/test/show");
}
@Test // SPR-17630
@Test // SPR-17630
public void uirTemplateExpandWithMismatchedCurlyBraces() {
assertThat(UriComponentsBuilder.fromUriString("/myurl/?q={{{{").encode().build().toUriString()).isEqualTo("/myurl/?q=%7B%7B%7B%7B");
UriComponents uri = UriComponentsBuilder.fromUriString("/myurl/?q={{{{").encode().build();
assertThat(uri.toUriString()).isEqualTo("/myurl/?q=%7B%7B%7B%7B");
}
@Test // gh-22447
@Test // gh-22447
public void expandWithFragmentOrder() {
UriComponents uriComponents = UriComponentsBuilder
UriComponents uri = UriComponentsBuilder
.fromUriString("https://{host}/{path}#{fragment}").build()
.expand("example.com", "foo", "bar");
assertThat(uriComponents.toUriString()).isEqualTo("https://example.com/foo#bar");
assertThat(uri.toUriString()).isEqualTo("https://example.com/foo#bar");
}
@Test // SPR-12123
@Test // SPR-12123
public void port() {
UriComponents uri1 = fromUriString("https://example.com:8080/bar").build();
UriComponents uri2 = fromUriString("https://example.com/bar").port(8080).build();
UriComponents uri3 = fromUriString("https://example.com/bar").port("{port}").build().expand(8080);
UriComponents uri4 = fromUriString("https://example.com/bar").port("808{digit}").build().expand(0);
assertThat(uri1.getPort()).isEqualTo(8080);
assertThat(uri1.toUriString()).isEqualTo("https://example.com:8080/bar");
assertThat(uri2.getPort()).isEqualTo(8080);
@@ -175,20 +171,22 @@ public class UriComponentsTests {
@Test
public void normalize() {
UriComponents uriComponents = UriComponentsBuilder.fromUriString("https://example.com/foo/../bar").build();
assertThat(uriComponents.normalize().toString()).isEqualTo("https://example.com/bar");
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/foo/../bar").build();
assertThat(uri.normalize().toString()).isEqualTo("https://example.com/bar");
}
@Test
public void serializable() throws Exception {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
UriComponents uri = UriComponentsBuilder.fromUriString(
"https://example.com").path("/{foo}").query("bar={baz}").build();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(uriComponents);
oos.writeObject(uri);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
UriComponents readObject = (UriComponents) ois.readObject();
assertThat(uriComponents.toString()).isEqualTo(readObject.toString());
assertThat(uri.toString()).isEqualTo(readObject.toString());
}
@Test
@@ -197,6 +195,7 @@ public class UriComponentsTests {
UriComponentsBuilder targetBuilder = UriComponentsBuilder.newInstance();
source.copyToUriComponentsBuilder(targetBuilder);
UriComponents result = targetBuilder.build().encode();
assertThat(result.getPath()).isEqualTo("/foo/bar/ba%2Fz");
assertThat(result.getPathSegments()).isEqualTo(Arrays.asList("foo", "bar", "ba%2Fz"));
}
@@ -207,6 +206,7 @@ public class UriComponentsTests {
UriComponents uric1 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bar={baz}").build();
UriComponents uric2 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bar={baz}").build();
UriComponents uric3 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bin={baz}").build();
assertThat(uric1).isInstanceOf(HierarchicalUriComponents.class);
assertThat(uric1).isEqualTo(uric1);
assertThat(uric1).isEqualTo(uric2);
@@ -219,6 +219,7 @@ public class UriComponentsTests {
UriComponents uric1 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bar").build();
UriComponents uric2 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bar").build();
UriComponents uric3 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bin").build();
assertThat(uric1).isInstanceOf(OpaqueUriComponents.class);
assertThat(uric1).isEqualTo(uric1);
assertThat(uric1).isEqualTo(uric2);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,9 +49,7 @@ public class UriTemplateTests {
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42"));
}
// SPR-9712
@Test
@Test // SPR-9712
public void expandVarArgsWithArrayValue() throws Exception {
UriTemplate template = new UriTemplate("/sum?numbers={numbers}");
URI result = template.expand(new int[] {1, 2, 3});
@@ -61,8 +59,7 @@ public class UriTemplateTests {
@Test
public void expandVarArgsNotEnoughVariables() throws Exception {
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
assertThatIllegalArgumentException().isThrownBy(() ->
template.expand("1"));
assertThatIllegalArgumentException().isThrownBy(() -> template.expand("1"));
}
@Test
@@ -156,7 +153,7 @@ public class UriTemplateTests {
assertThat(result).as("Invalid match").isEqualTo(expected);
}
@Test // SPR-13627
@Test // SPR-13627
public void matchCustomRegexWithNestedCurlyBraces() throws Exception {
UriTemplate template = new UriTemplate("/site.{domain:co.[a-z]{2}}");
Map<String, String> result = template.match("/site.co.eu");
@@ -181,8 +178,8 @@ public class UriTemplateTests {
assertThat(result).as("Invalid match").isEqualTo(expected);
}
@Test // SPR-16169
public void matchWithMultipleSegmentsAtTheEnd() {
@Test // SPR-16169
public void matchWithMultipleSegmentsAtTheEnd() throws Exception {
UriTemplate template = new UriTemplate("/account/{accountId}");
assertThat(template.matches("/account/15/alias/5")).isFalse();
}
@@ -202,21 +199,20 @@ public class UriTemplateTests {
assertThat(template.matches("/search?query=foo#bar")).isTrue();
}
@Test // SPR-13705
public void matchesWithSlashAtTheEnd() {
UriTemplate uriTemplate = new UriTemplate("/test/");
assertThat(uriTemplate.matches("/test/")).isTrue();
@Test // SPR-13705
public void matchesWithSlashAtTheEnd() throws Exception {
assertThat(new UriTemplate("/test/").matches("/test/")).isTrue();
}
@Test
public void expandWithDollar() {
public void expandWithDollar() throws Exception {
UriTemplate template = new UriTemplate("/{a}");
URI uri = template.expand("$replacement");
assertThat(uri.toString()).isEqualTo("/$replacement");
}
@Test
public void expandWithAtSign() {
public void expandWithAtSign() throws Exception {
UriTemplate template = new UriTemplate("http://localhost/query={query}");
URI uri = template.expand("foo@bar");
assertThat(uri.toString()).isEqualTo("http://localhost/query=foo@bar");
@@ -129,11 +129,19 @@ public class UrlPathHelperTests {
public void getRequestKeepSemicolonContent() {
helper.setRemoveSemicolonContent(false);
request.setRequestURI("/foo;a=b;c=d");
assertThat(helper.getRequestUri(request)).isEqualTo("/foo;a=b;c=d");
testKeepSemicolonContent("/foo;a=b;c=d", "/foo;a=b;c=d");
testKeepSemicolonContent("/test;jsessionid=1234", "/test");
testKeepSemicolonContent("/test;JSESSIONID=1234", "/test");
testKeepSemicolonContent("/test;jsessionid=1234;a=b", "/test;a=b");
testKeepSemicolonContent("/test;a=b;jsessionid=1234;c=d", "/test;a=b;c=d");
testKeepSemicolonContent("/test;jsessionid=1234/anotherTest", "/test/anotherTest");
testKeepSemicolonContent("/test;jsessionid=;a=b", "/test;a=b");
testKeepSemicolonContent("/somethingLongerThan12;jsessionid=1234", "/somethingLongerThan12");
}
request.setRequestURI("/foo;jsessionid=c0o7fszeb1");
assertThat(helper.getRequestUri(request)).isEqualTo("/foo");
private void testKeepSemicolonContent(String requestUri, String expectedPath) {
request.setRequestURI(requestUri);
assertThat(helper.getRequestUri(request)).isEqualTo(expectedPath);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -93,7 +93,8 @@ public class CorsRegistration {
* {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
* {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an
* actual response might have and can be exposed.
* <p>Note that {@code "*"} is not supported on this property.
* <p>The special value {@code "*"} allows all headers to be exposed for
* non-credentialed requests.
* <p>By default this is not set.
*/
public CorsRegistration exposedHeaders(String... headers) {
@@ -263,9 +263,14 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
.reduce(ExchangeFilterFunction::andThen)
.map(filter -> filter.apply(exchange))
.orElse(exchange) : exchange);
HttpHeaders defaultHeaders = copyDefaultHeaders();
MultiValueMap<String, String> defaultCookies = copyDefaultCookies();
return new DefaultWebClient(filteredExchange, initUriBuilderFactory(),
this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null,
this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null,
defaultHeaders,
defaultCookies,
this.defaultRequest, new DefaultWebClientBuilder(this));
}
@@ -302,4 +307,28 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return factory;
}
@Nullable
private HttpHeaders copyDefaultHeaders() {
if (this.defaultHeaders != null) {
HttpHeaders copy = new HttpHeaders();
this.defaultHeaders.forEach((key, values) -> copy.put(key, new ArrayList<>(values)));
return HttpHeaders.readOnlyHttpHeaders(copy);
}
else {
return null;
}
}
@Nullable
private MultiValueMap<String, String> copyDefaultCookies() {
if (this.defaultCookies != null) {
MultiValueMap<String, String> copy = new LinkedMultiValueMap<>(this.defaultCookies.size());
this.defaultCookies.forEach((key, values) -> copy.put(key, new ArrayList<>(values)));
return CollectionUtils.unmodifiableMultiValueMap(copy);
}
else {
return null;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -66,7 +66,6 @@ public interface WebSocketSession {
* is closed. In a typical {@link WebSocketHandler} implementation this
* stream is composed into the overall processing flow, so that when the
* connection is closed, handling will end.
*
* <p>See the class-level doc of {@link WebSocketHandler} and the reference
* for more details and examples of how to handle the session.
*/
@@ -76,7 +75,6 @@ public interface WebSocketSession {
* Give a source of outgoing messages, write the messages and return a
* {@code Mono<Void>} that completes when the source completes and writing
* is done.
*
* <p>See the class-level doc of {@link WebSocketHandler} and the reference
* for more details and examples of how to handle the session.
*/
@@ -130,7 +130,8 @@ public class DefaultWebClientTests {
@Test
public void defaultHeaderAndCookie() {
WebClient client = this.builder
.defaultHeader("Accept", "application/json").defaultCookie("id", "123")
.defaultHeader("Accept", "application/json")
.defaultCookie("id", "123")
.build();
client.get().uri("/path").exchange().block(Duration.ofSeconds(10));
@@ -157,6 +158,35 @@ public class DefaultWebClientTests {
assertThat(request.cookies().getFirst("id")).isEqualTo("456");
}
@Test
public void defaultHeaderAndCookieCopies() {
WebClient client1 = this.builder
.defaultHeader("Accept", "application/json")
.defaultCookie("id", "123")
.build();
WebClient client2 = this.builder
.defaultHeader("Accept", "application/xml")
.defaultCookies(cookies -> cookies.set("id", "456"))
.build();
client1.get().uri("/path")
.exchange().block(Duration.ofSeconds(10));
ClientRequest request = verifyAndGetRequest();
assertThat(request.headers().getFirst("Accept")).isEqualTo("application/json");
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
client2.get().uri("/path")
.exchange().block(Duration.ofSeconds(10));
request = verifyAndGetRequest();
assertThat(request.headers().getFirst("Accept")).isEqualTo("application/xml");
assertThat(request.cookies().getFirst("id")).isEqualTo("456");
}
@Test
public void defaultRequest() {
ThreadLocal<String> context = new NamedThreadLocal<>("foo");
@@ -162,7 +162,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
private static final boolean javaxValidationPresent;
private static boolean romePresent;
private static final boolean romePresent;
private static final boolean jaxb2Present;
@@ -208,7 +208,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
if (element.hasAttribute("enable-matrix-variables")) {
Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute("enable-matrix-variables"));
boolean enableMatrixVariables = Boolean.parseBoolean(element.getAttribute("enable-matrix-variables"));
handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -81,7 +81,7 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
private static final String RESOURCE_URL_PROVIDER = "mvcResourceUrlProvider";
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
private static final boolean webJarsPresent = ClassUtils.isPresent(
"org.webjars.WebJarAssetLocator", ResourcesBeanDefinitionParser.class.getClassLoader());
@@ -331,7 +331,7 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
if (isAutoRegistration) {
if (isWebJarsAssetLocatorPresent) {
if (webJarsPresent) {
RootBeanDefinition webJarsResolverDef = new RootBeanDefinition(WebJarsResourceResolver.class);
webJarsResolverDef.setSource(source);
webJarsResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -95,7 +95,8 @@ public class CorsRegistration {
* {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
* {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an
* actual response might have and can be exposed.
* <p>Note that {@code "*"} is not supported on this property.
* <p>The special value {@code "*"} allows all headers to be exposed for
* non-credentialed requests.
* <p>By default this is not set.
*/
public CorsRegistration exposedHeaders(String... headers) {
@@ -1377,6 +1377,7 @@
Comma-separated list of response headers other than simple headers (i.e.
Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma) that an
actual response might have and can be exposed.
The special value "*" allows all headers to be exposed for non-credentialed requests.
Empty by default.
]]></xsd:documentation>
</xsd:annotation>
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -84,7 +84,7 @@ public class DispatcherServletTests {
@BeforeEach
public void setUp() throws ServletException {
public void setup() throws ServletException {
MockServletConfig complexConfig = new MockServletConfig(getServletContext(), "complex");
complexConfig.addInitParameter("publishContext", "false");
complexConfig.addInitParameter("class", "notWritable");
@@ -105,6 +105,7 @@ public class DispatcherServletTests {
return servletConfig.getServletContext();
}
@Test
public void configuredDispatcherServlets() {
assertThat(("simple" + FrameworkServlet.DEFAULT_NAMESPACE_SUFFIX).equals(simpleDispatcherServlet.getNamespace())).as("Correct namespace").isTrue();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 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.
@@ -120,7 +120,6 @@ public interface WebSocketSession extends Closeable {
/**
* Send a WebSocket message: either {@link TextMessage} or {@link BinaryMessage}.
*
* <p><strong>Note:</strong> The underlying standard WebSocket session (JSR-356) does
* not allow concurrent sending. Therefore sending must be synchronized. To ensure
* that, one option is to wrap the {@code WebSocketSession} with the
@@ -27,6 +27,7 @@ import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.user.DestinationUserNameProvider;
import org.springframework.messaging.simp.user.SimpSession;
@@ -34,7 +35,6 @@ import org.springframework.messaging.simp.user.SimpSubscription;
import org.springframework.messaging.simp.user.SimpSubscriptionMatcher;
import org.springframework.messaging.simp.user.SimpUser;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.Assert;
/**
@@ -84,19 +84,16 @@ public class DefaultSimpUserRegistry implements SimpUserRegistry, SmartApplicati
public void onApplicationEvent(ApplicationEvent event) {
AbstractSubProtocolEvent subProtocolEvent = (AbstractSubProtocolEvent) event;
Message<?> message = subProtocolEvent.getMessage();
MessageHeaders headers = message.getHeaders();
SimpMessageHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, SimpMessageHeaderAccessor.class);
Assert.state(accessor != null, "No SimpMessageHeaderAccessor");
String sessionId = accessor.getSessionId();
String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);
Assert.state(sessionId != null, "No session id");
if (event instanceof SessionSubscribeEvent) {
LocalSimpSession session = this.sessions.get(sessionId);
if (session != null) {
String id = accessor.getSubscriptionId();
String destination = accessor.getDestination();
String id = SimpMessageHeaderAccessor.getSubscriptionId(headers);
String destination = SimpMessageHeaderAccessor.getDestination(headers);
if (id != null && destination != null) {
session.addSubscription(id, destination);
}
@@ -137,7 +134,7 @@ public class DefaultSimpUserRegistry implements SimpUserRegistry, SmartApplicati
else if (event instanceof SessionUnsubscribeEvent) {
LocalSimpSession session = this.sessions.get(sessionId);
if (session != null) {
String subscriptionId = accessor.getSubscriptionId();
String subscriptionId = SimpMessageHeaderAccessor.getSubscriptionId(headers);
if (subscriptionId != null) {
session.removeSubscription(subscriptionId);
}
@@ -326,8 +326,13 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
}
catch (Throwable ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to send client message to application via MessageChannel" +
" in session " + session.getId() + ". Sending STOMP ERROR to client.", ex);
String errorText = "Failed to send message to MessageChannel in session " + session.getId();
if (logger.isDebugEnabled()) {
logger.debug(errorText, ex);
}
else {
logger.error(errorText + ":" + ex.getMessage());
}
}
handleError(session, ex, message);
}
+95 -106
View File
@@ -925,7 +925,6 @@ You can declare before advice in an aspect by using the `@Before` annotation:
public void doAccessCheck() {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -941,7 +940,6 @@ You can declare before advice in an aspect by using the `@Before` annotation:
fun doAccessCheck() {
// ...
}
}
----
@@ -961,7 +959,6 @@ following example:
public void doAccessCheck() {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim",role="secondary"]
@@ -977,7 +974,6 @@ following example:
fun doAccessCheck() {
// ...
}
}
----
@@ -985,8 +981,8 @@ following example:
[[aop-advice-after-returning]]
==== After Returning Advice
After returning advice runs when a matched method execution returns normally. You can
declare it by using the `@AfterReturning` annotation:
After returning advice runs when a matched method execution returns normally.
You can declare it by using the `@AfterReturning` annotation:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -1001,7 +997,6 @@ declare it by using the `@AfterReturning` annotation:
public void doAccessCheck() {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -1017,16 +1012,16 @@ declare it by using the `@AfterReturning` annotation:
fun doAccessCheck() {
// ...
}
}
----
NOTE: You can have multiple advice declarations (and other members
as well), all inside the same aspect. We show only a single advice declaration in
these examples to focus the effect of each one.
NOTE: You can have multiple advice declarations (and other members as well),
all inside the same aspect. We show only a single advice declaration in these
examples to focus the effect of each one.
Sometimes, you need access in the advice body to the actual value that was returned. You
can use the form of `@AfterReturning` that binds the return value to get that access, as
the following example shows:
Sometimes, you need access in the advice body to the actual value that was returned.
You can use the form of `@AfterReturning` that binds the return value to get that
access, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -1043,7 +1038,6 @@ the following example shows:
public void doAccessCheck(Object retVal) {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -1061,15 +1055,14 @@ the following example shows:
fun doAccessCheck(retVal: Any) {
// ...
}
}
----
The name used in the `returning` attribute must correspond to the name of a parameter in
the advice method. When a method execution returns, the return value is passed to
The name used in the `returning` attribute must correspond to the name of a parameter
in the advice method. When a method execution returns, the return value is passed to
the advice method as the corresponding argument value. A `returning` clause also
restricts matching to only those method executions that return a value of the specified
type (in this case, `Object`, which matches any return value).
restricts matching to only those method executions that return a value of the
specified type (in this case, `Object`, which matches any return value).
Please note that it is not possible to return a totally different reference when
using after returning advice.
@@ -1079,8 +1072,8 @@ using after returning advice.
==== After Throwing Advice
After throwing advice runs when a matched method execution exits by throwing an
exception. You can declare it by using the `@AfterThrowing` annotation, as the following
example shows:
exception. You can declare it by using the `@AfterThrowing` annotation, as the
following example shows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -1095,7 +1088,6 @@ example shows:
public void doRecoveryActions() {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -1111,15 +1103,14 @@ example shows:
fun doRecoveryActions() {
// ...
}
}
----
Often, you want the advice to run only when exceptions of a given type are thrown, and
you also often need access to the thrown exception in the advice body. You can use the
`throwing` attribute to both restrict matching (if desired -- use `Throwable` as the
exception type otherwise) and bind the thrown exception to an advice parameter. The
following example shows how to do so:
Often, you want the advice to run only when exceptions of a given type are thrown,
and you also often need access to the thrown exception in the advice body. You can
use the `throwing` attribute to both restrict matching (if desired -- use `Throwable`
as the exception type otherwise) and bind the thrown exception to an advice parameter.
The following example shows how to do so:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -1136,7 +1127,6 @@ following example shows how to do so:
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -1154,15 +1144,22 @@ following example shows how to do so:
fun doRecoveryActions(ex: DataAccessException) {
// ...
}
}
----
The name used in the `throwing` attribute must correspond to the name of a parameter in
the advice method. When a method execution exits by throwing an exception, the exception
is passed to the advice method as the corresponding argument value. A `throwing`
clause also restricts matching to only those method executions that throw an exception
of the specified type ( `DataAccessException`, in this case).
is passed to the advice method as the corresponding argument value. A `throwing` clause
also restricts matching to only those method executions that throw an exception of the
specified type (`DataAccessException`, in this case).
[NOTE]
====
Note that `@AfterThrowing` does not indicate a general exception handling callback.
Specifically, an `@AfterThrowing` advice method is only supposed to receive exceptions
from the join point (user-declared target method) itself but not from an accompanying
`@After`/`@AfterReturning` method.
====
[[aop-advice-after-finally]]
@@ -1170,8 +1167,8 @@ of the specified type ( `DataAccessException`, in this case).
After (finally) advice runs when a matched method execution exits. It is declared by
using the `@After` annotation. After advice must be prepared to handle both normal and
exception return conditions. It is typically used for releasing resources and similar purposes.
The following example shows how to use after finally advice:
exception return conditions. It is typically used for releasing resources and similar
purposes. The following example shows how to use after finally advice:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -1186,7 +1183,6 @@ The following example shows how to use after finally advice:
public void doReleaseLock() {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -1202,30 +1198,37 @@ The following example shows how to use after finally advice:
fun doReleaseLock() {
// ...
}
}
----
[NOTE]
====
Note that `@After` advice in AspectJ is defined as "after finally advice", analogous
to a finally block in a try-catch statement. It will be invoked for any outcome,
normal return or exception thrown from the join point (user-declared target method),
in contrast to `@AfterReturning` which only applies to successful normal returns.
====
[[aop-ataspectj-around-advice]]
==== Around Advice
The last kind of advice is around advice. Around advice runs "`around`" a matched method's
execution. It has the opportunity to do work both before and after the method runs
and to determine when, how, and even if the method actually gets to run at all.
The last kind of advice is around advice. Around advice runs "`around`" a matched
method's execution. It has the opportunity to do work both before and after the method
runs and to determine when, how, and even if the method actually gets to run at all.
Around advice is often used if you need to share state before and after a method
execution in a thread-safe manner (starting and stopping a timer, for example). Always
use the least powerful form of advice that meets your requirements (that is, do not use
around advice if before advice would do).
execution in a thread-safe manner (starting and stopping a timer, for example).
Always use the least powerful form of advice that meets your requirements (that is,
do not use around advice if before advice would do).
Around advice is declared by using the `@Around` annotation. The first parameter of the
advice method must be of type `ProceedingJoinPoint`. Within the body of the advice,
calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to
run. The `proceed` method can also pass in an `Object[]`. The values
in the array are used as the arguments to the method execution when it proceeds.
calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to run.
The `proceed` method can also pass in an `Object[]`. The values in the array are used
as the arguments to the method execution when it proceeds.
NOTE: The behavior of `proceed` when called with an `Object[]` is a little different than the
behavior of `proceed` for around advice compiled by the AspectJ compiler. For around
NOTE: The behavior of `proceed` when called with an `Object[]` is a little different than
the behavior of `proceed` for around advice compiled by the AspectJ compiler. For around
advice written using the traditional AspectJ language, the number of arguments passed to
`proceed` must match the number of arguments passed to the around advice (not the number
of arguments taken by the underlying join point), and the value passed to proceed in a
@@ -1257,7 +1260,6 @@ The following example shows how to use around advice:
// stop stopwatch
return retVal;
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -1277,34 +1279,31 @@ The following example shows how to use around advice:
// stop stopwatch
return retVal
}
}
----
The value returned by the around advice is the return value seen by the caller of
the method. For example, a simple caching aspect could return a value from a cache if it
The value returned by the around advice is the return value seen by the caller of the
method. For example, a simple caching aspect could return a value from a cache if it
has one and invoke `proceed()` if it does not. Note that `proceed` may be invoked once,
many times, or not at all within the body of the around advice. All of these are
legal.
many times, or not at all within the body of the around advice. All of these are legal.
[[aop-ataspectj-advice-params]]
==== Advice Parameters
Spring offers fully typed advice, meaning that you declare the parameters you need
in the advice signature (as we saw earlier for the returning and throwing examples) rather
than work with `Object[]` arrays all the time. We see how to make argument and other
contextual values available to the advice body later in this section. First, we take a look at
how to write generic advice that can find out about the method the advice is currently
advising.
Spring offers fully typed advice, meaning that you declare the parameters you need in the
advice signature (as we saw earlier for the returning and throwing examples) rather than
work with `Object[]` arrays all the time. We see how to make argument and other contextual
values available to the advice body later in this section. First, we take a look at how to
write generic advice that can find out about the method the advice is currently advising.
[[aop-ataspectj-advice-params-the-joinpoint]]
===== Access to the Current `JoinPoint`
Any advice method may declare, as its first parameter, a parameter of type
`org.aspectj.lang.JoinPoint` (note that around advice is required to declare
a first parameter of type `ProceedingJoinPoint`, which is a subclass of `JoinPoint`. The
`JoinPoint` interface provides a number of useful methods:
`org.aspectj.lang.JoinPoint` (note that around advice is required to declare a first
parameter of type `ProceedingJoinPoint`, which is a subclass of `JoinPoint`.
The `JoinPoint` interface provides a number of useful methods:
* `getArgs()`: Returns the method arguments.
* `getThis()`: Returns the proxy object.
@@ -1320,9 +1319,9 @@ See the https://www.eclipse.org/aspectj/doc/released/runtime-api/org/aspectj/lan
We have already seen how to bind the returned value or exception value (using after
returning and after throwing advice). To make argument values available to the advice
body, you can use the binding form of `args`. If you use a parameter name in place of a
type name in an args expression, the value of the corresponding argument is
passed as the parameter value when the advice is invoked. An example should make this
clearer. Suppose you want to advise the execution of DAO operations that take an `Account`
type name in an args expression, the value of the corresponding argument is passed as
the parameter value when the advice is invoked. An example should make this clearer.
Suppose you want to advise the execution of DAO operations that take an `Account`
object as the first parameter, and you need access to the account in the advice body.
You could write the following:
@@ -1654,20 +1653,23 @@ the higher precedence.
[NOTE]
====
Each of the distinct advice types of a particular aspect is conceptually meant to apply
to the join point directly. As a consequence, an `@AfterThrowing` advice method is not
supposed to receive an exception from an accompanying `@After`/`@AfterReturning` method.
As of Spring Framework 5.2.7, advice methods defined in the same `@Aspect` class that
need to run at the same join point are assigned precedence based on their advice type in
the following order, from highest to lowest precedence: `@Around`, `@Before`, `@After`,
`@AfterReturning`, `@AfterThrowing`. Note, however, that due to the implementation style
in Spring's `AspectJAfterAdvice`, an `@After` advice method will effectively be invoked
after any `@AfterReturning` or `@AfterThrowing` advice methods in the same aspect.
`@AfterReturning`, `@AfterThrowing`. Note, however, that an `@After` advice method will
effectively be invoked after any `@AfterReturning` or `@AfterThrowing` advice methods
in the same aspect, following AspectJ's "after finally advice" semantics for `@After`.
When two pieces of the same type of advice (for example, two `@After` advice methods)
defined in the same `@Aspect` class both need to run at the same join point, the ordering
is undefined (since there is no way to retrieve the source code declaration order through
reflection for javac-compiled classes). Consider collapsing such advice methods into one
advice method per join point in each `@Aspect` class or refactor the pieces of advice
into separate `@Aspect` classes that you can order at the aspect level via `Ordered` or
`@Order`.
advice method per join point in each `@Aspect` class or refactor the pieces of advice into
separate `@Aspect` classes that you can order at the aspect level via `Ordered` or `@Order`.
====
@@ -1678,11 +1680,11 @@ Introductions (known as inter-type declarations in AspectJ) enable an aspect to
that advised objects implement a given interface, and to provide an implementation of
that interface on behalf of those objects.
You can make an introduction by using the `@DeclareParents` annotation. This annotation is used
to declare that matching types have a new parent (hence the name). For example, given an
interface named `UsageTracked` and an implementation of that interface named `DefaultUsageTracked`,
the following aspect declares that all implementors of service interfaces also implement
the `UsageTracked` interface (to expose statistics via JMX for example):
You can make an introduction by using the `@DeclareParents` annotation. This annotation
is used to declare that matching types have a new parent (hence the name). For example,
given an interface named `UsageTracked` and an implementation of that interface named
`DefaultUsageTracked`, the following aspect declares that all implementors of service
interfaces also implement the `UsageTracked` interface (e.g. for statistics via JMX):
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -1764,7 +1766,6 @@ annotation. Consider the following example:
public void recordServiceUsage() {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -1779,7 +1780,6 @@ annotation. Consider the following example:
fun recordServiceUsage() {
// ...
}
}
----
@@ -1854,7 +1854,6 @@ call `proceed` multiple times. The following listing shows the basic aspect impl
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -2066,7 +2065,6 @@ as the following example shows:
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
@@ -2088,7 +2086,6 @@ collects the `this` object as the join point context and passes it to the advice
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
@@ -2179,7 +2176,6 @@ a `pointcut` attribute, as follows:
method="doAccessCheck"/>
...
</aop:aspect>
----
@@ -2209,7 +2205,6 @@ shows how to declare it:
method="doAccessCheck"/>
...
</aop:aspect>
----
@@ -2227,7 +2222,6 @@ the return value should be passed, as the following example shows:
method="doAccessCheck"/>
...
</aop:aspect>
----
@@ -2263,7 +2257,6 @@ as the following example shows:
method="doRecoveryActions"/>
...
</aop:aspect>
----
@@ -2281,13 +2274,12 @@ which the exception should be passed as the following example shows:
method="doRecoveryActions"/>
...
</aop:aspect>
----
The `doRecoveryActions` method must declare a parameter named `dataAccessEx`. The type of
this parameter constrains matching in the same way as described for `@AfterThrowing`. For
example, the method signature may be declared as follows:
The `doRecoveryActions` method must declare a parameter named `dataAccessEx`.
The type of this parameter constrains matching in the same way as described for
`@AfterThrowing`. For example, the method signature may be declared as follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -2304,8 +2296,8 @@ example, the method signature may be declared as follows:
[[aop-schema-advice-after-finally]]
==== After (Finally) Advice
After (finally) advice runs no matter how a matched method execution exits. You can declare it
by using the `after` element, as the following example shows:
After (finally) advice runs no matter how a matched method execution exits.
You can declare it by using the `after` element, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -2316,7 +2308,6 @@ by using the `after` element, as the following example shows:
method="doReleaseLock"/>
...
</aop:aspect>
----
@@ -2327,17 +2318,17 @@ by using the `after` element, as the following example shows:
The last kind of advice is around advice. Around advice runs "around" a matched method
execution. It has the opportunity to do work both before and after the method runs
and to determine when, how, and even if the method actually gets to run at all.
Around advice is often used to share state before and after a method
execution in a thread-safe manner (starting and stopping a timer, for example). Always
use the least powerful form of advice that meets your requirements. Do not use around
advice if before advice can do the job.
Around advice is often used to share state before and after a method execution in a
thread-safe manner (starting and stopping a timer, for example). Always use the least
powerful form of advice that meets your requirements. Do not use around advice if
before advice can do the job.
You can declare around advice by using the `aop:around` element. The first parameter of the
advice method must be of type `ProceedingJoinPoint`. Within the body of the advice,
calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to
run. The `proceed` method may also be called with an `Object[]`. The values
in the array are used as the arguments to the method execution when it proceeds. See
<<aop-ataspectj-around-advice>> for notes on calling `proceed` with an `Object[]`.
You can declare around advice by using the `aop:around` element. The first parameter of
the advice method must be of type `ProceedingJoinPoint`. Within the body of the advice,
calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to run.
The `proceed` method may also be called with an `Object[]`. The values in the array
are used as the arguments to the method execution when it proceeds.
See <<aop-ataspectj-around-advice>> for notes on calling `proceed` with an `Object[]`.
The following example shows how to declare around advice in XML:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -2349,7 +2340,6 @@ The following example shows how to declare around advice in XML:
method="doBasicProfiling"/>
...
</aop:aspect>
----
@@ -2763,7 +2753,6 @@ call `proceed` multiple times. The following listing shows the basic aspect impl
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
+2 -1
View File
@@ -666,7 +666,8 @@ integrate such parsers into the Spring IoC container.
To facilitate authoring configuration files that use a schema-aware XML editor,
Spring's extensible XML configuration mechanism is based on XML Schema. If you are not
familiar with Spring's current XML configuration extensions that come with the standard
Spring distribution, you should first read the appendix entitled <<appendix.adoc#xsd-configuration>>.
Spring distribution, you should first read the previous section on <<xsd-schemas>>.
To create new XML configuration extensions:
+1 -1
View File
@@ -563,7 +563,7 @@ files named `services.xml` and `daos.xml` (which are on the classpath) can be in
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "daos.xml"), MessengerService::class.java)
----
See the api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]
See the {api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]
javadoc for details on the various constructors.
+64 -53
View File
@@ -184,7 +184,7 @@ transaction management. The following listing shows the definition of the
@Throws(TransactionException::class)
fun rollback(status: TransactionStatus)
}
----
----
This is primarily a service provider interface (SPI), although you can use it
<<transaction-programmatic-ptm, programmatically>> from your application code. Because
@@ -241,7 +241,7 @@ listing shows the transaction strategy defined by
@Throws(TransactionException::class)
fun rollback(status: ReactiveTransaction): Mono<Void>
}
----
----
The reactive transaction manager is primarily a service provider interface (SPI),
although you can use it <<transaction-programmatic-rtm, programmatically>> from your
@@ -566,7 +566,7 @@ abstractions mentioned earlier.
[[transaction-declarative]]
=== Declarative transaction management
=== Declarative Transaction Management
NOTE: Most Spring Framework users choose declarative transaction management. This option has
the least impact on application code and, hence, is most consistent with the ideals of a
@@ -637,7 +637,7 @@ around method invocations.
NOTE: Spring AOP is covered in <<core.adoc#aop, the AOP section>>.
Spring Frameworks's `TransactionInterceptor` provides transaction management for
Spring Framework's `TransactionInterceptor` provides transaction management for
imperative and reactive programming models. The interceptor detects the desired flavor of
transaction management by inspecting the method return type. Methods returning a reactive
type such as `Publisher` or Kotlin `Flow` (or a subtype of those) qualify for reactive
@@ -648,6 +648,18 @@ Transaction management flavors impact which transaction manager is required. Imp
transactions require a `PlatformTransactionManager`, while reactive transactions use
`ReactiveTransactionManager` implementations.
[NOTE]
====
`@Transactional` commonly works with thread-bound transactions managed by
`PlatformTransactionManager`, exposing a transaction to all data access operations within
the current execution thread. Note: This does _not_ propagate to newly started threads
within the method.
A reactive transaction managed by `ReactiveTransactionManager` uses the Reactor context
instead of thread-local attributes. As a consequence, all participating data access
operations need to execute within the same Reactor context in the same reactive pipeline.
====
The following image shows a conceptual view of calling a method on a transactional proxy:
image::images/tx.png[]
@@ -1737,7 +1749,7 @@ in the application context:
@Transactional("account")
public void doSomething() { ... }
@Transactional("reactive-account")
public Mono<Void> doSomethingReactive() { ... }
}
@@ -2442,7 +2454,7 @@ the `TransactionOperator` resembles the next example:
// the code in this method runs in a transactional context
Mono<Object> update = updateOperation1();
return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
}
}
@@ -2529,7 +2541,7 @@ following example shows customization of the transactional settings for a specif
public SimpleService(ReactiveTransactionManager transactionManager) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// the transaction settings can be set here explicitly if so desired
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
definition.setTimeout(30); // 30 seconds
@@ -2627,7 +2639,7 @@ following example shows how to do so:
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);
reactiveTx.flatMap(status -> {
Mono<Object> tx = ...; // put your business logic here
@@ -2841,30 +2853,29 @@ specific to each technology.
Spring provides a convenient translation from technology-specific exceptions, such as
`SQLException` to its own exception class hierarchy, which has `DataAccessException` as
the root exception. These exceptions wrap the original exception so that there is never any
risk that you might lose any information about what might have gone wrong.
the root exception. These exceptions wrap the original exception so that there is never
any risk that you might lose any information about what might have gone wrong.
In addition to JDBC exceptions, Spring can also wrap JPA- and Hibernate-specific exceptions,
converting them to a set of focused runtime exceptions.
This lets you handle most non-recoverable persistence exceptions
in only the appropriate layers, without having annoying boilerplate
catch-and-throw blocks and exception declarations in your DAOs. (You can still trap
and handle exceptions anywhere you need to though.) As mentioned above, JDBC
exceptions (including database-specific dialects) are also converted to the same
converting them to a set of focused runtime exceptions. This lets you handle most
non-recoverable persistence exceptions in only the appropriate layers, without having
annoying boilerplate catch-and-throw blocks and exception declarations in your DAOs.
(You can still trap and handle exceptions anywhere you need to though.) As mentioned above,
JDBC exceptions (including database-specific dialects) are also converted to the same
hierarchy, meaning that you can perform some operations with JDBC within a consistent
programming model.
The preceding discussion holds true for the various template classes in Spring's support for various ORM
frameworks. If you use the interceptor-based classes, the application must care
about handling `HibernateExceptions` and `PersistenceExceptions` itself, preferably by
delegating to the `convertHibernateAccessException(..)` or
`convertJpaAccessException()` methods, respectively, of `SessionFactoryUtils`. These methods convert the exceptions
The preceding discussion holds true for the various template classes in Spring's support
for various ORM frameworks. If you use the interceptor-based classes, the application must
care about handling `HibernateExceptions` and `PersistenceExceptions` itself, preferably by
delegating to the `convertHibernateAccessException(..)` or `convertJpaAccessException(..)`
methods, respectively, of `SessionFactoryUtils`. These methods convert the exceptions
to exceptions that are compatible with the exceptions in the `org.springframework.dao`
exception hierarchy. As `PersistenceExceptions` are unchecked, they can get
thrown, too (sacrificing generic DAO abstraction in terms of exceptions, though).
exception hierarchy. As `PersistenceExceptions` are unchecked, they can get thrown, too
(sacrificing generic DAO abstraction in terms of exceptions, though).
The following image shows the exception hierarchy that Spring provides. (Note that the
class hierarchy detailed in the image shows only a subset of the entire
The following image shows the exception hierarchy that Spring provides.
(Note that the class hierarchy detailed in the image shows only a subset of the entire
`DataAccessException` hierarchy.)
image::images/DataAccessException.png[]
@@ -2989,7 +3000,7 @@ this `DataSource`. The following example autowires a `DataSource`:
----
@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {
private val jdbcTemplate = JdbcTemplate(dataSource)
// ...
@@ -3250,8 +3261,8 @@ The following query finds and populates a single domain object:
----
val actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
arrayOf(1212L)) { rs, _ ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
arrayOf(1212L)) { rs, _ ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
}
----
@@ -3503,7 +3514,7 @@ method with `@Autowired`. The following example shows how to do so:
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { // <2>
private val jdbcTemplate = JdbcTemplate(dataSource) // <3>
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
----
@@ -3842,10 +3853,10 @@ translator:
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
// create a custom translator and set the DataSource for the default translation lookup
exceptionTranslator = CustomSQLErrorCodesTranslator().apply {
this.dataSource = dataSource
this.dataSource = dataSource
}
}
fun updateShippingCharge(orderId: Long, pct: Long) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate!!.update("update orders" +
@@ -4069,8 +4080,8 @@ on Oracle but may not work on other platforms:
val name = "Rob"
val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({
it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) }
jdbcTemplate.update({
it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) }
}, keyHolder)
// keyHolder.getKey() now contains the generated key
@@ -4229,14 +4240,14 @@ interface that wraps a single `Connection` that is not closed after each use.
This is not multi-threading capable.
If any client code calls `close` on the assumption of a pooled connection (as when using
persistence tools), you should set the `suppressClose` property to `true`. This setting returns a
close-suppressing proxy that wraps the physical connection. Note that you can no longer
cast this to a native Oracle `Connection` or a similar object.
persistence tools), you should set the `suppressClose` property to `true`. This setting
returns a close-suppressing proxy that wraps the physical connection. Note that you can
no longer cast this to a native Oracle `Connection` or a similar object.
`SingleConnectionDataSource` is primarily a test class. For example, it enables easy testing of code outside an
application server, in conjunction with a simple JNDI environment. In contrast to
`DriverManagerDataSource`, it reuses the same connection all the time, avoiding
excessive creation of physical connections.
`SingleConnectionDataSource` is primarily a test class. It typically enables easy testing
of code outside an application server, in conjunction with a simple JNDI environment.
In contrast to `DriverManagerDataSource`, it reuses the same connection all the time,
avoiding excessive creation of physical connections.
@@ -5008,7 +5019,7 @@ the constructor of your `SimpleJdbcCall`. The following example shows this confi
private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_actor")
// ... additional methods
}
----
@@ -5766,7 +5777,7 @@ the supplied `ResultSet`, as follows:
import org.springframework.jdbc.core.RowMapper
class GenreMapper : RowMapper<Genre> {
override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
return Genre(rs.getString("name"))
}
@@ -6777,7 +6788,7 @@ chapter then cover the other ORM technologies and show brief examples.
NOTE: As of Spring Framework 5.0, Spring requires Hibernate ORM 4.3 or later for JPA support
and even Hibernate ORM 5.0+ for programming against the native Hibernate Session API.
Note that the Hibernate team does not maintain any versions prior to 5.1 anymore and
is likely to focus on 5.3+ exclusively soon.
is likely to focus on 5.4+ exclusively soon.
[[orm-session-factory-setup]]
@@ -6884,7 +6895,7 @@ implementation resembles the following example, based on the plain Hibernate API
.Kotlin
----
class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {
fun loadProductsByCategory(category: String): Collection<*> {
return sessionFactory.currentSession
.createQuery("from test.Product product where product.category=?")
@@ -7092,7 +7103,7 @@ and an example for a business method implementation:
----
class ProductServiceImpl(transactionManager: PlatformTransactionManager,
private val productDao: ProductDao) : ProductService {
private val transactionTemplate = TransactionTemplate(transactionManager)
fun increasePriceOfAllProductsInCategory(category: String) {
@@ -7354,7 +7365,7 @@ This includes web containers such as Tomcat, stand-alone applications, and
integration tests with sophisticated persistence requirements.
NOTE: If you want to specifically configure a Hibernate setup, an immediate alternative is
to go with Hibernate 5.2 or 5.3 and set up a native Hibernate `LocalSessionFactoryBean`
to go with Hibernate 5.2/5.3/5.4 and set up a native Hibernate `LocalSessionFactoryBean`
instead of a plain JPA `LocalContainerEntityManagerFactoryBean`, letting it interact
with JPA access code as well as native Hibernate access code.
See <<orm-jpa-hibernate, Native Hibernate setup for JPA interaction>> for details.
@@ -7726,7 +7737,7 @@ Spring provides dialects for the EclipseLink and Hibernate JPA implementations.
See the <<orm-jpa-dialect, next section>> for details on the `JpaDialect` mechanism.
NOTE: As an immediate alternative, Spring's native `HibernateTransactionManager` is capable
of interacting with JPA access code as of Spring Framework 5.1 and Hibernate 5.2/5.3,
of interacting with JPA access code as of Spring Framework 5.1 and Hibernate 5.2/5.3/5.4,
adapting to several Hibernate specifics and providing JDBC interaction.
This makes particular sense in combination with `LocalSessionFactoryBean` setup.
See <<orm-jpa-hibernate, Native Hibernate Setup for JPA Interaction>> for details.
@@ -7801,7 +7812,7 @@ less portable) but is set up for the server's JTA environment.
[[orm-jpa-hibernate]]
==== Native Hibernate Setup and Native Hibernate Transactions for JPA Interaction
As of Spring Framework 5.1 and Hibernate 5.2/5.3, a native `LocalSessionFactoryBean`
As of Spring Framework 5.1 and Hibernate 5.2/5.3/5.4, a native `LocalSessionFactoryBean`
setup in combination with `HibernateTransactionManager` allows for interaction with
`@PersistenceContext` and other JPA access code. A Hibernate
`SessionFactory` natively implements JPA's `EntityManagerFactory` interface now
@@ -8160,8 +8171,8 @@ can do so by using the following `applicationContext.xml`:
----
This application context uses XStream, but we could have used any of the other marshaller
instances described later in this chapter. Note that, by default, XStream does not require any further
configuration, so the bean definition is rather simple. Also note that the
instances described later in this chapter. Note that, by default, XStream does not require
any further configuration, so the bean definition is rather simple. Also note that the
`XStreamMarshaller` implements both `Marshaller` and `Unmarshaller`, so we can refer to the
`xstreamMarshaller` bean in both the `marshaller` and `unmarshaller` property of the
application.
@@ -8179,8 +8190,8 @@ This sample application produces the following `settings.xml` file:
[[oxm-schema-based-config]]
=== XML Configuration Namespace
You can configure marshallers more concisely by using tags from the OXM namespace. To
make these tags available, you must first reference the appropriate schema in the
You can configure marshallers more concisely by using tags from the OXM namespace.
To make these tags available, you must first reference the appropriate schema in the
preamble of the XML configuration file. The following example shows how to do so:
[source,xml,indent=0]
@@ -8423,7 +8434,7 @@ vulnerabilities do not get invoked.
NOTE: Note that XStream is an XML serialization library, not a data binding library.
Therefore, it has limited namespace support. As a result, it is rather unsuitable for usage
within Web services.
within Web Services.
@@ -67,7 +67,7 @@ The following example defines a class that has a dependency on the `Messenger` i
The following example implements the `Messenger` interface in Groovy:
[source,java,indent=0]
[source,groovy,indent=0]
[subs="verbatim,quotes"]
----
// from the file 'Messenger.groovy'
@@ -92,7 +92,7 @@ container. Using the dynamic-language-backed beans with a plain `BeanFactory`
implementation is supported, but you have to manage the plumbing of the Spring internals
to do so.
For more information on schema-based configuration, see <<appendix.adoc#xsd-configuration,
For more information on schema-based configuration, see <<xsd-schemas-lang,
XML Schema-based Configuration>>.
====
@@ -176,7 +176,7 @@ of your dynamic language source files.
The final step in the list in the <<dynamic-language-beans-concepts, preceding section>>
involves defining dynamic-language-backed bean definitions, one for each bean that you
want to configure (this is no different from normal JavaBean configuration). However,
instead of specifying the fully qualified classname of the class that is to be
instead of specifying the fully qualified class name of the class that is to be
instantiated and configured by the container, you can use the `<lang:language/>`
element to define the dynamic language-backed bean.
@@ -282,7 +282,7 @@ surrounded by quotation marks. The following listing shows the changes that you
(the developer) should make to the `Messenger.groovy` source file when the
execution of the program is paused:
[source,java,indent=0]
[source,groovy,indent=0]
[subs="verbatim,quotes"]
----
package org.springframework.scripting
@@ -371,7 +371,7 @@ constructors and properties 100% clear, the following mixture of code and config
does not work:
.An approach that cannot work
[source,java,indent=0]
[source,groovy,indent=0]
[subs="verbatim,quotes"]
----
// from the file 'Messenger.groovy'
@@ -480,9 +480,9 @@ Finally, the following small application exercises the preceding configuration:
public class Main {
public static void Main(String[] args) {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
Calculator calc = (Calculator) ctx.getBean("calculator");
Calculator calc = ctx.getBean("calculator", Calculator.class);
System.out.println(calc.add(2, 8));
}
}
@@ -697,7 +697,7 @@ beans, you have to enable the "`refreshable beans`" functionality. See
The following example shows an `org.springframework.web.servlet.mvc.Controller` implemented
by using the Groovy dynamic language:
[source,java,indent=0]
[source,groovy,indent=0]
[subs="verbatim,quotes"]
----
// from the file '/WEB-INF/groovy/FortuneController.groovy'
@@ -848,7 +848,7 @@ The `lang` elements in Spring XML configuration deal with exposing objects that
written in a dynamic language (such as Groovy or BeanShell) as beans in the Spring container.
These elements (and the dynamic language support) are comprehensively covered in
<<integration.adoc#dynamic-language, Dynamic Language Support>>. See that chapter
<<dynamic-language, Dynamic Language Support>>. See that section
for full details on this support and the `lang` elements.
To use the elements in the `lang` schema, you need to have the following preamble at the