Detta inlägg ingår i serien Spring från början och kommer att behandla hur man med hjälp av Spring kan exponera och anropa distribuerade tjänster via olika teknologier såsom RMI, Hessian osv.

Varför behöver man då distribuerade tjänster? I många fall finns inget behov av en distribuerad tjänst. Om man exempelvis bygger en webapp kan ofta tjänsterna snurra i samma kontext som själva webappen och då finns inget egentligt behov av att göra en tjänst tillgänglig från andra punkter i nätverket. Dock förändras detta en aning när en rik klient måste komma åt backendprocesserna, detta kan exempelvis vara en klient skriven med Swing, SWT eller kanske C#. Distribuerade tjänster behövs också när olika system måste prata med varandra, exempelvis kan två olika backendapplikationer kommunicera via distribuerade tjänster och dessa system kan till och med vara skrivna i olika språk.

Spring erbjuder stöd för distribuerade tjänster genom att exponera vanliga springbönor via av olika tekniker. För att exempelvis exponera en springböna som en RMI-tjänst används en service exporter som helt enkelt skapar en proxy runt den bönan som ska exponeras och det är denna proxy som RMI-klienter sedan kan nå. Detta innebär att själva springbönan fortfarande är en vanlig POJO och allt som har med distributionen av bönan att göra sker via konfiguration.

De tekniker som diskuteras är:

  • Remote Method Invocation (RMI)RMI är ett API som erbjuder möjligheten att anropa distribuerade tjänster med fullt serialiseringsstöd (man kan alltså skicka serialiserbara javaobjekt till en distribuerad tjänst). Det finns två vanligt förekommande implementationer, RMI-JRMP och RMI-IIOP. RMI-JRMP är en ren javaimplementation och erbjuder alltså endast stöd för anrop från en JVM till en annan JVM. RMI-IIOP baseras på Corba och kan därför anropas av andra typer av klienter (Java, C++ etc).
  • HessianHessian är ett binärt HTTP-baserat protokoll från Caucho som inte erbjuder full serialisering av javaobjekt. Hessianstöd finns för flera programmeringsspråk såsom Java, C++, C#, Erlang, PHP, Ruby etc.
  • BurlapBurlap är Cauchos XML-variant av Hessian och det finns stöd för flera programmeringsspråk såsom Java, C++ och C#.
  • Springs HTTP Invoker – Detta HTTP-protokoll erbjuder till skillnad från Hessian och Burlap fullt serialiseringsstöd för javaobjekt. Genom att använda denna teknologi låser man sig till att inte bara använda Java utan även till att använda Spring (Spring måste alltså både finnas på klientsidan och på servern).
  • Java Management Extensions (JMX)JMX är en javateknologi som bland annat används för övervakning av applikationer genom att exportera så kallade MBeans (Managed Beans). MBeans lever i en så kallad MBeanServer som är tillgänglig lokalt i en JVM men de kan även nås som distribuerade tjänster. JMX erbjuder endast javasstöd vilket innebär att både klienten och den distribuerade tjänsten måste implementeras i java.

WebServices är en annan vedertagen teknik för att exponera distribuerade tjänster och kommer att beskrivas senare i denna serie. Spring erbjuder även JMS-stöd för distribuerade tjänster men det kommer inte att diskuteras i detta inlägg.

Vi utgår från följande exempel – en beräkningskomponent som kan utföra enkla operationer såsom addition, subtraktion etc. Javagränssnittet ser ut så här:

package se.cygni.demo.exporters;

public interface CalculatorService {
    int add(int first, int second);
    int subtract(int first, int second);
    int divide(int first, int second);
    int multiply(int first, int second);
}

Tjänsten implementeras som en vanlig POJO enligt nedan:

package se.cygni.demo.exporters;

public class PojoCalculatorService implements CalculatorService {
    public int add(int first, int second) {
        return first + second;
    }

    public int subtract(int first, int second) {
        return first - second;
    }

    public int divide(int first, int second) {
        return first / second;
    }

    public int multiply(int first, int second) {
        return first * second;
    }
}

POJO:n konfigureras via XML på följande sätt:

<bean
    id="pojoCalculatorService"
    class="se.cygni.demo.exporters.PojoCalculatorService" />

RMI

Spring erbjuder RMI-stöd som transparent exporterar bönor som RMI-tjänster via en så kallad service exporter. Att exportera en tjänst är enkelt genom användning av klassen RmiServiceExporter.

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
    <property name="serviceName" value="CalculatorService"/>
    <property name="service" ref="pojoCalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
    <property name="registryPort" value="1199"/>
</bean>

De properties som anges är:

  • serviceName – Ett logiskt namn i RMI-registryt som används av klienter för att nå denna tjänst.
  • service -En referens till den faktiska implementationen av tjänsten det vill säga POJO-implementationen.
  • serviceInterface – Det javagränssnitt som ska exporteras.
  • registryPort – Den RMI-port som används, defaultvärde för denna port är 1099.

