Wednesday, May 26, 2010

EJBs in Scala schreiben

Was spricht eigentlich dagegen, eine EJB in Scala zu implementieren? Um diese Frage zu beantworten, habe ich ein Demo-Projekt aufgesetzt, in dem ich zwei EJBs in Scala implementiere. Die Eckdaten des Projekts sind wie folgt:

  • EJB 3.0
  • Scala 2.8.0 RC2 (auch mit 2.7.7 probiert)
  • Build-Prozess mit Maven
  • Getestet mit OpenEJB 3.1.2 im Unit-Test und als deploytes EAR
  • Getestet als deploytes EAR in JBoss 5.1.0.GA
  • Der Code ist hier veröffentlicht

Show me the Code!

import javax.ejb._
import javax.annotation.PostConstruct

@Remote
trait CalcBean {
  def add(x: Int, y: Int): Int
}

@Local
trait CalcBeanLocal {
  def add(x: Int, y: Int): Int
}

@Stateless
class CalcBeanImpl extends CalcBean with CalcBeanLocal {

  @PostConstruct
  private def init() {
    println("Post construct called for " + this)
  }
 
  override def add(x: Int, y: Int) = x + y
}

CalcBeanImpl ist die konkrete EJB, welche das Remote-Interface CalcBean und das Local-Interface CalcBeanLocal implementiert. Was in Java ein Interface ist, ist in Scala ein trait ohne konkrete Methoden, wie hier eben CalcBean und CalcBeanLocal.

Bei dem Versuch, Local- und Remote-Interface in einem einzigen Interface zu implementieren hat mir JBoss auf die Finger geklopft: Das ist gegen die EJB-Spezifikation.

Statt das Local-Interface mit @Local zu annotieren, ist es ja auch möglich die Bean-Klasse mit @Local zu annotieren und dieser ein Array der Local-Interface-Klassen als Parameter zu übergeben. In Scala würde das so aussehen:

@Local(Array(classOf[CalcBeanLocal]))
@Stateless
class CalcBeanImpl extends CalcBean with CalcBeanLocal {
...

[update] Seit Scala 2.8.0 RC3 funktioniert der o.g. Code auch, wer also diese Version oder eine die mit 2.7 beginnt hat, kann den folgenden Absatz überspringen.

In Scala 2.7.7 funktioniert das auch so, in Scala 2.8.0 RC2 leider nicht, sondern man erhält beim Kompilieren folgende Fehlermeldung:

C:\workspace\scala-ejb\scala-ejb-ejb\src\main\scala\net\thunderklaus\scala_ejb\CalcBean.scala:19: error: type mismatch;
  found   : java.lang.Class[net.thunderklaus.scala_ejb.CalcBeanLocal](classOf[net.thunderklaus.scala_ejb.CalcBeanLocal])
  required: java.lang.Class
@Local(Array(classOf[CalcBeanLocal]))

Dieses Problem ist im Scala-Trunk bereits behoben, sollte also im nächsten Release gelöst sein (siehe auch das zugehörige Scala-Ticket).

Und jetzt mit Dependency Injection

@Remote
trait CalcBean2 {
  def add2(x: Int): Int
}

@Stateless
class CalcBean2Impl extends CalcBean2 {
 
  @EJB
  private val calcBean: CalcBeanLocal = null
 
