Wednesday, December 12, 2007

Zero to RMI with Groovy and Spring

In a recent post, I leveraged Spring to write a simple client-server app using 50 lines of Java.

I've moved it to Groovy. I've included the code below, but here are some fast facts:
  • I used groovyc to compile the Groovy to class files, then used the java command.
  • I could only reduce the code by an import or two. However, I did save 220+ characters (notice how clean the Groovy is). Groovy gurus might be able to improve on this.
  • Note that the Spring config files have more namespaces for Groovy support. Discovering this was the hardest part of the exercise.

Here is the contract between the client and server. This would be in a common package:


interface EasyServer {
Message exchangeMessage(String user)
}

Here is a simple Message class. Of course, for RMI, being Serializable is vital:

import java.io.Serializable

class Message implements Serializable {
private final String message

Message(String message) { this.message = message }

String getMessage() { return message }
}

So far, so good. Now the server implementation:

import org.springframework.context.support.ClassPathXmlApplicationContext

class EasyServerImpl implements EasyServer {

Message exchangeMessage(String user) {
return new Message(user + ", Spring with Groovy rocks!")
}

static void main(String[] args) {
def context =
new ClassPathXmlApplicationContext("server_config.xml")
context.getBean("easyServer")
println("server ready")
}
}

It defines an easy exchangeMessage method and a starting main method. Spring takes care of the rest, as shown in the server_config.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang-2.0.xsd">

<lang:groovy id="easyServer" script-source="classpath:EasyServerImpl.groovy"/>

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
<!-- does not necessarily have to be the same name as the bean to be exported -->
<property name="serviceName" value="easyServerService"/>
<property name="service" ref="easyServer"/>
<property name="serviceInterface" value="EasyServer"/>
<!-- defaults to 1099 -->
<property name="registryPort" value="1199"/>
</bean>

</beans>

For running and compiling the server, simply ensure that these are in the CLASSPATH:
  • SPRING_HOME/dist/spring.jar
  • SPRING_HOME/lib/jakarta-commons/commons-logging.jar

Then it is up and away:

$ java EasyServerImpl

[ Spring magic dust snipped ]

server ready

The client is even simpler:

import org.springframework.context.support.ClassPathXmlApplicationContext

class EasyClient {
private EasyServer easyServer

void setEasyServer(EasyServer easyServer) { this.easyServer = easyServer }

Message exchangeMessage(String user) {
return easyServer.exchangeMessage(user)
}

static void main(String[] args) {
def context =
new ClassPathXmlApplicationContext("client_config.xml")

def easyClient = context.getBean("easyClient")

String output =
easyClient.exchangeMessage("CtJ Reader").getMessage()
println( output )
}
}

The client-side configuration XML:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang-2.0.xsd">

<lang:groovy id="easyClient" script-source="classpath:EasyClient.groovy">
<lang:property name="easyServer" ref="easyServer"/>
</lang:groovy>

<bean id="easyServer" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
<property name="serviceUrl" value="rmi://localhost:1199/easyServerService"/>
<property name="serviceInterface" value="EasyServer"/>
</bean>

</beans>

Using the same CLASSPATH, but with a separate window, we establish contact like so:

$ java EasyClient

[ Spring noise snipped ]

CtJ Reader, Spring with Groovy rocks!


Cool stuff...

4 comments:

  1. There's a couple of useful Groovy techniques you can use to make the code much shorter, hence more readable.

    First, Groovy supports "bean-scripting" declarations. Declaring an unscoped class member (say 'message') is the equivalent in Java of declaring a private member 'member' and public accessors 'getMember' and 'setMember'. Outside of the class, 'obj.message' invokes the appropriate accessor.

    Second, Groovy automatically imports all the classes in java.io, java.util, and java.net, so you don't need these import statements.

    So your Message class can collapse to 4 lines:


    class Message implements Serializable {

    final String message

    Message(String arg ) { message = arg }

    }


    (The 'final' keyword results in 'getMessage' but not 'setMessage' being defined.)

    This 'bean scripting' facility gets increasingly important as the number of properties in a class grows: it eliminates a lot of boilerplate.

    ReplyDelete
  2. If you remove the final constraint, it could be more tiny:
    class Message implements Serializable {
    String message
    }

    An so, create message objects using:
    Message message = new Message(message:'Hi, I am a message')

    At "exchangeMessage" server method, you could use GStrings and autoreturn and dynamic type for parameter:
    Message exchangeMessage(user) {
    new Message("$user, Spring with Groovy rocks!")
    }

    Nice post. Kind Regards

    ReplyDelete
  3. Thanks for the comments, Randy and Marcos.... good stuff.

    I did try leaving off the getter but something was strange. I almost mentioned it in a draft but removed it for brevity. I knew you Groovites would spot it :-)

    Big +1 on auto import of the common java packages.... good to know

    ReplyDelete
  4. Groovy allows for code to be written as scripts. To leverage this, I would remove EasyServerImpl.main(), rename EasyServer* to EasyService*, and create RunEasyServer.groovy whose contents would look like this:

    new org.springframework.context.support.ClassPathXmlApplicationContext('server_config.xml')
    println('server ready')

    Then I simply type the following in the terminal:
    groovy RunEasyServer.groovy

    ReplyDelete