Genom att deklarera denna böna har tjänsten exporterats i RMI-registryt, observera att om inget RMI-registry fanns skapat så skapar denna böna det åt dig.

En klient vill normalt inte ha kännedom om att detta är en RMI-tjänst utan typiskt bör klienten endast känna till tjänstegränssnittet (det vill säga CalculatorService). För att anropa denna tjänst från en klient kan följande kod användas:

public class MyClass {
    private CalculatorService calculatorService;

    public void setCalculatorService(CalculatorService service) {
        this.calculatorService = service;
    }

    public void doSomeCalc() {
        int answer = calculatorService.add(1, 2);
        ... // do more stuff
    }
}

Gränssnittet skapas som en RMI-proxy via springklassen RmiProxyFactoryBean enligt nedan:

<bean id="calculatorService"
        class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
    <property name="serviceUrl"
        value="rmi://localhost:1199/CalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
</bean>

Detta innebär att transporten är helt transparent för klienten. Följande properties sätts på RMI-proxyn:

  • serviceInterface – Tjänstegränssnittet som proxyn ska implementera.
  • serviceUrl – En URL till den RMI-tjänst som efterfrågas. Denna URL baseras på de properties som är satta i den exporterade tjänsten enligt följande mönster: rmi://:/

Hessian

Hessianstödet i Spring fungerar enligt samma princip som för RMI. En service exporter används för att exportera tjänsten enligt nedan:

<bean name="hessianCalculatorService"
        class="org.springframework.remoting.caucho.HessianServiceExporter">
    <property name="service" ref="pojoCalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
</bean>

De properties som anges är:

  • service – En referens till den faktiska POJO-implementationen
  • serviceInterface – Det javagränssnitt som ska exporteras

Om man använder Java och Spring på klientsidan kan man använda följande proxy:

<bean id="calculatorService"
  class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
    <property name="serviceUrl"
        value="http://localhost:8080/exporters/hessianCalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
</bean>

Hessian är som tidigare nämnt ett HTTP-baserat protokoll vilket innebär att den distribuerade tjänsten måste köras i en webbcontainer exempelvis Tomcat eller Jetty. Konfiguationen i web.xml blir nåt i stil med nedanstående exempel där servletnamnet hessianCalculatorService mappas mot springbönan med samma namn:

<servlet>
    <servlet-name>hessianCalculatorService</servlet-name>
    <servlet-class>
        org.springframework.web.context.support.HttpRequestHandlerServlet
    </servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>hessianCalculatorService</servlet-name>
    <url-pattern>/hessianCalculatorService</url-pattern>
</servlet-mapping>

Burlap

Springstödet för Burlap är i stort sett identiskt med springstödet för Hessian, det enda som skiljer är att ordet Hessian bytts ut mot ordet Burlap. För att exportera en tjänst krävs följande konfiguration:

<bean name="burlapCalculatorService"
  class="org.springframework.remoting.caucho.BurlapServiceExporter">
    <property name="service" ref="pojoCalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
</bean>

Klientproxyn ser ut enligt följande:

<bean id="calculatorService"
  class="org.springframework.remoting.caucho.BurlapProxyFactoryBean">
    <property name="serviceUrl"
        value="http://localhost:8080/exporters/burlapCalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
</bean>

Burlap är ju liksom Hessian också baserat på HTTP vilket innebär att web.xml kan se ut enligt följande:

<servlet>
    <servlet-name>burlapCalculatorService</servlet-name>
    <servlet-class>
        org.springframework.web.context.support.HttpRequestHandlerServlet
    </servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>burlapCalculatorService</servlet-name>
    <url-pattern>/burlapCalculatorService</url-pattern>
</servlet-mapping>

Springs HTTP Invoker

Springs HTTP Invoker är också baserat på HTTP vilket såklart kräver att tjänsten exporteras i en webbcontainer liksom Hessian och Burlap. I övrigt är konfigurationen väldigt lik Hessian och Burlap. När man använder Springs HTTP Invoker krävs det dock att man kör Java och Spring på både klient- och serversidan.

Export av tjänst:

<bean name="httpCalculatorService"
  class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
    <property name="service" ref="pojoCalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
</bean>

Klientproxy:

<bean id="calculatorService"
  class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
    <property name="serviceUrl"
        value="http://localhost:8080/exporters/httpCalculatorService"/>
    <property name="serviceInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
</bean>

Konfiguration i web.xml:

<servlet>
    <servlet-name>httpCalculatorService</servlet-name>
    <servlet-class>
        org.springframework.web.context.support.HttpRequestHandlerServlet
    </servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>httpCalculatorService</servlet-name>
    <url-pattern>/httpCalculatorService</url-pattern>
