GH-9436: Add support for SpEL IndexAccessor configuration (#9451)

Fixes: #9436
Issue link: https://github.com/spring-projects/spring-integration/issues/9436

* Expose `IndexAccessor` configuration options on the `AbstractEvaluationContextFactoryBean`
and `SpelPropertyAccessorRegistrar`
* Expose `<index-accessors>` sub-element for the `<spel-property-accessors>`
* Adjust tests
* Document the feature, including recently added `JsonIndexAccessor`
This commit is contained in:
Artem Bilan
2024-09-12 15:38:58 -04:00
committed by GitHub
parent 775bfd6168
commit e3a46ca528
12 changed files with 265 additions and 148 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018-2019 the original author or authors.
* Copyright 2018-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,11 +28,13 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.IndexAccessor;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -45,14 +47,19 @@ import org.springframework.util.Assert;
*/
public abstract class AbstractEvaluationContextFactoryBean implements ApplicationContextAware, InitializingBean {
private Map<String, PropertyAccessor> propertyAccessors = new LinkedHashMap<String, PropertyAccessor>();
private Map<String, PropertyAccessor> propertyAccessors = new LinkedHashMap<>();
private Map<String, Method> functions = new LinkedHashMap<String, Method>();
private Map<String, IndexAccessor> indexAccessors = new LinkedHashMap<>();
private Map<String, Method> functions = new LinkedHashMap<>();
private TypeConverter typeConverter = new StandardTypeConverter();
private ApplicationContext applicationContext;
@Nullable
private SpelPropertyAccessorRegistrar propertyAccessorRegistrar;
private boolean initialized;
protected TypeConverter getTypeConverter() {
@@ -68,22 +75,45 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio
this.applicationContext = applicationContext;
}
public void setPropertyAccessors(Map<String, PropertyAccessor> accessors) {
public void setPropertyAccessors(Map<String, PropertyAccessor> propertyAccessors) {
Assert.isTrue(!this.initialized, "'propertyAccessors' can't be changed after initialization.");
Assert.notNull(accessors, "'accessors' must not be null.");
Assert.noNullElements(accessors.values().toArray(), "'accessors' cannot have null values.");
this.propertyAccessors = new LinkedHashMap<String, PropertyAccessor>(accessors);
Assert.notNull(propertyAccessors, "'propertyAccessors' must not be null.");
Assert.noNullElements(propertyAccessors.values().toArray(), "'propertyAccessors' cannot have null values.");
this.propertyAccessors = new LinkedHashMap<>(propertyAccessors);
}
public Map<String, PropertyAccessor> getPropertyAccessors() {
return this.propertyAccessors;
}
/**
* Set a map of {@link IndexAccessor}s to use in the target {@link org.springframework.expression.EvaluationContext}
* @param indexAccessors the map of {@link IndexAccessor}s to use
* @since 6.4
* @see org.springframework.expression.EvaluationContext#getIndexAccessors()
*/
public void setIndexAccessors(Map<String, IndexAccessor> indexAccessors) {
Assert.isTrue(!this.initialized, "'indexAccessors' can't be changed after initialization.");
Assert.notNull(indexAccessors, "'indexAccessors' must not be null.");
Assert.noNullElements(indexAccessors.values().toArray(), "'indexAccessors' cannot have null values.");
this.indexAccessors = new LinkedHashMap<>(indexAccessors);
}
/**
* Return the map of {@link IndexAccessor}s to use in the target {@link org.springframework.expression.EvaluationContext}
* @return the map of {@link IndexAccessor}s to use
* @since 6.4
* @see org.springframework.expression.EvaluationContext#getIndexAccessors()
*/
public Map<String, IndexAccessor> getIndexAccessors() {
return this.indexAccessors;
}
public void setFunctions(Map<String, Method> functionsArg) {
Assert.isTrue(!this.initialized, "'functions' can't be changed after initialization.");
Assert.notNull(functionsArg, "'functions' must not be null.");
Assert.noNullElements(functionsArg.values().toArray(), "'functions' cannot have null values.");
this.functions = new LinkedHashMap<String, Method>(functionsArg);
this.functions = new LinkedHashMap<>(functionsArg);
}
public Map<String, Method> getFunctions() {
@@ -94,7 +124,14 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio
if (this.applicationContext != null) {
conversionService();
functions();
try {
this.propertyAccessorRegistrar = this.applicationContext.getBean(SpelPropertyAccessorRegistrar.class);
}
catch (@SuppressWarnings("unused") NoSuchBeanDefinitionException e) {
// There is no 'SpelPropertyAccessorRegistrar' bean in the application context.
}
propertyAccessors();
indexAccessors();
processParentIfPresent(beanName);
}
this.initialized = true;
@@ -108,28 +145,43 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio
}
private void functions() {
Map<String, SpelFunctionFactoryBean> functionFactoryBeanMap = BeanFactoryUtils
.beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class);
for (SpelFunctionFactoryBean spelFunctionFactoryBean : functionFactoryBeanMap.values()) {
if (!getFunctions().containsKey(spelFunctionFactoryBean.getFunctionName())) {
getFunctions().put(spelFunctionFactoryBean.getFunctionName(), spelFunctionFactoryBean.getObject());
Map<String, SpelFunctionFactoryBean> spelFunctions =
BeanFactoryUtils.beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class);
for (SpelFunctionFactoryBean spelFunctionFactoryBean : spelFunctions.values()) {
String functionName = spelFunctionFactoryBean.getFunctionName();
if (!this.functions.containsKey(functionName)) {
this.functions.put(functionName, spelFunctionFactoryBean.getObject());
}
}
}
private void propertyAccessors() {
try {
SpelPropertyAccessorRegistrar propertyAccessorRegistrar =
this.applicationContext.getBean(SpelPropertyAccessorRegistrar.class);
for (Entry<String, PropertyAccessor> entry : propertyAccessorRegistrar.getPropertyAccessors()
.entrySet()) {
if (!getPropertyAccessors().containsKey(entry.getKey())) {
getPropertyAccessors().put(entry.getKey(), entry.getValue());
}
if (this.propertyAccessorRegistrar != null) {
propertyAccessors(this.propertyAccessorRegistrar.getPropertyAccessors());
}
}
private void propertyAccessors(Map<String, PropertyAccessor> propertyAccessors) {
for (Entry<String, PropertyAccessor> entry : propertyAccessors.entrySet()) {
String key = entry.getKey();
if (!this.propertyAccessors.containsKey(key)) {
this.propertyAccessors.put(key, entry.getValue());
}
}
catch (@SuppressWarnings("unused") NoSuchBeanDefinitionException e) {
// There is no 'SpelPropertyAccessorRegistrar' bean in the application context.
}
private void indexAccessors() {
if (this.propertyAccessorRegistrar != null) {
indexAccessors(this.propertyAccessorRegistrar.getIndexAccessors());
}
}
private void indexAccessors(Map<String, IndexAccessor> indexAccessors) {
for (Entry<String, IndexAccessor> entry : indexAccessors.entrySet()) {
String key = entry.getKey();
if (!this.indexAccessors.containsKey(key)) {
this.indexAccessors.put(key, entry.getValue());
}
}
}
@@ -138,16 +190,12 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio
if (parent != null && parent.containsBean(beanName)) {
AbstractEvaluationContextFactoryBean parentFactoryBean = parent.getBean("&" + beanName, getClass());
for (Entry<String, PropertyAccessor> entry : parentFactoryBean.getPropertyAccessors().entrySet()) {
if (!getPropertyAccessors().containsKey(entry.getKey())) {
getPropertyAccessors().put(entry.getKey(), entry.getValue());
}
}
propertyAccessors(parentFactoryBean.getPropertyAccessors());
indexAccessors(parentFactoryBean.getIndexAccessors());
for (Entry<String, Method> entry : parentFactoryBean.getFunctions().entrySet()) {
if (!getFunctions().containsKey(entry.getKey())) {
getFunctions().put(entry.getKey(), entry.getValue());
String key = entry.getKey();
if (!this.functions.containsKey(key)) {
this.functions.put(key, entry.getValue());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.IndexAccessor;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeLocator;
import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -102,6 +103,10 @@ public class IntegrationEvaluationContextFactoryBean extends AbstractEvaluationC
evaluationContext.addPropertyAccessor(new MapAccessor());
for (IndexAccessor indexAccessor : getIndexAccessors().values()) {
evaluationContext.addIndexAccessor(indexAccessor);
}
for (Entry<String, Method> functionEntry : getFunctions().entrySet()) {
evaluationContext.registerFunction(functionEntry.getKey(), functionEntry.getValue());
}

View File

@@ -22,6 +22,7 @@ import java.util.Map.Entry;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.IndexAccessor;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.spel.support.DataBindingPropertyAccessor;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
@@ -80,6 +81,7 @@ public class IntegrationSimpleEvaluationContextFactoryBean extends AbstractEvalu
accessorArray[accessors.size() + 1] = DataBindingPropertyAccessor.forReadOnlyAccess();
SimpleEvaluationContext evaluationContext =
SimpleEvaluationContext.forPropertyAccessors(accessorArray)
.withIndexAccessors(getIndexAccessors().values().toArray(new IndexAccessor[0]))
.withTypeConverter(getTypeConverter())
.withInstanceMethods()
.withAssignmentDisabled()

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-2024 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,13 +16,10 @@
package org.springframework.integration.config.xml;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
@@ -33,7 +30,9 @@ import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
/**
* Parser for the &lt;spel-property-accessors&gt; element.
@@ -46,69 +45,61 @@ import org.springframework.util.StringUtils;
*/
public class SpelPropertyAccessorsParser implements BeanDefinitionParser {
private final Lock lock = new ReentrantLock();
private final Map<String, Object> propertyAccessors = new ManagedMap<String, Object>();
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
initializeSpelPropertyAccessorRegistrarIfNecessary(parserContext);
Map<String, Object> propertyAccessors = new ManagedMap<>();
Map<String, Object> indexAccessors = new ManagedMap<>();
parseTargetedAccessors(element, parserContext, propertyAccessors);
BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
String propertyAccessorName;
Object propertyAccessor;
if (node instanceof Element &&
!delegate.nodeNameEquals(node, BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT)) {
Element ele = (Element) node;
if (delegate.nodeNameEquals(ele, BeanDefinitionParserDelegate.BEAN_ELEMENT)) {
propertyAccessorName = ele.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE);
if (!StringUtils.hasText(propertyAccessorName)) {
parserContext.getReaderContext()
.error("The '<bean>' 'id' attribute is required within 'spel-property-accessors'.", ele);
return null;
}
propertyAccessor = delegate.parseBeanDefinitionElement(ele);
}
else if (delegate.nodeNameEquals(ele, BeanDefinitionParserDelegate.REF_ELEMENT)) {
BeanReference propertyAccessorRef = (BeanReference) delegate.parsePropertySubElement(ele, null);
propertyAccessorName = propertyAccessorRef.getBeanName(); // NOSONAR not null
propertyAccessor = propertyAccessorRef;
}
else {
parserContext.getReaderContext().error("Only '<bean>' and '<ref>' elements are allowed.", element);
return null;
}
this.propertyAccessors.put(propertyAccessorName, propertyAccessor);
}
Element indexAccessorsElement = DomUtils.getChildElementByTagName(element, "index-accessors");
if (indexAccessorsElement != null) {
parseTargetedAccessors(indexAccessorsElement, parserContext, indexAccessors);
}
BeanDefinitionBuilder registrarBuilder =
BeanDefinitionBuilder.genericBeanDefinition(SpelPropertyAccessorRegistrar.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
if (!CollectionUtils.isEmpty(propertyAccessors)) {
registrarBuilder.addConstructorArgValue(propertyAccessors);
}
if (!CollectionUtils.isEmpty(indexAccessors)) {
registrarBuilder.addPropertyValue("indexAccessors", indexAccessors);
}
parserContext.getRegistry()
.registerBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME,
registrarBuilder.getBeanDefinition());
return null;
}
private void initializeSpelPropertyAccessorRegistrarIfNecessary(ParserContext parserContext) {
this.lock.lock();
try {
if (!parserContext.getRegistry()
.containsBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME)) {
private static void parseTargetedAccessors(Element accessorsElement, ParserContext parserContext,
Map<String, Object> accessorsMap) {
BeanDefinitionBuilder registrarBuilder = BeanDefinitionBuilder
.genericBeanDefinition(SpelPropertyAccessorRegistrar.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
.addConstructorArgValue(this.propertyAccessors);
parserContext.getRegistry()
.registerBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME,
registrarBuilder.getBeanDefinition());
BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
List<Element> accessorElements = DomUtils.getChildElementsByTagName(accessorsElement,
BeanDefinitionParserDelegate.BEAN_ELEMENT, BeanDefinitionParserDelegate.REF_ELEMENT);
for (Element accessorElement : accessorElements) {
String accessorName;
Object accessor;
if (delegate.nodeNameEquals(accessorElement, BeanDefinitionParserDelegate.BEAN_ELEMENT)) {
accessorName = accessorElement.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE);
if (!StringUtils.hasText(accessorName)) {
parserContext.getReaderContext()
.error("The '<bean>' 'id' attribute is required within 'spel-property-accessors'.",
accessorElement);
return;
}
accessor = delegate.parseBeanDefinitionElement(accessorElement);
}
}
finally {
this.lock.unlock();
else {
BeanReference propertyAccessorRef =
(BeanReference) delegate.parsePropertySubElement(accessorElement, null);
accessorName = propertyAccessorRef.getBeanName(); // NOSONAR not null
accessor = propertyAccessorRef;
}
accessorsMap.put(accessorName, accessor);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,9 @@ import java.beans.Introspector;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.expression.IndexAccessor;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TargetedAccessor;
import org.springframework.util.Assert;
/**
@@ -35,14 +37,16 @@ import org.springframework.util.Assert;
*/
public class SpelPropertyAccessorRegistrar {
private final Map<String, PropertyAccessor> propertyAccessors = new LinkedHashMap<String, PropertyAccessor>();
private final Map<String, PropertyAccessor> propertyAccessors = new LinkedHashMap<>();
private final Map<String, IndexAccessor> indexAccessors = new LinkedHashMap<>();
public SpelPropertyAccessorRegistrar() {
}
/**
* Create an instance with the provided property accessors. Each accessor name
* will be the class simple name.
* Create an instance with the provided {@link PropertyAccessor} instances.
* Each accessor name will be the class simple name.
* @param propertyAccessors the accessors.
* @since 4.3.8
*/
@@ -54,7 +58,7 @@ public class SpelPropertyAccessorRegistrar {
}
/**
* Create an instance with the provided named property accessors.
* Create an instance with the provided named {@link PropertyAccessor} instances.
* @param propertyAccessors a map of name:accessor.
* @since 4.3.8
*/
@@ -64,7 +68,8 @@ public class SpelPropertyAccessorRegistrar {
}
/**
* Return the registered accessors.
* Return the registered {@link PropertyAccessor} instances to use
* in the target {@link org.springframework.expression.EvaluationContext}.
* @return the map of name:accessor.
* @since 4.3.8
*/
@@ -72,6 +77,26 @@ public class SpelPropertyAccessorRegistrar {
return this.propertyAccessors;
}
/**
* Add a map of {@link IndexAccessor} instances to use
* in the target {@link org.springframework.expression.EvaluationContext}.
* @param indexAccessors the map of name:accessor.
* @since 6.4
*/
public void setIndexAccessors(Map<String, IndexAccessor> indexAccessors) {
Assert.notEmpty(indexAccessors, "'indexAccessors' must not be empty");
this.indexAccessors.putAll(indexAccessors);
}
/**
* Return the registered {@link IndexAccessor} instances.
* @return the map of name:accessor.
* @since 6.4
*/
public Map<String, IndexAccessor> getIndexAccessors() {
return this.indexAccessors;
}
/**
* Add the provided named property accessor.
* @param name the name.
@@ -101,8 +126,37 @@ public class SpelPropertyAccessorRegistrar {
return this;
}
private static String obtainAccessorKey(PropertyAccessor propertyAccessor) {
return Introspector.decapitalize(propertyAccessor.getClass().getSimpleName());
/**
* Add the provided named {@link IndexAccessor}.
* @param name the name.
* @param indexAccessor the accessor.
* @return this registrar.
* @since 6.4
*/
public SpelPropertyAccessorRegistrar add(String name, IndexAccessor indexAccessor) {
Assert.hasText(name, "'name' must not be empty");
Assert.notNull(indexAccessor, "'indexAccessor' must not be null");
this.indexAccessors.put(name, indexAccessor);
return this;
}
/**
* Add the provided {@link IndexAccessor} instances.
* Each accessor name will be the class simple name.
* @param indexAccessors the accessors.
* @return this registrar.
* @since 6.4
*/
public SpelPropertyAccessorRegistrar add(IndexAccessor... indexAccessors) {
Assert.notEmpty(indexAccessors, "'indexAccessors' must not be empty");
for (IndexAccessor indexAccessor : indexAccessors) {
this.indexAccessors.put(obtainAccessorKey(indexAccessor), indexAccessor);
}
return this;
}
private static String obtainAccessorKey(TargetedAccessor targetedAccessor) {
return Introspector.decapitalize(targetedAccessor.getClass().getSimpleName());
}
}

View File

@@ -4698,10 +4698,20 @@ The list of component name patterns you want to track (e.g., tracked-components
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="beans:bean"/>
<xsd:element ref="beans:ref"/>
</xsd:choice>
<xsd:sequence>
<xsd:element name="index-accessors" minOccurs="0">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="beans:bean"/>
<xsd:element ref="beans:ref"/>
</xsd:choice>
</xsd:complexType>
</xsd:element>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="beans:bean"/>
<xsd:element ref="beans:ref"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
</xsd:element>

View File

@@ -64,7 +64,7 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.log.LogAccessor;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.IndexAccessor;
import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.annotation.Aggregator;
@@ -112,6 +112,7 @@ import org.springframework.integration.handler.ServiceActivatingHandler;
import org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.history.MessageHistoryConfigurer;
import org.springframework.integration.json.JsonIndexAccessor;
import org.springframework.integration.json.JsonPropertyAccessor;
import org.springframework.integration.scheduling.PollerMetadata;
import org.springframework.integration.support.MessageBuilder;
@@ -786,15 +787,17 @@ public class EnableIntegrationTests {
@Test
public void testIntegrationEvaluationContextCustomization() {
EvaluationContext evaluationContext = this.context.getBean(StandardEvaluationContext.class);
List<?> propertyAccessors = TestUtils.getPropertyValue(evaluationContext, "propertyAccessors", List.class);
StandardEvaluationContext evaluationContext = this.context.getBean(StandardEvaluationContext.class);
List<?> propertyAccessors = evaluationContext.getPropertyAccessors();
assertThat(propertyAccessors.size()).isEqualTo(4);
assertThat(propertyAccessors.get(0)).isInstanceOf(JsonPropertyAccessor.class);
assertThat(propertyAccessors.get(1)).isInstanceOf(EnvironmentAccessor.class);
assertThat(propertyAccessors.get(2)).isInstanceOf(MapAccessor.class);
assertThat(propertyAccessors.get(3)).isInstanceOf(ReflectivePropertyAccessor.class);
Map<?, ?> variables = TestUtils.getPropertyValue(evaluationContext, "variables", Map.class);
Object testSpelFunction = variables.get("testSpelFunction");
List<IndexAccessor> indexAccessors = evaluationContext.getIndexAccessors();
assertThat(indexAccessors.size()).isEqualTo(1);
assertThat(indexAccessors.get(0)).isInstanceOf(JsonIndexAccessor.class);
Object testSpelFunction = evaluationContext.lookupVariable("testSpelFunction");
assertThat(testSpelFunction).isEqualTo(ClassUtils.getStaticMethod(TestSpelFunction.class, "bar",
Object.class));
}
@@ -1244,7 +1247,9 @@ public class EnableIntegrationTests {
@Bean
public SpelPropertyAccessorRegistrar spelPropertyAccessorRegistrar() {
return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor(), new EnvironmentAccessor());
return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor())
.add(new EnvironmentAccessor())
.add(new JsonIndexAccessor());
}
@Bean

View File

@@ -42,10 +42,13 @@
</beans:property>
</beans:bean>
<beans:import resource="property-accessor-import-context.xml" />
<!-- import twice to verify override is ok -->
<beans:import resource="property-accessor-import-context.xml" />
<spel-property-accessors>
<index-accessors>
<beans:bean id="jsonIndex" class="org.springframework.integration.json.JsonIndexAccessor"/>
</index-accessors>
<beans:bean id="fooAccessor1" class="org.springframework.integration.transformer.SpelTransformerIntegrationTests$FooAccessor"/>
<beans:ref bean="fooAccessor"/>
</spel-property-accessors>
<beans:bean id="fooAccessor" class="org.springframework.integration.transformer.SpelTransformerIntegrationTests$FooAccessor"/>

View File

@@ -16,10 +16,7 @@
package org.springframework.integration.transformer;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -33,13 +30,12 @@ import org.springframework.integration.config.IntegrationEvaluationContextFactor
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.handler.ReplyRequiredException;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
@@ -49,8 +45,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Artem Bilan
* @author Gary Russell
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext
public class SpelTransformerIntegrationTests {
@Autowired
@@ -96,7 +92,7 @@ public class SpelTransformerIntegrationTests {
@Test
public void testInt2755ChainChildIdWithinExceptionMessage() {
try {
this.transformerChainInput.send(new GenericMessage<String>("foo"));
this.transformerChainInput.send(new GenericMessage<>("foo"));
}
catch (ReplyRequiredException e) {
assertThat(e.getMessage()).contains("No reply produced by handler 'transformerChain$child#0'");
@@ -108,20 +104,20 @@ public class SpelTransformerIntegrationTests {
QueueChannel outputChannel = new QueueChannel();
fooHandler.setOutputChannel(outputChannel);
Foo foo = new Foo("baz");
fooHandler.handleMessage(new GenericMessage<Foo>(foo));
fooHandler.handleMessage(new GenericMessage<>(foo));
Message<?> reply = outputChannel.receive(0);
assertThat(reply).isNotNull();
assertThat(reply.getPayload() instanceof String).isTrue();
assertThat(reply.getPayload()).isEqualTo("baz");
assertThat(TestUtils.getPropertyValue(this.evaluationContextFactoryBean, "propertyAccessors", Map.class).size())
.isEqualTo(3);
assertThat(this.evaluationContextFactoryBean.getPropertyAccessors()).hasSize(3);
assertThat(this.evaluationContextFactoryBean.getIndexAccessors()).hasSize(1);
}
@Test
public void testCustomFunction() {
QueueChannel outputChannel = new QueueChannel();
barHandler.setOutputChannel(outputChannel);
barHandler.handleMessage(new GenericMessage<String>("foo"));
barHandler.handleMessage(new GenericMessage<>("foo"));
Message<?> reply = outputChannel.receive(0);
assertThat(reply).isNotNull();
assertThat(reply.getPayload()).isEqualTo("bar");
@@ -169,7 +165,7 @@ public class SpelTransformerIntegrationTests {
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
public boolean canRead(EvaluationContext context, Object target, String name) {
return "bar".equals(name);
}
@@ -180,7 +176,7 @@ public class SpelTransformerIntegrationTests {
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
public boolean canWrite(EvaluationContext context, Object target, String name) {
return "bar".equals(name);
}

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd">
<int:spel-property-accessors>
<bean id="fooAccessor1" class="org.springframework.integration.transformer.SpelTransformerIntegrationTests$FooAccessor"/>
<ref bean="fooAccessor"/>
</int:spel-property-accessors>
</beans>

View File

@@ -20,6 +20,11 @@ Starting with Spring Integration 3.0, you can add additional `PropertyAccessor`
The framework provides the (read-only) `JsonPropertyAccessor`, which you can use to access fields from a `JsonNode` or JSON in a `String`.
You can also create your own `PropertyAccessor` if you have specific needs.
Starting with version 6.4, the `JsonIndexAccessor` implementation is provided that knows how to read indexes from JSON arrays, using Jackson's `ArrayNode` API.
Supports indexes supplied as an integer literal, for example, `myJsonArray[1]`.
Also supports negative indexes, for example, `myJsonArray[-1]` which equates to `myJsonArray[myJsonArray.length - 1]`.
Furthermore, `null` is returned for any index that is out of bounds (see `ArrayNode.get(int)` for details).
In addition, you can add custom functions.
Custom functions are `static` methods declared on a class.
Functions and property accessors are available in any SpEL expression used throughout the framework.
@@ -45,6 +50,9 @@ The following configuration shows how to directly configure the `IntegrationEval
</bean>
----
Starting with version 6.4, the `AbstractEvaluationContextFactoryBean` supports an injection of `IndexAccessor` instances.
See `AbstractEvaluationContextFactoryBean` method JavaDocs for more information.
For convenience, Spring Integration provides namespace support for both property accessors and functions, as described in the following sections.
The framework automatically configures the factory bean on your behalf.
@@ -122,7 +130,7 @@ Each context has its own instance of the `integrationEvaluationContext` factory
[[built-in-spel-functions]]
=== Built-in SpEL Functions
Spring Integration provides the folloiwng standard functions, which are registered with the application context automatically on start up:
Spring Integration provides the following standard functions, which are registered with the application context automatically on start up:
* `#jsonPath`: Evaluates a 'jsonPath' on a specified object.
This function invokes `JsonPathUtils.evaluate(...)`, which delegates to the https://github.com/json-path/JsonPath[Jayway JsonPath library].
@@ -157,12 +165,16 @@ For more information regarding XML and XPath, see xref:xml.adoc[XML Support - De
Spring Integration provides namespace support to let you create SpEL custom https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/expression/PropertyAccessor.html[`PropertyAccessor`] implementations.
You can use the `<spel-property-accessors/>` component to provide a list of custom `PropertyAccessor` instances to the `EvaluationContext` used throughout the framework.
Instead of configuring the factory bean shown earlier, you can add one or more of these components, and the framework automatically adds the accessors to the default `integrationEvaluationContext` factory bean.
Instead of configuring the factory bean shown earlier, you can add this component, and the framework automatically adds the accessors to the default `integrationEvaluationContext` factory bean.
Also, starting with version 6.4, a dedicated `<index-accessors>` sub-element is provided to configure `IndexAccessor` beans similar way.
The following example shows how to do so:
[source,xml]
----
<int:spel-property-accessors>
<index-accessors>
<beans:bean id="jsonIndex" class="org.springframework.integration.json.JsonIndexAccessor"/>
</index-accessors>
<bean id="jsonPA" class="org.springframework.integration.json.JsonPropertyAccessor"/>
<ref bean="fooPropertyAccessor"/>
</int:spel-property-accessors>
@@ -170,15 +182,17 @@ The following example shows how to do so:
In the preceding example, two custom `PropertyAccessor` instances are injected into the `EvaluationContext` (in the order in which they are declared).
To provide `PropertyAccessor` instances by using Java Configuration, you should declare a `SpelPropertyAccessorRegistrar` bean with a name of `spelPropertyAccessorRegistrar` (dictated by the `IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME` constant).
The following example shows how to configure two custom `PropertyAccessor` instances with Java:
The following example shows how to configure two custom `PropertyAccessor` (and `IndexAccessor` starting with version 6.4) instances with Java:
[source,java]
----
@Bean
public SpelPropertyAccessorRegistrar spelPropertyAccessorRegistrar() {
return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor())
.add(fooPropertyAccessor());
.add(fooPropertyAccessor())
.add(new JsonIndexAccessor());
}
----

View File

@@ -23,6 +23,10 @@ See xref:control-bus.adoc[Control Bus] for more information.
Also, a `ControlBusController` (together with an `@EnableControlBusController`) is introduced for managing exposed commands by the mentioned `ControlBusCommandRegistry`.
See xref:http.adoc[HTTP Support] for more information.
The SpEL evaluation infrastructure now supports configuration for `IndexAccessor`.
Also, an out-of-the-box `JsonIndexAccessor` is provided.
See xref:spel.adoc[SpEL Support] for more information.
[[x6.4-general]]
=== General Changes
@@ -74,4 +78,3 @@ See xref:sftp/session-factory.adoc[SFTP Session Factory] for more information.
Multiple instances of `MqttPahoMessageDrivenChannelAdapter` and `Mqttv5PahoMessageDrivenChannelAdapter` can now be added at runtime using corresponding `ClientManager` through `IntegrationFlowContext`
Also a `MqttMessageNotDeliveredEvent` event has been introduced to emit when action callback reacts to the delivery failure.
See xref:mqtt.adoc[MQTT Support] for more information.