Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Contributing to Spring Boot with a pull request

DZone's Guide to

Contributing to Spring Boot with a pull request

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

Originally authored by Greg Turnquist on the SpringSource blog. 

In case you missed this year's SpringOne 2GX conference, one of the hot keynote items was the announcement of Spring Boot. Dave Syer showed how to rapidly create a Spring MVC app with code that would fit inside a single tweet. In this blog entry, I will peel back the covers of Spring Boot and show you how it works by putting together a pull request.

Autoconfiguration

Spring Boot has a powerful autoconfiguration feature. When it detects certain things on the classpath, it automatically creates beans. But one feature it doesn't yet have is support for Spring JMS. I need that feature!

The first step is to code an autoconfiguration class:

package org.springframework.boot.autoconfigure.jms;




. . .some import statements. . .




@Configuration
@ConditionalOnClass(JmsTemplate.class)
public class JmsTemplateAutoConfiguration {




@Configuration
@ConditionalOnMissingBean(JmsTemplate.class)
protected static class JmsTemplateCreator {




@Autowired
ConnectionFactory connectionFactory;




@Bean
public JmsTemplate jmsTemplate() {
JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory);
jmsTemplate.setPubSubDomain(true);
return jmsTemplate;
}




}




@Configuration
@ConditionalOnClass(ActiveMQConnectionFactory.class)
@ConditionalOnMissingBean(ConnectionFactory.class)
protected static class ActiveMQConnectionFactoryCreator {
@Bean
ConnectionFactory connectionFactory() {
return new ActiveMQConnectionFactory("vm://localhost");
}
}




}

My Spring JMS autoconfiguration class is flagged with Spring's @Configuration annotation, marking it as a source of Spring beans to be picked up and put into the application context. It leverages Spring 4's @Conditional annotations, limiting it to only add this set of beans ifJmsTemplate is on the classpath. This is a telltale sign that spring-jms is on the classpath. Perfect!

My new class has two inner classes, also tagged for Spring Java Configuration and with additional conditions. This makes it easy to consolidate all my configuration needs to automate Spring JMS configuration.

  • JmsTemplateCreator creates a JmsTemplate. It only works if there isn't already aJmsTemplate defined elsewhere. This is how Spring Boot can have an opinion on how to create a JmsTemplate, but will quickly back off if you supply your own.
  • ActiveMQConnectionFactoryCreator creates an ActiveMQConnectionFactory, but only if it detects ActiveMQ on the classpath and if there is no other ConnectionFactory defined amongst all the Spring beans. This factory is necessary to create a JmsTemplate. It it set up for in-memory mode, meaning you don't even need to install a stand alone broker to begin using JMS. But you can easily substitute your own ConnectionFactory, and either way, Spring Boot will autowire it into the JmsTemplate.

All this autoconfiguration will be for naught if I don't properly register my newJmsTemplateAutoConfiguration. I do that by adding the FQDN to Spring Boot'sspring.factories file.

. . .
org.springframework.boot.autoconfigure.jms.JmsTemplateAutoConfiguration,\
. . .

Of course no pull request is complete without some automated unit tests. I won't put all the tests I wrote in this blog entry, but you can inspect the tests I submitted with my pull request. Just be ready to write your own battery of tests before submitting a pull request!

And that's all there is to adding autoconfiguration to Spring Boot! It's not that complex. In fact, you can take a tour of the existing autoconfiguration classes for more examples.

Spring Boot's Groovy support

One of the biggest features of Spring Boot that drew a lot of attention was its strong support for Groovy. This drew much applause during the keynote and was eaten up during Dave and Phil's talk the next day. In case you missed it, here is the Spring Boot REST service Dave Syer demonstrated:

@RestController
class ThisWillActuallyRun {
@RequestMapping("/")
String home() {
"Hello World!"
}
}

After putting that code inside app.groovy, Dave launched it by typing:

$ spring run app.groovy

Spring Boot's command line tool uses an embedded Groovy compiler and looks at all the symbols (like RestController). Then it automatically adds @Grab and import statements. It essentially expands the previous fragment into this:

@Grab("org.springframework.boot:spring-boot-starter-web:0.5.0.BUILD-SNAPSHOT")




import org.springframework.web.bind.annotation.*
import org.springframework.web.servlet.config.annotation.*
import org.springframework.web.servlet.*
import org.springframework.web.servlet.handler.*
import org.springframework.http.*
static import org.springframework.boot.cli.template.GroovyTemplate.template
import org.springframework.boot.cli.compiler.autoconfigure.WebConfiguration




