Stacktrace

Denna artikel behandlar hur man kan kombinera traditionell J2EE-utveckling (applikationsserver med Stateless Session Beans och liknande) med Spring Framework och samtidigt erhålla en kort Code-Build-Test-cykel, hur man får det bästa av två världar.

Traditionellt sett…

Varför väljer man att bygga traditionella J2EE-applikationer med Stateless Session Beans, Message Driven Beans och kanske rent av Enterprise Entity Beans? Svaret på den frågan har traditionellt sätt varit att en applikationsserver erbjuder det stöd som behövs för:

  • Transaktionshantering
  • Konfiguration av lokala tjänster och fjärrtjänster (ejb-jar.xml, web.xml, application.xml)
  • Enkel uppslagning av resurser (JNDI etc.)

Det är säkert redan bekant att det finns andra sätt att lösa ovanstående problem varav ett sätt är att använda sig av Spring Framework som är en ”lättviktscontainer” med inbyggt stöd för bland annat transaktionshantering, konfiguration och resursuppslagning. Spring Framework kan på många sätt ersätta användande av J2EE applikationsservrar (såsom JBoss AS, WebSphere AS, WebLogic AS etc) och erbjuder större flexibilitet när det gäller att knyta ihop komponenter via så kallad Dependency Injection (DI).

Det finns dock andra skäl till att använda en applikationsserver, exempelvis:

  • Hantering för failover, high availability och klustring
  • Övervakningsverktyg för drift
  • Stöd för transaktioner över flera datakällor (XA)
  • Infrastrukturen finns redan på plats hos den aktuella kunden
  • J2EE/EJB är ett standardiserat gränssnitt

Traditionellt sett har utveckling i applikationsservermiljö varit tungt och långsamt eftersom en J2EE-applikation måste deployas i en applikationsserver vilket förmodligen innebär antingen en hot-deploy (som ofta brukar fungera lite skakigt) eller att ett Enterprise/Web-arkiv byggs, assembleras och deployas i applikationsservern tillsammans med en omstart av applikationsservern (vilket brukar vara relativt tidskrävande).

EJB 3.0 förenklar utveckling av traditionella J2EE applikationer på många sätt, exempelvis genom användning av annotations vilket kan eliminera byggsteg såsom XDoclet vilket avsevärt minskar Code-Build-Test-cykeln. Dependency Injection har till viss del lagts till i och med EJB 3.0 men det går endast att injicera resurser, inte rena POJO:s vilket är en kraftig begränsning. EJB 3.0 saknar helt enkelt den flexibilitet och konfigurerbarhet som finns i Spring Framework.

Det bästa av två världar

Så hur kan man kombinera traditionell J2EE-utveckling med traditionella Stateless Session Beans osv. med Spring men samtidigt ha en kort Code-Build-Test-cykel? Hur får man det bästa av två världar? Svaret är att använda sig av en kombination av de Spring add-ons och funktioner som finns tillgängliga. Denna artikel kommer att beskriva några av dessa add-ons och hur de kan användas tillsammans för att kunna exekvera en J2EE applikation med Stateless Session Beans i Spring Containern (alltså helt utan applikationsserver).

  • Pitchfork är en add-on till Spring Framework som stödjer EJB 3.0 annotations (JSR 220) och Common Annotations for the Java Platform (JSR 250). Detta innebär att Pitchfork bland annat kan användas för att utföra den Dependency Injection som krävs för en EJB 3.0 applikation (@EJB, @Resource etc.) trots att applikationen körs i en Spring Container. Ett annat användningsområde för Pitchfork är att faktiskt hantera den Dependency Injection som krävs i en riktig applikationsserver, Pitchfork kommer att användas som DI-processor i kommande versioner av BEA WebLogic AS.

    Nedanstående exempel visar hur @Resource annotationen fungerar (exemplet är hämtat från Pitchfork-dokumentationen):

    public class SomeBean {
        private DataSource myDB;
    
        @Resource(name="jdbc/myCustomDB")
        public void setMyDB(DataSource myDB) {
            this.myDB = myDB;
        }
        ...
    }
    

    Detta motsvaras av följande Spring Bean definition:

            <beans>
            ...
            <bean id="myBean" class="...SomeBean">
                <property name="myOtherDB">
                    <jee:jndi-lookup jndi-name="jdbc/myCustomDB"/>
                </property>
            </bean>
            ...
        </beans>
    

    Observera att ingen XML-konfiguration av EJBs stöds av Pitchfork, endast EJB 3.0 annotations kan alltså hanteras av Spring Containern.

  • Spring Transactions är ett transaktionsramverk som enkelt hanterar de flesta typer av transaktioner som krävs vid normal applikationsutveckling. Spring Transactions hanterar inte transaktioner över multipla resurser, för att få detta stöd måste en JTA från en applikationsserver användas men denna JTA kan enkelt integreras i din Spring-applikation.

    De transaktionsattribut som finns specificerade för EJB 3.0 kan enkelt mappas om till användande av Spring Transactions eftersom EJB 3.0 transaktioner är ett sub-set av Spring Transactions.

  • RMI Export
    Det finns många sätt att exportera tjänster så att de blir tillgängliga som fjärrtjänster. När en applikationsserver används och en tjänst är en så kallad Stateless Session Bean blir tjänsten tillgänglig via JNDI. När endast Spring Containern används finns det andra sätt att exportera tjänster varav ett enkelt sätt är att använda RMI export. Klassen org.springframework.remoting.rmi.RmiServiceExporter kan användas för att exportera tjänster.

