Spring Boot / MongoDB / Heroku
With the advent of embedded containers, the table has finally turned in favor of the applications. Java EE apps no longer depend on container's settings for memory allocation and class loading, apps can now decide how to deploy, where to deploy and when to deploy.Spring Boot takes it one step further by being "opinionated". By selecting the default bootstrap behaviors and dependencies such as Tomcat, Jackson, Hibernate Validator, Logback and SLF4J, it makes a developer's life easier and allows them to focus on writing code, instead of XML's.
So, for the back-end service of my next mobile app, I decided to use Spring Boot, MongoDB and deploy them on a PaaS. The first step was to choose a PaaS provider and Heroku made it a very easy decision because it's:
- Popular, with lots of Q&A and howto's on the web.
- Easy to set up and use
- Supported by plenty of plugins
- Free to sign up and free to run small scale apps
- Got support for Java 6, 7, and 8
Setting up Heroku was a breeze. I created an account and went through the steps described in its dev center and I had Hello World up and running in less than half an hour.
To push a Spring Boot project to Heroku was equally easy. I'd recommend reading Spring's blog, and Spring Boot reference docs here and here.
My Project
To make life easier, I've created a GitHub project with all the necessary bits and pieces needed to run Spring boot and Mongodb on Heroku. It should serve as a good starting point for anyone who want to use the same stack.Here is the steps I took to create this project:
1. Create a pom.xml file. This is where we rely on Spring Boot to manage dependencies and plugins. Only include additional dependent jars if they're not already provided by spring-boot-starter-web and make sure to avoid version conflicts. spring-boot-maven-plugin provides convenient Sprint Boot commands in maven:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.2.1.RELEASE</version> </parent> <dependencies> <!-- ======== Spring Boot ======== --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- ========= Spring Data ========= --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-mongodb</artifactId> </dependency> <!-- ========= Testing ======== --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) @Import(AppContext.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
@Configuration @EnableWebMvc @EnableAsync @ComponentScan(basePackages="com.jackfluid") @EnableMongoRepositories("com.jackfluid.repo") public class AppContext extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("/static/"); } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } }
@RestController public class HomeController { @RequestMapping("/") public String index() { return "Spring Boot on Heroku"; } }
web: java -Dserver.port=$PORT -jar target/spring-boot-heroku-1.0.0-SNAPSHOT.jar
6. Create a system.properties file next to pom.xml. This file contains directives to Heroku as to what the runtime environment should be.
java.runtime.version=1.8
7. Create a MongoDB repository. My sample app aims to read live Twitter feeds and persist them as they stream in, so I simply need a repo for the tweets.:
public interface TweetRepo extends MongoRepository<Status, String> { }
8. Create application.properties file under /src/main/resources. As long as this file is on the classpath, Spring Boot will pick it up and make the settings available in our code through @Value. Spring boot also recognizes a special variable called "spring.data.mongodb.uri" and uses it as the connection string to the MongoDB instance. If this variable is undefined, Spring boot would try to connect to MongoDB on localhost. Since I installed MongoLab as a plugin for my Heroku instance, I'll connect to that instance instead.
# look for MONGOLAB_URI config var in Heroku spring.data.mongodb.uri=mongodb://<username>:<password>@<hostname>:<port>/<database_name>
9. Create the entity classes and controller classes. In this project, I've created a TweetFeederController that accepts API calls to start reading live Twitter feed and send them to any give URL using HTTP POST. By default, you can send the Tweet to the second controller, TweetController and let it persist to the MongoDB. Sample request payloads can be found under /test/resources.
{ "searchTerm": "${searchTerm}", "maxResults": 10, "maxTotDuration": 10, "sendTo": "http://localhost:${port}/tweet" }
10. Create test classes. Here I used mostly integration tests. This is another advantage of using Spring Boot. No longer are we limited to MockMVC for testing or dependent on web.xml and a running Tomcat for testing, we can actually perform end-to-end testing completely inside Spring.
@Test public void testPopularTwitterTerm() { String json = super.fileResourceToString(tweetFeederFilterTestResource).replace("${port}", port+"") .replace("${searchTerm}", "obama"); testTweetFeeder(json); } protected void testTweetFeeder(String json){ HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<String> requestEntity = new HttpEntity<String>(json, headers); ResponseEntity<Object> response = template.exchange(tweetFeederUrl, HttpMethod.POST, requestEntity, Object.class); assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); }
11. Rename /src/main/resources/system.properties.template to system.properties. Update the values with actual MongoDB connection string and Twitter's developer credentials.
# look for MONGOLAB_URI config var in Heroku spring.data.mongodb.uri=mongodb://<username>:<password>@<hostname>:<port>/<database_name> # twitter api credentials twitter.api.consumerKey=<your_twitter_api_consumerKey> twitter.api.consuerSecret=<your_twitter_api_consumerSecret> twitter.api.token=<your_twiter_api_token> twitter.api.secret=<your_twitter_api_secret>
12. Push this project to Heroku and watch how it works:
# create a new Heroku instance and repository. $ heroku create # push all the local changes to Heroku's Git repository. This will kick of a build and deploy process $ git push heroku master # optionally, start a Heroku instance, or two. $ heroku ps:scale web=1 # open the home page of the application $ heroku open # watch Heroku's log, live $ heroku logs --tail
Much to my surprise, even with just 1 instance running, it was more than capable of handling the live streaming sample data from Twitter.