Three Simple Steps to Convert from EJB 3 to Spring 4

1:46 PM , , 1 Comments

While the merits of doing so are debatable, a client of mine wanted to covert his EJB application into a Maven, Spring, Hibernate application.

His application was small--about 75 classes. The only significant aspects of EJB that he was using was dependency injection and RMI. He did not up to that point have a need for persistence, so there were no existing ties to JPA. He was; however, tied to Jersey 1.x and currently needed a dependency from Netbeans in order to build the product (weird?).

Some of the above may sound similar to your situation. What I'll describe below is how we

1. Migrated the product from Ant to Maven, turning it into a multi-module project.
2. Migrated from EJB to Spring
3. Upgraded from Jersey 1.x to 2.x

Maven


As the product stood, it would build an .ear file that exposed a remote interface over RMI and a .jar file that invoked this remote interface. Insofar as we were abandoning EJB for Spring, it would suffice to create a jar file on both sides.

These had a common dependency, which was the shared interface, so I recommended that we create a multi-module maven project which would allow the client and server to both be dependent on an api project. Also, they could all be built with a single command and be automatically related to each other in modern IDEs.

The new project structure

The file structure will change a bit; here is what it will look like in the end:


/product-name
  -- pom.xml
  -- /api
  ---- pom.xml
  ---- /src
  -- /client
  ---- pom.xml
  ---- /src
  -- /server
  ---- pom.xml
  ---- /src

The master pom

The master pom ties everything together, specifying which and in what order modules should be built:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns="http://maven.apache.org/POM/4.0.0"
 xsi:schemalocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/maven-v4_0_0.xsd">
 <modelversion>4.0.0</modelversion>

 <groupid>com.company</groupid>
 <artifactid>product-parent</artifactid>
 <packaging>pom</packaging>
 <version>0.0.1-SNAPSHOT</version>
 <name>Product Parent Pom</name>

 <modules>
  <module>api</module>
  <module>server</module>
  <module>client</module>
 </modules>

 <properties>
  <spring.version>4.1.4.RELEASE</spring.version>
 </properties>

 <build>
  <plugins>
   <plugin>
    <groupid>org.apache.maven.plugins</groupid>
    <artifactid>maven-compiler-plugin</artifactid>
    <configuration>
     <source>1.8</source>
     <target>1.8</target>
    </configuration>
   </plugin>
  </plugins>
 </build>

 <dependencies>
  <dependency>
   <groupid>junit</groupid>
   <artifactid>junit</artifactid>
   <version>4.12</version>
   <scope>test</scope>
  </dependency>
 </dependencies>
</project>

The API Project

Then, the API project. For now, this will only contain a single file that needs to be shared between the client and server. In all likelihood, RMI will be abandoned in favor of JAX-RS or Spring MVC down the road, but right now we are changing as little code as humanly possible.


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>com.companylt;/groupId>
  <artifactId>product-parent</artifactId>
  <version>0.0.1-SNAPSHOT</version>
 </parent>

 <artifactId>api</artifactId>

 <name>SDA Api</name>
</project>

The API has just one interface and it not annotated with any EJB or Spring annotations; those are placed on the implementations. Because of this, notice that this api has no dependencies, which can come in handy later on if the client wants to use different libraries from the server.

Client and Server

