Intro
Types of beans:
- Session
- Entity
- Message-driven
Basic call: client -> stub -> skeleton -> server
EJB classes
Classes (and other files) required to implement an EJB:
- bean class
- remote interface
- local interface
- home interface
- local home interface
- primary key class
- deployment descriptor
- vendor-specify descriptors
The bean class:
- entity beans implement javax.ejb.EntityBean
- session beans implement javax.ejb.SessionBean
- message-driven beans implement javax.ejb.MessageDrivenBean
- each of these extends javax.ejb.EnterpriseBean
- javax.ejb.EnterpriseBean extends java.io.Serializable
A request is delegated to the bean object. Before delegation,
the middleware handles:
- transactions
- security
- resource management (threads, db connections)
- persistence
- remote connection
- bean instance management
- threading
- serialization of requests to a particular bean
- location transparency
- monitoring
The request is handled by an 'EJB Object', which delegates to the bean.
The remote interface implements javax.ejb.EJBObject
import javax.ejb.*;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface EJBObject extends Remote {
EJBHome getEJBHome() throws RemoteException;
Object getPrimaryKey() throws RemoteException;
void remove() throws RemoteException, RemoveException;
Handle getHandle() throws RemoteException;
boolean isIdentical(EJBObject) throws RemoteException;
}
The remote interface implements these, plus business methods.
The home class must implement the EJBHome interface.
import javax.ejb.*;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface EJBHome extends Remote {
EJBMetaData getEJBMetaData() throws RemoteException;
HomeHandle getHomeHandle() throws RemoteException;
void remove(Handle handle) throws RemoteException, RemoveException;
void remove(Object primaryKey) throws RemoteException, RemoveException;
}
The local interface implements javax.ejb.EJBLocalObject
import javax.ejb.*;
public interface EJBLocalObject {
EJBLocalHome getEJBLocalHome() throws EJBException;
Object getPrimaryKey() throws EJBException;
void remove() throws EJBException, RemoveException;
boolean isIdentical(EJBLocalObject) throws EJBException;
}
The local home class must implement the EJBLocalHome interface.
import javax.ejb.*;
public interface EJBLocalHome extends Remote {
void remove(Object object) throws EJBException, RemoveException;
}
Outline for a session bean:
import javax.ejb.*;
public class XxxxBean implements SessionBean {
private SessionContext context;
// ejb methods
public void ejbCreate() {
...
}
public void ejbActivate() {
...
}
public void ejbPassivate() {
...
}
public void ejbRemove() {
...
}
public void setSessionContext(SessionContext ctx) {
context = ctx;
}
// business methods
public ... businessMethod1(...) throws ... {
...
}
public ... businessMethod2(...) throws ... {
...
}
}
The context object provides access to container services:
public interface EJBContext {
EJBHome getEJBHome();
EJBLocalHome getEJBLocalHome();
boolean getRollbackOnly();
void setRollbackOnly();
javax.transaction.UserTransaction getUserTransaction();
boolean isCallerInRole(String role);
java.security.Principal getCallerPrincipal();
}
public interface SessionContext extends EJBContext {
EJBObject getEJBObject() throws IllegalStateException;
EJBLocalObject getEJBLocalObject() throws IllegalStateException;
MessageContext getMessageContext() throws IllegalStateException;
}
public interface EntityContext extends EJBContext {
Object getPrimaryKey() throws IllegalStateException;
EJBObject getEJBObject() throws IllegalStateException;
EJBLocalObject getEJBLocalObject() throws IllegalStateException;
}
public interface MessageDrivenContext extends EJBContext {
}
A complete session example
This example uses JBoss.
Server side
The directory structure:
project
testejb
src
net
alethis
testejb
Calc.java
CalcBean.java
CalcHome.java
CalcLocal.java
CalcLocalHome.java
META-INF
ejb-jar.xml
bin // classes go here
dist // test.jar file goes here
The remote interface:
package net.alethis.testejb;
import java.rmi.RemoteException;
import javax.ejb.*;
public interface Calc extends EJBObject {
double averageWordLength(String input) throws RemoteException;
}
The local interface:
package net.alethis.testejb;
import javax.ejb.*;
public interface CalcLocal extends EJBLocalObject {
double averageWordLength(String input);
}
The home interface:
package net.alethis.testejb;
import java.rmi.RemoteException;
import javax.ejb.*;
public interface CalcHome extends EJBHome {
Calc create() throws RemoteException, CreateException;
}
The local home interface:
package net.alethis.testejb;
import javax.ejb.*;
public interface CalcLocalHome extends EJBLocalHome {
CalcLocal create() throws CreateException;
}
The bean class:
package net.alethis.testejb;
import javax.ejb.*;
import java.util.*;
public class CalcBean implements SessionBean {
static final long serialVersionUID = -3235974807011685965L;
private SessionContext context;
// ejb methods
public void ejbCreate() {
System.out.println("ejbCreate");
}
public void ejbActivate() {
System.out.println("ejbActivate");
}
public void ejbPassivate() {
System.out.println("ejbPassivate");
}
public void ejbRemove() {
System.out.println("ejbRemove");
}
public void setSessionContext(SessionContext sc) {
System.out.println("setSessionContext");
context = sc;
}
// business methods
public double averageWordLength(String in) {
double wordCount = 0;
double charCount = 0;
StringTokenizer st = new StringTokenizer(in, " \t\n\r");
while (st.hasMoreTokens()) {
String t = st.nextToken();
wordCount++;
charCount += t.length();
}
double ratio = (wordCount == 0) ? 0 : (charCount/wordCount);
return ratio;
}
}
The ejb-jar.xml file:
<?xml version='1.0'?>
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd">
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>Calc</ejb-name>
<home>net.alethis.testejb.CalcHome</home>
<remote>net.alethis.testejb.Calc</remote>
<local-home>net.alethis.testejb.CalcLocalHome</local-home>
<local>net.alethis.testejb.CalcLocal</local>
<ejb-class>net.alethis.testejb.CalcBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
</ejb-jar>
The ant build.xml file:
<?xml version='1.0'?>
<project name="testejb" default="deploy" basedir=".">
<property name="src.dir" value="src"/>
<property name="bin.dir" value="bin"/>
<property name="dist.dir" value="dist"/>
<property name="jboss.deploy.dir" value="/software/jboss-4.0.1sp1/server/default/deploy"/>
<path id="jars">
<pathelement location="/software/j2ee1.4/lib/j2ee.jar"/>
</path>
<target name="prepare">
<delete dir="${bin.dir}"/>
<delete dir="${build.dir}"/>
<mkdir dir="${bin.dir}"/>
<mkdir dir="${bin.dir}/META-INF"/>
<mkdir dir="${dist.dir}"/>
</target>
<target name="compile" depends="prepare">
<javac srcdir="${src.dir}" destdir="${bin.dir}">
<classpath refid="jars"/>
</javac>
</target>
<target name="build" depends="compile">
<copy file="${src.dir}/META-INF/ejb-jar.xml" todir="${bin.dir}/META-INF"/>
<jar basedir="${bin.dir}" destfile="${dist.dir}/test.jar"/>
</target>
<target name="deploy" depends="build">
<copy file="${dist.dir}/test.jar" todir="${jboss.deploy.dir}"/>
</target>
</project>
Client side
Directory structure:
project
testejbclient
src
net
alethis
testejbclient
CalcClient.java
bin
// class files go here
build.xml
The client class:
package net.alethis.testejbclient;
import javax.naming.*;
import java.util.*;
import net.alethis.testejb.*;
public class CalcClient {
public static void main(String[] args) {
try {
Hashtable env = new Hashtable();
env.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory");
env.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces");
env.put("java.naming.provider.url", "localhost");
Context c = new InitialContext(env);
Object o = c.lookup("Calc");
CalcHome home = (CalcHome) javax.rmi.PortableRemoteObject.narrow(o, CalcHome.class);
Calc calc = home.create();
double ratio = calc.averageWordLength("Them Penguins is done like dinner");
System.out.println(ratio);
calc.remove();
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
The ant build.xml file:
<?xml version='1.0'?>
<project name="testejb" default="run" basedir=".">
<property name="src.dir" value="src"/>
<property name="bin.dir" value="bin"/>
<path id="jars">
<pathelement path="${bin.dir}"/>
<pathelement location="/project/testejb/dist/test.jar"/>
<pathelement location="/software/j2ee1.4/lib/j2ee.jar"/>
<pathelement location="/software/jboss-4.0.1sp1/client/jbossall-client.jar"/>
</path>
<target name="compile">
<javac srcdir="${src.dir}" destdir="${bin.dir}">
<classpath refid="jars"/>
</javac>
</target>
<target name="run" depends="compile">
<java classname="net.alethis.testejbclient.CalcClient" fork="true">
<classpath refid="jars"/>
</java>
</target>
</project>
Session Beans
Two types:
- stateless
- stateful
Stateful session beans are subject to passivation. A bean involved
in a transaction cannot be passivated until the transaction ends. There's no point
passivating/activating a stateless session bean, as there's no state; the container
is better just to create or destroy instances.
Any member of a stateful session bean class is part of the state if
it's not transient. Any class having an instance in a stateful session bean state
must be serializable. The state can include container objects such as EJB object
references, EJB home references, EJB context references and JNDI naming contexts.
A stateful session bean may provided several ejbCreate(), with varying sets
of parameters; it must provide at least one such method. A stateless session bean must
provide a single, parameterless ejbCreate() method.
ejbPassivate() and ejbActivate() are useless for stateless session beans,
and would only be defined for stateful session beans in the case where the bean
holds persistent resources, such as sockets or database connections.
Entity Beans
For each create() method in the home interface, there's an ejbCreate method, with the
same parameters in the bean class. The bean class ejbCreate() returns a primary key,
while the remote method returns the EJB object.
Outline for a BMP entity bean:
import javax.ejb.*;
public class XxxxBean implements EntityBean {
private EntityContext context;
// ejb methods
public PrimaryKey ejbCreate(...) throws CreateException {
...
}
public void ejbPostCreate(...) {
...
}
public void ejbActivate() {
...
}
public void ejbPassivate() {
...
}
public void ejbRemove() {
...
}
public void ejbLoad() {
...
}
public void ejbStore() {
...
}
public void setEntityContext(EntityContext ctx) {
context = ctx;
}
public void unsetEntityContext() {
context = null;
}
// finders
public PrimaryKey findByPrimaryKey(PrimaryKey key) throws FinderException {
// required for BMP
// not needed for CMP
...
}
public Collection findBy...(...) throws FinderException {
// other finders are optional
// not needed for CMP
...
}
// home methods
public ??? ejbHome...(...) {
// correspond to methods on the home object
...
}
// business methods
public ... businessMethod1(...) throws ... {
...
}
public ... businessMethod2(...) throws ... {
...
}
}
A complete, entity example
This example uses JBoss.
Server side
The directory structure:
project
testejb
src
net
alethis
testejb
Galaxy.java
GalaxyBean.java
GalaxyHome.java
GalaxyLocal.java
GalaxyLocalHome.java
META-INF
ejb-jar.xml
bin // classes go here
dist // test.jar file goes here
The remote interface:
package net.alethis.testejb;
import java.rmi.RemoteException;
import javax.ejb.*;
public interface Calc extends EJBObject {
double averageWordLength(String input) throws RemoteException;
}
The local interface:
package net.alethis.testejb;
import javax.ejb.*;
public interface CalcLocal extends EJBLocalObject {
double averageWordLength(String input);
}
The home interface:
package net.alethis.testejb;
import java.rmi.RemoteException;
import javax.ejb.*;
public interface CalcHome extends EJBHome {
Calc create() throws RemoteException, CreateException;
}
The local home interface:
package net.alethis.testejb;
import javax.ejb.*;
public interface CalcLocalHome extends EJBLocalHome {
CalcLocal create() throws CreateException;
}
The bean class:
package net.alethis.testejb;
import javax.ejb.*;
import java.util.*;
public class CalcBean implements SessionBean {
static final long serialVersionUID = -3235974807011685965L;
private SessionContext context;
// ejb methods
public void ejbCreate() {
System.out.println("ejbCreate");
}
public void ejbActivate() {
System.out.println("ejbActivate");
}
public void ejbPassivate() {
System.out.println("ejbPassivate");
}
public void ejbRemove() {
System.out.println("ejbRemove");
}
public void setSessionContext(SessionContext sc) {
System.out.println("setSessionContext");
context = sc;
}
// business methods
public double averageWordLength(String in) {
double wordCount = 0;
double charCount = 0;
StringTokenizer st = new StringTokenizer(in, " \t\n\r");
while (st.hasMoreTokens()) {
String t = st.nextToken();
wordCount++;
charCount += t.length();
}
double ratio = (wordCount == 0) ? 0 : (charCount/wordCount);
return ratio;
}
}
The ejb-jar.xml file:
<?xml version='1.0'?>
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd">
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>Calc</ejb-name>
<home>net.alethis.testejb.CalcHome</home>
<remote>net.alethis.testejb.Calc</remote>
<local-home>net.alethis.testejb.CalcLocalHome</local-home>
<local>net.alethis.testejb.CalcLocal</local>
<ejb-class>net.alethis.testejb.CalcBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
</ejb-jar>
The ant build.xml file:
<?xml version='1.0'?>
<project name="testejb" default="deploy" basedir=".">
<property name="src.dir" value="src"/>
<property name="bin.dir" value="bin"/>
<property name="dist.dir" value="dist"/>
<property name="jboss.deploy.dir" value="/software/jboss-4.0.1sp1/server/default/deploy"/>
<path id="jars">
<pathelement location="/software/j2ee1.4/lib/j2ee.jar"/>
</path>
<target name="prepare">
<delete dir="${bin.dir}"/>
<delete dir="${build.dir}"/>
<mkdir dir="${bin.dir}"/>
<mkdir dir="${bin.dir}/META-INF"/>
<mkdir dir="${dist.dir}"/>
</target>
<target name="compile" depends="prepare">
<javac srcdir="${src.dir}" destdir="${bin.dir}">
<classpath refid="jars"/>
</javac>
</target>
<target name="build" depends="compile">
<copy file="${src.dir}/META-INF/ejb-jar.xml" todir="${bin.dir}/META-INF"/>
<jar basedir="${bin.dir}" destfile="${dist.dir}/test.jar"/>
</target>
<target name="deploy" depends="build">
<copy file="${dist.dir}/test.jar" todir="${jboss.deploy.dir}"/>
</target>
</project>
Client side
Directory structure:
project
testejbclient
src
net
alethis
testejbclient
CalcClient.java
bin
// class files go here
build.xml
The client class:
package net.alethis.testejbclient;
import javax.naming.*;
import java.util.*;
import net.alethis.testejb.*;
public class CalcClient {
public static void main(String[] args) {
try {
Hashtable env = new Hashtable();
env.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory");
env.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces");
env.put("java.naming.provider.url", "localhost");
Context c = new InitialContext(env);
Object o = c.lookup("Calc");
CalcHome home = (CalcHome) javax.rmi.PortableRemoteObject.narrow(o, CalcHome.class);
Calc calc = home.create();
double ratio = calc.averageWordLength("Them Penguins is done like dinner");
System.out.println(ratio);
calc.remove();
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
The ant build.xml file:
<?xml version='1.0'?>
<project name="testejb" default="run" basedir=".">
<property name="src.dir" value="src"/>
<property name="bin.dir" value="bin"/>
<path id="jars">
<pathelement path="${bin.dir}"/>
<pathelement location="/project/testejb/dist/test.jar"/>
<pathelement location="/software/j2ee1.4/lib/j2ee.jar"/>
<pathelement location="/software/jboss-4.0.1sp1/client/jbossall-client.jar"/>
</path>
<target name="compile">
<javac srcdir="${src.dir}" destdir="${bin.dir}">
<classpath refid="jars"/>
</javac>
</target>
<target name="run" depends="compile">
<java classname="net.alethis.testejbclient.CalcClient" fork="true">
<classpath refid="jars"/>
</java>
</target>
</project>
EJB-QL
Format in descriptor file:
<query>
<query-method>
<method-name>findAllTrains</method-name>
<method-params>
<method-param>java.lang.String</method-param>
</method-params>
</query-method>
<ejb-ql>SELECT OBJECT(t) FROM Train t WHERE t.serial = ?1</ejb-ql>
<result-mapping>Local</result-mapping>
</query>
The result-mapping is not needed for finders, as they are defined
in the local or remote interface, and the result will be set up accordingly.
For finders, the query must always produce data corresponding to
an EJBObject or EJBLocalObject.
For ejbSelect...(), the result is ambiguous, as the method is declared
in the bean implementation class.
In this case, if the result will be an EJBObject or EJBLocalObject,
the result-mapping must be supplied.
On the other hand, queries for ejbSelect() methods can return primitive values.
Example queries:
<!-- general select -->
SELECT OBJECT(t) FROM Train t
<!-- 'AS' is optional -->
SELECT OBJECT(t) FROM Train AS t
<!-- select with condition -->
SELECT OBJECT(t) FROM Train t WHERE t.builtYear < 1980
<!-- select with parameterized condition -->
SELECT OBJECT(t) FROM Train t WHERE t.builtYear < ?1
<!-- traversing the object graph -->
SELECT t.factory FROM Train t WHERE t.serial = ?1
<!-- collection variables -->
SELECT OBJECT(t) FROM Company c, IN(c.trains) t
<!-- collection variables -->
SELECT OBJECT(c) FROM Company c WHERE c.trains IS NOT EMPTY
<!-- collection variables -->
SELECT OBJECT(t) FROM Company c, IN(c.trains) t WHERE c.id = ?1 AND t.location = 'Chicago'
<!-- distinct -->
SELECT OBJECT(s) FROM Class c, IN(c.students) s
<!-- aggregation -->
SELECT MIN(t.builtYear) FROM Train t
<!-- order -->
SELECT OBJECT(t) FROM Train t ORDER BY t.buildYear DESC
<!-- select returning a related object -->
SELECT t.company FROM Train t WHERE t.builtYear < 1980
</query>
Built-in functions
- CONCAT(String, String) - string concatenation
- SUBSTRING(String, start, length) - substring extraction
- LOCATE(String, String [, start]) - substring searching
- LENGTH(String) - string length
- ABS(number) - absolute value
- SQRT(double) - square root
- MOD(int, int) - modulus
Operations
- math: +, -, *, /
- comparison: =, <, <=, >, >=, <>
- logical: NOT, AND, OR
- BETWEEN, NOT BETWEEN
- IN, NOT IN
- LIKE, NOT LIKE
- IS NULL, IS NOT NULL
- IS EMPTY, IS NOT EMPTY
- MEMBER OF, NOT MEMBER OF
Aggregation functions
- SUM
- MIN
- MAX
- COUNT
- AVG
String literals are in single quotes. Use doubled single quote to
encode a contained single quote. Boolean literals are TRUE and FALSE (not case-sensitive).