Two months ago, I had to take the responsibility of one of our mobile app’s back end services. The legacy project was implemented as a Java project. Even though Spring framework was used in the project, there were some portability and stability issues.
I tried to understand and add new features on the legacy version but there was no luck. To prevent losing more time, I decided to rewrite the project by using new Spring Boot and Spring Framework 4.
After only one week, I had a brand new back end which would run on all the operating systems and within any application server just as expected with a much better performance and %100 availability.
After deploying only the new backend application, without updating mobile app clients (Android, iOS), we gained almost double number of visistors and 20 times more bandwith usage, just because we had a robust, available and better performing back end
Here, I would like to share my experience and how easy it was to migrate to Spring Boot and Spring framework 4.
Following tools are used in the project together with Spring framework
Apache Camel : http://camel.apache.org/
At the end of the first hour
There are plenty of ready-to-run guides in spring.io you can follow. I followed this tutorial for setting up my brand new project. I will not go into details as you can find all the details in the guide
http://spring.io/guides/gs/actuator-service/
pom.xml
...
<properties>
<start-class>org.eg.videoplatform.Application</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mongo.version>2.11.2</mongo.version>
<jongo.version>1.0</jongo.version>
<elasticsearch.version>0.90.11</elasticsearch.version>
<elasticsearch.spring.version>0.2.0</elasticsearch.spring.version>
<camel.version>2.11.0</camel.version>
<camel.spring.amqp.version>1.5</camel.spring.amqp.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RC1</version>
</parent>
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
...
</dependencies>
Application.java
package org.eg.videoplatform;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
@ImportResource("classpath:common-beans.xml")
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
WelcomeHandler.java
package org.eg.videoplatform.api;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class WelcomeHandler extends AbstractHandler {
@RequestMapping("/welcome")
public @ResponseBody String welcome() {
return "welcome to Video";
}
}
At the end of the first day
After setting up the project, I fisinhed integrating mongo db and elastic search to the spring context.
common-beans.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
<context:property-placeholder location="classpath*:video-platform-config.properties" ignore-unresolvable="true" />
<context:component-scan base-package="org.eg.videoplatform"/>
<aop:aspectj-autoproxy />
<!-- ===== -->
<!-- mongo -->
<!-- ===== -->
<bean id="mongo" class="com.mongodb.Mongo">
<constructor-arg value="${mongodb.host}"/>
<constructor-arg value="${mongodb.port}"/>
</bean>
<bean id="mongoDb" class="org.eg.videoplatform.domain.factory.MongoFactory">
<property name="mongo" ref="mongo"/>
<property name="name" value="videobul"/>
</bean>
<bean id="jongo" class="org.eg.videoplatform.domain.factory.JongoFactory">
<property name="mongoDb" ref="mongoDb"/>
</bean>
<bean id="objectMapper" class="org.eg.videoplatform.domain.factory.ObjectMapperFactory"/>
<bean id="videoRepository" class="org.eg.videoplatform.domain.repository.VideoRepository">
<constructor-arg ref="jongo"/>
</bean>
<!-- ====== -->
<!-- elastic search -->
<!-- ====== -->
<bean id="esClient" class="fr.pilato.spring.elasticsearch.ElasticsearchTransportClientFactoryBean" >
<property name="settingsFile" value="search.properties"/>
<property name="esNodes">
<list>
<value>${elasticsearch.nodes}</value>
</list>
</property>
</bean>
pom.xml
...
<dependencies>
...
<!--mongo -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>${mongo.version}</version>
</dependency>
<dependency>
<groupId>org.jongo</groupId>
<artifactId>jongo</artifactId>
<version>${jongo.version}</version>
</dependency>
<!--elasticsearch -->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<dependency>
<groupId>fr.pilato.spring</groupId>
<artifactId>spring-elasticsearch</artifactId>
<version>${elasticsearch.spring.version}</version>
</dependency>
...
</dependencies>
MongoFactory.java
package org.eg.videoplatform.domain.factory;
import org.springframework.beans.factory.FactoryBean;
import com.mongodb.DB;
import com.mongodb.Mongo;
/**
* MongoFactory
*/
public class MongoFactory implements FactoryBean {
private String name;
private Mongo mongo;
public MongoFactory() {}
public void setMongo(Mongo mongo) {
this.mongo = mongo;
}
public void setName(String name) {
this.name = name;
}
public DB getObject() throws Exception {
return mongo.getDB(name);
}
public Class<?> getObjectType() {
return DB.class;
}
public boolean isSingleton() {
return true;
}
}
JongoFactory.java
package org.eg.videoplatform.domain.factory;
import org.jongo.Jongo;
import org.jongo.marshall.jackson.JacksonMapper;
import org.springframework.beans.factory.FactoryBean;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.mongodb.DB;
/**
* JongoFactory
*/
public class JongoFactory implements FactoryBean {
private DB mongo;
private Jongo jongo;
public void setMongoDb(DB mongo) {
this.mongo = mongo;
}
public Jongo getObject() throws Exception {
jongo = new Jongo(mongo,new JacksonMapper.Builder().
registerModule(new JodaModule()).
registerModule(new ObjectIdModule()).
enable(MapperFeature.AUTO_DETECT_GETTERS).
build());
return jongo;
}
public Class<?> getObjectType() {
return Jongo.class;
}
public boolean isSingleton() {
return true;
}
public static class ObjectIdModule extends SimpleModule {
public ObjectIdModule() {
}
}
}
ObjectMapperFactory.java
package org.eg.videoplatform.domain.factory;
import java.io.IOException;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.FactoryBean;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;
/**
* ObjectMapperFactory
*/
public class ObjectMapperFactory implements FactoryBean{
public ObjectMapper getObject() throws Exception {
return new CustomObjectMapper();
}
public Class<?> getObjectType() {
return ObjectMapper.class;
}
public boolean isSingleton() {
return true;
}
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
SimpleModule module = new SimpleModule("ObjectIdModule");
module.addSerializer(ObjectId.class, new JsonSerializer() {
@Override
public void serialize(ObjectId objectId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeString(objectId.toString());
}
});
this.registerModule(module);
this.registerModule(new JodaModule());
}
}
}
AbstractRepository.java
package org.eg.videoplatform.domain.repository;
import org.bson.types.ObjectId;
import org.jongo.Jongo;
import org.jongo.MongoCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mongodb.WriteResult;
import org.eg.videoplatform.domain.Model;
import org.eg.videoplatform.domain.Repository;
public abstract class AbstractRepository implements Repository{
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected final Class clazz;
protected final MongoCollection collection;
public AbstractRepository(Class clazz, Jongo jongo, String name) {
this.clazz = clazz;
this.collection = jongo.getCollection(name);
}
public long count() {
return collection.count();
}
public long count(String fieldName, Object fieldValue) {
return collection.count("{" + fieldName + ":#}", fieldValue);
}
public M findById(String id) {
return collection.findOne(new ObjectId(id)).as(clazz);
}
public M findByName(String name) {
return collection.findOne("{name:#}",name).as(clazz);
}
public Iterable findByAnyField(String fieldName, Object fieldValue) {
return collection.find("{" + fieldName + ":#}", fieldValue).as(clazz);
}
public Iterable findByAnyFieldSorted(String fieldName, Object fieldValue, String sortField, int sortOrder) {
return collection.find("{" + fieldName + ":#}", fieldValue).sort("{" + fieldName + ": " + sortOrder + "}").as(clazz);
}
public void deleteById(String id) {
collection.remove(new ObjectId(id));
}
public void deleteByName(String name) {
collection.remove("{name:#}",name);
}
public int deleteByQuery(String fieldName, String fieldValue) {
WriteResult result = collection.remove("{" + fieldName + ":#}", fieldValue);
return result.getN();
}
public void delete() {
collection.remove();
}
public boolean idExists(String id) {
return findById(id) != null;
}
public boolean nameExists(String name) {
return findByName(name) != null;
}
public void save(M m) {
collection.save(m);
}
public Iterable all() {
return collection.find().as(clazz);
}
public Iterable allSorted(String fieldName, int sortOrder) {
return collection.find().sort("{" + fieldName + ": " + sortOrder + "}").as(clazz);
}
}
VideoRepository.java
package org.eg.videoplatform.domain.repository;
import org.jongo.Jongo;
import org.eg.videoplatform.domain.model.Video;
public class VideoRepository extends AbstractRepository{
public VideoRepository(Jongo jongo) {
super(Video.class, jongo, "videos");
}
}
At the end of the second day
I fisinhed integrating Apache Camel to the spring context.
common-beans.xml
<beans
...
xmlns:camel="http://camel.apache.org/schema/spring"
xsi:schemaLocation="
...
http://camel.apache.org/schema/spring
http://camel.apache.org/schema/spring/camel-spring.xsd ">
...
<camel:camelContext id="context">
<camel:jmxAgent id="agent" createConnector="false" disabled="true"/>
<camel:template id="template"/>
<camel:routeBuilder ref="categoryVideosRouteBuilder"/>
...
</camel:camelContext>
...
</beans>
pom.xml
...
<dependencies>
...
<!--camel -->
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-spring</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-stream</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-http4</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-ahc</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-quartz</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>com.bluelock</groupId>
<artifactId>camel-spring-amqp</artifactId>
<version>${camel.spring.amqp.version}</version>
</dependency>
...
</dependencies>
CategoryVideosRouteBuilder.java
package org.eg.videoplatform.integration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component("categoryVideosRouteBuilder")
public class CategoryVideosRouteBuilder extends ExecutorRouteBuilder {
@Value("${route.categoryVideos.uris}")
public String[] categoryVideosRouteUriArray;
@Value("${route.scheduler.enabled}")
protected Boolean routeSchedulerEnabled;
@Value("${route.scheduler.category.uri}")
protected String routeSchedulerUri;
@Override
public void configure() throws Exception {
from("direct:categoryVideosRoute")
.routeId("categoryVideosRoute")
.setProperty(PROP_ROUTE_ID, simple("categoryVideosRoute"))
.to("bean:" + beanName + "?method=logRouteBegin")
.multicast()
.to(categoryVideosRouteUriArray)
.end()
.to("bean:" + beanName + "?method=logRouteEnd");
if (routeSchedulerEnabled != null && routeSchedulerEnabled) {
from(routeSchedulerUri) // example : quartz://videos/category?cron=0 0/60 * * * ?
.routeId("categoryVideosRoute_Scheduler")
.setProperty(PROP_ROUTE_ID, simple("categoryVideosRoute_Scheduler"))
.to("bean:" + beanName + "?method=logRouteBegin")
.to("direct:categoryVideosRoute")
.to("bean:" + beanName + "?method=logRouteEnd");
}
}
}
ExecutorRouteBuilder.java
package org.eg.videoplatform.integration;
import java.util.List;
import java.util.Stack;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.RouteDefinition;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.annotation.Autowired;
abstract public class ExecutorRouteBuilder extends RouteBuilder implements BeanNameAware {
public static final String PROP_ROUTE_ID_STACK = "routeIdStack";
public static final String PROP_ROUTE_ID = "routeId";
protected String beanName;
@Autowired
protected CamelContext camelContext;
protected RouteDefinition createMulticastRouteDefinition(String routeId, List<String> routeIdList) {
RouteDefinition routeDefinition = from("direct:" + routeId);
routeDefinition.routeId(routeId)
.setProperty(PROP_ROUTE_ID, simple(routeId))
.to("bean:" + beanName + "?method=logRouteBegin")
.multicast()
.to(routeIdList.toArray(new String[]{}))
.end()
.to("bean:" + beanName + "?method=logRouteEnd");
return routeDefinition;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
public void logRouteBegin(Exchange exchange) {
Stack<String> routeIdStack = exchange.getProperty(PROP_ROUTE_ID_STACK, Stack.class);
if (routeIdStack == null) {
routeIdStack = new Stack<String>();
exchange.setProperty(PROP_ROUTE_ID_STACK, routeIdStack);
}
String routeId = exchange.getFromRouteId();
if (exchange.getProperty(PROP_ROUTE_ID) != null)
routeId = exchange.getProperty(PROP_ROUTE_ID, String.class);
routeIdStack.push(routeId);
log.info("ROUTE STARTED : " + routeId + " - " + this.getClass().getSimpleName() + ". Exhange body : " + exchange.getIn().getBody());
}
public void logRouteEnd(Exchange exchange) {
Stack<String> routeIdStack = exchange.getProperty(PROP_ROUTE_ID_STACK, Stack.class);
String routeId = exchange.getFromRouteId();
if (! routeIdStack.empty()) {
routeId = routeIdStack.pop();
}
log.info("ROUTE FINISHED : " + routeId + " - " + this.getClass().getSimpleName());
}
}
Summary
It was a great pleasure and such an easy work to rewrite the whole project in Spring Boot and Spring Framework 4.0. We have now a robust, available back end that is serving all of the Android and iOS mobile clients perfectly.