Migrating from Java EE to Spring Boot | TwiNNatioN

This concerns the migration of Java EE 6 to Spring Boot 1.5.x. Even though Spring Boot 2.x is not backward compatible with 1.5.x, it's fairly easy to migrate from 1.5.x to 2.x (vs J2EE to Spring Boot).

Also, for the sake of making the explanation easier to understand, the guide does not follow RESTful's best practices.


During my first internship, I was assigned to continue an already existing Java web application (with Java EE 6).

My first migration

You can skip this part, it won't help you migrate faster

At that time, I was an intern and I didn't know too much about Spring Boot and the application needed some serious reworking. It was slow, very slow. There were a lot of bugs and the code wasn't pretty. Anyways, fast forward a year later, no longer an intern and I am told that the application I worked on was appreciated by many with the numerous new features I had added and that they wanted to make it bigger, much bigger. I can't exactly explain in detail due to my NDA, but it was then that I realized, that I did what they asked me to do and even more, I made this application amazing and very useful to other employees who weren't even targeted by the application's purpose. The code is alright, but it's not me alright.

Back then, completely rebuilding an application which I thought wasn't going to get anywhere was definitely not worth it. There were thousands of lines of code which were absolutely horrendous, especially that DAO package. Most of my internship was spent adding new features I thought would be useful as well as refactoring the seemingly endless incomprehensible code.

Don't get me wrong, when it was made, the code was probably considered good, but now that we have Spring Boot with Hibernate (JPA), a lot of time could have been saved.

For instance, would you pick this:

public class PotatoDAO {

	// ...

	public static List<Potato> getPotatoesReadyToHarvest() throws SQLException {
		List<Potato> maturePotatoes = new ArrayList<>();
		Connection connection = null;
		Statement statement = null;
		ResultSet rs = null;
		try {
			connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS);
			statement = connection.createStatement();
			rs = statement.executeQuery("SELECT potato_id, potato_life_stage, potato_color
				FROM potato WHERE potato_life_stage = 'mature' AND potato_color = 'brown'");
				
			while (rs.next()) {
				maturePotatoes.add(new Potato(rs.getLong(1), rs.getString(2), rs.getString(3)));
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (rs != null) { rs.close(); }
			if (statement != null) { statement.close(); }
			if (connection != null) { connection.close(); }
		}
		return maturePotatoes;
	}

} 

or this?

@Repository
public interface PotatoRepository implements JpaRepository<Potato, Long> {

	List<Potato> findAllByLifeStageAndColor(String lifeStage, String color);
	
}

Even if you use the @Query annotation to directly get the potatoes ready to harvest, it's significantly easier

@Repository
public interface PotatoRepository implements JpaRepository<Potato, Long> {

	@Query("SELECT p FROM Potato p WHERE p.lifeStage = 'mature' AND p.color = 'brown'")
	List<Potato> getPotatoesReadyToHarvest();
	
}

Obviously, using hibernate with JpaRepository is much easier, as it automatically generates the queries for you. The downside is that you need to configure Hibernate, which may be problematic if you have multiple relationships.

To cut the story short, I ended up asking for 2 weeks to migrate to Spring Boot, which my manager accepted. It was complicated at times, but I love challenges and I love programming. It was worth it. The performance greatly improved and unlike before, the application was now maintainable.

I'm telling a story that nobody cares about, so let me get to the topic.

Migrating a Java EE Web Application to Spring Boot

I use IntelliJ IDEA, because it's that great to me, even more so when working on a Spring Boot project.

If you made it on this page, it means you're already determined to migrate your project to Spring Boot, so you probably weighted the pros and cons of what you're about to do. By now, the thought of "It's probably better to start from scratch" may have crossed your mind. Chill, it's not that bad, assuming the code doesn't look like the output of a Javascript obfuscator.

First, if you're not already using a version control system, use it.

My migration went smooth mainly because I already knew the code and I had already cleaned it up as much as I could at the time. However, that isn't to say I didn't encounter any obstacle, and the same applies to you.

So like I said, to make your life easier, use a VCS like Git and set it up on your IDE. A VCS will allow you to make more mistakes with less worries.

If your project doesn't already use Maven, then convert your project to Maven. Using IntelliJ, you need to right click the root folder of your project (module) and then "Add Framework Support..."

Add framework support

Then, you need to restructure your project to follow Maven's Maven's standard layout structure. It's a pain, but for Spring Boot to work flawlessly, you have to. Make sure that you right click on the java folder (/src/main/java) and then click on "Mark directory as Sources root".

Try to run the application to make sure it still compiles and works after having restructured it, if it doesn't, then fix it.

As for the web content (html, js, css), just put them in src/main/java/resources/static, assuming you're not using jsp. If you do use jsp, it's a bit more complicated. I'll make a guide for that later, but since it concerns very little people, I'll just assume you don't have jsp.

Now that you have a working Maven project, let's shape up the pom.xml file. Add the following at the bottom of the 'project' block:

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath />
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.196</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Add as much libraries as you can from your Java EE's web application library folder (should be WEB-INF/lib/) into your pom.xml

Try to run your application again (on the Tomcat server) to make sure there are no dependencies issues, and if you made it this far without any issues, the fun begins now.

Create your main Spring Boot class. Usually, that class should be the name of your project followed by Application.

@SpringBootApplication
@ComponentScan
@EnableJpaRepositories
@ServletComponentScan
public class PotatoApplication {