</servlet-mapping>

JMX

MBeans exporteras i en MBeanServer som exekveras i en viss JVM. För att komma åt en distribuerad MBean kan olika transporter användas (RMI, HTTP etc). I exemplet nedan används RMI som transport. Man kan exportera en MBean på olika sätt, exempelvis kan man deklarativt i en context-fil specifiera den information som behövs för att MBeanServern ska kunna hantera MBean-instansten. Man kan även använda Java 5 annoteringar för att deklarera vilka metoder som ska exporteras enligt exemplet nedan:

@ManagedResource(
    objectName="bean:name=jmxCalculatorService",
    description="JMX exported calculator service")
public class PojoCalculatorService implements CalculatorService {

    @ManagedOperation(description="Add two numbers")
        @ManagedOperationParameters({
            @ManagedOperationParameter(
                name = "first", description = "The first number"),
            @ManagedOperationParameter(
                name = "second", description = "The second number")})
    public int add(int first, int second) {
        return first + second;
    }

    ...

Dessa annoteringar kan skrivas in direkt i POJO-implementationen av klassen om man vill. @ManagedResource deklareras på klassnivå och där beskrivs MBean-instanstens namn det vill säga bean:name=jmxCalculatorService. Detta namn kommer sedan att användas av klienten för att nå MBean-instansen. Varje operation som ska exporteras annoteras med @ManagedOperation och eventuella parametrar annoteras med @ManagedOperationParameter som i exemplet ovan.

Utöver de annoteringar som visas i exemplet krävs även viss infrastrukturell XML-konfiguration enligt nedan:

<bean id="registry"
        class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
    <property name="port" value="1199"/>
</bean>

<bean id="serverConnector"
  class="org.springframework.jmx.support.ConnectorServerFactoryBean">
    <property name="objectName" value="connector:name=rmi"/>
    <property name="serviceUrl"
        value="service:jmx:rmi://localhost/jndi/rmi://localhost:1199/jmxconnector"/>
</bean>

<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
    <property name="assembler" ref="assembler"/>
    <property name="namingStrategy" ref="namingStrategy"/>
    <property name="autodetect" value="true"/>
</bean>

<bean id="jmxAttributeSource"
  class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>

<bean id="assembler"
  class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
    <property name="attributeSource" ref="jmxAttributeSource"/>
</bean>

<bean id="namingStrategy"
  class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
    <property name="attributeSource" ref="jmxAttributeSource"/>
</bean>

Bönorna registry och serverConnector exporterar MBeanServern i ett RMI-registry så att den kan anropas från en annan JVM. Här kan man alltså välja att använda en annan transport exempelvis HTTP. Klassen MBeanExporter används för att exportera ut MBeans till MBeanServern. Genom att koppla ihop denna med bönorna jmxAttributeSource, assembler och namingStrategy kommer alla bönor som har MBean-annotationer automatiskt att bli exporterade i MBeanServern. Eftersom RMI används som transport kan ovanstående exempel köras utan webbcontainer.

På klientsidan skapas en sedvanlig proxy. Denna proxy måste dock kopplas ihop med en ”server connection” enligt exemplet nedan:

<bean id="serverConnection"
  class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
    <property name="serviceUrl"
        value="service:jmx:rmi://localhost/jndi/rmi://localhost:1199/jmxconnector"/>
</bean>

<bean id="calculatorService"
        class="org.springframework.jmx.access.MBeanProxyFactoryBean">
    <property name="objectName"
        value="bean:name=jmxCalculatorService"/>
    <property name="proxyInterface"
        value="se.cygni.demo.exporters.CalculatorService"/>
    <property name="server" ref="serverConnection"/>
</bean>

Bönan serverConnection innehåller en URL-referens som måste matcha den exporterade MBeanServern i den tidigare figuren (bönan serverConnector). Proxy-bönan det vill säga calculatorService innehåller en referens till serverConnection och som vanligt en specifikation av vilket interface som proxyn ska implementera. Dessutom specificeras namnet på den exporterade MBean-instansen via propertyn objectName, detta namn måste matcha attributet objectName i annoteringen @ManagedBean.

Summering

Spring erbjuder stöd för att exponera distribuerade tjänster på flera sätt. Vilken teknik som bör användas är olika från fall till fall. Man måste ta hänsyn till om ett slimmat binärt protokoll behövs, om det är Java-till-Java kommunikation och så vidare. Generellt sätt kan dock sägas att WebServices inte bör användas om det är Java-till-Java kommunikation, då rekommenderas istället exempelvis Hessian eller Burlap.

För mer information om distribution av komponenter via Spring rekommenderas läsning på Springs egen hemsida, speciellt kapitlen gällande Remoting och JMX.