Thursday, 12 February 2015

Spring Boot / MongoDB / Heroku

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
In comparison, Engine Yard requires one to install JVM and Google App Engine is still tied to Servlet 2.5, something that's incompatible with Spring Boot. See paasify.it for more comparisons.

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> 
 
2. Create an executable class. I personally do not like to mix this class with the configuration class. Use @Import to link the two together:
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@Import(AppContext.class)
public class Application {

 public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
}
 
3. Create a configuration class. Spring Boot documentations recommend configuration classes over XML's. There's no denying the world is moving away from XML every single day:
@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();
    }

}
 
4. Create a controller. In my case, since this is just a REST back-end, I settled for a very simple home page.
@RestController
public class HomeController {
 @RequestMapping("/")
 public String index() {
  return "Spring Boot on Heroku";
 }
}
 
5. Create a Procfile next to pom.xml. This files contains instruction to Heroku on how to launch the application. The important thing here is to declare the app as a web app and to pass server.port as a system parameter to the code. Each time Heroku starts the app, a different port number may be assigned. Additional JAVA_OPTS can also be defined here.
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
The reason why I chose to consume live Twitter feed is because it's a good load testing substitute for this application as well as the MongoLab plugin for Heroku. The Twitter sample streaming data comes in at approximately 8 requests per second and each request is about 5KB. So it won't take long to build up a database of the size of hundreds of megabytes in a very short time.
Much to my surprise, even with just 1 instance running, it was more than capable of handling the live streaming sample data from Twitter.



















No comments:

Post a Comment