	public static void main(String[] args) {
		SpringApplication.run(PotatoApplication.class, args);
	}

}
@SpringBootApplication Shortcut to @Configuration, @EnableAutoConfiguration and @ComponentScan
@ComponentScan Even though the above annotation should include this annotation, it doesn't always work for me.
@EnableJpaRepositories Search and enable all interfaces with @Repository annotation
@ServletComponentScan Detect all WebFilter, WebListener and WebServlet. So this should detect your HttpServlet's @WebServlet annotation

If everything went fine, you should be able to run the main of that class you just made (PotatoApplication, in my case). Your servlet should have been detected and thus, everything should work.

The next step is to convert your WebServlet into a @Controller. I think it's easier if I just show the code, so here goes:

Before

@WebServlet(name = "JavaEEWebAppServlet", urlPatterns = "/servlet")
public class JavaEEWebAppServlet extends HttpServlet {

	private static final Logger logger = LoggerFactory.getLogger(JavaEEWebAppServlet.class);


	protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String action = request.getParameter("a");
		PrintWriter writer = response.getWriter();
		response.setContentType("application/json");
		try {
			if (action.equalsIgnoreCase("getAllPotatoes")) {
				writer.write(new Gson().toJson(PotatoService.getInstance().getAllPotatoes()));
			} else if (action.equalsIgnoreCase("getMaturePotatoes")) {
				writer.write(new Gson().toJson(PotatoService.getInstance().getMaturePotatoes()));
			} else if (action.equalsIgnoreCase("addPotato")) {
				PotatoService.getInstance().addPotato(request.getParameter("lifeStage"), request.getParameter("color"));
				writer.write("{}");
			} else if (action.equalsIgnoreCase("deletePotato")) {
				PotatoService.getInstance().deletePotato(Long.parseLong(request.getParameter("id")));
				writer.write("{}");
			} else if (action.equalsIgnoreCase("populate")) {
				PotatoService.getInstance().addSomePotatoes();
				writer.write("{}");
			} else {
				logger.debug("[processRequest] Invalid action: " + action);
			}
		} catch (Exception e) {
			logger.error("[processRequest] Error processing request with action '" + action + "': " + e.getMessage());
		}
	}


	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		processRequest(request, response);
	}


	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		processRequest(request, response);
	}

}

After

@RestController
@RequestMapping(value = "/api", produces = "application/json")
public class PotatoController {

	@RequestMapping(value = "/getAllPotatoes", method = {RequestMethod.GET, RequestMethod.POST})
	public String getAllPotatoes() throws Exception {
		return new Gson().toJson(PotatoService.getInstance().getAllPotatoes());
	}


	@RequestMapping(value = "/getMaturePotatoes", method = {RequestMethod.GET, RequestMethod.POST})
	public String getMaturePotatoes() throws Exception {
		return new Gson().toJson(PotatoService.getInstance().getMaturePotatoes());
	}


