Contributing to Spring Boot with a pull request

Engineering | Greg L. Turnquist | September 20, 2013 | ...

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 if JmsTemplate 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 a JmsTemplate 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 new JmsTemplateAutoConfiguration. I do that by adding the FQDN to Spring Boot's spring.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 a SimpleMessageListenerContainer, it will add the right bits.

Before writing that code, I first wrote a simple Groovy script that uses the Spring JMS stuff in jms.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 a JmsTemplate, DefaultMessageListenerContainer, or SimpleMessageListenerContainer, 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 Spring newsletter

Thank you for your interest. Someone will get back to you shortly.

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring Runtime offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all