INT-4229: SpEL Customization Via Java Config

JIRA: https://jira.spring.io/browse/INT-4229

* Make `SpelPropertyAccessorRegistrar` as `public` class and provide more API to customize it.
This class allows to register `PropertyAccessor` s for shared `EvaluationContext`
* Document how to configure SpEL functions and `PropertyAccessor` s with Java Config

**cherry-pick to 4.3.x**
This commit is contained in:
Artem Bilan
2017-02-27 17:29:36 -05:00
committed by Gary Russell
parent e872791a69
commit 3bf2ca12ba
8 changed files with 182 additions and 65 deletions

View File

@@ -39,12 +39,12 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint;
import org.springframework.integration.config.IntegrationEvaluationContextFactoryBean;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
import org.springframework.integration.handler.MessageProcessor;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ClassUtils;
/**
* @author Mark Fisher
@@ -91,8 +91,7 @@ public class OutboundGatewayTests {
}
}).when(context).getBean(anyString());
when(context.containsBean(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME)).thenReturn(true);
when(context.getBean(ClassUtils.forName("org.springframework.integration.config.SpelPropertyAccessorRegistrar",
context.getClassLoader())))
when(context.getBean(SpelPropertyAccessorRegistrar.class))
.thenThrow(NoSuchBeanDefinitionException.class);
IntegrationEvaluationContextFactoryBean integrationEvaluationContextFactoryBean =
new IntegrationEvaluationContextFactoryBean();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-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.
@@ -38,6 +38,7 @@ import org.springframework.expression.TypeLocator;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.util.Assert;
@@ -69,6 +70,7 @@ import org.springframework.util.Assert;
*
* @author Artem Bilan
* @author Gary Russell
*
* @since 3.0
*/
public class IntegrationEvaluationContextFactoryBean implements FactoryBean<StandardEvaluationContext>,

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2013-2016 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
*
* http://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.integration.config;
import java.util.Map;
import org.springframework.expression.PropertyAccessor;
/**
* Utility class that keeps track of a Set of SpEL {@link PropertyAccessor}s
* in order to register them with the "integrationEvaluationContext" upon initialization.
*
* @author Artem Bilan
* @since 3.0
*/
class SpelPropertyAccessorRegistrar {
private final Map<String, PropertyAccessor> propertyAccessors;
SpelPropertyAccessorRegistrar(Map<String, PropertyAccessor> propertyAccessors) {
this.propertyAccessors = propertyAccessors;
}
Map<String, PropertyAccessor> getPropertyAccessors() {
return this.propertyAccessors;
}
void addPropertyAccessor(String name, PropertyAccessor propertyAccessor) {
this.propertyAccessors.put(name, propertyAccessor);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-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.
@@ -25,12 +25,12 @@ import org.w3c.dom.NodeList;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.IntegrationConfigUtils;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
import org.springframework.util.StringUtils;
/**
@@ -43,11 +43,9 @@ public class SpelPropertyAccessorsParser implements BeanDefinitionParser {
private final Map<String, Object> propertyAccessors = new ManagedMap<String, Object>();
private volatile boolean initialized;
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
this.initializeSpelPropertyAccessorRegistrarIfNecessary(parserContext);
initializeSpelPropertyAccessorRegistrarIfNecessary(parserContext);
BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
@@ -55,9 +53,10 @@ public class SpelPropertyAccessorsParser implements BeanDefinitionParser {
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
String propertyAccessorName = null;
Object propertyAccessor = null;
if (node instanceof Element && !delegate.nodeNameEquals(node, BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT)) {
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)) {
@@ -87,14 +86,16 @@ public class SpelPropertyAccessorsParser implements BeanDefinitionParser {
}
private synchronized void initializeSpelPropertyAccessorRegistrarIfNecessary(ParserContext parserContext) {
if (!this.initialized) {
if (!parserContext.getRegistry()
.containsBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME)) {
BeanDefinitionBuilder registrarBuilder = BeanDefinitionBuilder
.genericBeanDefinition(IntegrationConfigUtils.BASE_PACKAGE + ".config.SpelPropertyAccessorRegistrar")
.genericBeanDefinition(SpelPropertyAccessorRegistrar.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
.addConstructorArgValue(this.propertyAccessors);
BeanDefinitionReaderUtils.registerWithGeneratedName(registrarBuilder.getBeanDefinition(),
parserContext.getRegistry());
this.initialized = true;
parserContext.getRegistry()
.registerBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME,
registrarBuilder.getBeanDefinition());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -81,6 +81,8 @@ public abstract class IntegrationContextUtils {
public static final String INTEGRATION_GRAPH_SERVER_BEAN_NAME = "integrationGraphServer";
public static final String SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME = "spelPropertyAccessorRegistrar";
/**
* @param beanFactory BeanFactory for lookup, must not be null.
* @return The {@link MetadataStore} bean whose name is "metadataStore".

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://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.integration.expression;
import java.util.HashMap;
import java.util.Map;
import org.springframework.expression.PropertyAccessor;
import org.springframework.util.Assert;
/**
* Utility class that keeps track of a Set of SpEL {@link PropertyAccessor}s
* in order to register them with the "integrationEvaluationContext" upon initialization.
* Accessors must be added before context refresh.
*
* @author Artem Bilan
* @author Gary Russell
*
* @since 3.0
*/
public class SpelPropertyAccessorRegistrar {
private final Map<String, PropertyAccessor> propertyAccessors = new HashMap<String, PropertyAccessor>();
public SpelPropertyAccessorRegistrar() {
}
/**
* Create an instance with the provided property accessors. Each accessor name
* will be the class simple name.
* @param propertyAccessors the accessors.
* @since 4.3.8
*/
public SpelPropertyAccessorRegistrar(PropertyAccessor... propertyAccessors) {
Assert.notEmpty(propertyAccessors, "'propertyAccessors' must not be empty");
for (PropertyAccessor propertyAccessor : propertyAccessors) {
this.propertyAccessors.put(propertyAccessors.getClass().getSimpleName(), propertyAccessor);
}
}
/**
* Create an instance with the provided named property accessors.
* @param propertyAccessors a map of name:accessor.
* @since 4.3.8
*/
public SpelPropertyAccessorRegistrar(Map<String, PropertyAccessor> propertyAccessors) {
Assert.notEmpty(propertyAccessors, "'propertyAccessors' must not be empty");
this.propertyAccessors.putAll(propertyAccessors);
}
/**
* Return the registered accessors.
* @return the map of name:accessor.
* @since 4.3.8
*/
public Map<String, PropertyAccessor> getPropertyAccessors() {
return this.propertyAccessors;
}
/**
* Add the provided named property accessor.
* @param name the name.
* @param propertyAccessor the accessor.
* @since 4.3.8
*/
public SpelPropertyAccessorRegistrar add(String name, PropertyAccessor propertyAccessor) {
Assert.hasText(name, "'name' must not be empty");
Assert.notNull(propertyAccessor, "'propertyAccessor' must not be null");
this.propertyAccessors.put(name, propertyAccessor);
return this;
}
/**
* Add the provided property accessors. Each accessor name
* will be the class simple name.
* @param propertyAccessors the accessors.
* @since 4.3.8
*/
public SpelPropertyAccessorRegistrar add(PropertyAccessor... propertyAccessors) {
Assert.notEmpty(propertyAccessors, "'propertyAccessors' must not be empty");
for (PropertyAccessor propertyAccessor : propertyAccessors) {
this.propertyAccessors.put(propertyAccessors.getClass().getSimpleName(), propertyAccessor);
}
return this;
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.configuration;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
@@ -70,6 +71,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.expression.EvaluationContext;
import org.springframework.integration.annotation.Aggregator;
import org.springframework.integration.annotation.BridgeFrom;
import org.springframework.integration.annotation.BridgeTo;
@@ -94,14 +96,17 @@ import org.springframework.integration.config.EnablePublisher;
import org.springframework.integration.config.ExpressionControlBusFactoryBean;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.integration.config.IntegrationConverter;
import org.springframework.integration.config.SpelFunctionFactoryBean;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.MethodInvokingMessageSource;
import org.springframework.integration.endpoint.PollingConsumer;
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
import org.springframework.integration.gateway.GatewayProxyFactoryBean;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.history.MessageHistoryConfigurer;
import org.springframework.integration.json.JsonPropertyAccessor;
import org.springframework.integration.scheduling.PollerMetadata;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.MutableMessageBuilder;
@@ -128,6 +133,7 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
import reactor.rx.Promise;
@@ -684,6 +690,16 @@ public class EnableIntegrationTests {
this.autoCreatedChannelMessageSourceAdapter.stop();
}
@Test
public void testIntegrationEvaluationContextCustomization() {
EvaluationContext evaluationContext = this.context.getBean(EvaluationContext.class);
List<?> propertyAccessors = TestUtils.getPropertyValue(evaluationContext, "propertyAccessors", List.class);
assertThat(propertyAccessors.get(0), instanceOf(JsonPropertyAccessor.class));
Map<?, ?> variables = TestUtils.getPropertyValue(evaluationContext, "variables", Map.class);
Object testSpelFunction = variables.get("testSpelFunction");
assertEquals(ClassUtils.getStaticMethod(TestSpelFunction.class, "bar", Object.class), testSpelFunction);
}
@Configuration
@ComponentScan
@IntegrationComponentScan
@@ -1086,6 +1102,16 @@ public class EnableIntegrationTests {
return new AnnotationTestServiceImpl();
}
@Bean
public SpelFunctionFactoryBean testSpelFunction() {
return new SpelFunctionFactoryBean(TestSpelFunction.class, "bar");
}
@Bean
public SpelPropertyAccessorRegistrar spelPropertyAccessorRegistrar() {
return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor());
}
}
@Configuration
@@ -1589,4 +1615,13 @@ public class EnableIntegrationTests {
// @MessagingGateway(defaultRequestChannel = "gatewayChannel", defaultHeaders = @GatewayHeader(name = "foo", value = "FOO"))
// public static class TestGateway2 { }
public static class TestSpelFunction {
public static Object bar(Object o) {
return o;
}
}
}

View File

@@ -104,6 +104,17 @@ With this sample:
* That `EvaluationContext` instance is injected into the `ExpressionEvaluatingTransformer` bean.
To provide a SpEL Function via Java Configuration you should declare a `SpelFunctionFactoryBean` bean for each function.
The sample above can be configured as follows:
[source,java]
----
@Bean
public SpelFunctionFactoryBean xpath() {
return new SpelFunctionFactoryBean(XPathUtils.class, "evaluate");
}
----
NOTE: SpEL functions declared in a parent context are also made available in any child context(s).
Each context has its own instance of the _integrationEvaluationContext_ factory bean because each needs a different `BeanResolver`, but the function declarations are inherited and can be overridden if needed by declaring a SpEL function with the same name.
@@ -157,6 +168,18 @@ Instead of configuring the factory bean above, simply add one or more of these c
With this sample, two custom `PropertyAccessor` s will be injected to the `EvaluationContext` in the order that they are declared.
To provide `PropertyAccessor` s via Java Configuration you should declare `SpelPropertyAccessorRegistrar` bean with the `spelPropertyAccessorRegistrar` (the `IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME` constant) name.
The sample above can be configured like:
[source,java]
----
@Bean
public SpelPropertyAccessorRegistrar spelPropertyAccessorRegistrar() {
return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor())
.add(fooPropertyAccessor());
}
----
NOTE: Custom `PropertyAccessor` s declared in a parent context are also made available in any child context(s).
They are placed at the end of result list (but before the default `org.springframework.context.expression.MapAccessor` and `o.s.expression.spel.support.ReflectivePropertyAccessor`).
If a `PropertyAccessor` with the same bean id is declared in a child context(s), it will override the parent accessor.