	@RequestMapping(value = "/deletePotato", method = {RequestMethod.GET, RequestMethod.POST})
	public String deletePotato(@RequestParam Long id) throws Exception {
		PotatoService.getInstance().deletePotato(id);
		return "{}";
	}


	@RequestMapping(value = "/addPotato", method = {RequestMethod.GET, RequestMethod.POST})
	public String addPotato(@RequestParam String lifeStage, @RequestParam String color) throws Exception {
		PotatoService.getInstance().addPotato(lifeStage, color);
		return "{}";
	}


	@RequestMapping(value = "/populate", method = {RequestMethod.GET, RequestMethod.POST})
	public String populate() throws Exception {
		PotatoService.getInstance().addSomePotatoes();
		return "{}";
	}

}

As a consequence, the path will change:

Before After
/servlet?a=getAllPotatoes /api/getAllPotatoes
/servlet?a=getMaturePotatoes /api/getMaturePotatoes
/servlet?a=addPotato&lifeStage=&color= /api/addPotato?lifeStage=&color=
/servlet?a=deletePotato /api/deletePotato?id=
/servlet?a=populate /api/populate

If you're migrating a huge project, let me give you a small piece of advice: Don't delete the old code until you are 100% sure everything is fully functional and the whole project has been migrated.

I personally use the @Deprecated annotation on every class and every method I finished migrating so that I can see what's done and what isn't. The reason why it's better to not delete old code just yet is that depending on the size of some classes, you might not be able to test old features until some other stuffs have been migrated, and so when you do test them, you'll be able to know what's the difference because you still have the old working code. It's just a piece of advice, do what you want, but keep in mind that the bigger the project you're migrating is, the easier it is to overlook a small detail. However, if some old classes break because you're migrating new stuffs, then just comment those old methods, there's no point in updating them since you're getting rid of them. We're just keeping them as a reference.

Run the application and see if you get the same result from the old url and the new url. In my case, I'd check that /servlet?a=getAllPotatoes gives me the same result as /api/getAllPotatoes

Now that you know it's working, you can replace the old paths in your JavaScript code. I feel like showing the code here is redundant, but I'll do it anyways.

Before

function getPotatoes() {
    $.getJSON("/servlet?a=getAllPotatoes", function(data) {
        $("#potatoes").html(JSON.stringify(data));
    });
}


function getMaturePotatoes() {
    $.getJSON("/servlet?a=getMaturePotatoes", function(data) {
        $("#potatoes").html(JSON.stringify(data));
    });
}


function addSomePotatoes() {
    $.getJSON("/servlet?a=populate", function(data) {
        getPotatoes();
    });
}


function deletePotato() {
    var id = prompt("Enter the potato id");
    $.getJSON("/servlet?a=deletePotato&id="+id, function(data) {
        getPotatoes();
    });
}

After

function getPotatoes() {
    $.getJSON("/api/getAllPotatoes", function(data) {
        $("#potatoes").html(JSON.stringify(data));
    });
}


function getMaturePotatoes() {
    $.getJSON("/api/getMaturePotatoes", function(data) {
        $("#potatoes").html(JSON.stringify(data));
    });
}


function addSomePotatoes() {
    $.getJSON("/api/populate", function(data) {
        getPotatoes();
    });
}


function deletePotato() {
    var id = prompt("Enter the potato id");
    $.getJSON("/api/deletePotato?id="+id, function(data) {
        getPotatoes();
    });
}

Restart your application, and make sure everything is working.

Now that the controller is migrated, we're moving on to the service. As we migrated the service, some other classes might break, but we'll get to that later.

Make sure you commit, because from this point on, you're going to see a lot of classes painted in red with errors.

I assumed that the old service would be a singleton, because migration will be easier that way. Since one of Spring Framework's main aspect is inversion of control, you'll need to autowire most of your services.

A part of the following code will be cut because it's irrelevant and takes too much space for no reason.

Before

public class PotatoService {

	private static PotatoService instance;


	public static PotatoService getInstance() {
		if (instance == null) {
			instance = new PotatoService();
		}
		return instance;
	}


