Sunday, May 13, 2007

Spring Remoting Strategies compared

A few weeks ago, we needed to integrate a fairly resource-intensive module into our web application. Because it was so resource-intensive, it was not practical to run it embedded within the application, so we decided to isolate it into it own application and access it as a service. Since our application is Spring based, we decided to use one of the remoting strategies that comes built into Spring. This post describes my preliminary investigations into this, and a comparison of the performance between the various strategies.

The nice thing about Spring is that you can build your server and client without regard for the actual transport protocol that you will finally use. The supported protocols are RMI, Burlap, Hessian and Spring's Java serialization over HTTP. For each of the supported protocols, Spring provides an Exporter and a ProxyFactory component. The Exporter "exports" your server POJO as a service, while the ProxyFactory proxies the service interface at the client.

Java code

Since the client and server components are going to be sharing the classes for the service interface and common domain objects, I built this experiment up as a multi-module Maven2 project, with three sub-projects: service, common and client. To make this easy to create, I decided to follow the Account example in the Spring reference guide. However, my Account object is beefier, comparable in complexity to the type of object I expect to be sending and receiving remotely. Here is the service interface and the domain objects from the common module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// AccountService.java
public interface AccountService {
  public void insertAccount(Account account);
  public List<Account> getAccounts(String accountName);
}

// Account.java
public class Account implements Serializable {

  private String customerName;
  private Map<String,List<Transaction>> transactions;
 ...
}

// Transaction.java
public class Transaction implements Serializable {

  private Date date;
  private String description;
  private int type;
  private Float amount;
  ...
}

I had originally created a Java 1.5 Enum for the Transaction.type variable, but that caused Burlap and Hessian remoting to fail, since there is no mapping for user-defined types. Even using the Java 1.4 style Type-safe Enum Pattern won't work, since they don't have the public default constructor that is needed for Burlap and Hessian remoting to work. I suspect that they also need public getter and setter methods, but did not test that, so can't say for sure.

The server component is just a shell that reports what came in and went out, without actually doing something useful, such as putting it in a database or something. The MockAccountHelper is a class in the common module which returns canned instances of Account objects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class AccountServiceImpl implements AccountService {
  private static final Logger LOGGER = Logger.getLogger(AccountServiceImpl.class);
  public void insertAccount(Account account) {
    LOGGER.debug("Server inserts:" + account.toString());
  }
  public List<Account> getAccounts(String name) {
    List<Account> accounts =  MockAccountHelper.getMockAccountList();
    LOGGER.debug("Server returns:" + accounts.toString());
    return accounts;
  }
}

The client component is a plain POJO which references the proxy implementation of the AccountService API and exposes methods which simply delegate to the proxy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class AccountClient {
  private AccountService accountService;

  public void setAccountService(AccountService accountService) {
    this.accountService = accountService;
  }
  public void insertAccount(Account account) {
    accountService.insertAccount(account);
  }
  public List<Account> getAccounts(String name) {
    return accountService.getAccounts(name);
  }
}

Configuration

