From 3bf2ca12ba11af719b0398bab379ecd9deb96353 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 27 Feb 2017 17:29:36 -0500 Subject: [PATCH] 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** --- .../amqp/config/OutboundGatewayTests.java | 5 +- ...tegrationEvaluationContextFactoryBean.java | 4 +- .../config/SpelPropertyAccessorRegistrar.java | 46 -------- .../xml/SpelPropertyAccessorsParser.java | 29 ++--- .../context/IntegrationContextUtils.java | 4 +- .../SpelPropertyAccessorRegistrar.java | 101 ++++++++++++++++++ .../configuration/EnableIntegrationTests.java | 35 ++++++ src/reference/asciidoc/spel.adoc | 23 ++++ 8 files changed, 182 insertions(+), 65 deletions(-) delete mode 100644 spring-integration-core/src/main/java/org/springframework/integration/config/SpelPropertyAccessorRegistrar.java create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java index 434e3484a3..ef6435b2fc 100644 --- a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java @@ -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(); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java index 2ef8380f81..b36e5ce6a4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java @@ -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, diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/SpelPropertyAccessorRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/SpelPropertyAccessorRegistrar.java deleted file mode 100644 index e792dd9e71..0000000000 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/SpelPropertyAccessorRegistrar.java +++ /dev/null @@ -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 propertyAccessors; - - SpelPropertyAccessorRegistrar(Map propertyAccessors) { - this.propertyAccessors = propertyAccessors; - } - - Map getPropertyAccessors() { - return this.propertyAccessors; - } - - void addPropertyAccessor(String name, PropertyAccessor propertyAccessor) { - this.propertyAccessors.put(name, propertyAccessor); - } - -} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java index 2c496258ea..1aa2e59f54 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java @@ -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 propertyAccessors = new ManagedMap(); - 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()); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java index 3d51dc40ff..a283bcebdd 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java @@ -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". diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java new file mode 100644 index 0000000000..1a3c530a69 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java @@ -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 propertyAccessors = new HashMap(); + + 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 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 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; + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java index 05589fbae2..ad91bd618f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java @@ -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; + } + + } + } diff --git a/src/reference/asciidoc/spel.adoc b/src/reference/asciidoc/spel.adoc index 090df44c2c..f4b7cef608 100644 --- a/src/reference/asciidoc/spel.adoc +++ b/src/reference/asciidoc/spel.adoc @@ -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.