	public void addPotato(String lifeStage, String color) throws SQLException {
		PotatoDAO.insertPotato(new Potato(lifeStage, color));
	}


	public void deletePotato(Long id) throws SQLException {
		PotatoDAO.deletePotato(id);
	}

    // ...

After

@Service
public class PotatoService {

	public void addPotato(String lifeStage, String color) throws SQLException {
		PotatoDAO.insertPotato(new Potato(lifeStage, color));
	}


	public void deletePotato(Long id) throws SQLException {
		PotatoDAO.deletePotato(id);
	}

    // ...

Yeah ok, relax, I get it, there's red everywhere. Chill. In your controller, this is what you'll do:

@RestController
@RequestMapping(value = "/api", produces = "application/json")
public class PotatoController {

	@Autowired
	private PotatoService potatoService;


	@RequestMapping(value = "/getAllPotatoes", method = {RequestMethod.GET, RequestMethod.POST})
	public String getAllPotatoes() throws Exception {
		return new Gson().toJson(potatoService.getAllPotatoes());
	}


	@RequestMapping(value = "/getMaturePotatoes", method = {RequestMethod.GET, RequestMethod.POST})
	public String getMaturePotatoes() throws Exception {
		return new Gson().toJson(potatoService.getMaturePotatoes());
	}

    // ...

We added @Autowired private PotatoService potatoService and we replaced PotatoService.getInstance() by potatoService. Spring will take care of instantiating potatoService for us.

To get a bit more technical, that's what @Autowired does. One of Spring Framework's main feature is called "Inversion of Control", which means that instead of the programmer being in control of what happens, it's the framework. By using @Autowired, the framework will look for a bean with the corresponding type "PotatoService", and it will find it because PotatoService has been registered as a @Service, which is a component, which is a Spring managed bean.

If you have other services who references the services you've already migrated, then do the same as you did in the controller:

@Service
public class VegetationService {

	@Autowired
	private PotatoService potatoService;

    // ...

More than one service? That's alright.

@Service
public class SomeOtherService {

	@Autowired
	private PotatoService potatoService;

	@Autowired
    private VegetationService vegetationService;

    // ...

Now, it's very likely that you have non-service classes referencing those services, well don't worry, it's easy to fix that too. Just add @Component on top of that class and then do like you did in the other classes, @Autowired private PotatoService potatoService;

@Component
public class SomeClassWhichIsNotAServiceButNeedsAccessToAService {

    @Autowired
    private PotatoService potatoService;

    // ...

The reason why you need to add @Component is because if you don't, the class won't be registered by Spring, thus Spring will not manage that class and it won't see the @Autowired.

Likewise, if a service needs access to a component, then you'd do the same thing:

@Service
public class SomeService {

    @Autowired
    private SomeComponent someComponent; // this is a class with the @Component annotation

    // ...

Like I said earlier, you can do each service one by one and test between every single time, or if you're feeling wild, you can do them all at the same time.

Once you're done migrating the services, you know what's about to happen - DAO - but no, not yet. Make sure you commit and get ready because, if your project is huge, so will the next step. If you're satisfied with what you have right now, then you're dumb because the next step is where you start feeling how good of an idea it was to migrate to Spring Boot.

Entities

Let me get one thing straight before we proceed - I'm not going to hold your hand all the way through the next part, because the next part is the hardest, especially if you have a lot of beans. In my case, I have one bean, "Potato". There's a lot of posts on StackOverflow about relationships in Hibernate, and I'm definitely not going to go through every possible case.

In other words, I am assuming you have some basic knowledge about Hibernate. If you don't, then you should probably read up a bit before continuing your migration.

Before

package org.twinnation.migration.bean;

public class Potato {

	private Long id;
	private String lifeStage;
	private String color;


	public Potato(Long id, String lifeStage, String color) {
		this.id = id;
		this.lifeStage = lifeStage;
		this.color = color;
	}


	public Potato(String lifeStage, String color) {
		this.lifeStage = lifeStage;
		this.color = color;
	}


	public Long getId() {
		return id;
	}


	public void setId(Long id) {
		this.id = id;
	}


	public String getLifeStage() {
		return lifeStage;
	}


