mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 967637d763 | |||
| 2899652d8e | |||
| 560a504a83 | |||
| 79f79e9306 | |||
| bd2640a9d6 | |||
| 6825287360 | |||
| 737d77a739 | |||
| 1f13516528 | |||
| 6bb3ad793e | |||
| c73cff8bad | |||
| e592634290 | |||
| b25637e8ad | |||
| 0c347769a2 | |||
| 2497d4285f | |||
| 32238cc996 | |||
| d80dd50c9d | |||
| bdf120f1ef | |||
| e781d21097 | |||
| 8cef13cc30 | |||
| 2214ff8ce0 | |||
| 411539ffef | |||
| 11aa901634 | |||
| 6e51370490 | |||
| bcd2b9a8a7 | |||
| 17dd7785f6 | |||
| 21f9d05bd9 | |||
| 19911af30a | |||
| 6acb091c4e | |||
| 99ed01e3f7 | |||
| 2657757566 | |||
| e1f51b4bf8 | |||
| f2f84bfa7a | |||
| 6d67bdf04f | |||
| 97c8628bd6 | |||
| 990a9c74b9 | |||
| b815accca9 | |||
| 4e8d6bea4d | |||
| 079ca80854 | |||
| 412aa06d86 | |||
| d5b3e65718 | |||
| a4679b87b9 | |||
| fbf8e37f94 | |||
| 8c3cab7ead | |||
| 0015fd6734 | |||
| b077e4cd85 | |||
| 0a0299693b | |||
| 41bdde5d6b | |||
| 2e2d662158 | |||
| 68934f1b79 | |||
| f781c459b4 | |||
| 5628c8cd63 | |||
| 0f6038af70 | |||
| 3eb2c5e22f | |||
| 3f722a9c7f | |||
| acbbf61be8 | |||
| 154f0c71c9 | |||
| 7be7e5beb4 | |||
| 37504e75e9 | |||
| b89b37729c | |||
| 141c79bc8f | |||
| 885a5048c1 | |||
| e981bf34b3 | |||
| 44239b2d7b | |||
| a00c483927 | |||
| 96ffa14141 | |||
| a9dec6a4af |
+5
-5
@@ -30,9 +30,9 @@ configure(allprojects) { project ->
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.0"
|
||||
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8"
|
||||
mavenBom "io.rsocket:rsocket-bom:1.1.0"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.33.v20201020"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102"
|
||||
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.10"
|
||||
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.0"
|
||||
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1"
|
||||
mavenBom "org.junit:junit-bom:5.7.0"
|
||||
}
|
||||
dependencies {
|
||||
@@ -123,7 +123,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.23.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"
|
||||
@@ -190,14 +190,14 @@ configure(allprojects) { project ->
|
||||
dependency "org.testng:testng:7.3.0"
|
||||
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.0"
|
||||
dependencySet(group: 'org.xmlunit', version: '2.6.2') {
|
||||
entry 'xmlunit-assertj'
|
||||
entry('xmlunit-matchers') {
|
||||
exclude group: "org.hamcrest", name: "hamcrest-core"
|
||||
}
|
||||
}
|
||||
dependencySet(group: 'org.mockito', version: '3.5.15') {
|
||||
dependencySet(group: 'org.mockito', version: '3.6.0') {
|
||||
entry('mockito-core') {
|
||||
exclude group: "org.hamcrest", name: "hamcrest-core"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
logging:
|
||||
level:
|
||||
io.spring.concourse: DEBUG
|
||||
distribute:
|
||||
optional-deployments:
|
||||
- '.*\\.zip'
|
||||
spring:
|
||||
main:
|
||||
banner-mode: off
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=5.3.0-SNAPSHOT
|
||||
version=5.3.1
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
+5
-3
@@ -221,10 +221,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
|
||||
|
||||
+5
-16
@@ -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.
|
||||
@@ -160,23 +160,12 @@ public class AspectJProxyFactory extends ProxyCreatorSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton aspect instance for the supplied aspect type. An instance
|
||||
* is created if one cannot be found in the instance cache.
|
||||
* Get the singleton aspect instance for the supplied aspect type.
|
||||
* An instance is created if one cannot be found in the instance cache.
|
||||
*/
|
||||
private Object getSingletonAspectInstance(Class<?> aspectClass) {
|
||||
// Quick check without a lock...
|
||||
Object instance = aspectCache.get(aspectClass);
|
||||
if (instance == null) {
|
||||
synchronized (aspectCache) {
|
||||
// To be safe, check within full lock now...
|
||||
instance = aspectCache.get(aspectClass);
|
||||
if (instance == null) {
|
||||
instance = new SimpleAspectInstanceFactory(aspectClass).getAspectInstance();
|
||||
aspectCache.put(aspectClass, instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
return aspectCache.computeIfAbsent(aspectClass,
|
||||
clazz -> new SimpleAspectInstanceFactory(clazz).getAspectInstance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -114,6 +114,15 @@ public interface Advised extends TargetClassAware {
|
||||
*/
|
||||
Advisor[] getAdvisors();
|
||||
|
||||
/**
|
||||
* Return the number of advisors applying to this proxy.
|
||||
* <p>The default implementation delegates to {@code getAdvisors().length}.
|
||||
* @since 5.3.1
|
||||
*/
|
||||
default int getAdvisorCount() {
|
||||
return getAdvisors().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an advisor at the end of the advisor chain.
|
||||
* <p>The Advisor may be an {@link org.springframework.aop.IntroductionAdvisor},
|
||||
|
||||
@@ -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.
|
||||
@@ -95,12 +95,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
*/
|
||||
private List<Advisor> advisors = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Array updated on changes to the advisors list, which is easier
|
||||
* to manipulate internally.
|
||||
*/
|
||||
private Advisor[] advisorArray = new Advisor[0];
|
||||
|
||||
|
||||
/**
|
||||
* No-arg constructor for use as a JavaBean.
|
||||
@@ -244,7 +238,12 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
|
||||
@Override
|
||||
public final Advisor[] getAdvisors() {
|
||||
return this.advisorArray;
|
||||
return this.advisors.toArray(new Advisor[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAdvisorCount() {
|
||||
return this.advisors.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -292,7 +291,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
}
|
||||
}
|
||||
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
|
||||
@@ -339,7 +337,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
Assert.notNull(advisor, "Advisor must not be null");
|
||||
this.advisors.add(advisor);
|
||||
}
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
}
|
||||
@@ -363,27 +360,18 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
"Illegal position " + pos + " in advisor list with size " + this.advisors.size());
|
||||
}
|
||||
this.advisors.add(pos, advisor);
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the array up to date with the list.
|
||||
*/
|
||||
protected final void updateAdvisorArray() {
|
||||
this.advisorArray = this.advisors.toArray(new Advisor[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows uncontrolled access to the {@link List} of {@link Advisor Advisors}.
|
||||
* <p>Use with care, and remember to {@link #updateAdvisorArray() refresh the advisor array}
|
||||
* and {@link #adviceChanged() fire advice changed events} when making any modifications.
|
||||
* <p>Use with care, and remember to {@link #adviceChanged() fire advice changed events}
|
||||
* when making any modifications.
|
||||
*/
|
||||
protected final List<Advisor> getAdvisorsInternal() {
|
||||
return this.advisors;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void addAdvice(Advice advice) throws AopConfigException {
|
||||
int pos = this.advisors.size();
|
||||
@@ -521,7 +509,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
Assert.notNull(advisor, "Advisor must not be null");
|
||||
this.advisors.add(advisor);
|
||||
}
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
|
||||
@@ -536,7 +523,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
copy.advisorChainFactory = this.advisorChainFactory;
|
||||
copy.interfaces = this.interfaces;
|
||||
copy.advisors = this.advisors;
|
||||
copy.updateAdvisorArray();
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -553,7 +539,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
this.methodCache = new ConcurrentHashMap<>(32);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toProxyConfigString() {
|
||||
return toString();
|
||||
|
||||
@@ -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.
|
||||
@@ -206,7 +206,7 @@ public abstract class AopProxyUtils {
|
||||
* Check equality of the advisors behind the given AdvisedSupport objects.
|
||||
*/
|
||||
public static boolean equalsAdvisors(AdvisedSupport a, AdvisedSupport b) {
|
||||
return Arrays.equals(a.getAdvisors(), b.getAdvisors());
|
||||
return a.getAdvisorCount() == b.getAdvisorCount() && Arrays.equals(a.getAdvisors(), b.getAdvisors());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
*/
|
||||
public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
|
||||
Assert.notNull(config, "AdvisedSupport must not be null");
|
||||
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
throw new AopConfigException("No advisors and no TargetSource specified");
|
||||
}
|
||||
this.advised = config;
|
||||
@@ -942,11 +942,11 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
}
|
||||
// Advice instance identity is unimportant to the proxy class:
|
||||
// All that matters is type and ordering.
|
||||
Advisor[] thisAdvisors = this.advised.getAdvisors();
|
||||
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
|
||||
if (thisAdvisors.length != thatAdvisors.length) {
|
||||
if (this.advised.getAdvisorCount() != otherAdvised.getAdvisorCount()) {
|
||||
return false;
|
||||
}
|
||||
Advisor[] thisAdvisors = this.advised.getAdvisors();
|
||||
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
|
||||
for (int i = 0; i < thisAdvisors.length; i++) {
|
||||
Advisor thisAdvisor = thisAdvisors[i];
|
||||
Advisor thatAdvisor = thatAdvisors[i];
|
||||
|
||||
@@ -104,7 +104,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
|
||||
*/
|
||||
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
|
||||
Assert.notNull(config, "AdvisedSupport must not be null");
|
||||
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
throw new AopConfigException("No advisors and no TargetSource specified");
|
||||
}
|
||||
this.advised = config;
|
||||
|
||||
+6
-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.
|
||||
@@ -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;
|
||||
|
||||
+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());
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -644,21 +644,20 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -157,7 +146,8 @@ public class InjectionMetadata {
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-4
@@ -1288,10 +1288,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
* @param registry the PropertyEditorRegistry to initialize
|
||||
*/
|
||||
protected void registerCustomEditors(PropertyEditorRegistry registry) {
|
||||
PropertyEditorRegistrySupport registrySupport =
|
||||
(registry instanceof PropertyEditorRegistrySupport ? (PropertyEditorRegistrySupport) registry : null);
|
||||
if (registrySupport != null) {
|
||||
registrySupport.useConfigValueEditors();
|
||||
if (registry instanceof PropertyEditorRegistrySupport) {
|
||||
((PropertyEditorRegistrySupport) registry).useConfigValueEditors();
|
||||
}
|
||||
if (!this.propertyEditorRegistrars.isEmpty()) {
|
||||
for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
|
||||
|
||||
+1
-2
@@ -155,8 +155,7 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing
|
||||
* @param name the name of the cache to be added
|
||||
*/
|
||||
private void updateCacheNames(String name) {
|
||||
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames.size() + 1);
|
||||
cacheNames.addAll(this.cacheNames);
|
||||
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames);
|
||||
cacheNames.add(name);
|
||||
this.cacheNames = Collections.unmodifiableSet(cacheNames);
|
||||
}
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
* @see #setBeanNameGenerator
|
||||
*/
|
||||
public static final AnnotationBeanNameGenerator IMPORT_BEAN_NAME_GENERATOR =
|
||||
new FullyQualifiedAnnotationBeanNameGenerator();
|
||||
FullyQualifiedAnnotationBeanNameGenerator.INSTANCE;
|
||||
|
||||
private static final String IMPORT_REGISTRY_BEAN_NAME =
|
||||
ConfigurationClassPostProcessor.class.getName() + ".importRegistry";
|
||||
|
||||
+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();
|
||||
|
||||
+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();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -264,14 +264,12 @@ public class ReactiveAdapterRegistry {
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.multiValue(io.reactivex.Observable.class, io.reactivex.Observable::empty),
|
||||
source -> ((io.reactivex.Observable<?>) source).toFlowable(io.reactivex.BackpressureStrategy.BUFFER),
|
||||
source -> io.reactivex.Flowable.fromPublisher(source)
|
||||
.toObservable()
|
||||
io.reactivex.Observable::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.Single.class),
|
||||
source -> ((io.reactivex.Single<?>) source).toFlowable(),
|
||||
source -> io.reactivex.Flowable.fromPublisher(source)
|
||||
.toObservable().singleElement().toSingle()
|
||||
io.reactivex.Single::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleOptionalValue(io.reactivex.Maybe.class, io.reactivex.Maybe::empty),
|
||||
@@ -282,8 +280,7 @@ public class ReactiveAdapterRegistry {
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.noValue(io.reactivex.Completable.class, io.reactivex.Completable::complete),
|
||||
source -> ((io.reactivex.Completable) source).toFlowable(),
|
||||
source -> io.reactivex.Flowable.fromPublisher(source)
|
||||
.toObservable().ignoreElements()
|
||||
io.reactivex.Completable::fromPublisher
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -304,30 +301,26 @@ public class ReactiveAdapterRegistry {
|
||||
io.reactivex.rxjava3.core.Observable::empty),
|
||||
source -> ((io.reactivex.rxjava3.core.Observable<?>) source).toFlowable(
|
||||
io.reactivex.rxjava3.core.BackpressureStrategy.BUFFER),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable()
|
||||
io.reactivex.rxjava3.core.Observable::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.rxjava3.core.Single.class),
|
||||
source -> ((io.reactivex.rxjava3.core.Single<?>) source).toFlowable(),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable().singleElement().toSingle()
|
||||
io.reactivex.rxjava3.core.Single::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleOptionalValue(
|
||||
io.reactivex.rxjava3.core.Maybe.class,
|
||||
io.reactivex.rxjava3.core.Maybe::empty),
|
||||
source -> ((io.reactivex.rxjava3.core.Maybe<?>) source).toFlowable(),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable().singleElement()
|
||||
io.reactivex.rxjava3.core.Maybe::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.noValue(
|
||||
io.reactivex.rxjava3.core.Completable.class,
|
||||
io.reactivex.rxjava3.core.Completable::complete),
|
||||
source -> ((io.reactivex.rxjava3.core.Completable) source).toFlowable(),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable().ignoreElements()
|
||||
io.reactivex.rxjava3.core.Completable::fromPublisher
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -118,7 +118,7 @@ public final class Property {
|
||||
}
|
||||
|
||||
|
||||
// package private
|
||||
// Package private
|
||||
|
||||
MethodParameter getMethodParameter() {
|
||||
return this.methodParameter;
|
||||
@@ -132,7 +132,7 @@ public final class Property {
|
||||
}
|
||||
|
||||
|
||||
// internal helpers
|
||||
// Internal helpers
|
||||
|
||||
private String resolveName() {
|
||||
if (this.readMethod != null) {
|
||||
@@ -142,10 +142,13 @@ public final class Property {
|
||||
}
|
||||
else {
|
||||
index = this.readMethod.getName().indexOf("is");
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException("Not a getter method");
|
||||
if (index != -1) {
|
||||
index += 2;
|
||||
}
|
||||
else {
|
||||
// Record-style plain accessor method, e.g. name()
|
||||
index = 0;
|
||||
}
|
||||
index += 2;
|
||||
}
|
||||
return StringUtils.uncapitalize(this.readMethod.getName().substring(index));
|
||||
}
|
||||
|
||||
+55
-30
@@ -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,34 +63,36 @@ 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;
|
||||
private volatile ChildClassLoader childClassLoader;
|
||||
|
||||
// Counter suffix for generated classes within this SpelCompiler instance
|
||||
private final AtomicInteger suffixId = new AtomicInteger(1);
|
||||
|
||||
|
||||
private SpelCompiler(@Nullable ClassLoader classloader) {
|
||||
this.ccl = new ChildClassLoader(classloader);
|
||||
this.childClassLoader = new ChildClassLoader(classloader);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -135,7 +137,7 @@ public final class SpelCompiler implements Opcodes {
|
||||
// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'
|
||||
String className = "spel/Ex" + getNextSuffix();
|
||||
ClassWriter cw = new ExpressionClassWriter();
|
||||
cw.visit(V1_5, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
|
||||
cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
|
||||
|
||||
// Create default constructor
|
||||
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
@@ -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
|
||||
@@ -196,12 +198,25 @@ public final class SpelCompiler implements Opcodes {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Class<? extends CompiledExpression> loadClass(String name, byte[] bytes) {
|
||||
if (this.ccl.getClassesDefinedCount() > CLASSES_DEFINED_LIMIT) {
|
||||
this.ccl = new ChildClassLoader(this.ccl.getParent());
|
||||
ChildClassLoader ccl = this.childClassLoader;
|
||||
if (ccl.getClassesDefinedCount() >= CLASSES_DEFINED_LIMIT) {
|
||||
synchronized (this) {
|
||||
ChildClassLoader currentCcl = this.childClassLoader;
|
||||
if (ccl == currentCcl) {
|
||||
// Still the same ClassLoader that needs to be replaced...
|
||||
ccl = new ChildClassLoader(ccl.getParent());
|
||||
this.childClassLoader = ccl;
|
||||
}
|
||||
else {
|
||||
// Already replaced by some other thread, let's pick it up.
|
||||
ccl = currentCcl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (Class<? extends CompiledExpression>) this.ccl.defineClass(name, bytes);
|
||||
return (Class<? extends CompiledExpression>) 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
|
||||
@@ -211,21 +226,28 @@ public final class SpelCompiler implements Opcodes {
|
||||
*/
|
||||
public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) {
|
||||
ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
|
||||
synchronized (compilers) {
|
||||
SpelCompiler compiler = compilers.get(clToUse);
|
||||
if (compiler == null) {
|
||||
compiler = new SpelCompiler(clToUse);
|
||||
compilers.put(clToUse, compiler);
|
||||
// Quick check for existing compiler without lock contention
|
||||
SpelCompiler compiler = compilers.get(clToUse);
|
||||
if (compiler == null) {
|
||||
// Full lock now since we're creating a child ClassLoader
|
||||
synchronized (compilers) {
|
||||
compiler = compilers.get(clToUse);
|
||||
if (compiler == null) {
|
||||
compiler = new SpelCompiler(clToUse);
|
||||
compilers.put(clToUse, compiler);
|
||||
}
|
||||
}
|
||||
return compiler;
|
||||
}
|
||||
return compiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
@@ -250,24 +272,27 @@ public final class SpelCompiler implements Opcodes {
|
||||
|
||||
private static final URL[] NO_URLS = new URL[0];
|
||||
|
||||
private int classesDefinedCount = 0;
|
||||
private final AtomicInteger classesDefinedCount = new AtomicInteger(0);
|
||||
|
||||
public ChildClassLoader(@Nullable ClassLoader classLoader) {
|
||||
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++;
|
||||
this.classesDefinedCount.incrementAndGet();
|
||||
return clazz;
|
||||
}
|
||||
|
||||
public int getClassesDefinedCount() {
|
||||
return this.classesDefinedCount.get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An ASM ClassWriter extension bound to the SpelCompiler's ClassLoader.
|
||||
*/
|
||||
private class ExpressionClassWriter extends ClassWriter {
|
||||
|
||||
public ExpressionClassWriter() {
|
||||
@@ -276,7 +301,7 @@ public final class SpelCompiler implements Opcodes {
|
||||
|
||||
@Override
|
||||
protected ClassLoader getClassLoader() {
|
||||
return ccl;
|
||||
return childClassLoader;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -395,6 +395,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
|
||||
if (method == null) {
|
||||
method = findMethodForProperty(getPropertyMethodSuffixes(propertyName),
|
||||
"is", clazz, mustBeStatic, 0, BOOLEAN_TYPES);
|
||||
if (method == null) {
|
||||
// Record-style plain accessor method, e.g. name()
|
||||
method = findMethodForProperty(new String[] {propertyName},
|
||||
"", clazz, mustBeStatic, 0, ANY_TYPES);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
}
|
||||
@@ -683,12 +688,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
|
||||
return true;
|
||||
}
|
||||
getterName = "is" + StringUtils.capitalize(name);
|
||||
return getterName.equals(method.getName());
|
||||
}
|
||||
else {
|
||||
Field field = (Field) this.member;
|
||||
return field.getName().equals(name);
|
||||
if (getterName.equals(method.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.member.getName().equals(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+16
-1
@@ -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 org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.expression.spel.testresources.Inventor;
|
||||
import org.springframework.expression.spel.testresources.Person;
|
||||
import org.springframework.expression.spel.testresources.RecordPerson;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -191,6 +192,20 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
parser.parseExpression("name='p3'").getValue(context, target));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void propertyReadOnlyWithRecordStyle() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
Expression expr = parser.parseExpression("name");
|
||||
RecordPerson target1 = new RecordPerson("p1");
|
||||
assertThat(expr.getValue(context, target1)).isEqualTo("p1");
|
||||
RecordPerson target2 = new RecordPerson("p2");
|
||||
assertThat(expr.getValue(context, target2)).isEqualTo("p2");
|
||||
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
|
||||
parser.parseExpression("name='p3'").getValue(context, target2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void propertyReadWrite() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
|
||||
|
||||
+19
-25
@@ -4180,6 +4180,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value4");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value4");
|
||||
|
||||
// record-style accessor
|
||||
expression = parser.parseExpression("strawberry");
|
||||
assertCantCompile(expression);
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value5");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value5");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -4553,23 +4560,9 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
Object v = expression.getValue(ctx,holder);
|
||||
assertThat(v).isEqualTo("abc");
|
||||
|
||||
// // time it interpreted
|
||||
// long stime = System.currentTimeMillis();
|
||||
// for (int i = 0; i < 100000; i++) {
|
||||
// v = expression.getValue(ctx,holder);
|
||||
// }
|
||||
// System.out.println((System.currentTimeMillis() - stime));
|
||||
|
||||
assertCanCompile(expression);
|
||||
v = expression.getValue(ctx,holder);
|
||||
assertThat(v).isEqualTo("abc");
|
||||
|
||||
// // time it compiled
|
||||
// stime = System.currentTimeMillis();
|
||||
// for (int i = 0; i < 100000; i++) {
|
||||
// v = expression.getValue(ctx,holder);
|
||||
// }
|
||||
// System.out.println((System.currentTimeMillis() - stime));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -4985,13 +4978,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(fast.compileExpression()).isTrue();
|
||||
r.setValue2(null);
|
||||
// try the numbers 0,1,2,null
|
||||
for (int i=0;i<4;i++) {
|
||||
r.setValue(i<3?i:null);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
r.setValue(i < 3 ? i : null);
|
||||
boolean slowResult = (Boolean)slow.getValue(ctx);
|
||||
boolean fastResult = (Boolean)fast.getValue(ctx);
|
||||
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
|
||||
assertThat(fastResult).as(" Differing results: expression="+expressionText+
|
||||
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
|
||||
assertThat(fastResult).as("Differing results: expression=" + expressionText +
|
||||
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5002,13 +4994,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(fast.compileExpression()).isTrue();
|
||||
Reg r = (Reg)ctx.getRootObject().getValue();
|
||||
// try the numbers 0,1,2,null
|
||||
for (int i=0;i<4;i++) {
|
||||
r.setValue(i<3?i:null);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
r.setValue(i < 3 ? i : null);
|
||||
boolean slowResult = (Boolean)slow.getValue(ctx);
|
||||
boolean fastResult = (Boolean)fast.getValue(ctx);
|
||||
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
|
||||
assertThat(fastResult).as(" Differing results: expression="+expressionText+
|
||||
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
|
||||
assertThat(fastResult).as("Differing results: expression=" + expressionText +
|
||||
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5839,7 +5830,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
|
||||
public String orange = "value1";
|
||||
public static String apple = "value2";
|
||||
|
||||
public long peach = 34L;
|
||||
|
||||
public String getBanana() {
|
||||
@@ -5849,6 +5839,10 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
public static String getPlum() {
|
||||
return "value4";
|
||||
}
|
||||
|
||||
public String strawberry() {
|
||||
return "value5";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.expression.spel.testresources;
|
||||
|
||||
public class RecordPerson {
|
||||
|
||||
private String name;
|
||||
|
||||
private Company company;
|
||||
|
||||
public RecordPerson(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public RecordPerson(String name, Company company) {
|
||||
this.name = name;
|
||||
this.company = company;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Company company() {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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) {
|
||||
|
||||
+4
-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.
|
||||
@@ -83,7 +83,7 @@ public abstract class NamedParameterUtils {
|
||||
Assert.notNull(sql, "SQL must not be null");
|
||||
|
||||
Set<String> namedParameters = new HashSet<>();
|
||||
String sqlToUse = sql;
|
||||
StringBuilder sqlToUse = new StringBuilder(sql);
|
||||
List<ParameterHolder> parameterList = new ArrayList<>();
|
||||
|
||||
char[] statement = sql.toCharArray();
|
||||
@@ -155,7 +155,7 @@ public abstract class NamedParameterUtils {
|
||||
int j = i + 1;
|
||||
if (j < statement.length && statement[j] == ':') {
|
||||
// escaped ":" should be skipped
|
||||
sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1);
|
||||
sqlToUse.deleteCharAt(i - escapes);
|
||||
escapes++;
|
||||
i = i + 2;
|
||||
continue;
|
||||
@@ -174,7 +174,7 @@ public abstract class NamedParameterUtils {
|
||||
}
|
||||
i++;
|
||||
}
|
||||
ParsedSql parsedSql = new ParsedSql(sqlToUse);
|
||||
ParsedSql parsedSql = new ParsedSql(sqlToUse.toString());
|
||||
for (ParameterHolder ph : parameterList) {
|
||||
parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
|
||||
}
|
||||
|
||||
+23
-24
@@ -56,6 +56,9 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
@Nullable
|
||||
private TcpOperations<byte[]> tcpClient;
|
||||
|
||||
@Nullable
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
private boolean autoStartup = true;
|
||||
|
||||
@Nullable
|
||||
@@ -64,8 +67,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
@Nullable
|
||||
private String userRegistryBroadcast;
|
||||
|
||||
@Nullable
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel,
|
||||
MessageChannel clientOutboundChannel, String[] destinationPrefixes) {
|
||||
@@ -181,8 +182,27 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
* specified are effectively ignored.
|
||||
* @since 4.3.15
|
||||
*/
|
||||
public void setTcpClient(TcpOperations<byte[]> tcpClient) {
|
||||
public StompBrokerRelayRegistration setTcpClient(TcpOperations<byte[]> tcpClient) {
|
||||
this.tcpClient = tcpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some STOMP clients (e.g. stomp-js) always send heartbeats at a fixed rate
|
||||
* but others (Spring STOMP client) do so only when no other messages are
|
||||
* sent. However messages with a non-broker {@link #getDestinationPrefixes()
|
||||
* destination prefix} aren't forwarded and as a result the broker may deem
|
||||
* the connection inactive.
|
||||
* <p>When this {@link TaskScheduler} is set, it is used to reset a count of
|
||||
* the number of messages sent from client to broker since the beginning of
|
||||
* the current heartbeat period. This is then used to decide whether to send
|
||||
* a heartbeat to the broker when ignoring a message with a non-broker
|
||||
* destination prefix.
|
||||
* @since 5.3
|
||||
*/
|
||||
public StompBrokerRelayRegistration setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,26 +248,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some STOMP clients (e.g. stomp-js) always send heartbeats at a fixed rate
|
||||
* but others (Spring STOMP client) do so only when no other messages are
|
||||
* sent. However messages with a non-broker {@link #getDestinationPrefixes()
|
||||
* destination prefix} aren't forwarded and as a result the broker may deem
|
||||
* the connection inactive.
|
||||
*
|
||||
* <p>When this {@link TaskScheduler} is set, it is used to reset a count of
|
||||
* the number of messages sent from client to broker since the beginning of
|
||||
* the current heartbeat period. This is then used to decide whether to send
|
||||
* a heartbeat to the broker when ignoring a message with a non-broker
|
||||
* destination prefix.
|
||||
*
|
||||
* @param taskScheduler the scheduler to use
|
||||
* @since 5.3
|
||||
*/
|
||||
public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String getUserRegistryBroadcast() {
|
||||
return this.userRegistryBroadcast;
|
||||
@@ -256,7 +256,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
|
||||
@Override
|
||||
protected StompBrokerRelayMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
|
||||
|
||||
StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(
|
||||
getClientInboundChannel(), getClientOutboundChannel(),
|
||||
brokerChannel, getDestinationPrefixes());
|
||||
|
||||
@@ -95,7 +95,7 @@ abstract class NamedParameterUtils {
|
||||
Assert.notNull(sql, "SQL must not be null");
|
||||
|
||||
Set<String> namedParameters = new HashSet<>();
|
||||
String sqlToUse = sql;
|
||||
StringBuilder sqlToUse = new StringBuilder(sql);
|
||||
List<ParameterHolder> parameterList = new ArrayList<>();
|
||||
|
||||
char[] statement = sql.toCharArray();
|
||||
@@ -171,8 +171,7 @@ abstract class NamedParameterUtils {
|
||||
int j = i + 1;
|
||||
if (j < statement.length && statement[j] == ':') {
|
||||
// escaped ":" should be skipped
|
||||
sqlToUse = sqlToUse.substring(0, i - escapes)
|
||||
+ sqlToUse.substring(i - escapes + 1);
|
||||
sqlToUse.deleteCharAt(i - escapes);
|
||||
escapes++;
|
||||
i = i + 2;
|
||||
continue;
|
||||
@@ -181,7 +180,7 @@ abstract class NamedParameterUtils {
|
||||
}
|
||||
i++;
|
||||
}
|
||||
ParsedSql parsedSql = new ParsedSql(sqlToUse);
|
||||
ParsedSql parsedSql = new ParsedSql(sqlToUse.toString());
|
||||
for (ParameterHolder ph : parameterList) {
|
||||
parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -83,7 +83,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);
|
||||
}
|
||||
|
||||
|
||||
+6
-7
@@ -492,10 +492,11 @@ public abstract class TestContextAnnotationUtils {
|
||||
Assert.notNull(annotation, "Annotation must not be null");
|
||||
this.rootDeclaringClass = rootDeclaringClass;
|
||||
this.declaringClass = declaringClass;
|
||||
this.annotation = (T) AnnotatedElementUtils.findMergedAnnotation(
|
||||
T mergedAnnotation = (T) AnnotatedElementUtils.findMergedAnnotation(
|
||||
rootDeclaringClass, annotation.annotationType());
|
||||
Assert.state(this.annotation != null,
|
||||
Assert.state(mergedAnnotation != null,
|
||||
() -> "Failed to find merged annotation for " + annotation);
|
||||
this.annotation = mergedAnnotation;
|
||||
}
|
||||
|
||||
public Class<?> getRootDeclaringClass() {
|
||||
@@ -545,15 +546,13 @@ public abstract class TestContextAnnotationUtils {
|
||||
/**
|
||||
* Find <strong>all</strong> annotations of the specified annotation type
|
||||
* that are present or meta-present on the {@linkplain #getRootDeclaringClass()
|
||||
* root declaring class} of this descriptor.
|
||||
* root declaring class} of this descriptor or on any interfaces that the
|
||||
* root declaring class implements.
|
||||
* @return the set of all merged, synthesized {@code Annotations} found,
|
||||
* or an empty set if none were found
|
||||
*/
|
||||
public Set<T> findAllLocalMergedAnnotations() {
|
||||
SearchStrategy searchStrategy =
|
||||
(getEnclosingConfiguration(getRootDeclaringClass()) == EnclosingConfiguration.INHERIT ?
|
||||
SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES :
|
||||
SearchStrategy.TYPE_HIERARCHY);
|
||||
SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY;
|
||||
return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none())
|
||||
.stream(getAnnotationType())
|
||||
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
|
||||
|
||||
+17
-7
@@ -16,8 +16,10 @@
|
||||
|
||||
package org.springframework.test.context.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
@@ -70,8 +72,8 @@ abstract class ActiveProfilesUtils {
|
||||
static String[] resolveActiveProfiles(Class<?> testClass) {
|
||||
Assert.notNull(testClass, "Class must not be null");
|
||||
|
||||
Set<String> activeProfiles = new TreeSet<>();
|
||||
AnnotationDescriptor<ActiveProfiles> descriptor = findAnnotationDescriptor(testClass, ActiveProfiles.class);
|
||||
List<String[]> profileArrays = new ArrayList<>();
|
||||
|
||||
if (descriptor == null && logger.isDebugEnabled()) {
|
||||
logger.debug(String.format(
|
||||
@@ -107,16 +109,24 @@ abstract class ActiveProfilesUtils {
|
||||
|
||||
String[] profiles = resolver.resolve(rootDeclaringClass);
|
||||
if (!ObjectUtils.isEmpty(profiles)) {
|
||||
for (String profile : profiles) {
|
||||
if (StringUtils.hasText(profile)) {
|
||||
activeProfiles.add(profile.trim());
|
||||
}
|
||||
}
|
||||
// Prepend to the list so that we can later traverse "down" the hierarchy
|
||||
// to ensure that we retain the top-down profile registration order
|
||||
// within a test class hierarchy.
|
||||
profileArrays.add(0, profiles);
|
||||
}
|
||||
|
||||
descriptor = (annotation.inheritProfiles() ? descriptor.next() : null);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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,13 +339,13 @@ 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
|
||||
void equalsWithSameDuplicateProfiles() {
|
||||
String[] activeProfiles1 = new String[] { "catbert", "dogbert" };
|
||||
String[] activeProfiles2 = new String[] { "dogbert", "catbert", "dogbert", "catbert" };
|
||||
String[] activeProfiles2 = new String[] { "catbert", "dogbert", "catbert", "dogbert", "catbert" };
|
||||
MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(),
|
||||
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles1, loader);
|
||||
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(),
|
||||
|
||||
+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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-5
@@ -373,6 +373,8 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
|
||||
void buildMergedConfigWithDuplicateConfigurationOnEnclosingClassAndNestedClass() {
|
||||
compareApplesToApples(AppleConfigTestCase.class, AppleConfigTestCase.Nested.class);
|
||||
compareApplesToApples(AppleConfigTestCase.Nested.class, AppleConfigTestCase.Nested.DoubleNested.class);
|
||||
compareApplesToOranges(ApplesAndOrangesConfigTestCase.class, ApplesAndOrangesConfigTestCase.Nested.class);
|
||||
compareApplesToOranges(ApplesAndOrangesConfigTestCase.Nested.class, ApplesAndOrangesConfigTestCase.Nested.DoubleNested.class);
|
||||
}
|
||||
|
||||
private void compareApplesToApples(Class<?> parent, Class<?> child) {
|
||||
@@ -400,7 +402,7 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
|
||||
DelegatingSmartContextLoader.class);
|
||||
|
||||
assertThat(parentMergedConfig.getActiveProfiles()).as("active profiles")
|
||||
.containsExactly("apples", "oranges")
|
||||
.containsExactly("oranges", "apples")
|
||||
.isEqualTo(childMergedConfig.getActiveProfiles());
|
||||
assertThat(parentMergedConfig).isEqualTo(childMergedConfig);
|
||||
}
|
||||
@@ -531,7 +533,7 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = AppleConfig.class)
|
||||
@ActiveProfiles({"apples", "oranges"})
|
||||
@ActiveProfiles({"oranges", "apples"})
|
||||
static class ApplesAndOrangesConfigTestCase {
|
||||
|
||||
@ContextConfiguration(classes = AppleConfig.class)
|
||||
@@ -539,19 +541,19 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
|
||||
class Nested {
|
||||
|
||||
@ContextConfiguration(classes = AppleConfig.class)
|
||||
@ActiveProfiles(profiles = {"apples", "oranges", "apples"}, inheritProfiles = false)
|
||||
@ActiveProfiles(profiles = {"oranges", "apples", "oranges"}, inheritProfiles = false)
|
||||
class DoubleNested {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = AppleConfig.class)
|
||||
@ActiveProfiles(profiles = {"oranges", "apples"}, inheritProfiles = false)
|
||||
@ActiveProfiles(profiles = {"oranges", "apples", "oranges"}, inheritProfiles = false)
|
||||
static class DuplicateConfigApplesAndOrangesConfigTestCase extends ApplesAndOrangesConfigTestCase {
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = AppleConfig.class)
|
||||
@ActiveProfiles(profiles = {"apples", "oranges", "apples"}, inheritProfiles = false)
|
||||
@ActiveProfiles(profiles = {"oranges", "apples", "oranges"}, inheritProfiles = false)
|
||||
static class SubDuplicateConfigApplesAndOrangesConfigTestCase extends DuplicateConfigApplesAndOrangesConfigTestCase {
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -352,9 +352,9 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
|
||||
}
|
||||
return new ReactiveTransactionSupport(adapter);
|
||||
});
|
||||
Publisher<?> publisher = (Publisher<?>) txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
|
||||
return (isSuspendingFunction ? (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow(publisher) :
|
||||
KotlinDelegate.awaitSingleOrNull(publisher, ((CoroutinesInvocationCallback) invocation).getContinuation())) : publisher);
|
||||
Object result = txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
|
||||
return (isSuspendingFunction ? (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow((Publisher<?>) result) :
|
||||
KotlinDelegate.awaitSingleOrNull((Publisher<?>) result, ((CoroutinesInvocationCallback) invocation).getContinuation())) : result);
|
||||
}
|
||||
|
||||
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
|
||||
@@ -875,8 +875,8 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
|
||||
|
||||
String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
|
||||
|
||||
// Optimize for Mono
|
||||
if (Mono.class.isAssignableFrom(method.getReturnType())) {
|
||||
// For Mono and suspending functions not returning kotlinx.coroutines.flow.Flow
|
||||
if (Mono.class.isAssignableFrom(method.getReturnType()) || (KotlinDetector.isSuspendingFunction(method) && !COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()))) {
|
||||
return TransactionContextManager.currentContext().flatMap(context ->
|
||||
createTransactionIfNecessary(rtm, txAttr, joinpointIdentification).flatMap(it -> {
|
||||
try {
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.transaction.annotation
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.aop.framework.ProxyFactory
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor
|
||||
import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class CoroutinesAnnotationTransactionInterceptorTests {
|
||||
|
||||
private val rtm = ReactiveCallCountingTransactionManager()
|
||||
|
||||
private val source = AnnotationTransactionAttributeSource()
|
||||
|
||||
@Test
|
||||
fun suspendingNoValueSuccess() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(TestWithCoroutines())
|
||||
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
|
||||
val proxy = proxyFactory.proxy as TestWithCoroutines
|
||||
runBlocking {
|
||||
proxy.suspendingNoValueSuccess()
|
||||
}
|
||||
assertReactiveGetTransactionAndCommitCount(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingNoValueFailure() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(TestWithCoroutines())
|
||||
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
|
||||
val proxy = proxyFactory.proxy as TestWithCoroutines
|
||||
runBlocking {
|
||||
try {
|
||||
proxy.suspendingNoValueFailure()
|
||||
}
|
||||
catch (ex: IllegalStateException) {
|
||||
}
|
||||
|
||||
}
|
||||
assertReactiveGetTransactionAndRollbackCount(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueSuccess() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(TestWithCoroutines())
|
||||
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
|
||||
val proxy = proxyFactory.proxy as TestWithCoroutines
|
||||
runBlocking {
|
||||
Assertions.assertThat(proxy.suspendingValueSuccess()).isEqualTo("foo")
|
||||
}
|
||||
assertReactiveGetTransactionAndCommitCount(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueFailure() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(TestWithCoroutines())
|
||||
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
|
||||
val proxy = proxyFactory.proxy as TestWithCoroutines
|
||||
runBlocking {
|
||||
try {
|
||||
proxy.suspendingValueFailure()
|
||||
Assertions.fail("No exception thrown as expected")
|
||||
}
|
||||
catch (ex: IllegalStateException) {
|
||||
}
|
||||
}
|
||||
assertReactiveGetTransactionAndRollbackCount(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingFlowSuccess() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(TestWithCoroutines())
|
||||
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
|
||||
val proxy = proxyFactory.proxy as TestWithCoroutines
|
||||
runBlocking {
|
||||
Assertions.assertThat(proxy.suspendingFlowSuccess().toList()).containsExactly("foo", "foo")
|
||||
}
|
||||
assertReactiveGetTransactionAndCommitCount(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun flowSuccess() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(TestWithCoroutines())
|
||||
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
|
||||
val proxy = proxyFactory.proxy as TestWithCoroutines
|
||||
runBlocking {
|
||||
Assertions.assertThat(proxy.flowSuccess().toList()).containsExactly("foo", "foo")
|
||||
}
|
||||
assertReactiveGetTransactionAndCommitCount(1)
|
||||
}
|
||||
|
||||
private fun assertReactiveGetTransactionAndCommitCount(expectedCount: Int) {
|
||||
Assertions.assertThat(rtm.begun).isEqualTo(expectedCount)
|
||||
Assertions.assertThat(rtm.commits).isEqualTo(expectedCount)
|
||||
}
|
||||
|
||||
private fun assertReactiveGetTransactionAndRollbackCount(expectedCount: Int) {
|
||||
Assertions.assertThat(rtm.begun).isEqualTo(expectedCount)
|
||||
Assertions.assertThat(rtm.rollbacks).isEqualTo(expectedCount)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
open class TestWithCoroutines {
|
||||
|
||||
open suspend fun suspendingNoValueSuccess() {
|
||||
delay(10)
|
||||
}
|
||||
|
||||
open suspend fun suspendingNoValueFailure() {
|
||||
delay(10)
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
open suspend fun suspendingValueSuccess(): String {
|
||||
delay(10)
|
||||
return "foo"
|
||||
}
|
||||
|
||||
open suspend fun suspendingValueFailure(): String {
|
||||
delay(10)
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
open fun flowSuccess(): Flow<String> {
|
||||
return flow {
|
||||
emit("foo")
|
||||
delay(10)
|
||||
emit("foo")
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun suspendingFlowSuccess(): Flow<String> {
|
||||
delay(10)
|
||||
return flow {
|
||||
emit("foo")
|
||||
delay(10)
|
||||
emit("foo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-2
@@ -21,6 +21,7 @@ import java.lang.annotation.Annotation;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
@@ -99,8 +100,20 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple
|
||||
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
|
||||
JavaType javaType = getObjectMapper().constructType(elementType.getType());
|
||||
// Skip String: CharSequenceDecoder + "*/*" comes after
|
||||
return (!CharSequence.class.isAssignableFrom(elementType.toClass()) &&
|
||||
getObjectMapper().canDeserialize(javaType) && supportsMimeType(mimeType));
|
||||
if (CharSequence.class.isAssignableFrom(elementType.toClass()) || !supportsMimeType(mimeType)) {
|
||||
return false;
|
||||
}
|
||||
if (!logger.isDebugEnabled()) {
|
||||
return getObjectMapper().canDeserialize(javaType);
|
||||
}
|
||||
else {
|
||||
AtomicReference<Throwable> causeRef = new AtomicReference<>();
|
||||
if (getObjectMapper().canDeserialize(javaType, causeRef)) {
|
||||
return true;
|
||||
}
|
||||
logWarningIfNecessary(javaType, causeRef.get());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+37
-6
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonEncoding;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
@@ -33,6 +34,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
import com.fasterxml.jackson.databind.SequenceWriter;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
|
||||
import com.fasterxml.jackson.databind.ser.FilterProvider;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -47,6 +49,7 @@ import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.HttpMessageEncoder;
|
||||
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -111,8 +114,23 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (Object.class == clazz ||
|
||||
(!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
|
||||
if (String.class.isAssignableFrom(elementType.resolve(clazz))) {
|
||||
return false;
|
||||
}
|
||||
if (Object.class == clazz) {
|
||||
return true;
|
||||
}
|
||||
if (!logger.isDebugEnabled()) {
|
||||
return getObjectMapper().canSerialize(clazz);
|
||||
}
|
||||
else {
|
||||
AtomicReference<Throwable> causeRef = new AtomicReference<>();
|
||||
if (getObjectMapper().canSerialize(clazz, causeRef)) {
|
||||
return true;
|
||||
}
|
||||
logWarningIfNecessary(clazz, causeRef.get());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,7 +150,7 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
|
||||
byte[] separator = getStreamingMediaTypeSeparator(mimeType);
|
||||
if (separator != null) { // streaming
|
||||
try {
|
||||
ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
|
||||
ObjectWriter writer = createObjectWriter(elementType, mimeType, null, hints);
|
||||
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
|
||||
JsonEncoding encoding = getJsonEncoding(mimeType);
|
||||
JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding);
|
||||
@@ -170,7 +188,18 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
|
||||
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
|
||||
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
ObjectWriter writer = createObjectWriter(valueType, mimeType, hints);
|
||||
Class<?> jsonView = null;
|
||||
FilterProvider filters = null;
|
||||
if (value instanceof MappingJacksonValue) {
|
||||
MappingJacksonValue container = (MappingJacksonValue) value;
|
||||
value = container.getValue();
|
||||
jsonView = container.getSerializationView();
|
||||
filters = container.getFilters();
|
||||
}
|
||||
ObjectWriter writer = createObjectWriter(valueType, mimeType, jsonView, hints);
|
||||
if (filters != null) {
|
||||
writer = writer.with(filters);
|
||||
}
|
||||
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
|
||||
try {
|
||||
JsonEncoding encoding = getJsonEncoding(mimeType);
|
||||
@@ -252,10 +281,12 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
|
||||
}
|
||||
|
||||
private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType,
|
||||
@Nullable Map<String, Object> hints) {
|
||||
@Nullable Class<?> jsonView, @Nullable Map<String, Object> hints) {
|
||||
|
||||
JavaType javaType = getJavaType(valueType.getType(), null);
|
||||
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);
|
||||
if (jsonView == null && hints != null) {
|
||||
jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
|
||||
}
|
||||
ObjectWriter writer = (jsonView != null ?
|
||||
getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer());
|
||||
|
||||
|
||||
+29
-1
@@ -26,6 +26,7 @@ import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
@@ -108,7 +109,34 @@ public abstract class Jackson2CodecSupport {
|
||||
|
||||
|
||||
protected boolean supportsMimeType(@Nullable MimeType mimeType) {
|
||||
return (mimeType == null || this.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
|
||||
if (mimeType == null) {
|
||||
return true;
|
||||
}
|
||||
for (MimeType supportedMimeType : this.mimeTypes) {
|
||||
if (supportedMimeType.isCompatibleWith(mimeType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether to log the given exception coming from a
|
||||
* {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.
|
||||
* @param type the class that Jackson tested for (de-)serializability
|
||||
* @param cause the Jackson-thrown exception to evaluate
|
||||
* (typically a {@link JsonMappingException})
|
||||
* @since 5.3.1
|
||||
*/
|
||||
protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) {
|
||||
if (cause == null) {
|
||||
return;
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") +
|
||||
"serialization for type [" + type + "]";
|
||||
logger.debug(msg, cause);
|
||||
}
|
||||
}
|
||||
|
||||
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
|
||||
|
||||
+4
-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.HttpMessageNotWritableException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.TypeUtils;
|
||||
|
||||
/**
|
||||
@@ -308,7 +310,8 @@ 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());
|
||||
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding)) {
|
||||
writePrefix(generator, object);
|
||||
|
||||
Object value = object;
|
||||
|
||||
+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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1023,15 +1023,21 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
|
||||
if (this.path.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
String path = this.path.toString();
|
||||
while (true) {
|
||||
int index = path.indexOf("//");
|
||||
if (index == -1) {
|
||||
break;
|
||||
String sanitized = getSanitizedPath(this.path);
|
||||
return new HierarchicalUriComponents.FullPathComponent(sanitized);
|
||||
}
|
||||
|
||||
private static String getSanitizedPath(final StringBuilder path) {
|
||||
int index = path.indexOf("//");
|
||||
if (index >= 0) {
|
||||
StringBuilder sanitized = new StringBuilder(path);
|
||||
while (index != -1) {
|
||||
sanitized.deleteCharAt(index);
|
||||
index = sanitized.indexOf("//", index);
|
||||
}
|
||||
path = path.substring(0, index) + path.substring(index + 1);
|
||||
return sanitized.toString();
|
||||
}
|
||||
return new HierarchicalUriComponents.FullPathComponent(path);
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
public void removeTrailingSlash() {
|
||||
|
||||
@@ -401,18 +401,17 @@ public class UrlPathHelper {
|
||||
* <li>replace all "//" by "/"</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String getSanitizedPath(final String path) {
|
||||
String sanitized = path;
|
||||
while (true) {
|
||||
int index = sanitized.indexOf("//");
|
||||
if (index < 0) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
sanitized = sanitized.substring(0, index) + sanitized.substring(index + 1);
|
||||
private static String getSanitizedPath(final String path) {
|
||||
int index = path.indexOf("//");
|
||||
if (index >= 0) {
|
||||
StringBuilder sanitized = new StringBuilder(path);
|
||||
while (index != -1) {
|
||||
sanitized.deleteCharAt(index);
|
||||
index = sanitized.indexOf("//", index);
|
||||
}
|
||||
return sanitized.toString();
|
||||
}
|
||||
return sanitized;
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -612,15 +611,21 @@ public class UrlPathHelper {
|
||||
removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri));
|
||||
}
|
||||
|
||||
private String removeSemicolonContentInternal(String requestUri) {
|
||||
private static String removeSemicolonContentInternal(String requestUri) {
|
||||
int semicolonIndex = requestUri.indexOf(';');
|
||||
while (semicolonIndex != -1) {
|
||||
int slashIndex = requestUri.indexOf('/', semicolonIndex);
|
||||
String start = requestUri.substring(0, semicolonIndex);
|
||||
requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;
|
||||
semicolonIndex = requestUri.indexOf(';', semicolonIndex);
|
||||
if (semicolonIndex == -1) {
|
||||
return requestUri;
|
||||
}
|
||||
return requestUri;
|
||||
StringBuilder sb = new StringBuilder(requestUri);
|
||||
while (semicolonIndex != -1) {
|
||||
int slashIndex = sb.indexOf("/", semicolonIndex + 1);
|
||||
if (slashIndex == -1) {
|
||||
return sb.substring(0, semicolonIndex);
|
||||
}
|
||||
sb.delete(semicolonIndex, slashIndex);
|
||||
semicolonIndex = sb.indexOf(";", semicolonIndex);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String removeJsessionid(String requestUri) {
|
||||
|
||||
+20
@@ -39,6 +39,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
|
||||
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3;
|
||||
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.web.testfixture.xml.Pojo;
|
||||
@@ -214,6 +215,25 @@ public class Jackson2JsonEncoderTests extends AbstractEncoderTests<Jackson2JsonE
|
||||
null, hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jacksonValue() {
|
||||
JacksonViewBean bean = new JacksonViewBean();
|
||||
bean.setWithView1("with");
|
||||
bean.setWithView2("with");
|
||||
bean.setWithoutView("without");
|
||||
|
||||
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
|
||||
jacksonValue.setSerializationView(MyJacksonView1.class);
|
||||
|
||||
ResolvableType type = ResolvableType.forClass(MappingJacksonValue.class);
|
||||
|
||||
testEncode(Mono.just(jacksonValue), type, step -> step
|
||||
.consumeNextWith(expectString("{\"withView1\":\"with\"}")
|
||||
.andThen(DataBufferUtils::release))
|
||||
.verifyComplete(),
|
||||
null, Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Test // gh-22771
|
||||
public void encodeWithFlushAfterWriteOff() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
+4
-1
@@ -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.
|
||||
@@ -47,6 +47,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.
|
||||
@@ -149,6 +151,7 @@ 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
|
||||
|
||||
+15
-9
@@ -55,8 +55,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");
|
||||
@@ -93,10 +93,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");
|
||||
@@ -113,12 +113,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);
|
||||
}
|
||||
|
||||
@@ -126,10 +126,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);
|
||||
}
|
||||
|
||||
@@ -614,7 +614,7 @@ class UriComponentsBuilderTests {
|
||||
assertThat(after.getPath()).isEqualTo("/foo/");
|
||||
}
|
||||
|
||||
@Test // gh-19890
|
||||
@Test // gh-19890
|
||||
void fromHttpRequestWithEmptyScheme() {
|
||||
HttpRequest request = new HttpRequest() {
|
||||
@Override
|
||||
@@ -896,7 +896,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)
|
||||
@@ -1156,7 +1156,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");
|
||||
@@ -1195,9 +1195,15 @@ 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");
|
||||
}
|
||||
|
||||
@Test // gh-26012
|
||||
void verifyDoubleSlashReplacedWithSingleOne() {
|
||||
String path = UriComponentsBuilder.fromPath("/home/").path("/path").build().getPath();
|
||||
assertThat(path).isEqualTo("/home/path");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -104,6 +104,9 @@ public class UrlPathHelperTests {
|
||||
|
||||
request.setRequestURI("/foo+bar");
|
||||
assertThat(helper.getRequestUri(request)).isEqualTo("/foo+bar");
|
||||
|
||||
request.setRequestURI("/home/" + "/path");
|
||||
assertThat(helper.getRequestUri(request)).isEqualTo("/home/path");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -112,6 +115,9 @@ public class UrlPathHelperTests {
|
||||
request.setRequestURI("/foo;f=F;o=O;o=O/bar;b=B;a=A;r=R");
|
||||
assertThat(helper.getRequestUri(request)).isEqualTo("/foo/bar");
|
||||
|
||||
request.setRequestURI("/foo;f=F;o=O;o=O/bar;b=B;a=A;r=R/baz;test");
|
||||
assertThat(helper.getRequestUri(request)).isEqualTo("/foo/bar/baz");
|
||||
|
||||
// SPR-13455
|
||||
request.setRequestURI("/foo/;test/1");
|
||||
request.setServletPath("/foo/1");
|
||||
|
||||
+51
-15
@@ -33,6 +33,7 @@ import java.util.function.Supplier;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -173,6 +174,9 @@ class DefaultWebClient implements WebClient {
|
||||
|
||||
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
|
||||
|
||||
@Nullable
|
||||
private Function<Context, Context> contextModifier;
|
||||
|
||||
@Nullable
|
||||
private Consumer<ClientHttpRequest> httpRequestConsumer;
|
||||
|
||||
@@ -181,6 +185,7 @@ class DefaultWebClient implements WebClient {
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
|
||||
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
|
||||
@@ -238,18 +243,6 @@ class DefaultWebClient implements WebClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attribute(String name, Object value) {
|
||||
this.attributes.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
|
||||
attributesConsumer.accept(this.attributes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec accept(MediaType... acceptableMediaTypes) {
|
||||
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes));
|
||||
@@ -298,6 +291,25 @@ class DefaultWebClient implements WebClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attribute(String name, Object value) {
|
||||
this.attributes.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
|
||||
attributesConsumer.accept(this.attributes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec context(Function<Context, Context> contextModifier) {
|
||||
this.contextModifier = (this.contextModifier != null ?
|
||||
this.contextModifier.andThen(contextModifier) : contextModifier);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
|
||||
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
|
||||
@@ -412,9 +424,15 @@ class DefaultWebClient implements WebClient {
|
||||
ClientRequest request = (this.inserter != null ?
|
||||
initRequestBuilder().body(this.inserter).build() :
|
||||
initRequestBuilder().build());
|
||||
return Mono.defer(() -> exchangeFunction.exchange(request)
|
||||
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
|
||||
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR));
|
||||
return Mono.defer(() -> {
|
||||
Mono<ClientResponse> responseMono = exchangeFunction.exchange(request)
|
||||
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
|
||||
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
|
||||
if (this.contextModifier != null) {
|
||||
responseMono = responseMono.contextWrite(this.contextModifier);
|
||||
}
|
||||
return responseMono;
|
||||
});
|
||||
}
|
||||
|
||||
private ClientRequest.Builder initRequestBuilder() {
|
||||
@@ -480,12 +498,14 @@ class DefaultWebClient implements WebClient {
|
||||
|
||||
private final List<StatusHandler> statusHandlers = new ArrayList<>(1);
|
||||
|
||||
|
||||
DefaultResponseSpec(Mono<ClientResponse> responseMono, Supplier<HttpRequest> requestSupplier) {
|
||||
this.responseMono = responseMono;
|
||||
this.requestSupplier = requestSupplier;
|
||||
this.statusHandlers.add(DEFAULT_STATUS_HANDLER);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ResponseSpec onStatus(Predicate<HttpStatus> statusPredicate,
|
||||
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
|
||||
@@ -622,6 +642,22 @@ class DefaultWebClient implements WebClient {
|
||||
handleBodyFlux(response, response.bodyToFlux(elementTypeRef))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(Class<T> elementType) {
|
||||
return this.responseMono.map(response ->
|
||||
ResponseEntity.status(response.rawStatusCode())
|
||||
.headers(response.headers().asHttpHeaders())
|
||||
.body(response.bodyToFlux(elementType)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(ParameterizedTypeReference<T> elementTypeReference) {
|
||||
return this.responseMono.map(response ->
|
||||
ResponseEntity.status(response.rawStatusCode())
|
||||
.headers(response.headers().asHttpHeaders())
|
||||
.body(response.bodyToFlux(elementTypeReference)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Void>> toBodilessEntity() {
|
||||
return this.responseMono.flatMap(response ->
|
||||
|
||||
+30
-2
@@ -274,9 +274,13 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
|
||||
.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));
|
||||
}
|
||||
|
||||
@@ -313,4 +317,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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+84
-59
@@ -29,6 +29,7 @@ import java.util.function.Predicate;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
@@ -470,6 +471,17 @@ public interface WebClient {
|
||||
*/
|
||||
S attributes(Consumer<Map<String, Object>> attributesConsumer);
|
||||
|
||||
/**
|
||||
* Provide a function to populate the Reactor {@code Context}. In contrast
|
||||
* to {@link #attribute(String, Object) attributes} which apply only to
|
||||
* the current request, the Reactor {@code Context} transparently propagates
|
||||
* to the downstream processing chain which may include other nested or
|
||||
* successive calls over HTTP or via other reactive clients.
|
||||
* @param contextModifier the function to modify the context with
|
||||
* @since 5.3.1
|
||||
*/
|
||||
S context(Function<Context, Context> contextModifier);
|
||||
|
||||
/**
|
||||
* Callback for access to the {@link ClientHttpRequest} that in turn
|
||||
* provides access to the native request of the underlying HTTP library.
|
||||
@@ -781,103 +793,116 @@ public interface WebClient {
|
||||
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Mono}. By default, if the response has status code 4xx or
|
||||
* 5xx, the {@code Mono} will contain a {@link WebClientException}. This can be overridden
|
||||
* with {@link #onStatus(Predicate, Function)}.
|
||||
* @param elementClass the expected response body element class
|
||||
* @param <T> response body type
|
||||
* @return a mono containing the body, or a {@link WebClientResponseException} if the
|
||||
* status code is 4xx or 5xx
|
||||
* Decode the body to the given target type. For an error response (status
|
||||
* code of 4xx or 5xx), the {@code Mono} emits a {@link WebClientException}.
|
||||
* Use {@link #onStatus(Predicate, Function)} to customize error response
|
||||
* handling.
|
||||
* @param elementClass the type to decode to
|
||||
* @param <T> the target body type
|
||||
* @return the decoded body
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(Class<T> elementClass);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Mono}. By default, if the response has status code 4xx or
|
||||
* 5xx, the {@code Mono} will contain a {@link WebClientException}. This can be overridden
|
||||
* with {@link #onStatus(Predicate, Function)}.
|
||||
* @param elementTypeRef a type reference describing the expected response body element type
|
||||
* @param <T> response body type
|
||||
* @return a mono containing the body, or a {@link WebClientResponseException} if the
|
||||
* status code is 4xx or 5xx
|
||||
* Variant of {@link #bodyToMono(Class)} with a {@link ParameterizedTypeReference}.
|
||||
* @param elementTypeRef the type to decode to
|
||||
* @param <T> the target body type
|
||||
* @return the decoded body
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(ParameterizedTypeReference<T> elementTypeRef);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Flux}. By default, if the response has status code 4xx or
|
||||
* 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden
|
||||
* with {@link #onStatus(Predicate, Function)}.
|
||||
* @param elementClass the class of elements in the response
|
||||
* @param <T> the type of elements in the response
|
||||
* @return a flux containing the body, or a {@link WebClientResponseException} if the
|
||||
* status code is 4xx or 5xx
|
||||
* Decode the body to a {@link Flux} with elements of the given type.
|
||||
* For an error response (status code of 4xx or 5xx), the {@code Mono}
|
||||
* emits a {@link WebClientException}. Use {@link #onStatus(Predicate, Function)}
|
||||
* to customize error response handling.
|
||||
* @param elementClass the type of element to decode to
|
||||
* @param <T> the body element type
|
||||
* @return the decoded body
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<T> elementClass);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Flux}. By default, if the response has status code 4xx or
|
||||
* 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden
|
||||
* with {@link #onStatus(Predicate, Function)}.
|
||||
* @param elementTypeRef a type reference describing the expected response body element type
|
||||
* @param <T> the type of elements in the response
|
||||
* @return a flux containing the body, or a {@link WebClientResponseException} if the
|
||||
* status code is 4xx or 5xx
|
||||
* Variant of {@link #bodyToMono(Class)} with a {@link ParameterizedTypeReference}.
|
||||
* @param elementTypeRef the type of element to decode to
|
||||
* @param <T> the body element type
|
||||
* @return the decoded body
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementTypeRef);
|
||||
|
||||
/**
|
||||
* Return the response as a delayed {@code ResponseEntity}. By default, if the response has
|
||||
* status code 4xx or 5xx, the {@code Mono} will contain a {@link WebClientException}. This
|
||||
* can be overridden with {@link #onStatus(Predicate, Function)}.
|
||||
* Return a {@code ResponseEntity} with the body decoded to an Object of
|
||||
* the given type. For an error response (status code of 4xx or 5xx), the
|
||||
* {@code Mono} emits a {@link WebClientException}. Use
|
||||
* {@link #onStatus(Predicate, Function)} to customize error response handling.
|
||||
* @param bodyClass the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return {@code Mono} with the {@code ResponseEntity}
|
||||
* @return the {@code ResponseEntity} with the decoded body
|
||||
* @since 5.2
|
||||
*/
|
||||
<T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyClass);
|
||||
|
||||
/**
|
||||
* Return the response as a delayed {@code ResponseEntity}. By default, if the response has
|
||||
* status code 4xx or 5xx, the {@code Mono} will contain a {@link WebClientException}. This
|
||||
* can be overridden with {@link #onStatus(Predicate, Function)}.
|
||||
* @param bodyTypeReference a type reference describing the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return {@code Mono} with the {@code ResponseEntity}
|
||||
* Variant of {@link #bodyToMono(Class)} with a {@link ParameterizedTypeReference}.
|
||||
* @param bodyTypeReference the expected response body type
|
||||
* @param <T> the response body type
|
||||
* @return the {@code ResponseEntity} with the decoded body
|
||||
* @since 5.2
|
||||
*/
|
||||
<T> Mono<ResponseEntity<T>> toEntity(ParameterizedTypeReference<T> bodyTypeReference);
|
||||
|
||||
/**
|
||||
* Return the response as a delayed list of {@code ResponseEntity}s. By default, if the
|
||||
* response has status code 4xx or 5xx, the {@code Mono} will contain a
|
||||
* {@link WebClientException}. This can be overridden with
|
||||
* {@link #onStatus(Predicate, Function)}.
|
||||
* @param elementClass the expected response body list element class
|
||||
* @param <T> the type of elements in the list
|
||||
* @return {@code Mono} with the list of {@code ResponseEntity}s
|
||||
* Return a {@code ResponseEntity} with the body decoded to a {@code List}
|
||||
* of elements of the given type. For an error response (status code of
|
||||
* 4xx or 5xx), the {@code Mono} emits a {@link WebClientException}.
|
||||
* Use {@link #onStatus(Predicate, Function)} to customize error response
|
||||
* handling.
|
||||
* @param elementClass the type of element to decode the target Flux to
|
||||
* @param <T> the body element type
|
||||
* @return the {@code ResponseEntity}
|
||||
* @since 5.2
|
||||
*/
|
||||
<T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementClass);
|
||||
|
||||
/**
|
||||
* Return the response as a delayed list of {@code ResponseEntity}s. By default, if the
|
||||
* response has status code 4xx or 5xx, the {@code Mono} will contain a
|
||||
* {@link WebClientException}. This can be overridden with
|
||||
* {@link #onStatus(Predicate, Function)}.
|
||||
* @param elementTypeRef the expected response body list element reference type
|
||||
* @param <T> the type of elements in the list
|
||||
* @return {@code Mono} with the list of {@code ResponseEntity}s
|
||||
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
|
||||
* @param elementTypeRef the type of element to decode the target Flux to
|
||||
* @param <T> the body element type
|
||||
* @return the {@code ResponseEntity}
|
||||
* @since 5.2
|
||||
*/
|
||||
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
|
||||
|
||||
/**
|
||||
* Return the response as a delayed {@code ResponseEntity} containing status and headers,
|
||||
* but no body. By default, if the response has status code 4xx or 5xx, the {@code Mono}
|
||||
* will contain a {@link WebClientException}. This can be overridden with
|
||||
* {@link #onStatus(Predicate, Function)}.
|
||||
* Calling this method will {@linkplain ClientResponse#releaseBody() release} the body of
|
||||
* the response.
|
||||
* @return {@code Mono} with the bodiless {@code ResponseEntity}
|
||||
* Return a {@code ResponseEntity} with the body decoded to a {@code Flux}
|
||||
* of elements of the given type. For an error response (status code of
|
||||
* 4xx or 5xx), the {@code Mono} emits a {@link WebClientException}.
|
||||
* Use {@link #onStatus(Predicate, Function)} to customize error response
|
||||
* handling.
|
||||
* <p><strong>Note:</strong> The {@code Flux} representing the body must
|
||||
* be subscribed to or else associated resources will not be released.
|
||||
* @param elementType the type of element to decode the target Flux to
|
||||
* @param <T> the body element type
|
||||
* @return the {@code ResponseEntity}
|
||||
* @since 5.3.1
|
||||
*/
|
||||
<T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(Class<T> elementType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
|
||||
* @param elementTypeReference the type of element to decode the target Flux to
|
||||
* @param <T> the body element type
|
||||
* @return the {@code ResponseEntity}
|
||||
* @since 5.3.1
|
||||
*/
|
||||
<T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(ParameterizedTypeReference<T> elementTypeReference);
|
||||
|
||||
/**
|
||||
* Return a {@code ResponseEntity} without a body. For an error response
|
||||
* (status code of 4xx or 5xx), the {@code Mono} emits a
|
||||
* {@link WebClientException}. Use {@link #onStatus(Predicate, Function)}
|
||||
* to customize error response handling.
|
||||
* @return the {@code ResponseEntity}
|
||||
* @since 5.2
|
||||
*/
|
||||
Mono<ResponseEntity<Void>> toBodilessEntity();
|
||||
|
||||
+6
-2
@@ -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,12 +75,17 @@ 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.
|
||||
*/
|
||||
Mono<Void> send(Publisher<WebSocketMessage> messages);
|
||||
|
||||
/**
|
||||
* Whether the underlying connection is open.
|
||||
* @since 5.3.1
|
||||
*/
|
||||
boolean isOpen();
|
||||
|
||||
/**
|
||||
* Close the WebSocket session with {@link CloseStatus#NORMAL}.
|
||||
*/
|
||||
|
||||
+5
@@ -113,6 +113,11 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession<Sess
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return getDelegate().isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> close(CloseStatus status) {
|
||||
getDelegate().close(status.getCode(), status.getReason());
|
||||
|
||||
+25
@@ -15,10 +15,13 @@
|
||||
*/
|
||||
package org.springframework.web.reactive.socket.adapter;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.Connection;
|
||||
import reactor.netty.NettyInbound;
|
||||
import reactor.netty.NettyOutbound;
|
||||
import reactor.netty.http.websocket.WebsocketInbound;
|
||||
@@ -93,6 +96,13 @@ public class ReactorNettyWebSocketSession
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
DisposedCallback callback = new DisposedCallback();
|
||||
getDelegate().getInbound().withConnection(callback);
|
||||
return callback.isDisposed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> close(CloseStatus status) {
|
||||
// this will notify WebSocketInbound.receiveCloseStatus()
|
||||
@@ -129,4 +139,19 @@ public class ReactorNettyWebSocketSession
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class DisposedCallback implements Consumer<Connection> {
|
||||
|
||||
private boolean disposed;
|
||||
|
||||
public boolean isDisposed() {
|
||||
return this.disposed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Connection connection) {
|
||||
this.disposed = connection.isDisposed();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -103,6 +103,11 @@ public class StandardWebSocketSession extends AbstractListenerWebSocketSession<S
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return getDelegate().isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> close(CloseStatus status) {
|
||||
try {
|
||||
|
||||
+5
@@ -108,6 +108,11 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSession<W
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return getDelegate().isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> close(CloseStatus status) {
|
||||
CloseMessage cm = new CloseMessage(status.getCode(), status.getReason());
|
||||
|
||||
+57
-1
@@ -129,6 +129,34 @@ public class DefaultWebClientTests {
|
||||
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextFromThreadLocal() {
|
||||
WebClient client = this.builder
|
||||
.filter((request, next) ->
|
||||
// Async, continue on different thread
|
||||
Mono.delay(Duration.ofMillis(10)).then(next.exchange(request)))
|
||||
.filter((request, next) ->
|
||||
Mono.deferContextual(contextView -> {
|
||||
String fooValue = contextView.get("foo");
|
||||
return next.exchange(ClientRequest.from(request).header("foo", fooValue).build());
|
||||
}))
|
||||
.build();
|
||||
|
||||
ThreadLocal<String> fooHolder = new ThreadLocal<>();
|
||||
fooHolder.set("bar");
|
||||
try {
|
||||
client.get().uri("/path")
|
||||
.context(context -> context.put("foo", fooHolder.get()))
|
||||
.retrieve().bodyToMono(Void.class).block(Duration.ofSeconds(10));
|
||||
}
|
||||
finally {
|
||||
fooHolder.remove();
|
||||
}
|
||||
|
||||
ClientRequest request = verifyAndGetRequest();
|
||||
assertThat(request.headers().getFirst("foo")).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void httpRequest() {
|
||||
this.builder.build().get().uri("/path")
|
||||
@@ -142,7 +170,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")
|
||||
@@ -170,6 +199,33 @@ 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")
|
||||
.retrieve().bodyToMono(Void.class).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")
|
||||
.retrieve().bodyToMono(Void.class).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");
|
||||
|
||||
+34
-1
@@ -279,7 +279,7 @@ class WebClientIntegrationTests {
|
||||
}
|
||||
|
||||
@ParameterizedWebClientTest
|
||||
void retrieveJsonArrayAsResponseEntity(ClientHttpConnector connector) {
|
||||
void retrieveJsonArrayAsResponseEntityList(ClientHttpConnector connector) {
|
||||
startServer(connector);
|
||||
|
||||
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
|
||||
@@ -309,6 +309,39 @@ class WebClientIntegrationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedWebClientTest
|
||||
void retrieveJsonArrayAsResponseEntityFlux(ClientHttpConnector connector) {
|
||||
startServer(connector);
|
||||
|
||||
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
|
||||
prepareResponse(response -> response
|
||||
.setHeader("Content-Type", "application/json").setBody(content));
|
||||
|
||||
ResponseEntity<Flux<Pojo>> entity = this.webClient.get()
|
||||
.uri("/json").accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.toEntityFlux(Pojo.class)
|
||||
.block(Duration.ofSeconds(3));
|
||||
|
||||
assertThat(entity).isNotNull();
|
||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
assertThat(entity.getHeaders().getContentLength()).isEqualTo(58);
|
||||
|
||||
assertThat(entity.getBody()).isNotNull();
|
||||
StepVerifier.create(entity.getBody())
|
||||
.expectNext(new Pojo("foo1", "bar1"))
|
||||
.expectNext(new Pojo("foo2", "bar2"))
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
expectRequestCount(1);
|
||||
expectRequest(request -> {
|
||||
assertThat(request.getPath()).isEqualTo("/json");
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
|
||||
});
|
||||
}
|
||||
|
||||
@Test // gh-24788
|
||||
void retrieveJsonArrayAsBodilessEntityShouldReleasesConnection() {
|
||||
|
||||
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* 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.reactive.function.server;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class CustomRouteBuilder {
|
||||
|
||||
public static final String OPERATION_ATTRIBUTE = CustomRouteBuilder.class.getName() + ".operation";
|
||||
|
||||
private final RouterFunctions.Builder delegate = RouterFunctions.route();
|
||||
|
||||
|
||||
private CustomRouteBuilder() {
|
||||
}
|
||||
|
||||
public static CustomRouteBuilder route() {
|
||||
return new CustomRouteBuilder();
|
||||
}
|
||||
|
||||
public CustomRouteBuilder GET(String pattern, HandlerFunction<ServerResponse> handlerFunction,
|
||||
Consumer<OperationBuilder> operationsConsumer) {
|
||||
|
||||
OperationBuilder builder = new OperationBuilder();
|
||||
operationsConsumer.accept(builder);
|
||||
|
||||
this.delegate.GET(pattern, handlerFunction)
|
||||
.withAttribute(OPERATION_ATTRIBUTE, builder.operation);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public RouterFunction<ServerResponse> build() {
|
||||
return this.delegate.build();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
RouterFunction<ServerResponse> routerFunction =
|
||||
route()
|
||||
.GET("/foo", request -> ServerResponse.ok().build(), ops -> ops
|
||||
.parameter("key1", "My key1 description")
|
||||
.parameter("key1", "My key1 description")
|
||||
.response(200, "This is normal response description")
|
||||
.response(404, "This is response description")
|
||||
)
|
||||
.build();
|
||||
|
||||
AttributesVisitor visitor = new AttributesVisitor();
|
||||
routerFunction.accept(visitor);
|
||||
}
|
||||
|
||||
|
||||
public static class OperationBuilder {
|
||||
|
||||
private final Operation operation = new Operation();
|
||||
|
||||
public OperationBuilder parameter(String name, String description) {
|
||||
this.operation.parameter(name, description);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OperationBuilder response(int statusCode, String description) {
|
||||
this.operation.response(statusCode, description);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class Operation {
|
||||
|
||||
private final Map<String, String> parameters = new LinkedHashMap<>();
|
||||
|
||||
private final Map<Integer, String > responses = new LinkedHashMap<>();
|
||||
|
||||
public void parameter(String name, String description) {
|
||||
this.parameters.put(name, description);
|
||||
}
|
||||
|
||||
public void response(int status, String description) {
|
||||
this.responses.put(status, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "parameters=" + parameters +
|
||||
", responses=" + responses;
|
||||
}
|
||||
}
|
||||
|
||||
static class AttributesVisitor implements RouterFunctions.Visitor {
|
||||
|
||||
@Nullable
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
@Override
|
||||
public void attributes(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
|
||||
System.out.printf("Route predicate %s->%s%nhas attributes %s", predicate, handlerFunction, this.attributes);
|
||||
this.attributes = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startNested(RequestPredicate predicate) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endNested(RequestPredicate predicate) {
|
||||
// TODO
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resources(Function<ServerRequest, Mono<Resource>> lookupFunction) {
|
||||
// TODO
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unknown(RouterFunction<?> routerFunction) {
|
||||
// TODO
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
+74
-57
@@ -19,9 +19,11 @@ package org.springframework.web.servlet.config;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -43,6 +45,7 @@ import org.springframework.web.util.UrlPathHelper;
|
||||
* Convenience methods for use in MVC namespace BeanDefinitionParsers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Brian Clozel
|
||||
* @author Marten Deinum
|
||||
* @since 3.1
|
||||
@@ -67,15 +70,15 @@ public abstract class MvcNamespaceUtils {
|
||||
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
|
||||
|
||||
|
||||
public static void registerDefaultComponents(ParserContext parserContext, @Nullable Object source) {
|
||||
registerBeanNameUrlHandlerMapping(parserContext, source);
|
||||
registerHttpRequestHandlerAdapter(parserContext, source);
|
||||
registerSimpleControllerHandlerAdapter(parserContext, source);
|
||||
registerHandlerMappingIntrospector(parserContext, source);
|
||||
registerThemeResolver(parserContext, source);
|
||||
registerLocaleResolver(parserContext, source);
|
||||
registerFlashMapManager(parserContext, source);
|
||||
registerViewNameTranslator(parserContext, source);
|
||||
public static void registerDefaultComponents(ParserContext context, @Nullable Object source) {
|
||||
registerBeanNameUrlHandlerMapping(context, source);
|
||||
registerHttpRequestHandlerAdapter(context, source);
|
||||
registerSimpleControllerHandlerAdapter(context, source);
|
||||
registerHandlerMappingIntrospector(context, source);
|
||||
registerLocaleResolver(context, source);
|
||||
registerThemeResolver(context, source);
|
||||
registerViewNameTranslator(context, source);
|
||||
registerFlashMapManager(context, source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,21 +87,21 @@ public abstract class MvcNamespaceUtils {
|
||||
* @return a RuntimeBeanReference to this {@link UrlPathHelper} instance
|
||||
*/
|
||||
public static RuntimeBeanReference registerUrlPathHelper(
|
||||
@Nullable RuntimeBeanReference urlPathHelperRef, ParserContext parserContext, @Nullable Object source) {
|
||||
@Nullable RuntimeBeanReference urlPathHelperRef, ParserContext context, @Nullable Object source) {
|
||||
|
||||
if (urlPathHelperRef != null) {
|
||||
if (parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME)) {
|
||||
parserContext.getRegistry().removeAlias(URL_PATH_HELPER_BEAN_NAME);
|
||||
if (context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME)) {
|
||||
context.getRegistry().removeAlias(URL_PATH_HELPER_BEAN_NAME);
|
||||
}
|
||||
parserContext.getRegistry().registerAlias(urlPathHelperRef.getBeanName(), URL_PATH_HELPER_BEAN_NAME);
|
||||
context.getRegistry().registerAlias(urlPathHelperRef.getBeanName(), URL_PATH_HELPER_BEAN_NAME);
|
||||
}
|
||||
else if (!parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&
|
||||
!parserContext.getRegistry().containsBeanDefinition(URL_PATH_HELPER_BEAN_NAME)) {
|
||||
else if (!context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&
|
||||
!context.getRegistry().containsBeanDefinition(URL_PATH_HELPER_BEAN_NAME)) {
|
||||
RootBeanDefinition urlPathHelperDef = new RootBeanDefinition(UrlPathHelper.class);
|
||||
urlPathHelperDef.setSource(source);
|
||||
urlPathHelperDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
parserContext.getRegistry().registerBeanDefinition(URL_PATH_HELPER_BEAN_NAME, urlPathHelperDef);
|
||||
parserContext.registerComponent(new BeanComponentDefinition(urlPathHelperDef, URL_PATH_HELPER_BEAN_NAME));
|
||||
context.getRegistry().registerBeanDefinition(URL_PATH_HELPER_BEAN_NAME, urlPathHelperDef);
|
||||
context.registerComponent(new BeanComponentDefinition(urlPathHelperDef, URL_PATH_HELPER_BEAN_NAME));
|
||||
}
|
||||
return new RuntimeBeanReference(URL_PATH_HELPER_BEAN_NAME);
|
||||
}
|
||||
@@ -109,21 +112,21 @@ public abstract class MvcNamespaceUtils {
|
||||
* @return a RuntimeBeanReference to this {@link PathMatcher} instance
|
||||
*/
|
||||
public static RuntimeBeanReference registerPathMatcher(@Nullable RuntimeBeanReference pathMatcherRef,
|
||||
ParserContext parserContext, @Nullable Object source) {
|
||||
ParserContext context, @Nullable Object source) {
|
||||
|
||||
if (pathMatcherRef != null) {
|
||||
if (parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
|
||||
parserContext.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);
|
||||
if (context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
|
||||
context.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);
|
||||
}
|
||||
parserContext.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);
|
||||
context.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);
|
||||
}
|
||||
else if (!parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&
|
||||
!parserContext.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
|
||||
else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&
|
||||
!context.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
|
||||
RootBeanDefinition pathMatcherDef = new RootBeanDefinition(AntPathMatcher.class);
|
||||
pathMatcherDef.setSource(source);
|
||||
pathMatcherDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
parserContext.getRegistry().registerBeanDefinition(PATH_MATCHER_BEAN_NAME, pathMatcherDef);
|
||||
parserContext.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATH_MATCHER_BEAN_NAME));
|
||||
context.getRegistry().registerBeanDefinition(PATH_MATCHER_BEAN_NAME, pathMatcherDef);
|
||||
context.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATH_MATCHER_BEAN_NAME));
|
||||
}
|
||||
return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME);
|
||||
}
|
||||
@@ -204,28 +207,14 @@ public abstract class MvcNamespaceUtils {
|
||||
* Registers an {@link HandlerMappingIntrospector} under a well-known name
|
||||
* unless already registered.
|
||||
*/
|
||||
private static void registerHandlerMappingIntrospector(ParserContext parserContext, @Nullable Object source) {
|
||||
if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
|
||||
private static void registerHandlerMappingIntrospector(ParserContext context, @Nullable Object source) {
|
||||
if (!context.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(HandlerMappingIntrospector.class);
|
||||
beanDef.setSource(source);
|
||||
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
beanDef.setLazyInit(true);
|
||||
parserContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef);
|
||||
parserContext.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an {@link FixedThemeResolver} under a well-known name
|
||||
* unless already registered.
|
||||
*/
|
||||
private static void registerThemeResolver(ParserContext parserContext, @Nullable Object source) {
|
||||
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(FixedThemeResolver.class);
|
||||
beanDef.setSource(source);
|
||||
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, beanDef);
|
||||
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.THEME_RESOLVER_BEAN_NAME));
|
||||
context.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef);
|
||||
context.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,27 +222,27 @@ public abstract class MvcNamespaceUtils {
|
||||
* Registers an {@link AcceptHeaderLocaleResolver} under a well-known name
|
||||
* unless already registered.
|
||||
*/
|
||||
private static void registerLocaleResolver(ParserContext parserContext, @Nullable Object source) {
|
||||
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)) {
|
||||
private static void registerLocaleResolver(ParserContext context, @Nullable Object source) {
|
||||
if (!containsBeanInHierarchy(context, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(AcceptHeaderLocaleResolver.class);
|
||||
beanDef.setSource(source);
|
||||
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, beanDef);
|
||||
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME));
|
||||
context.getRegistry().registerBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, beanDef);
|
||||
context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an {@link SessionFlashMapManager} under a well-known name
|
||||
* Registers an {@link FixedThemeResolver} under a well-known name
|
||||
* unless already registered.
|
||||
*/
|
||||
private static void registerFlashMapManager(ParserContext parserContext, @Nullable Object source) {
|
||||
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(SessionFlashMapManager.class);
|
||||
private static void registerThemeResolver(ParserContext context, @Nullable Object source) {
|
||||
if (!containsBeanInHierarchy(context, DispatcherServlet.THEME_RESOLVER_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(FixedThemeResolver.class);
|
||||
beanDef.setSource(source);
|
||||
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, beanDef);
|
||||
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME));
|
||||
context.getRegistry().registerBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, beanDef);
|
||||
context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.THEME_RESOLVER_BEAN_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,13 +250,29 @@ public abstract class MvcNamespaceUtils {
|
||||
* Registers an {@link DefaultRequestToViewNameTranslator} under a well-known name
|
||||
* unless already registered.
|
||||
*/
|
||||
private static void registerViewNameTranslator(ParserContext parserContext, @Nullable Object source) {
|
||||
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)) {
|
||||
private static void registerViewNameTranslator(ParserContext context, @Nullable Object source) {
|
||||
if (!containsBeanInHierarchy(context, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultRequestToViewNameTranslator.class);
|
||||
beanDef.setSource(source);
|
||||
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, beanDef);
|
||||
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME));
|
||||
context.getRegistry().registerBeanDefinition(
|
||||
DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, beanDef);
|
||||
context.registerComponent(
|
||||
new BeanComponentDefinition(beanDef, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an {@link SessionFlashMapManager} under a well-known name
|
||||
* unless already registered.
|
||||
*/
|
||||
private static void registerFlashMapManager(ParserContext context, @Nullable Object source) {
|
||||
if (!containsBeanInHierarchy(context, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(SessionFlashMapManager.class);
|
||||
beanDef.setSource(source);
|
||||
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
context.getRegistry().registerBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, beanDef);
|
||||
context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,4 +295,16 @@ public abstract class MvcNamespaceUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an existing bean of the given name, ideally in the entire
|
||||
* context hierarchy (through a {@code containsBean} call) since this
|
||||
* is also what {@code DispatcherServlet} does, or otherwise just in
|
||||
* the local context (through {@code containsBeanDefinition}).
|
||||
*/
|
||||
private static boolean containsBeanInHierarchy(ParserContext context, String beanName) {
|
||||
BeanDefinitionRegistry registry = context.getRegistry();
|
||||
return (registry instanceof BeanFactory ? ((BeanFactory) registry).containsBean(beanName) :
|
||||
registry.containsBeanDefinition(beanName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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);
|
||||
|
||||
+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.
|
||||
@@ -284,7 +284,7 @@ public class ViewResolverRegistry {
|
||||
|
||||
protected List<ViewResolver> getViewResolvers() {
|
||||
if (this.contentNegotiatingResolver != null) {
|
||||
return Collections.<ViewResolver>singletonList(this.contentNegotiatingResolver);
|
||||
return Collections.singletonList(this.contentNegotiatingResolver);
|
||||
}
|
||||
else {
|
||||
return this.viewResolvers;
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.servlet.mvc.method.annotation;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* Resolves an argument of type {@link Principal}, similar to
|
||||
* {@link ServletRequestMethodArgumentResolver} but irrespective of whether the
|
||||
* argument is annotated or not. This is done to enable custom argument
|
||||
* resolution of a {@link Principal} argument (with a custom annotation).
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3.1
|
||||
*/
|
||||
public class PrincipalMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return Principal.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
|
||||
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
if (request == null) {
|
||||
throw new IllegalStateException("Current request is not of type HttpServletRequest: " + webRequest);
|
||||
}
|
||||
|
||||
Principal principal = request.getUserPrincipal();
|
||||
if (principal != null && !parameter.getParameterType().isInstance(principal)) {
|
||||
throw new IllegalStateException("Current user principal is not of type [" +
|
||||
parameter.getParameterType().getName() + "]: " + principal);
|
||||
}
|
||||
|
||||
return principal;
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -715,6 +715,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||
}
|
||||
|
||||
// Catch-all
|
||||
resolvers.add(new PrincipalMethodArgumentResolver());
|
||||
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
|
||||
|
||||
return resolvers;
|
||||
|
||||
+3
-1
@@ -50,7 +50,9 @@ import org.springframework.web.servlet.support.RequestContextUtils;
|
||||
* <li>{@link MultipartRequest}
|
||||
* <li>{@link HttpSession}
|
||||
* <li>{@link PushBuilder} (as of Spring 5.0 on Servlet 4.0)
|
||||
* <li>{@link Principal}
|
||||
* <li>{@link Principal} but only if not annotated in order to allow custom
|
||||
* resolvers to resolve it, and the falling back on
|
||||
* {@link PrincipalMethodArgumentResolver}.
|
||||
* <li>{@link InputStream}
|
||||
* <li>{@link Reader}
|
||||
* <li>{@link HttpMethod} (as of Spring 4.0)
|
||||
|
||||
@@ -398,7 +398,7 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView {
|
||||
private Map<String, String> getCurrentRequestUriVariables(HttpServletRequest request) {
|
||||
String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
|
||||
Map<String, String> uriVars = (Map<String, String>) request.getAttribute(name);
|
||||
return (uriVars != null) ? uriVars : Collections.<String, String> emptyMap();
|
||||
return (uriVars != null) ? uriVars : Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+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();
|
||||
|
||||
+24
-2
@@ -50,6 +50,7 @@ import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -100,6 +101,7 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
import org.springframework.web.servlet.handler.MappedInterceptor;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor;
|
||||
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
|
||||
import org.springframework.web.servlet.mvc.ParameterizableViewController;
|
||||
@@ -122,9 +124,12 @@ import org.springframework.web.servlet.resource.ResourceUrlProvider;
|
||||
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
|
||||
import org.springframework.web.servlet.resource.VersionResourceResolver;
|
||||
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
|
||||
import org.springframework.web.servlet.support.SessionFlashMapManager;
|
||||
import org.springframework.web.servlet.theme.CookieThemeResolver;
|
||||
import org.springframework.web.servlet.theme.ThemeChangeInterceptor;
|
||||
import org.springframework.web.servlet.view.BeanNameViewResolver;
|
||||
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
|
||||
import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;
|
||||
import org.springframework.web.servlet.view.InternalResourceView;
|
||||
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
@@ -225,10 +230,10 @@ public class MvcNamespaceTests {
|
||||
assertThat(appContext.getBean(ConversionService.class)).isNotNull();
|
||||
assertThat(appContext.getBean(LocalValidatorFactoryBean.class)).isNotNull();
|
||||
assertThat(appContext.getBean(Validator.class)).isNotNull();
|
||||
assertThat(appContext.getBean("themeResolver", ThemeResolver.class)).isNotNull();
|
||||
assertThat(appContext.getBean("localeResolver", LocaleResolver.class)).isNotNull();
|
||||
assertThat(appContext.getBean("flashMapManager", FlashMapManager.class)).isNotNull();
|
||||
assertThat(appContext.getBean("themeResolver", ThemeResolver.class)).isNotNull();
|
||||
assertThat(appContext.getBean("viewNameTranslator", RequestToViewNameTranslator.class)).isNotNull();
|
||||
assertThat(appContext.getBean("flashMapManager", FlashMapManager.class)).isNotNull();
|
||||
|
||||
// default web binding initializer behavior test
|
||||
request = new MockHttpServletRequest("GET", "/");
|
||||
@@ -262,6 +267,23 @@ public class MvcNamespaceTests {
|
||||
assertThat(introspector.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class);
|
||||
}
|
||||
|
||||
@Test // gh-25290
|
||||
public void testDefaultConfigWithBeansInParentContext() throws Exception {
|
||||
StaticApplicationContext parent = new StaticApplicationContext();
|
||||
parent.registerSingleton("localeResolver", CookieLocaleResolver.class);
|
||||
parent.registerSingleton("themeResolver", CookieThemeResolver.class);
|
||||
parent.registerSingleton("viewNameTranslator", DefaultRequestToViewNameTranslator.class);
|
||||
parent.registerSingleton("flashMapManager", SessionFlashMapManager.class);
|
||||
parent.refresh();
|
||||
appContext.setParent(parent);
|
||||
|
||||
loadBeanDefinitions("mvc-config.xml");
|
||||
assertThat(appContext.getBean("localeResolver")).isSameAs(parent.getBean("localeResolver"));
|
||||
assertThat(appContext.getBean("themeResolver")).isSameAs(parent.getBean("themeResolver"));
|
||||
assertThat(appContext.getBean("viewNameTranslator")).isSameAs(parent.getBean("viewNameTranslator"));
|
||||
assertThat(appContext.getBean("flashMapManager")).isSameAs(parent.getBean("flashMapManager"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomConversionService() throws Exception {
|
||||
loadBeanDefinitions("mvc-config-custom-conversion-service.xml");
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.servlet.mvc.method.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.Principal;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PrincipalMethodArgumentResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class PrincipalMethodArgumentResolverTests {
|
||||
|
||||
private PrincipalMethodArgumentResolver resolver;
|
||||
|
||||
private ModelAndViewContainer mavContainer;
|
||||
|
||||
private MockHttpServletRequest servletRequest;
|
||||
|
||||
private ServletWebRequest webRequest;
|
||||
|
||||
private Method method;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
resolver = new PrincipalMethodArgumentResolver();
|
||||
mavContainer = new ModelAndViewContainer();
|
||||
servletRequest = new MockHttpServletRequest("GET", "");
|
||||
webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());
|
||||
|
||||
method = getClass().getMethod("supportedParams", ServletRequest.class, Principal.class);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void principal() throws Exception {
|
||||
Principal principal = () -> "Foo";
|
||||
servletRequest.setUserPrincipal(principal);
|
||||
|
||||
MethodParameter principalParameter = new MethodParameter(method, 1);
|
||||
assertThat(resolver.supportsParameter(principalParameter)).as("Principal not supported").isTrue();
|
||||
|
||||
Object result = resolver.resolveArgument(principalParameter, null, webRequest, null);
|
||||
assertThat(result).as("Invalid result").isSameAs(principal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void principalAsNull() throws Exception {
|
||||
MethodParameter principalParameter = new MethodParameter(method, 1);
|
||||
assertThat(resolver.supportsParameter(principalParameter)).as("Principal not supported").isTrue();
|
||||
|
||||
Object result = resolver.resolveArgument(principalParameter, null, webRequest, null);
|
||||
assertThat(result).as("Invalid result").isNull();
|
||||
}
|
||||
|
||||
@Test // gh-25780
|
||||
public void annotatedPrincipal() throws Exception {
|
||||
Principal principal = () -> "Foo";
|
||||
servletRequest.setUserPrincipal(principal);
|
||||
Method principalMethod = getClass().getMethod("supportedParamsWithAnnotatedPrincipal", Principal.class);
|
||||
|
||||
MethodParameter principalParameter = new MethodParameter(principalMethod, 0);
|
||||
assertThat(resolver.supportsParameter(principalParameter)).isTrue();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void supportedParams(ServletRequest p0, Principal p1) {}
|
||||
|
||||
@Target({ ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AuthenticationPrincipal {}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void supportedParamsWithAnnotatedPrincipal(@AuthenticationPrincipal Principal p) {}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -131,7 +130,7 @@ public interface WebSocketSession extends Closeable {
|
||||
void sendMessage(WebSocketMessage<?> message) throws IOException;
|
||||
|
||||
/**
|
||||
* Return whether the connection is still open.
|
||||
* Whether the underlying connection is open.
|
||||
*/
|
||||
boolean isOpen();
|
||||
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
+18
-10
@@ -269,13 +269,15 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
}
|
||||
|
||||
for (Message<byte[]> message : messages) {
|
||||
try {
|
||||
StompHeaderAccessor headerAccessor =
|
||||
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
Assert.state(headerAccessor != null, "No StompHeaderAccessor");
|
||||
StompHeaderAccessor headerAccessor =
|
||||
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
Assert.state(headerAccessor != null, "No StompHeaderAccessor");
|
||||
|
||||
StompCommand command = headerAccessor.getCommand();
|
||||
boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);
|
||||
StompCommand command = headerAccessor.getCommand();
|
||||
boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);
|
||||
|
||||
boolean sent = false;
|
||||
try {
|
||||
|
||||
headerAccessor.setSessionId(session.getId());
|
||||
headerAccessor.setSessionAttributes(session.getAttributes());
|
||||
@@ -305,7 +307,7 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
|
||||
try {
|
||||
SimpAttributesContextHolder.setAttributesFromMessage(message);
|
||||
boolean sent = outputChannel.send(message);
|
||||
sent = outputChannel.send(message);
|
||||
|
||||
if (sent) {
|
||||
if (this.eventPublisher != null) {
|
||||
@@ -327,9 +329,15 @@ 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);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to send message to MessageChannel in session " + session.getId(), ex);
|
||||
}
|
||||
else if (logger.isErrorEnabled()) {
|
||||
// Skip unsent CONNECT messages (likely auth issues)
|
||||
if (!isConnect || sent) {
|
||||
logger.error("Failed to send message to MessageChannel in session " + session.getId() +
|
||||
":" + ex.getMessage());
|
||||
}
|
||||
}
|
||||
handleError(session, ex, message);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -6805,7 +6805,7 @@ The following example extracts the `id` column and emits its value:
|
||||
.Kotlin
|
||||
----
|
||||
val names = client.sql("SELECT name FROM person")
|
||||
.map{ it.get("id", String.class) }
|
||||
.map{ row: Row -> row.get("id", String.class) }
|
||||
.flow()
|
||||
----
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ To change the limit for default codecs, use the following:
|
||||
.Java
|
||||
----
|
||||
WebClient webClient = WebClient.builder()
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024));
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
@@ -831,7 +831,7 @@ inline-style, through the built-in `BodyInserters`, as the following example sho
|
||||
|
||||
|
||||
[[webflux-client-filter]]
|
||||
== Client Filters
|
||||
== Filters
|
||||
|
||||
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder`
|
||||
in order to intercept and modify requests, as the following example shows:
|
||||
@@ -887,9 +887,36 @@ a filter for basic authentication through a static factory method:
|
||||
.build()
|
||||
----
|
||||
|
||||
Filters apply globally to every request. To change a filter's behavior for a specific
|
||||
request, you can add request attributes to the `ClientRequest` that can then be accessed
|
||||
by all filters in the chain, as the following example shows:
|
||||
You can create a new `WebClient` instance by using another as a starting point. This allows
|
||||
insert or removing filters without affecting the original `WebClient`. Below is an example
|
||||
that inserts a basic authentication filter at index 0:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
||||
|
||||
WebClient client = webClient.mutate()
|
||||
.filters(filterList -> {
|
||||
filterList.add(0, basicAuthentication("user", "password"));
|
||||
})
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val client = webClient.mutate()
|
||||
.filters { it.add(0, basicAuthentication("user", "password")) }
|
||||
.build()
|
||||
----
|
||||
|
||||
|
||||
[[webflux-client-attributes]]
|
||||
== Attributes
|
||||
|
||||
You can add attributes to a request. This is convenient if you want to pass information
|
||||
through the filter chain and influence the behavior of filters for a given request.
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
@@ -912,10 +939,11 @@ by all filters in the chain, as the following example shows:
|
||||
.Kotlin
|
||||
----
|
||||
val client = WebClient.builder()
|
||||
.filter { request, _ ->
|
||||
val usr = request.attributes()["myAttribute"];
|
||||
// ...
|
||||
}.build()
|
||||
.filter { request, _ ->
|
||||
val usr = request.attributes()["myAttribute"];
|
||||
// ...
|
||||
}
|
||||
.build()
|
||||
|
||||
client.get().uri("https://example.org/")
|
||||
.attribute("myAttribute", "...")
|
||||
@@ -923,29 +951,44 @@ by all filters in the chain, as the following example shows:
|
||||
.awaitBody<Unit>()
|
||||
----
|
||||
|
||||
You can also replicate an existing `WebClient`, insert new filters, or remove already
|
||||
registered filters. The following example, inserts a basic authentication filter at
|
||||
index 0:
|
||||
|
||||
[[webflux-client-context]]
|
||||
== Context
|
||||
|
||||
<<webflux-client-attributes>> provide a convenient way to pass information to the filter
|
||||
chain but they only influence the current request. If you want to pass information that
|
||||
propagates to additional requests that are nested, e.g. via `flatMap`, or executed after,
|
||||
e.g. via `concatMap`, then you'll need to use the Reactor `Context`.
|
||||
|
||||
`WebClient` exposes a method to populate the Reactor `Context` for a given request.
|
||||
This information is available to filters for the current request and it also propagates
|
||||
to subsequent requests or other reactive clients participating in the downstream
|
||||
processing chain. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
||||
|
||||
WebClient client = webClient.mutate()
|
||||
.filters(filterList -> {
|
||||
filterList.add(0, basicAuthentication("user", "password"));
|
||||
})
|
||||
WebClient client = WebClient.builder()
|
||||
.filter((request, next) ->
|
||||
Mono.deferContextual(contextView -> {
|
||||
String value = contextView.get("foo");
|
||||
// ...
|
||||
}))
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val client = webClient.mutate()
|
||||
.filters { it.add(0, basicAuthentication("user", "password")) }
|
||||
.build()
|
||||
|
||||
client.get().uri("https://example.org/")
|
||||
.context(context -> context.put("foo", ...))
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.flatMap(body -> {
|
||||
// perform nested request (context propagates automatically)...
|
||||
});
|
||||
----
|
||||
|
||||
Note that you can also specify how to populate the context through the `defaultRequest`
|
||||
method at the level of the `WebClient.Builder` and that applies to all requests.
|
||||
This could be used for to example to pass information from `ThreadLocal` storage onto
|
||||
a Reactor processing chain in a Spring MVC application.
|
||||
|
||||
|
||||
[[webflux-client-synchronous]]
|
||||
|
||||
Reference in New Issue
Block a user