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).
pull/176/head
Dave Syer 11 years ago
parent 72ae5d5a97
commit 75af18df7d

@ -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`: Here's a really simple web application. Create a file called `app.groovy`:
```groovy ```groovy
@Controller @RestController
class ThisWillActuallyRun { class ThisWillActuallyRun {
@RequestMapping("/") @RequestMapping("/")
@ResponseBody
String home() { String home() {
return "Hello World!" 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 $ 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 ## Commandline Completion
Spring Boot CLI ships with a script that provides command completion Spring Boot CLI ships with a script that provides command completion

@ -0,0 +1,13 @@
@RestController
class Application {
@Autowired
String foo
@RequestMapping("/")
String home() {
"Hello ${foo}!"
}
}
beans {
foo String, "World"
}

@ -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.grape.RepositoryConfiguration;
import org.springframework.boot.cli.compiler.transformation.DependencyAutoConfigurationTransformation; import org.springframework.boot.cli.compiler.transformation.DependencyAutoConfigurationTransformation;
import org.springframework.boot.cli.compiler.transformation.GrabResolversAutoConfigurationTransformation; import org.springframework.boot.cli.compiler.transformation.GrabResolversAutoConfigurationTransformation;
import org.springframework.boot.cli.compiler.transformation.GroovyBeansTransformation;
import org.springframework.boot.cli.compiler.transformation.ResolveDependencyCoordinatesTransformation; import org.springframework.boot.cli.compiler.transformation.ResolveDependencyCoordinatesTransformation;
/** /**
@ -110,6 +111,7 @@ public class GroovyCompiler {
this.transformations.add(new GrabResolversAutoConfigurationTransformation()); this.transformations.add(new GrabResolversAutoConfigurationTransformation());
this.transformations.add(new DependencyAutoConfigurationTransformation( this.transformations.add(new DependencyAutoConfigurationTransformation(
this.loader, this.coordinatesResolver, this.compilerAutoConfigurations)); this.loader, this.coordinatesResolver, this.compilerAutoConfigurations));
this.transformations.add(new GroovyBeansTransformation());
if (this.configuration.isGuessDependencies()) { if (this.configuration.isGuessDependencies()) {
this.transformations.add(new ResolveDependencyCoordinatesTransformation( this.transformations.add(new ResolveDependencyCoordinatesTransformation(
this.coordinatesResolver)); this.coordinatesResolver));

@ -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 <code>beans{}</code> 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<ClassNode>(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 <code>beans{}</code> 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<Statement>(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;
}
}
}

@ -54,6 +54,13 @@ public class SampleIntegrationTests {
output.contains("Hello World! From " + scriptUri)); 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 @Test
public void templateSample() throws Exception { public void templateSample() throws Exception {
String output = this.cli.run("template.groovy"); String output = this.cli.run("template.groovy");

@ -16,10 +16,13 @@
package org.springframework.boot; package org.springframework.boot;
import groovy.lang.Closure;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@ -77,7 +80,7 @@ class BeanDefinitionLoader {
this.sources = sources; this.sources = sources;
this.annotatedReader = new AnnotatedBeanDefinitionReader(registry); this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
this.xmlReader = new XmlBeanDefinitionReader(registry); this.xmlReader = new XmlBeanDefinitionReader(registry);
if (ClassUtils.isPresent("groovy.lang.MetaClass", null)) { if (isGroovyPresent()) {
this.groovyReader = new GroovyBeanDefinitionReader(this.xmlReader); this.groovyReader = new GroovyBeanDefinitionReader(this.xmlReader);
} }
this.scanner = new ClassPathBeanDefinitionScanner(registry); this.scanner = new ClassPathBeanDefinitionScanner(registry);
@ -144,6 +147,14 @@ class BeanDefinitionLoader {
} }
private int load(Class<?> source) { 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)) { if (isComponent(source)) {
this.annotatedReader.register(source); this.annotatedReader.register(source);
return 1; return 1;
@ -151,6 +162,13 @@ class BeanDefinitionLoader {
return 0; 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) { private int load(Resource source) {
if (source.getFilename().endsWith(".groovy")) { if (source.getFilename().endsWith(".groovy")) {
if (this.groovyReader == null) { if (this.groovyReader == null) {
@ -205,6 +223,10 @@ class BeanDefinitionLoader {
throw new IllegalArgumentException("Invalid source '" + resolvedSource + "'"); throw new IllegalArgumentException("Invalid source '" + resolvedSource + "'");
} }
private boolean isGroovyPresent() {
return ClassUtils.isPresent("groovy.lang.MetaClass", null);
}
private Resource[] findResources(String source) { private Resource[] findResources(String source) {
ResourceLoader loader = this.resourceLoader != null ? this.resourceLoader ResourceLoader loader = this.resourceLoader != null ? this.resourceLoader
: DEFAULT_RESOURCE_LOADER; : DEFAULT_RESOURCE_LOADER;
@ -281,4 +303,8 @@ class BeanDefinitionLoader {
} }
} }
protected interface GroovyBeanDefinitionSource {
Closure<?> getBeans();
}
} }

Loading…
Cancel
Save