@RestController
class ThisWillActuallyRun {
@RequestMapping("/")
String home() {
"Hello World!"
}




public static void main(String[] args) {
SpringApplication.run(ThisWillActuallyRun, args)
}
}

Adding your own Groovy integration

To add Spring JMS support, I need to add similar autoconfiguration to Boot's CLI so that whenever someone uses either a JmsTemplate, a DefaultMessageListenerContainer, or aSimpleMessageListenerContainer, it will add the right bits.

Before writing that code, I first wrote a simple Groovy script that uses the Spring JMS stuff injms.groovy:

package org.test




@Grab("org.apache.activemq:activemq-all:5.2.0")




import java.util.concurrent.CountDownLatch




@Configuration
@Log
class JmsExample implements CommandLineRunner {




private CountDownLatch latch = new CountDownLatch(1)




@Autowired
JmsTemplate jmsTemplate




@Bean
DefaultMessageListenerContainer jmsListener(ConnectionFactory connectionFactory) {
new DefaultMessageListenerContainer([
connectionFactory: connectionFactory,
destinationName: "spring-boot",
pubSubDomain: true,
messageListener: new MessageListenerAdapter(new Receiver(latch:latch)) {{
defaultListenerMethod = "receive"
}}
])
}




void run(String... args) { 
def messageCreator = { session ->
session.createObjectMessage("Greetings from Spring Boot via ActiveMQ")
} as MessageCreator
log.info "Sending JMS message..."
jmsTemplate.send("spring-boot", messageCreator)
latch.await()
}




}




@Log
class Receiver {
CountDownLatch latch




def receive(String message) {
log.info "Received ${message}"
latch.countDown()
}
}

This test script expects a JmsTemplate as well as a ConnectionFactory to be supplied automatically by Spring Boot. Notice that there are no import statements nor any @Grab's aside from pulling in activemq-all. It uses Spring Boot's CommandLineRunner interface to launch the run() method, which in turn sends a message through JmsTemplate. Then it uses the CountDownLatch to wait for a signal from the consumer.

On the other end is a DefaultMessageListener that upon receipt of the message, counts down. To invoke my script from inside Spring Boot's test suite, I added the following test method to SampleIntegrationTests to invoke jms.groovy:

 @Test
public void jmsSample() throws Exception {
start("samples/jms.groovy");
String output = this.outputCapture.getOutputAndRelease();
assertTrue("Wrong output: " + output,
output.contains("Received Greetings from Spring Boot via ActiveMQ"));
FileUtil.forceDelete(new File("activemq-data")); // cleanup ActiveMQ cruft
}

To test my new patch, I found it much easier to run a specific test. This definitely sped things up.

$ mvn clean -Dtest=SampleIntegrationTests#jmsSample test

Note: I had to run mvn -DskipTests install first to have my new JMS autoconfiguration feature deployed to my local maven repository.

Since I haven't written any Groovy autoconfiguration yet, the test will fail. Time to write the CLI autoconfiguration!

package org.springframework.boot.cli.compiler.autoconfigure;




. . .import statements. . .




public class JmsCompilerAutoConfiguration extends CompilerAutoConfiguration {




@Override
public boolean matches(ClassNode classNode) {
return AstUtils.hasAtLeastOneFieldOrMethod(classNode, "JmsTemplate",
"DefaultMessageListenerContainer", "SimpleMessageListenerContainer");
}




@Override
public void applyDependencies(DependencyCustomizer dependencies)
throws CompilationFailedException {
dependencies.add("org.springframework", "spring-jms",
dependencies.getProperty("spring.version")).add(
"org.apache.geronimo.specs", "geronimo-jms_1.1_spec", "1.1");




}




@Override
public void applyImports(ImportCustomizer imports) throws CompilationFailedException {
imports.addStarImports("javax.jms", "org.springframework.jms.core",
"org.springframework.jms.listener",
"org.springframework.jms.listener.adapter");
}




}

These callback hooks make it super easy to integrate with Spring Boot's CLI tool.

  • matches() lets you define what symbols trigger this behavior. For this one, if there is aJmsTemplateDefaultMessageListenerContainer, orSimpleMessageListenerContainer, it will trigger the action.
  • applyDependencies() specifies exactly what libraries to add to the classpath via Maven coordinates. This is akin to adding @Grab annotations to the application. For this one, we need spring-jms for JmsTemplate and geronimo-jms for JMS API spec classes.
  • applyImports() adds import statements to the top of your code. I basically looked at the Spring JMS imports from the autoconfiguration test code, and added them here.