  override def add2(x: Int) = calcBean.add(x, 2) 
}

Dependency Injection mit der Annotation @EJB funktioniert mit Scala 2.8.0 RC2 wie man es erwartet. Die Field-Injection funktioniert auch bei val-Feldern (diese sind final in Java), welche man auch initialisieren muss. Der Initialwert ist nicht weiter von Bedeutung, da der EJB-Container den Wert dieses Feldes bei der Injection neu setzen wird, es bietet sich also null an.

Mit Scala 2.7.7 hat die Field-Injection noch Probleme bereitet, was folgenden Hintergrund hat: Scala generiert für alle Felder automatisch Getter-Methoden. Bei Scala 2.7.7 ist es so, dass eine Annotation an einem Feld auch an die zugehörige Getter-Methode gehängt wird. Das bereitet im Fall von @EJB Probleme, da der Container nun probiert an dieser Methode eine Method-Injection durchzuführen. JBoss geht scheinbar davon aus, dass eine Methode die mit @EJB annotiert ist einen Parameter hat, über den die zu injizierende Bean übergeben wird. Man erhält folgenden Fehler:

2010-05-25 22:45:37,416 ERROR [org.jboss.kernel.plugins.dependency.AbstractKernelController] (HDScanner) Error installing to PostClassLoader: name=vfszip:/C:/lib/jboss-5.1.0.GA/server/default/deploy/scala-ejb-ear-0.1-SNAPSHOT.ear/ state=ClassLoader mode=Manual requiredState=PostClassLoader
org.jboss.deployers.spi.DeploymentException: Cannot process metadata
  at org.jboss.deployers.spi.DeploymentException.rethrowAsDeploymentException(DeploymentException.java:49)
  at org.jboss.deployment.AnnotationMetaDataDeployer.deploy(AnnotationMetaDataDeployer.java:181)
  at org.jboss.deployment.AnnotationMetaDataDeployer.deploy(AnnotationMetaDataDeployer.java:93)
...
Caused by: java.lang.ArrayIndexOutOfBoundsException: 0
  at org.jboss.metadata.annotation.creator.EJBMethodProcessor.getType(EJBMethodProcessor.java:67)
  at org.jboss.metadata.annotation.creator.EJBMethodProcessor.getType(EJBMethodProcessor.java:36)
  at org.jboss.metadata.annotation.creator.AbstractEJBProcessor.createEJB(AbstractEJBProcessor.java:91)
...

Seit Scala 2.8 haben Annotations mehr Funktionalität: Man kann nun mittels Annotations angeben, ob eine Annotation auf die erzeugten Getter- und Setter-Methoden übernommen werden soll (die steuernden Annotations liegen in dem package scala.annotation.target und heissen @field, @getter, @setter, @beanGetter, @beanSetter und @param). Ohne spezielle Angabe gilt eine Annotation ausschliesslich für das Feld und nicht für die erzeugten Methoden, was für unseren Anwendungsfall genau passend ist (für Details siehe auch dieses Paper).

Paketierung

Um die Scala Standardbibliothek (scala-library-2.8.0.RC2.jar) zur Laufzeit bereitzustellen gibt es im Wesentlichen zwei Möglichkeiten: Entweder man legt die immerhin knapp über 5MB große Datei ins Server-Bibliotheken-Verzeichnis oder man packt sie in jedes EAR.

Entschliesst man sich für die EAR-Variante, so muss das in dem EAR enthaltene EJB-JAR noch über die Bibliothek Bescheid kriegen. Dies erreicht man durch den Eintrag "Class-Path: scala-library-2.8.0.RC2.jar" in dessen MANIFEST.MF. Maven kann man dazu bringen, diesen Eintrag zu erzeugen, wenn man das ejb-plugin entsprechend konfiguriert:

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-ejb-plugin</artifactId>
        <version>2.2.1</version>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      ...
    </plugins>
    ...
  </build>
  ...
</project>

Ärger mit der ejb-jar.xml

Ganz unabhängig von Problemen mit der Integration von Scala habe ich mich über folgenden Fehler ärgern müssen. In diesem Projekt habe ich alle Metadaten über Annotationen angegeben, sodass keine weiteren Angaben mehr in der ejb-jar.xml gemacht werden müssen. Existieren muss die Datei laut Spezifikation auf jeden Fall, also habe ich ihren Inhalt wie folgt definiert:

<ejb-jar />

Das mag auf den ersten Blick gut aussehen, JBoss möchte ein so beschriebenes jar aber nicht deployen und meldet folgenden Fehler:

2010-05-25 20:17:18,842 ERROR [org.jboss.kernel.plugins.dependency.AbstractKernelController] (HDScanner) Error installing to Start: name=jboss.j2ee:ear=scala-ejb-ear-0.1-SNAPSHOT.ear,jar=scala-ejb-ejb-0.1-SNAPSHOT.jar,name=CalcBean2Impl,service=EJB3 state=Create
java.lang.NullPointerException
  at org.jboss.ejb3.proxy.factory.ProxyFactoryHelper.getRemoteAndBusinessRemoteInterfaces(ProxyFactoryHelper.java:613)
  at org.jboss.ejb3.proxy.factory.ProxyFactoryHelper.getJndiName(ProxyFactoryHelper.java:419)
  at org.jboss.ejb3.Ejb3Deployment.getEjbJndiName(Ejb3Deployment.java:403)
  at org.jboss.ejb3.EJBContainer.getEjbJndiName(EJBContainer.java:1521)
  at org.jboss.injection.EjbEncInjector.inject(EjbEncInjector.java:80)
...

Die Fehlerursache liegt darin, dass man die XML-Namespaces definieren muss (Auf das gleiche Problem ist zum Beispiel auch der Autor dieses Posts gestoßen). Die funktionierende ejb-jar.xml sieht nun so aus:

<ejb-jar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://java.sun.com/xml/ns/javaee" xmlns:ejb="http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee  http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
  version="3.0">
</ejb-jar>

Fazit

Bei der Umsetzung dieses Projekts bin ich auf Probleme gestoßen, welche ich entweder direkt lösen konnte oder die spätestens mit der finalen Version von Scala 2.8.0 gelöst sein werden. Klar ist, dass dieses Projekt nur die ersten Schritte für einen möglichen Einsatz getestet hat. Aber diese ersten Schritte sehen schon recht stabil aus und ermutigen zu weiteren Gehversuchen.

1 comment: