From 75af18df7d466d19674465f95e0a2dd914905b1d Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 18 Dec 2013 22:22:18 +0000 Subject: [PATCH] Add support for beans{} in CLI scripts User can add (a single) beans{} DSL declaration (see GroovyBeanDefinitionReader in Spring 4 for more detail) anywhere at the top level of an application source file. It will be compiled to a closure and fed in to the application context through a GroovyBeanDefinitionReader. Cool! The example spring-boot-cli/samples/beans.groovy runs in an integration test and passes (see SampleIntegrationTests). --- spring-boot-cli/README.md | 38 ++++- spring-boot-cli/samples/beans.groovy | 13 ++ .../boot/cli/compiler/GroovyCompiler.java | 2 + .../GroovyBeansTransformation.java | 136 ++++++++++++++++++ .../boot/cli/SampleIntegrationTests.java | 7 + .../boot/BeanDefinitionLoader.java | 28 +++- 6 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 spring-boot-cli/samples/beans.groovy create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/transformation/GroovyBeansTransformation.java diff --git a/spring-boot-cli/README.md b/spring-boot-cli/README.md index c8c3f5e608..9f164c20f7 100644 --- a/spring-boot-cli/README.md +++ b/spring-boot-cli/README.md @@ -81,11 +81,10 @@ Homebrew will install `spring` to `/usr/local/bin`. Now you can jump right to a Here's a really simple web application. Create a file called `app.groovy`: ```groovy -@Controller +@RestController class ThisWillActuallyRun { @RequestMapping("/") - @ResponseBody String home() { return "Hello World!" } @@ -149,6 +148,41 @@ the main application code, if that's what you prefer, e.g. $ spring test app/*.groovy test/*.groovy ``` +## Beans DSL + +Spring has native support for a `beans{}` DSL (borrowed from +[Grails](http://grails.org)), and you can embedd bean definitions in +your Groovy application scripts using the same format. This is +sometimes a good way to include external features like middleware +declarations. E.g. + +```groovy +@Configuration +class Application implements CommandLineRunner { + + @Autowired + SharedService service + + @Override + void run(String... args) { + println service.message + } + +} + +import my.company.SharedService + +beans { + service(SharedService) { + message "Hello World" + } +} +``` + +You can mix class declarations with `beans{}` in the same file as long +as they stay at the top level, or you can put the beans DSL in a +separate file if you prefer. + ## Commandline Completion Spring Boot CLI ships with a script that provides command completion diff --git a/spring-boot-cli/samples/beans.groovy b/spring-boot-cli/samples/beans.groovy new file mode 100644 index 0000000000..7a8661dc22 --- /dev/null +++ b/spring-boot-cli/samples/beans.groovy @@ -0,0 +1,13 @@ +@RestController +class Application { + @Autowired + String foo + @RequestMapping("/") + String home() { + "Hello ${foo}!" + } +} + +beans { + foo String, "World" +} \ No newline at end of file diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java index fa394e2c10..c8880af12c 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java @@ -49,6 +49,7 @@ import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller; import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration; import org.springframework.boot.cli.compiler.transformation.DependencyAutoConfigurationTransformation; import org.springframework.boot.cli.compiler.transformation.GrabResolversAutoConfigurationTransformation; +import org.springframework.boot.cli.compiler.transformation.GroovyBeansTransformation; import org.springframework.boot.cli.compiler.transformation.ResolveDependencyCoordinatesTransformation; /** @@ -110,6 +111,7 @@ public class GroovyCompiler { this.transformations.add(new GrabResolversAutoConfigurationTransformation()); this.transformations.add(new DependencyAutoConfigurationTransformation( this.loader, this.coordinatesResolver, this.compilerAutoConfigurations)); + this.transformations.add(new GroovyBeansTransformation()); if (this.configuration.isGuessDependencies()) { this.transformations.add(new ResolveDependencyCoordinatesTransformation( this.coordinatesResolver)); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/transformation/GroovyBeansTransformation.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/transformation/GroovyBeansTransformation.java new file mode 100644 index 0000000000..05a10aea1c --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/transformation/GroovyBeansTransformation.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2013 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.boot.cli.compiler.transformation; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassCodeVisitorSupport; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.ASTTransformation; + +/** + * {@link ASTTransformation} to resolve beans declarations inside application source + * files. Users only need to define a beans{} DSL element, and this + * transformation will remove it and make it accessible to the Spring application via an + * interface. + * + * @author Dave Syer + */ +public class GroovyBeansTransformation implements ASTTransformation { + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + for (ASTNode node : nodes) { + if (node instanceof ModuleNode) { + ModuleNode module = (ModuleNode) node; + for (ClassNode classNode : new ArrayList(module.getClasses())) { + if (classNode.isScript()) { + classNode.visitContents(new ClassVisitor(source, classNode)); + } + } + } + } + } + + private class ClassVisitor extends ClassCodeVisitorSupport { + + private static final String SOURCE_INTERFACE = "org.springframework.boot.BeanDefinitionLoader.GroovyBeanDefinitionSource"; + private static final String BEANS = "beans"; + private final SourceUnit source; + private ClassNode classNode; + private boolean xformed = false; + + public ClassVisitor(SourceUnit source, ClassNode classNode) { + this.source = source; + this.classNode = classNode; + } + + @Override + protected SourceUnit getSourceUnit() { + return this.source; + } + + @Override + public void visitBlockStatement(BlockStatement block) { + if (block.isEmpty() || this.xformed) { + return; + } + ClosureExpression closure = beans(block); + if (closure != null) { + // Add a marker interface to the current script + this.classNode.addInterface(ClassHelper.make(SOURCE_INTERFACE)); + // Implement the interface by adding a public read-only property with the + // same name as the method in the interface (getBeans). Make it return the + // closure. + this.classNode.addProperty(new PropertyNode(BEANS, Modifier.PUBLIC + | Modifier.FINAL, ClassHelper.CLOSURE_TYPE + .getPlainNodeReference(), this.classNode, closure, null, null)); + // Only do this once per class + this.xformed = true; + } + } + + /** + * Extract a top-level beans{} closure from inside this block if + * there is one. Removes it from the block at the same time. + * + * @param block a block statement (class definition) + * @return a beans Closure if one can be found, null otherwise + */ + private ClosureExpression beans(BlockStatement block) { + + for (Statement statement : new ArrayList(block.getStatements())) { + if (statement instanceof ExpressionStatement) { + Expression expression = ((ExpressionStatement) statement) + .getExpression(); + if (expression instanceof MethodCallExpression) { + MethodCallExpression call = (MethodCallExpression) expression; + Expression methodCall = call.getMethod(); + if (methodCall instanceof ConstantExpression) { + ConstantExpression method = (ConstantExpression) methodCall; + if (BEANS.equals(method.getValue())) { + ArgumentListExpression arguments = (ArgumentListExpression) call + .getArguments(); + block.getStatements().remove(statement); + ClosureExpression closure = (ClosureExpression) arguments + .getExpression(0); + return closure; + } + } + } + } + } + + return null; + + } + } +} diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java index 96e51ad7a4..a277ee3773 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java @@ -54,6 +54,13 @@ public class SampleIntegrationTests { output.contains("Hello World! From " + scriptUri)); } + @Test + public void beansSample() throws Exception { + this.cli.run("beans.groovy"); + String output = this.cli.getHttpOutput(); + assertTrue("Wrong output: " + output, output.contains("Hello World!")); + } + @Test public void templateSample() throws Exception { String output = this.cli.run("template.groovy"); diff --git a/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java b/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java index 962ec56cb0..fff74ab6ab 100644 --- a/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java @@ -16,10 +16,13 @@ package org.springframework.boot; +import groovy.lang.Closure; + import java.io.IOException; import java.util.HashSet; import java.util.Set; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -77,7 +80,7 @@ class BeanDefinitionLoader { this.sources = sources; this.annotatedReader = new AnnotatedBeanDefinitionReader(registry); this.xmlReader = new XmlBeanDefinitionReader(registry); - if (ClassUtils.isPresent("groovy.lang.MetaClass", null)) { + if (isGroovyPresent()) { this.groovyReader = new GroovyBeanDefinitionReader(this.xmlReader); } this.scanner = new ClassPathBeanDefinitionScanner(registry); @@ -144,6 +147,14 @@ class BeanDefinitionLoader { } private int load(Class source) { + if (isGroovyPresent()) { + // Any GroovyLoaders added in beans{} DSL can contribute beans here + if (GroovyBeanDefinitionSource.class.isAssignableFrom(source)) { + GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, + GroovyBeanDefinitionSource.class); + load(loader); + } + } if (isComponent(source)) { this.annotatedReader.register(source); return 1; @@ -151,6 +162,13 @@ class BeanDefinitionLoader { return 0; } + private int load(GroovyBeanDefinitionSource source) { + int before = this.xmlReader.getRegistry().getBeanDefinitionCount(); + this.groovyReader.beans(source.getBeans()); + int after = this.xmlReader.getRegistry().getBeanDefinitionCount(); + return after - before; + } + private int load(Resource source) { if (source.getFilename().endsWith(".groovy")) { if (this.groovyReader == null) { @@ -205,6 +223,10 @@ class BeanDefinitionLoader { throw new IllegalArgumentException("Invalid source '" + resolvedSource + "'"); } + private boolean isGroovyPresent() { + return ClassUtils.isPresent("groovy.lang.MetaClass", null); + } + private Resource[] findResources(String source) { ResourceLoader loader = this.resourceLoader != null ? this.resourceLoader : DEFAULT_RESOURCE_LOADER; @@ -281,4 +303,8 @@ class BeanDefinitionLoader { } } + protected interface GroovyBeanDefinitionSource { + Closure getBeans(); + } + }