This time, if you run the test suite, it should pass.

$ mvn clean -Dtest=SampleIntegrationTests#jmsSample test
 . ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v0.5.0.BUILD-SNAPSHOT)




2013-09-18 11:47:03.800 INFO 22969 --- [ runner-0] o.s.boot.SpringApplication : Starting application on retina with PID 22969 (/Users/gturnquist/.groovy/grapes/org.springframework.boot/spring-boot/jars/spring-boot-0.5.0.BUILD-SNAPSHOT.jar started by gturnquist)
2013-09-18 11:47:03.825 INFO 22969 --- [ runner-0] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4670f288: startup date [Wed Sep 18 11:47:03 CDT 2013]; root of context hierarchy
2013-09-18 11:47:04.428 INFO 22969 --- [ runner-0] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 2147483647
2013-09-18 11:47:04.498 INFO 22969 --- [ runner-0] o.apache.activemq.broker.BrokerService : Using Persistence Adapter: AMQPersistenceAdapter(activemq-data/localhost)
2013-09-18 11:47:04.501 INFO 22969 --- [ runner-0] o.a.a.store.amq.AMQPersistenceAdapter : AMQStore starting using directory: activemq-data/localhost
2013-09-18 11:47:04.515 INFO 22969 --- [ runner-0] org.apache.activemq.kaha.impl.KahaStore : Kaha Store using data directory activemq-data/localhost/kr-store/state
2013-09-18 11:47:04.541 INFO 22969 --- [ runner-0] o.a.a.store.amq.AMQPersistenceAdapter : Active data files: []
2013-09-18 11:47:04.586 INFO 22969 --- [ runner-0] o.apache.activemq.broker.BrokerService : ActiveMQ null JMS Message Broker (localhost) is starting
2013-09-18 11:47:04.587 INFO 22969 --- [ runner-0] o.apache.activemq.broker.BrokerService : For help or more information please see: http://activemq.apache.org/
2013-09-18 11:47:04.697 INFO 22969 --- [ JMX connector] o.a.a.broker.jmx.ManagementContext : JMX consoles can connect to service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi
2013-09-18 11:47:04.812 INFO 22969 --- [ runner-0] org.apache.activemq.kaha.impl.KahaStore : Kaha Store using data directory activemq-data/localhost/kr-store/data
2013-09-18 11:47:04.814 INFO 22969 --- [ runner-0] o.apache.activemq.broker.BrokerService : ActiveMQ JMS Message Broker (localhost, ID:retina-51737-1379522824687-0:0) started
2013-09-18 11:47:04.817 INFO 22969 --- [ runner-0] o.a.activemq.broker.TransportConnector : Connector vm://localhost Started
2013-09-18 11:47:04.867 INFO 22969 --- [ runner-0] o.s.boot.SpringApplication : Started application in 1.218 seconds
2013-09-18 11:47:04.874 INFO 22969 --- [ runner-0] org.test.JmsExample : Sending JMS message...
2013-09-18 11:47:04.928 INFO 22969 --- [ jmsListener-1] org.test.Receiver : Received Greetings from Spring Boot via ActiveMQ
2013-09-18 11:47:04.931 INFO 22969 --- [ main] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4670f288: startup date [Wed Sep 18 11:47:03 CDT 2013]; root of context hierarchy
2013-09-18 11:47:04.932 INFO 22969 --- [ main] o.s.c.support.DefaultLifecycleProcessor : Stopping beans in phase 2147483647
2013-09-18 11:47:05.933 INFO 22969 --- [ main] o.a.activemq.broker.TransportConnector : Connector vm://localhost Stopped
2013-09-18 11:47:05.933 INFO 22969 --- [ main] o.apache.activemq.broker.BrokerService : ActiveMQ Message Broker (localhost, ID:retina-51737-1379522824687-0:0) is shutting down
2013-09-18 11:47:05.944 INFO 22969 --- [ main] o.apache.activemq.broker.BrokerService : ActiveMQ JMS Message Broker (localhost, ID:retina-51737-1379522824687-0:0) stopped
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.432 sec - in org.springframework.boot.cli.SampleIntegrationTests

Ta dah!

At this stage, all I have to do is check out the contribution guidelines to ensure I am following Spring Boot's coding standards, and then submit my pull request. Feel free to see my contribution and follow up comments. (P.S. It was accepted after some fine tuning.)

I hope you have enjoyed this deep dive into Spring Boot and how it works. Hopefully, you will be able to code your own patch.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}