	public void setLifeStage(String lifeStage) {
		this.lifeStage = lifeStage;
	}


	public String getColor() {
		return color;
	}


	public void setColor(String color) {
		this.color = color;
	}

}

After

package org.twinnation.migration.bean;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;


@Entity
public class Potato {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String lifeStage;
	private String color;


	public Potato(Long id, String lifeStage, String color) {
		this.id = id;
		this.lifeStage = lifeStage;
		this.color = color;
	}


	public Potato(String lifeStage, String color) {
		this.lifeStage = lifeStage;
		this.color = color;
	}


	public Potato() {}


	public Long getId() {
		return id;
	}


	public void setId(Long id) {
		this.id = id;
	}


	public String getLifeStage() {
		return lifeStage;
	}


	public void setLifeStage(String lifeStage) {
		this.lifeStage = lifeStage;
	}


	public String getColor() {
		return color;
	}


	public void setColor(String color) {
		this.color = color;
	}

}

Make sure that you don't forget the empty constructor, otherwise Hibernate will throw InstantiationException. There's no point in checking if everything work just yet, so don't try to test it.

application.properties

Before we move on to the repositories, we'll set up an embedded/in-memory database using H2. The required dependency is already in the pom.xml so all you need to do is create a file named application.properties in your /resources folder

spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.platform=h2
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=update

Repositories

Create a new package for your JPA repositories. This will be your new DAO.

Your repository is your DAO, so everything in your DAO has to be in your repository except a few default methods that will work even if your repository interface is empty (ex.: findAll(), findOne(id), deleteAll(), delete(id), save(Object), count(), ...) Depending on your class, your repositories will be different, but I'll show you an example from my PotatoApplication anyways:

PotatoRepository

package org.twinnation.migration.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.twinnation.migration.bean.Potato;

import java.util.List;


@Repository
public interface PotatoRepository extends JpaRepository<Potato, Long> {

	List<Potato> findAllByLifeStageAndColor(String lifeStage, String color);

}

And to complete the migration, we replace the old DAO references for your repository.

package org.twinnation.migration.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.twinnation.migration.bean.Potato;
import org.twinnation.migration.dao.PotatoDAO;
import org.twinnation.migration.repository.PotatoRepository;

import java.sql.SQLException;
import java.util.List;


@Service
public class PotatoService {

	@Autowired
	private PotatoRepository potatoRepository;


	public Potato addPotato(String lifeStage, String color) throws SQLException {
		//PotatoDAO.insertPotato(new Potato(lifeStage, color));
		return potatoRepository.save(new Potato(lifeStage, color));
	}


	public void deletePotato(Long id) throws SQLException {
		//PotatoDAO.deletePotato(id);
		potatoRepository.delete(id);
	}


	public List<Potato> getMaturePotatoes() throws SQLException {
		//return PotatoDAO.getPotatoesReadyToHarvest();
		return potatoRepository.findAllByLifeStageAndColor("mature", "brown");
	}


	public List<Potato> getAllPotatoes() throws SQLException {
		//return PotatoDAO.getAllPotatoes();
		return potatoRepository.findAll();
	}


	public void addSomePotatoes() throws SQLException {
		//PotatoDAO.insertPotato(new Potato("sprouting", "green"));
		//PotatoDAO.insertPotato(new Potato("mature", "brown"));
		//PotatoDAO.insertPotato(new Potato("mature", "brown"));
		//PotatoDAO.insertPotato(new Potato("dead", "grey"));
		//PotatoDAO.insertPotato(new Potato("dormant", "brown"));
		potatoRepository.save(new Potato("sprouting", "green"));
		potatoRepository.save(new Potato("mature", "brown"));
		potatoRepository.save(new Potato("mature", "brown"));
		potatoRepository.save(new Potato("dead", "grey"));
		potatoRepository.save(new Potato("dormant", "brown"));
	}

}

And this is it. That's basically how you migrate from Java EE to Spring Boot.

It'll take a while, but it's not impossible, and above all, it's definitely worth it.

Once you're done, you can delete all unused classes and you can remove @ServletComponentScan from your main class since you replaced @WebServlet by @RestController

Good luck!