Exemplet nedan visar hur en enkel fjärrtjänst kan implementeras som kan exekveras både i en Spring Container och en applikationsserver.

Nedanstående enkla tjänstegränssnitt kommer att vara grunden för exemplet:

package com.acme.server;

/**
 * Test service to illustrate the simplicity of a service interface.
 *
 * @author Tommy Wassgren, Cygni AB
 */
public interface TestService {
    /**
     * Updates something.
     * @param theUpdate The update string.
     */
    void updateSomething(String theUpdate);

    /**
     * Gets something.
     * @return Something.
     */
    String getSomething();
}

Nedanstående figur visar J2EE-implementationen (dvs en Stateless Session Bean) som delegerar vidare alla anrop till en enkel POJO-implementation. Detta är ett mönster som är rekommenderat av Spring för integration mellan EJB och Spring (se även http://www.springframework.org/docs/reference/ejb.html).

package com.acme.server;

import javax.ejb.CreateException;
import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

import org.springframework.transaction.annotation.Transactional;

/**
 * Stateless Session Bean implementation of the <code>TestService</code>. This
 * bean simply delegates all calls to the POJO implementation that is configured
 * in the Spring Container.
 *
 * @author Tommy Wassgren, Cygni AB
 */
@Stateless
@Remote(TestService.class)
@Local(TestService.class)
public class TestServiceBean extends AbstractSessionBean implements TestService {

    /** The delegate service i.e. the actual concrete POJO implementation. */
    private TestService delegate;

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    @Transactional(readOnly = true)
    public String getSomething() {
        return delegate.getSomething();
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    @Transactional(readOnly = false)
    public void updateSomething(String theUpdate) {
        delegate.updateSomething(theUpdate);
    }

    @Override
    protected void onEjbCreate() throws CreateException {
        super.onEjbCreate();

        // Lookup the actual service implementation from the Spring Container
        delegate = (TestService) getBeanFactory().getBean("TestServiceImplementation");
    }
}

Notera att TestServiceBean ärver från klassen AbstractSessonBean. Denna klass ger tillgång till det ApplicationContext som används. När denna klass startas i Spring Containern injiceras ett ApplicationContext via interfacet ApplicationContextAware (detta är en inbyggd mekanism i Spring, för mer info klicka här), när denna bean startas i en applikationsserver hämtas ApplicationContext från JNDI-trädet enligt gängse Spring/EJB integrationsmönster. Se nedan för implementation av den abstrakta basklassen:

package com.acme.server;

import javax.annotation.PostConstruct;
import javax.ejb.CreateException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationContextException;

/**
 * Base class for service wrappers that are Stateless Session Beans. This base
 * class is <code>ApplicationContextAware</code> and if it is executed within
 * a spring container it will use the Spring <code>ApplicationContext</code>
 * instead of the JNDI bound <code>BeanFactory</code>.
 *
 * @author Tommy Wassgren, Cygni AB
 */
public abstract class AbstractSessionBean implements ApplicationContextAware {

    /** The application context. */
    private ApplicationContext applicationContext;

    /**
     * Gets the bean factory (from the JNDI or from the spring server).
     * @return The BeanFactory.
     */
    protected BeanFactory getBeanFactory() {
        if (applicationContext == null) {
            try {
                Context context = new InitialContext();
                applicationContext =
                    (ApplicationContext) context.lookup("java:/ApplicationContext");
            } catch (NamingException e) {
                throw new ApplicationContextException(
                        "Unable to retrieve application context from JNDI "
                        + "tree [reason=" + e.getMessage() + "]",
                        e);
            }
        }
        return applicationContext;
    }

    @PostConstruct
    public void ejbCreate() throws CreateException {
        // This method is invoked after the bean has been created (both when
        // executed in the Spring Container and in the Application Server.
        onEjbCreate();
    }

    /**
     * Post construct method called to the subclasses.
     * @throws CreateException If anything fails.
     */
    protected void onEjbCreate() throws CreateException {
        // No superclass implementation needed.
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        // Only invoked if this bean is created in the Spring Container
        // (otherwise retrieved from the JNDI).
        this.applicationContext = applicationContext;
    }
}

Den faktiska implementationen av tjänsten är en enkel POJO enligt nedan:

package com.acme.server;

/**
 * The concrete implementation of the <code>TestService</code>.
 *
 * @author Tommy Wassgren, Cygni AB
 */
public class ConcreteTestService implements TestService {
    private String something;

    public String getSomething() {
        return something;
    }

    public void updateSomething(String theUpdate) {
        something = theUpdate;
    }
}

För att konfigurera upp ovanstående beans i en Spring Container kan man dela upp konfigurationen i två delar.

  • Allmän konfiguration som krävs för att starta tjänsten i och utanför en applikationsserver
  • Extra ic-konfiguration som krävs för att starta J2EE simulering i Spring Containern (ic = in container d.v.s. en applikation som körs i Spring Containern)

Nedan visas ett exempel på det förstnämnda:

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

    <!--
        This file should always be included and contains the concrete service
        implementation. If the server is started within the Spring Container
        this bean will simply be available through the ApplicationContext. If
        the server is started within the Application Server this bean will be
        available through the ApplicationContext that is bound in the JNDI tree.
    -->
    <bean
        id="TestServiceImplementation"
        class="com.acme.server.ConcreteTestService"
        lazy-init="true"/>
</beans>

Nedanstående visar den extra konfiguration som krävs för att starta Pitchfork, simulera EJB:s och exportera tjänster i Spring Containern.

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

    <!--
        This file should only be included when the server is started within the
        Spring Container (ic = in container).
    -->

    <!-- Enable Pitchfork -->
    <bean class="org.springframework.jee.ejb.config.JeeEjbBeanFactoryPostProcessor"/>

    <!-- Enable the configuration of transactional behavior based on annotations -->
    <tx:annotation-driven transaction-manager="JpaTransactionManager" />

    <bean name="JpaTransactionManager" class="x.y.z.JPA">
        <!--
            ...
            JPA is not included in this sample
        -->
    </bean>

    <!--
        The Stateless Session Bean as a simple Spring Bean, will be processed by
        the Pitchfork add-on
    -->
    <bean
        id="TestServiceBean"
        class="com.acme.server.TestServiceBean"
        lazy-init="false"
        scope="singleton"/>

    <!-- The RMI exporter -->
    <bean class="org.springframework.remoting.rmi.RmiServiceExporter">
        <property name="serviceName" value="TestService/remote"/>
        <property name="service" ref="TestServiceBean"/>
        <property name="serviceInterface" value="com.acme.server.TestService"/>
        <property name="registryPort" value="1099"/>
    </bean>
</beans>

Detta innebär att när tjänsterna ska startas i Spring Containern måste fler filer inkluderas än när servern startas i applikationsservern. Detta är dock inte ett problem eftersom namnkonventioner kan användas för att lösa detta, exempelvis kan alla ic-filer namnges xxx-ic.xml och wildcards kan användas för att inkludera dessa filer.

För att komma åt de tjänster som exporterats bör ett Dependency Injection mönster användas på klienten.

package com.acme.client;

import org.springframework.beans.factory.InitializingBean;

import com.acme.server.TestService;

/**
 * A test client used for invoking a server side test service.
 *
 * @author Tommy Wassgren, Cygni AB
 */
public class TestClient implements InitializingBean {
    private TestService testService;

    public void setTestService(TestService testService) {
        this.testService = testService;
    }

    public void afterPropertiesSet() throws Exception {
        testService.updateSomething("Something to update");
        String something = testService.getSomething();
        System.out.println("Something: " + something);
    }
}

Konfiguration av testklienten:

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

    <!--
        This file contains the test client. The client is injected with a
        RMI proxy to the TestService.
    -->
    <bean id="TestClient" class="com.acme.client.TestClient" lazy-init="false">
        <property name="testService" ref="RmiTestService"/>
    </bean>

    <bean
            id="RmiTestService"
            class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
        <property name="serviceUrl" value="rmi://localhost:1099/TestService/remote"/>
        <property name="serviceInterface" value="com.acme.server.TestService"/>
    </bean>
<beans>

Även för klienten går det att dela upp filerna i container-specifika konfigurationsfiler och allmänna konfigurationsfiler. Exempelvis kan tjänsteproxys mot tjänsterna som ska användas när servern körs i en applikationsserver deklareras i filer med prefixet xxx-oc.xml (oc = out of container) och tjänsteproxys för tjänster som ska användas när de körs i Spring Containern deklareras i filer med prefixet xxx-ic.xml. Klasserna org.springframework.jndi.JndiObjectFactoryBean och org.springframework.remoting.rmi.RmiProxyFactoryBean är användbara för att skapa proxyobjekt via JNDI respektive RMI.

Summering

Genom att använda sig av tekniker såsom Pitchfork, Spring Transactions och RMI Export går det att på ett enkelt sätt simulera en J2EE servermiljö under utveckling, det går faktiskt att helt utesluta applikationsservern. Detta medför en drastiskt förkortad Code-Build-Test-cykel vilket så klart avsevärt förkortar utvecklingstiden. Fördelen med detta angreppssätt är att all kod faktiskt exekveras både i utvecklingsläge och driftsläge (EJB:s och dylikt exekveras även under utveckling) men med betydligt kortare turnaround-tid. I de fall där Message Driven Beans och JMS implementationer krävs rekommenderas istället användning av Message Driven POJOs och användning av en enklare JMS-implementation såsom ActiveMQ (ActiveMQ startas på någon sekund jämfört med flera minuter för de större applikationsservrarna).

Referenser

Kommentarer

Skriv kommentar