We expect the server to be a web application which exposes the service over RMI or HTTP POST. So we have to setup the following files on the server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!--===== web.xml =====-->
<web-app ...>
  <servlet>
    <servlet-name>service</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>service</servlet-name>
    <url-pattern>/service/*</url-pattern>
  </servlet-mapping>
</web-app>

<!-- ===== service-servlet.xml ===== -->
<beans ...>
  <bean id="accountService" class="com.mycompany.myapp.server.AccountServiceImpl"/>

  <!-- Specific exporter configuration goes here -->
  ...
</beans>

On the client, we set up our client and inject it with a reference to the appropriate proxy, as shown below. Our client will ultimately be a web application, but for our experiment, we just have a JUnit test, so we set up an applicationContext-client.xml as shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- ====== applicationContext-client.xml ===== -->
<beans ...>
  <bean id="accountClient" class="com.mycompany.myapp.client.AccountClient">
    <property name="accountService" ref="accountService"/>
  </bean>

  <!-- ProxyFactoryBean implementation for the selected protocol -->
  <bean id="accountService" class="...">
    ...
  </bean>

</beans>

Protocol specific configurations

We describe the server and client configurations for each of our supported protocols below. We just plug-in the appropriate exporter configuration into the service-servlet.xml and the corresponding proxy factory configuration into the applicationContext-client.xml file. Tinou has a nice diagram of this setup in his blog entry A critique of Spring, although he cites this as one of the things he dislikes about Spring.

Protocol Service config Client config
RMI
Burlap
Hessian
Spring

Testing

For testing, we have to bring up the server web application. We can do this using the built-in Maven2 Jetty plugin, with the command mvn jett6:run. On the client, we use a JUnit test to hit this service. By the way, I started using JUnit 4 quite recently, after I could not find answers to one of my JUnit 3.8 questions. You can get up and running with it quickly using Gunjan Doshi's blog post "JUnit 4.0 in 10 minutes". By the way, its also backward compatibie, so your existing 3.8 test will run without problems under JUnit 4.0. Here is the code for my unit test, which I invoke from the command line as "mvn test -Dtest=AccountClientTest".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class AccountClientTest {

  private static final Logger LOGGER = Logger.getLogger(AccountClientTest.class);

  private AccountClient client;

  @Before
  public void setUp() throws Exception {
    ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext-client.xml");
    client = (AccountClient) context.getBean("accountClient");
  }

  @Test
  public void testInsertAccount() throws Exception {
    Account account = MockAccountHelper.getMockAccount();
    StopWatch watch = new StopWatch();
    watch.start("insert");
    for (int i = 0; i < 50; i++) {
      client.insertAccount(account);
    }
    watch.stop();
    LOGGER.debug("Avg insert time:" + (watch.getTotalTimeMillis() / 50) + "ms");  }

  @Test
  public void testGetAccounts() throws Exception {
    StopWatch watch = new StopWatch();
    watch.start("get");
    for (int i = 0; i < 50; i++) {
      List<Account> accounts = client.getAccounts("");
    }
    watch.stop();
    LOGGER.debug("Avg get time:" + (watch.getTotalTimeMillis() / 50) + "ms");
  }
}

Results

As you can see, all I am doing is hitting each exported remote method 50 times and computing the average response time. I also created a baseline by doing something similar on the server application, so there is no network overhead. My objective was to compare the network overhead added in by each protocol. Here are my results, running on my laptop with 512MB RAM and a 2.80 GHz Pentium 4 processor.

Protocol insertAccount (ms) getAccounts (ms)
Locel (baseline) 9 9
RMI 13 13
Burlap 19 17
Hessian 17 14
Spring HttpInvoker 41 16

7 comments (moderated to prevent spam):

Harith Elrufaie said...

Sujit, this is very interesting comparision. I have used Spring HttpInvoker in a playground project where the client is Eclipse RCP application. I loved it and I liked how easy the configuration is.. but now after seeing the numbers, I have to think about it again if I am working on something that has high traffic volume.

Sujit Pal said...

Yes, the numbers are kind of depressing for Spring HttpInvoker, given that it is the cleanest of the four implementations offered. Some speed increases can probably be achieved by using Jakarta HttpClient instead of the default client.

Anonymous said...

Hi, I don't suppose you have any feeling how a SOAP remoting would compare?

Alistair

Sujit Pal said...

Hi Alistair, no I don't have numbers for this. I tried the SOAP/Spring remoting strategy described in the Spring docs, but then ran into all sorts of issues, some of which may be due to my lack of understanding of how the Axis remoting works. If you manage to get it working, would appreciate your letting me know, I would be very interested.

TIA

Anonymous said...

Webservices(i.e. SOAP) will incur considerable overhead in marshalling and unmarshalling the communication between the presentation layer and the service layer so it probably will be slowest of all

Marcos said...

Hi,

Great post,

Where can I get the complete project?

Sujit Pal said...

Hi Marcos, I don't think I have the project anymore...I wrote this couple of years ago and the driver was to learn the various ways Spring allowed remoting and see which one would be the "best" for me. I settled on Burlap/Hessian.