Recent Tweets @

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.

image
  
image

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

spring.io : http://spring.io

Apache Camel : http://camel.apache.org/

Mongo Db : https://www.mongodb.org/

Elastic search : http://www.elasticsearch.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.

  1. quitada reblogged this from egunaytech
  2. egunaytech posted this