The client and server have several dependencies; I'll try and highlight the important ones:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>com.company</groupId>
  <artifactId>product-parent</artifactId>
  <version>0.0.1-SNAPSHOT</version>
 </parent>

 <artifactId>client</artifactId>
 <packaging>jar</packaging>

 <name>Product Client</name>

 <dependencies>
  <!-- API Dependency -->
  <dependency>
   <groupId>biz.keyinsights.sda</groupId>
   <artifactId>api</artifactId>
   <version>${project.version}</version>
  </dependency>

  <!-- Spring Dependencies -->
  <dependency>
   <groupId>javax.inject</groupId>
   <artifactId>javax.inject</artifactId>
   <version>1</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-core</artifactId>
   <version>${spring.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-web</artifactId>
   <version>${spring.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-beans</artifactId>
   <version>${spring.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-expression</artifactId>
   <version>${spring.version}</version>
  </dependency>

  <!-- A particularly important dependency -->
  <dependency>
   <groupId>org.springframework.integration</groupId>
   <artifactId>spring-integration-rmi</artifactId>
   <version>4.1.2.RELEASE</version>
  </dependency>

  <!-- Other dependencies not listed -->
 </dependencies>
 <build>
  <finalName>product-client</finalName>
  <plugins>
   <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.1.1</version>
    <executions>
     <execution>
      <phase>package</phase>
      <goals>
       <goal>java</goal>
      </goals>
      <configuration>
       <mainClass>userInterface.ClientUserInterface</mainClass>
      </configuration>
     </execution>
    </executions>
   </plugin>
  </plugins>
 </build>
</project>

Finally, the Server's pom, which I won't post because it is largely the same as the client pom.

The last thing I needed/wanted to do was rearrange the project structure so that it matched Maven's default. This means putting all the source code into src/main/java, all the resources into src/main/resources, and all the JUnit tests into src/test/resources. Pretty simple.

Now, a simple "mvn clean install" from the parent will do all the work!

Spring



RMI

When in EJB, the interface was exposed via the @Remote EJB annotation. Spring has a nice RMI library, so it was a simple matter of removing the EJB anntotations and providing the correct Spring wiring:

Client-side

@Configuration
@ComponentScan("userInterface")
public class AppConfig {
 @Bean(name = "userInterface")
 public RmiProxyFactoryBean remote() {
  RmiProxyFactoryBean b = new RmiProxyFactoryBean();
  b.setServiceUrl("rmi://localhost:1199/RegressionService");
  b.setServiceInterface(UserInterfaceRemote.class);
  return b;
 }
}

Server-side

@Configuration
@ComponentScan({"session.client"})
public class AppConfig {
 @Inject UserInterfaceRemote service;
 
 @Bean
 public RmiServiceExporter rmiServiceExporter() {
  RmiServiceExporter exporter = new RmiServiceExporter();
  exporter.setServiceName("RegressionService");
  exporter.setService(service);
  exporter.setServiceInterface(UserInterfaceRemote.class);
  exporter.setRegistryPort(1199);
  return exporter;
 }
}

So far, no business logic or design changes; just wiring.

Dependency Injection


In EJB, beans can be made eligible for dependency injection via the @Stateless annotation, which was the case here. I replaced these with the appropriate Spring stereotype, e.g. @Service, @Component, etc.

There were a couple of @Stateful annotations (ew) which I could have replaced with the @Scope annotation in Spring. Fortunately, however, the "statefulness" of those beans turned out to be an artifact from a discarded approach, so I replaced those with @Service as well.

On the auto-wiring end, I replaced @EJB with @Inject

And finally, I added the Java Config on both the client and the server to bootstrap Spring:

Server-side


public class MyWebApplicationInitializer implements WebApplicationInitializer {

 public void onStartup(ServletContext servletContext)
   throws ServletException {
  servletContext.addListener(new ContextLoaderListener(createWebAppContext()));    
 }

    private WebApplicationContext createWebAppContext() {
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(AppConfig.class);
        return appContext;
    }
}

Client-side


public static void main(String[] args) {
     AnnotationConfigApplicationContext context =
             new AnnotationConfigApplicationContext(AppConfig.class);
        ClientUserInterface ui = context.getBean(ClientUserInterface.class);
        ui.go();
}

Excellent, no code changes! To the existing code, we only modified annotations. We added wiring code and maven configurations.

Jersey


No project goes perfectly, though, and my excitement was tempered by discovering that Jersey's support for Spring 3+ was added in Jersey 2.x. Grrr. Upon attempting to upgrade, by changing the version in the maven pom, there were several places in the code where compilation failed.

The Jersey Migration Guide to the rescue! Especially section 27.16.2, which covered how to change client request from 1.x style to the 2.x style and 27.16.3 where JSON support is explained.

Updating Jersey Client API

I found the Jersey 2.x Client API to be a little confusing when it came to setting headers. Somehow this:

Response response = target.request("text/plain").get();

is setting the Accept header for the request to be "text/plain", which is a change from using the accept() method to set the Accept header.

And how to set the content type for the request? There doesn't appear to be a contentType() method or something similar; it appears to be retreived from the entity sent in the post method:


Response response = target.request("text/plain").post(Entity.entity(payload, "application/xml"));

The typical Jersey Client request appears to be a three-line cocktail of obtaining a Client, deriving a WebTarget via the Client, and then invoking the target to get a Response. In the code I was upgrading, there were several repeated sections of these three lines of code. Grrr! However, at the time I just needed to be surgical about it and not do a bunch of refactoring, so I left the copy-pasta for another day.

JSON Support

Replacing JSON support was super easy: It was simply adding the appropriate jackson-jaxrs-json-provider dependency to maven.

HACK: Ghost applicationContext.xml

There was only one really cheesy thing that I didn't find a good way around. After upgrading to Jersey and following the docs to get Spring and Jersey to play nicely (at least as I understood it to that point), I would always get an exception that said that Spring could not instantiate multiple ContextLoaders. This was really frustrating as I knew that I already had one in my Java config (see above).

By doing some debugging into Jersey, I found this in Jersey's SpringWebApplicationInitializer:


@Override
    public void onStartup(ServletContext sc) throws ServletException {
        if (sc.getInitParameter(PAR_NAME_CTX_CONFIG_LOCATION) == null) {
            LOGGER.config(LocalizationMessages.REGISTERING_CTX_LOADER_LISTENER());
            sc.setInitParameter(PAR_NAME_CTX_CONFIG_LOCATION, "classpath:applicationContext.xml");
            sc.addListener("org.springframework.web.context.ContextLoaderListener");
            sc.addListener("org.springframework.web.context.request.RequestContextListener");
        } else {
            LOGGER.config(LocalizationMessages.SKIPPING_CTX_LODAER_LISTENER_REGISTRATION());
        }
    }

In short, this is looking to see if there is an applicationContext.xml referenced in the ServletContext already; if not, then create one and add a ContextLoaderListener to the servlet context as well.

The fact is that since I'm using Java config, I don't have an applicationContext.xml! So the first half of the if statement was always running. To get around it, I added a ghost reference to the web.xml file to an empty applicationContext.xml file and it worked like a charm!

Conclusion


Other than one particular library incompatibility, migrating to Maven, Spring 4, Hibernate, and Jersey 2.x was pretty straightforward. Enjoy!





Josh Cummings

"I love to teach, as a painter loves to paint, as a singer loves to sing, as a musician loves to play" - William Lyon Phelps

1 comment:

  1. Thanks for the post, I (contractor), am going to try to propose to move rmi/ejb to be inside spring app. I've to find a performance/latency arguement. Do you have some numbers. did perf/latency improve, what % / sec ?

    ReplyDelete