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();
+ }
+
}