From a12f23936ca2a3e857ce333a718e4d95914ea219 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Thu, 21 Jan 2016 16:14:16 -0800 Subject: [PATCH] Allow use of '&' prefix to access factory bean in SpEL Prior to this change SpEL did not have an syntactic construct enabling easy access to a FactoryBean. With this change it is now possible to use &foo in an expression when the factory bean should be returned. Issue: SPR-9511 --- .../expression/FactoryBeanAccessTests.java | 129 ++++++++++++++++++ .../expression/BeanResolver.java | 9 +- .../expression/spel/SpelMessage.java | 4 +- .../expression/spel/ast/BeanReference.java | 12 +- .../InternalSpelExpressionParser.java | 13 +- .../expression/spel/standard/TokenKind.java | 4 +- .../expression/spel/standard/Tokenizer.java | 12 +- .../expression/spel/SpelReproTests.java | 31 ++++- src/asciidoc/core-expressions.adoc | 12 ++ 9 files changed, 207 insertions(+), 19 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java diff --git a/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java b/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java new file mode 100644 index 00000000000..2e29bd31d2e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-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.context.expression; + +import org.junit.Test; +import org.springframework.beans.factory.BeanIsNotAFactoryException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.expression.FactoryBeanAccessTests.SimpleBeanResolver.Boat; +import org.springframework.context.expression.FactoryBeanAccessTests.SimpleBeanResolver.CarFactoryBean; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.junit.Assert.*; + +/** + * Unit tests for expressions accessing beans and factory beans. + * + * @author Andy Clement + */ +public class FactoryBeanAccessTests { + + @Test + public void factoryBeanAccess() { // SPR9511 + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new SimpleBeanResolver()); + Expression expr = new SpelExpressionParser().parseRaw("@car.colour"); + assertEquals("red", expr.getValue(context)); + expr = new SpelExpressionParser().parseRaw("&car.class.name"); + assertEquals(CarFactoryBean.class.getName(), expr.getValue(context)); + + expr = new SpelExpressionParser().parseRaw("@boat.colour"); + assertEquals("blue",expr.getValue(context)); + expr = new SpelExpressionParser().parseRaw("&boat.class.name"); + try { + assertEquals(Boat.class.getName(), expr.getValue(context)); + fail("Expected BeanIsNotAFactoryException"); + } catch (BeanIsNotAFactoryException binafe) { + // success + } + + // No such bean + try { + expr = new SpelExpressionParser().parseRaw("@truck"); + assertEquals("red", expr.getValue(context)); + fail("Expected NoSuchBeanDefinitionException"); + } + catch (NoSuchBeanDefinitionException nsbde) { + // success + } + + // No such factory bean + try { + expr = new SpelExpressionParser().parseRaw("&truck"); + assertEquals(CarFactoryBean.class.getName(), expr.getValue(context)); + fail("Expected NoSuchBeanDefinitionException"); + } + catch (NoSuchBeanDefinitionException nsbde) { + // success + } + } + + static class SimpleBeanResolver + implements org.springframework.expression.BeanResolver { + + static class Car { + + public String getColour() { + return "red"; + } + } + + static class CarFactoryBean implements FactoryBean { + + public Car getObject() { + return new Car(); + } + + public Class getObjectType() { + return Car.class; + } + + public boolean isSingleton() { + return false; + } + + } + + static class Boat { + + public String getColour() { + return "blue"; + } + + } + + StaticApplicationContext ac = new StaticApplicationContext(); + + public SimpleBeanResolver() { + ac.registerSingleton("car", CarFactoryBean.class); + ac.registerSingleton("boat", Boat.class); + } + + @Override + public Object resolve(EvaluationContext context, String beanName) + throws AccessException { + return ac.getBean(beanName); + } + } + +} \ No newline at end of file diff --git a/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java b/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java index c95501baa8b..1f6726f1f10 100644 --- a/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -18,7 +18,9 @@ package org.springframework.expression; /** * A bean resolver can be registered with the evaluation context - * and will kick in for {@code @myBeanName} still expressions. + * and will kick in for {@code @myBeanName} and {@code &myBeanName} expressions. + * The & variant syntax allows access to the factory bean where + * relevant. * * @author Andy Clement * @since 3.0.3 @@ -26,7 +28,8 @@ package org.springframework.expression; public interface BeanResolver { /** - * Look up the named bean and return it. + * Look up the named bean and return it. If attempting to access a factory + * bean the name will have a & prefix. * @param context the current evaluation context * @param beanName the name of the bean to lookup * @return an object representing the bean diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index 1431554a49b..40c7054c156 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-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. @@ -214,7 +214,7 @@ public enum SpelMessage { "A problem occurred when trying to resolve bean ''{0}'':''{1}''"), INVALID_BEAN_REFERENCE(Kind.ERROR, 1059, - "@ can only be followed by an identifier or a quoted name"), + "@ or & can only be followed by an identifier or a quoted name"), TYPE_NAME_EXPECTED_FOR_ARRAY_CONSTRUCTION(Kind.ERROR, 1060, "Expected the type of the new array to be specified as a String but found ''{0}''"), diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java index 5f0c84046ed..67021482708 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java @@ -25,12 +25,15 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; /** - * Represents a bean reference to a type, for example "@foo" or "@'foo.bar'" - * + * Represents a bean reference to a type, for example @foo or @'foo.bar'. + * For a FactoryBean the syntax &foo can be used to access the factory itself. + * * @author Andy Clement */ public class BeanReference extends SpelNodeImpl { + private final static String FACTORY_BEAN_PREFIX = "&"; + private final String beanName; @@ -59,7 +62,10 @@ public class BeanReference extends SpelNodeImpl { @Override public String toStringAST() { - StringBuilder sb = new StringBuilder("@"); + StringBuilder sb = new StringBuilder(); + if (!this.beanName.startsWith(FACTORY_BEAN_PREFIX)) { + sb.append("@"); + } if (!this.beanName.contains(".")) { sb.append(this.beanName); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 8531aa0d425..600ffda6b51 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-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. @@ -525,7 +525,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { // parse: @beanname @'bean.name' // quoted if dotted private boolean maybeEatBeanReference() { - if (peekToken(TokenKind.BEAN_REF)) { + if (peekToken(TokenKind.BEAN_REF) || peekToken(TokenKind.FACTORY_BEAN_REF)) { Token beanRefToken = nextToken(); Token beanNameToken = null; String beanName = null; @@ -543,7 +543,14 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { SpelMessage.INVALID_BEAN_REFERENCE); } - BeanReference beanReference = new BeanReference(toPos(beanNameToken) ,beanName); + BeanReference beanReference = null; + if (beanRefToken.getKind() == TokenKind.FACTORY_BEAN_REF) { + String beanNameString = new StringBuilder().append(TokenKind.FACTORY_BEAN_REF.tokenChars).append(beanName).toString(); + beanReference = new BeanReference(toPos(beanRefToken.startPos,beanNameToken.endPos),beanNameString); + } + else { + beanReference = new BeanReference(toPos(beanNameToken) ,beanName); + } this.constructedNodes.push(beanReference); return true; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java index 4a31941ee80..0b577554d74 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-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. @@ -112,6 +112,8 @@ enum TokenKind { BEAN_REF("@"), + FACTORY_BEAN_REF("&"), + SYMBOLIC_OR("||"), SYMBOLIC_AND("&&"), diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java index fb912228044..e0408f10db3 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-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. @@ -182,12 +182,12 @@ class Tokenizer { } break; case '&': - if (!isTwoCharToken(TokenKind.SYMBOLIC_AND)) { - throw new InternalParseException(new SpelParseException( - this.expressionString, this.pos, SpelMessage.MISSING_CHARACTER, - "&")); + if (isTwoCharToken(TokenKind.SYMBOLIC_AND)) { + pushPairToken(TokenKind.SYMBOLIC_AND); + } + else { + pushCharToken(TokenKind.FACTORY_BEAN_REF); } - pushPairToken(TokenKind.SYMBOLIC_AND); break; case '|': if (!isTwoCharToken(TokenKind.SYMBOLIC_OR)) { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index b1743e50043..d824703720c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-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. @@ -718,6 +718,9 @@ public class SpelReproTests extends AbstractExpressionTests { else if (beanName.equals("foo.bar")) { return "trouble"; } + else if (beanName.equals("&foo")) { + return "foo factory"; + } else if (beanName.equals("goo")) { throw new AccessException("DONT ASK ME ABOUT GOO"); } @@ -1950,6 +1953,32 @@ public class SpelReproTests extends AbstractExpressionTests { res = parser.parseExpression("#root.![values()]").getValue(context, List.class); assertEquals("[[test12, test11], [test22, test21]]", res.toString()); } + + @Test + public void AccessingFactoryBean_spr9511() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new MyBeanResolver()); + Expression expr = new SpelExpressionParser().parseRaw("@foo"); + assertEquals("custard", expr.getValue(context)); + expr = new SpelExpressionParser().parseRaw("&foo"); + assertEquals("foo factory",expr.getValue(context)); + + try { + expr = new SpelExpressionParser().parseRaw("&@foo"); + fail("Illegal syntax, error expected"); + } catch (SpelParseException spe) { + assertEquals(SpelMessage.INVALID_BEAN_REFERENCE,spe.getMessageCode()); + assertEquals(0,spe.getPosition()); + } + + try { + expr = new SpelExpressionParser().parseRaw("@&foo"); + fail("Illegal syntax, error expected"); + } catch (SpelParseException spe) { + assertEquals(SpelMessage.INVALID_BEAN_REFERENCE,spe.getMessageCode()); + assertEquals(0,spe.getPosition()); + } + } @Test public void SPR12035() { diff --git a/src/asciidoc/core-expressions.adoc b/src/asciidoc/core-expressions.adoc index 390ead9f897..dcd8f1db955 100644 --- a/src/asciidoc/core-expressions.adoc +++ b/src/asciidoc/core-expressions.adoc @@ -1056,6 +1056,18 @@ lookup beans from an expression using the (@) symbol. Object bean = parser.parseExpression("@foo").getValue(context); ---- +To access a factory bean itself, the bean name should instead be prefixed with a (&) symbol. + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new MyBeanResolver()); + + // This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation + Object bean = parser.parseExpression("&foo").getValue(context); +---- [[expressions-operator-ternary]]