mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
48 Commits
main
...
v5.2.12.RELEASE
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c5e9faf90 | |||
| 33476a2eae | |||
| a8091b916b | |||
| 396fdf125f | |||
| 085825fbc0 | |||
| b0c6e5e322 | |||
| a7efa9659a | |||
| f7605eaf58 | |||
| ce67b89861 | |||
| 736af46fc0 | |||
| a495bd6679 | |||
| 7c33c70742 | |||
| 81c1b60f19 | |||
| 3703be5aaf | |||
| b929edb221 | |||
| 50803ce142 | |||
| 346445ee7e | |||
| e238c8a87c | |||
| 9e99fd5df2 | |||
| 990c74b1e2 | |||
| 37bda566eb | |||
| 322babc04a | |||
| 4e720e8104 | |||
| fdab75a1d8 | |||
| 240cfb1224 | |||
| db2786264b | |||
| 763fa98bdf | |||
| 25cbc263f0 | |||
| 9949a91048 | |||
| d8b7a593c5 | |||
| d3d8f1a487 | |||
| a637f6a27c | |||
| e9416b369e | |||
| cde95e1446 | |||
| da4e37dc1d | |||
| 010d0947c7 | |||
| 5b06c23a1b | |||
| 7881329cf7 | |||
| 58aa0659cc | |||
| dbbedc6c86 | |||
| 09c1e986b9 | |||
| 10bff054a9 | |||
| e713e0d6d5 | |||
| b4f8fc8177 | |||
| 8c3cdc6118 | |||
| 01827fd8d2 | |||
| af1d721aa3 | |||
| 0acb1e5513 |
+8
-8
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -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
|
||||
|
||||
+3
-2
@@ -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")
|
||||
|
||||
+6
-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.
|
||||
@@ -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.
|
||||
|
||||
+33
-20
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+7
-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.
|
||||
@@ -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;
|
||||
|
||||
+3
-14
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-5
@@ -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);
|
||||
|
||||
+16
-11
@@ -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;
|
||||
|
||||
+16
-22
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -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
-2
@@ -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,
|
||||
|
||||
|
||||
+8
-6
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+7
-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.
|
||||
@@ -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
|
||||
|
||||
+55
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -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() }
|
||||
|
||||
+30
-17
@@ -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;
|
||||
}
|
||||
|
||||
+8
-8
@@ -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);
|
||||
|
||||
+3
-6
@@ -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
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+18
@@ -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;
|
||||
|
||||
+7
-3
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
+23
-15
@@ -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() {
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
-1
@@ -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);
|
||||
|
||||
+4
-2
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-15
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-15
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-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.
|
||||
@@ -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++) {
|
||||
|
||||
+11
-9
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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(
|
||||
|
||||
+3
-8
@@ -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);
|
||||
|
||||
+9
-6
@@ -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
-2
@@ -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());
|
||||
|
||||
+14
-12
@@ -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) {
|
||||
|
||||
+52
-20
@@ -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()
|
||||
|
||||
+35
-8
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -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
|
||||
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+18
-7
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+7
-21
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+7
-7
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
-5
@@ -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
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-1
@@ -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)) {
|
||||
|
||||
+6
-1
@@ -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);
|
||||
|
||||
+22
-4
@@ -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);
|
||||
}
|
||||
|
||||
+5
-5
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+29
-15
@@ -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 {
|
||||
|
||||
|
||||
+28
@@ -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
|
||||
|
||||
+69
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+9
-9
@@ -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
|
||||
|
||||
+3
-2
@@ -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) {
|
||||
|
||||
+31
-2
@@ -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
-3
@@ -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.
|
||||
*/
|
||||
|
||||
+31
-1
@@ -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");
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+3
-2
@@ -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>
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+6
-9
@@ -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);
|
||||
}
|
||||
|
||||
+7
-2
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user