java:comp/env' names onto Hypersonic databases.
Ant to compile sources and assemble JAR files for jBoss
2. I receive a lot of e-mail about jBoss (hundreds of messages every week). I do my best to respond to them all, but I don't have the time to provide detailed guidance to people who can't get my examples to work, unless they have tried them on a system compatible with my own. In particular, there are minor version-to-version changes between releases of jBoss, and I don't even attempt to keep track of the implications these have on my tutorials. Sorry, but I have a day-job.
3. Readers who know me, or have looked at my Web site, may be aware that I teach EJB courses for Sun Microsystems. However, this article and others that I have written on EJBs and jBoss have nothing to do with my work for Sun, and should not be taken as an indication of Sun's position on anything. I am no more privy to Sun's long-term plans for EJBs or the J2EE architecture than anyone else (it's not my part of the Company), so please don't ask.
Ant build
script to automate most of the compile operations; if you don't want to use
Ant, you can inspect the script build.xml to get full
details of the command line operations. Ant is available
free-of-charge from the Jakarta Web site;
it is written entirely in Java so should work on most platforms.
[jboss_root]' to refer to the
root of the jBoss installation directory. Please replace this with the directory
in which jBoss is installed on your system.
[source_root]' to refer to the
root of the development source tree, or the place where you have unpacked
the source code distribution. Again, replace this with the working directory
you have chosen on your system.
this is \ a long lineshould be reassembled into one line:
this is a long lineMost Unix command-line shells allow the back-slash character to be entered directly on the command line to indicate that the entry continues onto the next line, but many Windows systems don't. In the latter case you should enter one single long line. In both cases, of course, you should not be entering these commands manually, but using a build file, script or batch file according to your preference.
id, name,
address, and email, of which id is the
primary key. For simplicity we will use the Java String class to
model the primary key, rather than creating a custom primary key class. The persistent
properties will be both readable and writable, and clients of the EJB will be
able to create and delete new customers. These requirements imply that we must implement
ejbStore, ejbCreate, and ejbRemove, as well as
ejbLoad and the finder methods.
getXXX and
setXXX methods by which clients can get and set the persistent
properties of the EJB.
findByPrimaryKey method on the
home interface, and typically a number of other `finder' methods. In this example
we will implement a findByNameContaining method that returns a
collection of references to EJBs whose `name' property contains the specified
string. In reality we would probably need other `finder' methods, but this one
will be adequate for demonstration purposes.
[source_root]
com
web_tomorrow
bmp_properly
ejb -- EJB source code
client -- test client source code
ejb_utils -- general utilities for EJBs
utils -- source code for the sample data loader (see below)
clienttarget -- directory into which client-side .class files will be generated
servertarget -- directory into which server-side .class files will be generated
build.xml -- Ant build script (see below)
init.sql -- SQL script to install the initial data
Ant, you will need to edit the build script
build.xml to indicate the location of jBoss. Change the line
<property name="jboss_home" value="../../lib/JBoss-2.2.2" />to whatever is appropriate on your system.
Ant build script sets the class search path appropriately
for the build process, using configuration parameters in the `init' section.
I strongly advise disabling any system-wide CLASSPATH settings before
using the build script. On Linux, execute the following at the
prompt before running ant
CLASSPATH=
The Windows equivalent is
set CLASSPATH=
In both cases, this change only affects the current terminal session; changes to
system configuration is necessary to make this permanent.
The reason for this is that many people have a number of different EJB products installed,
and they often have classes with the same name. For example, implementations of
|
init.sql in
the source distribution. This tutorial assumes the use of the Hypersonic database supplied
with jBoss. Because the jBoss distribution does not (as far as I know)
include a stand-alone SQL client, I have provided a simple data loader that reads
SQL from a file and executes it using JDBC calls. This should work with other databases
than Hypersonic, although most have their own SQL clients.
utils.LoadSQL. This source code is in
the file LoadSQL.java in directory [source_root]/utils.
It can be compiled at the command line by changing directory to [source_root]
and executing:
javac -d clienttarget utils/LoadSQL.java(if you are using Windows, remember to change directory separators from `/' to `\').
Ant, you should be able to compile the data loader
with the command
ant compileutils
java -classpath [...] [JDBC options] filename database_url user_id password
filename is the file containing the SQL to execute (init.sql
in this case). database_url is the JDBC URL for the database to be
modified. Hypersonic URLs all begin jdbc:HypersonicSQL. user_id
and password are user credentials that allow writes to the database.
Because some command lines have difficulty accepting an empty string as an
argument, and this is the standard way to indicate `no password' in JDBC, the
data loader interprets a password of `*' as `no password'. This is important, because
the user account we will be using with Hypersonic has no password. This account is
the built-in user `sa' that should have been created when jBoss was installed.
classpath option should be set to enable the JVM
to find the following classes and JARs:
hsql.jar
in the jBoss libraries directory
-Djdbc.drivers=org.hsql.jdbcDriverFor other databases you should be able to find the driver class from the vendor's product information.
jboss_root to the appropriate value for your system):
java -Djdbc.drivers=org.hsql.jdbcDriver \ -classpath clienttarget:\[jboss_root]/lib/ext/hsql.jar \ utils/LoadSQL init.sql jdbc:HypersonicSQL:hsql://localhost:1476 \ sa *(don't forget that the `\' at the end of the listing indicates that this is all one long line).
Ant, simply execute the command:
ant installdatawhich does exactly the same job.
findByPrimaryKey(), and an additional, multi-object finder
findByNameContaining(). The home interface therefore looks like this
(CustomerHome.java):
package com.web_tomorrow.bmp_properly.ejb;
import javax.ejb.*;
import java.rmi.*;
import java.util.*;
/**
Home interface for `Customer' EJB
(c)2001 Kevin Boone/Web-Tomorrow
*/
public interface CustomerHome extends EJBHome
{
public Customer findByPrimaryKey(final String id) throws FinderException, RemoteException;
public Customer create(final String id, final String name,
final String address, final String email)
throws CreateException, RemoteException;
public Collection findByNameContaining (final String search)
throws FinderException, RemoteException;
}
Note that findByPrimaryKey and create both return
the primary key type (String), while the multi-object finder
findByNameContaining() returns a Collection. As per the
specification, all methods are declared to throw RemoteException,
the finders throw FinderException, and create() throws
CreateException.
Customer.java)
exposes the business methods of the EJB which, in this
case, are all `getters' and `setters'.
package com.web_tomorrow.bmp_properly.ejb;
import javax.ejb.*;
import java.rmi.*;
/**
Remote interface for `Customer' EJB
(c)2001 Kevin Boone/Web-Tomorrow
*/
public interface Customer extends EJBObject
{
public String getName() throws RemoteException;
public void setName(final String name) throws RemoteException;
public String getEmail() throws RemoteException;
public void setEmail(final String email) throws RemoteException;
public String getAddress() throws RemoteException;
public void setAddress(final String address) throws RemoteException;
public String getId() throws RemoteException;
}
As always, these methods are declared to throw RemoteException.
CustomerBeanVersion1.java). Comments on the implementation are below the
listing.
package com.web_tomorrow.bmp_properly.ejb;
import javax.ejb.*;
import java.rmi.*;
import java.sql.*;
import java.util.*;
import javax.sql.*;
import javax.naming.*;
import com.web_tomorrow.ejb_utils.*;
public class CustomerBeanVersion1 implements EntityBean
{
// Non-persistent instance variables
protected EntityContext entityContext;
protected Logger logger;
protected DataSource datasource;
// Persistent instance variables
protected String id;
protected String name;
protected String address;
protected String email;
/**
Save the entity context for future use, and initialize the datasource so we can
use it to get connections in subsequent method calls
*/
public void setEntityContext (EntityContext entityContext)
{
logger = new Logger ("java:comp/env/debug");
logger.log (2, "Called setEntityContext");
this.entityContext = entityContext;
try
{
Context c = new InitialContext();
datasource = (DataSource)c.lookup("java:/DefaultDS");
}
catch (NamingException e)
{
logger.log (2, "Caught NamingException in setEntityContext: " + e);
throw new EJBException ("Datasource name lookup failed: " + e);
}
}
/**
Does nothing in this example
*/
public void unsetEntityContext ()
{
logger.log (2, "Called unsetEntityContext");
}
/**
Delete an entry from the database
*/
public void ejbRemove () throws RemoveException
{
logger.log (2, "Entering ejbRemove");
try
{
id = (String)entityContext.getPrimaryKey();
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "DELETE FROM BMP_customer WHERE id='" + id + "'";
logger.log (2, "Executing update: " + q);
s.executeUpdate(q);
s.close();
c.close();
}
catch (SQLException e)
{
// If we get an SQL exceptino here, convention dicates that we
// must throw a RemoveException to indicate to the client that
// the deletion probably failed
logger.log (0, "Caught SQLException in ejbRemove: " + e);
throw new RemoveException (e.toString());
}
logger.log (2, "Leaving ejbRemove");
}
/**
Read data from the database. On entry `id' will be set correctly, because it
was set in ejbActivate(). All other persistent instance variables are
meaningless, and must be read from the database
*/
public void ejbLoad ()
{
logger.log (2, "Entering ejbLoad");
try
{
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
logger.log (2, "Executing query: " + q);
ResultSet rs = s.executeQuery(q);
rs.next(); // SHOULD always succeed: the container should never have to
// call on a non-existent row
name = rs.getString ("name");
address = rs.getString ("address");
email = rs.getString ("email");
rs.close();
s.close();
c.close();
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbLoad: " + e);
throw new EJBException (e);
}
logger.log (2, "Leaving ejbLoad");
}
/**
Write data to the database
*/
public void ejbStore ()
{
logger.log (2, "Entering ejbStore");
try
{
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "UPDATE BMP_customer "
+ "SET name='" + name + "',"
+ "address='" + address + "',"
+ "email='" + email+ "' "
+ "WHERE id='" + id + "'";
logger.log (2, "Executing update: " + q);
s.executeUpdate(q);
s.close();
c.close();
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbStore: " + e);
throw new EJBException (e);
}
logger.log (2, "Leaving ejbStore");
}
/**
Gets the primary key from the container. In ejbLoad and ejbStore, assume that
the primary key stored in the `id' field is correct
*/
public void ejbActivate ()
{
logger.log (2, "Called ejbActivate");
id = (String)entityContext.getPrimaryKey();
}
/**
Does nothing in this implementation
*/
public void ejbPassivate ()
{
logger.log (2, "Called ejbPassivate");
}
/**
ejbFindByPrimaryKey simply checks whether the data exists in the database; it
does not load it or set instance variables. The container will call ejbLoad
when it wants this to happen. According to the EJB Specification, we must throw
ObjectNotFoundException if the database entry does not exist
*/
public String ejbFindByPrimaryKey(final String id)
throws FinderException
{
try
{
logger.log (2, "Entering ejbFindByPrimaryKey");
boolean ok = false;
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
logger.log (2, "Executing query: " + q);
ResultSet rs = s.executeQuery(q);
if (rs.next()) ok = true;
rs.close();
s.close();
c.close();
if (ok)
{
logger.log (2, "Leaving ejbFindByPrimaryKey");
return id;
}
logger.log (2, "Leaving ejbFindByPrimaryKey, by throwing exception");
throw new ObjectNotFoundException ("No Customer with id=" + id);
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbFindByPrimaryKey: " + e);
throw new EJBException (e);
}
}
/**
ejbFindByNameContaining returns a Collection of primary keys, one for each
row of the customer table whose `name' field contains the supplied string.
If the String is empty, then a collection containing all EJBs is returned
to the client. Clearly this may well be very inefficient if the customer
table has many entries, and this action should be discouraged.
*/
public Collection ejbFindByNameContaining (final String search)
throws FinderException
{
try
{
logger.log (2, "Entering ejbFindByNameContaining");
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "SELECT * FROM BMP_customer WHERE name like '%" + search + "%'";
logger.log (2, "Executing query: " + q);
ResultSet rs = s.executeQuery(q);
ArrayList al = new ArrayList();
while (rs.next())
{
al.add (rs.getString("id"));
}
rs.close();
s.close();
c.close();
logger.log (2, "Leaving ejbFindByNameContaining");
return al;
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbFindByNameContaining: " + e);
throw new FinderException (e.toString());
}
}
/**
ejbCreate must inspect the supplied arguments, create a database entry, and
return the primary key to the container. In this example this is
straightforward, since the primary key is just the `id' field, which is
supplied as a parameter by the client
*/
public String ejbCreate(final String id, final String name,
final String address, final String email)
throws CreateException
{
try
{
logger.log (2, "Entering ejbCreate");
boolean ok = false;
// Set the instance variables from the arguments
this.id = id;
this.name = name;
this.address = address;
this.email = email;
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "INSERT INTO BMP_customer (id,name,address,email) VALUES ("
+ "'" + id + "',"
+ "'" + name + "',"
+ "'" + address + "',"
+ "'" + email + "')";
logger.log (2, "Executing query: " + q);
s.execute(q);
s.close();
c.close();
logger.log (2, "Leaving ejbCreate");
return id;
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbCreate: " + e);
throw new EJBException (e);
}
}
public void ejbPostCreate(final String id, final String name,
final String address, final String email)
{
logger.log (2, "Called ejbPostCreate");
}
// Business methods: only `set' and `get' in this example
public String getId()
{
logger.log (2, "Called getId");
return id;
}
// Note no `setId()' : primary key can't be set
public String getName()
{
logger.log (2, "Called getName");
return name;
}
public void setName(final String name)
{
logger.log (2, "Called setName");
this.name = name;
}
public String getAddress()
{
logger.log (2, "Called getAddress");
return address;
}
public void setAddress(final String address)
{
logger.log (2, "Called setAddress");
this.address = address;
}
public String getEmail()
{
logger.log (2, "Called getEmail");
return email;
}
public void setEmail(final String email)
{
logger.log (2, "Called setEmail");
this.email = email;
}
}
This implementation is fairly naive, for reasons that will be discussed later. However,
it is fully functional. Here are a few other points to note about the implementation.
ejbFindByPrimaryKey verifies the existence of the selected item,
no more, no less. This is in accordance with the EJB specification. Note that this
method does not set the values of any instance variables. Why? The EJB container is
compelled to call ejbLoad() before any business methods, because it can't
be sure that the data in the database will not change between the call to
ejbFindByPrimaryKey() and the business method. Therefore, manipulating
instance variables in ejbFindByPrimaryKey() is typically redundant.
ejbLoad() reads data from the database and sets the values of the
instance variables. In this example it assumes that the instance variable id
holds the correct primary key for the data to be loaded. How can it make that assumption?
When the EJB container readied the instance from the pool, it would have called
ejbActivate, and at this point a call to getPrimaryKey()
gets the key from the container and stores it in id.
DataSource in an instance variable, as this does not
hold any resources. We do this in setEntityContext as this is always the
first method called by the container. Note that we get access to jBoss's default
database by doing
a JNDI lookup for `java:/defaultDS'. This is not necessarily
good practice, as we shall see.
ejbCreate inserts data in the database and initializes the instance
variables, in accordance with the Specification. Depending on the settings of the
transaction attributes, the EJB container may not call ejbLoad()
between ejbCreate() and a subsequent business method, so these instance
variables need to be set at create time. In addition, if we are using (Version 1.1)
container-managed persistence, then the EJB container will attempt to synchronize
the instance variables to the database after calling ejbCreate(), so
they need to be set correctly here.
Logger (not shown here)
for logging debug information. Logger is initialized with the
the JNDI name of an EJB environment variable, and this is expected to contain
an integer representing the amount of debug output required. This technique allows
the debug level to be set in the deployment descriptor, on a per-ejb level, without
changing code. Other techniques are, of course, available. In this implementation
Logger simply logs to standard output, and relies on the EJB container
to pick this up and do something sensible with it. By default, jBoss simply relays
this output to the console. Note that over-use of debugging output is a
significant performance penalty in a production system; it is crucial that
the developer provides a mechanism to control the volume of debugging output.
javac program needs to have a class path that
will enable it to find the EJB classes (in javax.ejb) and the
JDBC-2.0 classes (in javax.sql). On jBoss, the following should
do the trick:
javac -classpath \ .:[jboss_root]/lib/ext/ejb.jar:[jboss_root]/lib/ext/jdbc2_0-stdext.jar \ com/web_tomorrow/bmp_properly/ejb/*.javaAlternatively, if you are using
Ant you can do this:
ant compileejb
Note that this will also compile the other versions of the EJB as well, but this
won't cause any problems.
ejb-jar.xml in a directory
META-INF in the EJB JAR file. There are various graphical tools
available for creating deployment descriptors, but in the simple example a
text editor will be adequate if you know the format of the deployment descriptor
well enough. Note that jBoss does some fairly thorough testing of the
deployment descriptor at deployment time, so you'll soon find out if you've
got it wrong. Here is a suitable deployment descriptor for version 1 of the
Customer EJB.
<?xml version="1.0" encoding="ISO-8859-1"?>
<ejb-jar>
<description>BMPProperly</description>
<display-name>BMPProperly</display-name>
<enterprise-beans>
<entity>
<description>Version 1</description>
<display-name>Customer1</display-name>
<ejb-name>Customer1</ejb-name>
<home>com.web_tomorrow.bmp_properly.ejb.CustomerHome</home>
<remote>com.web_tomorrow.bmp_properly.ejb.Customer</remote>
<ejb-class>com.web_tomorrow.bmp_properly.ejb.CustomerBeanVersion1</ejb-class>
<persistence-type>Bean</persistence-type>
<prim-key-class>java.lang.String</prim-key-class>
<reentrant>False</reentrant>
<env-entry>
<env-entry-name>debug</env-entry-name>
<env-entry-type>java.lang.Integer</env-entry-type>
<env-entry-value>2</env-entry-value>
</env-entry>
</entity>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>Customer1</ejb-name>
<method-name>"*"</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
(Please note that the version in the source code package contains entries for
the other version of the EJB as well, so it won't look exactly like this listing).
Required, which is a sensible default. It also defines an environment
variable `debug', which is used by the debug logger. The value `2'
means `maximum output' in this example, which should lead to a whole heap of
debugging output when the EJB is invoked. The tag
<ejb-name>Customer1</ejb-name>becomes the JNDI name of the EJB, which is what its clients will look up.
[source_root]/servertarget, so the XML file needs to be saved as
[source_root]/servertarget/META-INF/ejb-jar.xml.
servertarget directory and
execute:
jar cvf bmp_properly.jar *If you are using
Ant:
ant buildjar
[jboss_root]/deploy. jBoss does some checks on the
structure of the JAR file and the classes it contains and, if successful, you
will see an output similar to the following in the console log:
[J2EE Deployer Default] Deploy J2EE application: file:/home/kevin/lib/JBoss-2.2.2/deploy/bmp_properly.jar [J2EE Deployer Default] Create application bmp_properly.jar [J2EE Deployer Default] install module bmp_properly.jar [Container factory] Deploying:file:/home/kevin/lib/JBoss-2.2.2/tmp/deploy/Default/bmp_properly.jar [Verifier] Verifying file:/home/kevin/lib/JBoss-2.2.2/tmp/deploy/Default/bmp_properly.jar/ejb1002.jar [Container factory] Deploying Customer1 [Container factory] Deployed application: file:/home/kevin/lib/JBoss-2.2.2/tmp/deploy/Default/bmp_properly.jar [J2EE Deployer Default] J2EE application: file:/home/kevin/lib/JBoss-2.2.2/deploy/bmp_properly.jar is deployed.If you are using
Ant, just do this:
ant deploy(Please note that this deploys the other versions of the EJB as well, but that shouldn't cause any problems).
findByPrimaryKey() to locate the EJB with customer ID `1';
package com.web_tomorrow.bmp_properly.client;
import javax.ejb.*;
import java.rmi.*;
import javax.rmi.*;
import javax.naming.*;
import java.util.*;
import com.web_tomorrow.bmp_properly.ejb.*;
/**
Test client for the `Customer' EJB
(c)2001 Kevin Boone/Web-Tomorrow
*/
public class Test
{
public static void main (String[] args) throws Exception
{
if (args.length != 1)
{
System.err.println ("Specify EJB JNDI name on command line");
System.exit(-1);
}
System.setProperty("java.naming.factory.initial",
"org.jnp.interfaces.NamingContextFactory");
System.setProperty("java.naming.provider.url",
"localhost:1099");
String ejbName = args[0];
Context c = new InitialContext();
Object o = c.lookup (ejbName);
CustomerHome h = (CustomerHome) PortableRemoteObject.narrow
(o, CustomerHome.class);
System.out.println ("Locating customer '1'");
try
{
Customer customer = h.findByPrimaryKey ("1");
System.out.println ("Customer exists: deleting");
customer.remove();
}
catch (ObjectNotFoundException e)
{
System.out.println ("Customer does not exist");
}
System.out.println ("Creating customer 1");
Customer customer = h.create ("1", "Kevin Boone", "Palmers Green, London", "kb@kevinboone.com");
System.out.println ("Properties of new Customer EJB:");
dumpCustomer (customer);
System.out.println ("Finding Customer EJBs matching name `Boone'");
Collection cc = h.findByNameContaining("Boone");
Iterator customers = cc.iterator();
while (customers.hasNext())
{
customer = (Customer)customers.next();
System.out.println ("Found customer with ID " + customer.getId());
}
}
// Dumps the properties of a Customer EJB
public static void dumpCustomer (Customer customer) throws RemoteException
{
String customerId = customer.getId();
String customerName = customer.getName();
String customerAddress = customer.getAddress();
String customerEmail = customer.getEmail();
System.out.println ("id=" + customerId);
System.out.println ("name=" + customerName);
System.out.println ("address=" + customerAddress);
System.out.println ("email=" + customerEmail);
}
}
To enable the test client to be used against different versions of the same EJB, I
have coded it to take the JNDI name of the EJB on the command line. To run the test
client the Java runtime needs to be able to find the following on its class search
path:
javax.ejb
java -classpath \ clienttarget:\ servertarget:\ [jboss_root]/lib/ext/jnpserver.jar:\ [jboss_root]/lib/ext/ejb.jar:\ [jboss_root]/lib/ext/jta-spec1_0_1.jar:\ [jboss_root]/lib/jboss-jaas.jar \ com.web_tomorrow.bmp_properly.client.Test Customer1With
Ant, just execute:
ant runclient1You should see output similar to the following on the console where the client is executed:
Locating customer '1' Customer exists: deleting Creating customer 1 Properties of new Customer EJB: id=1 name=Kevin Boone address=Palmers Green, London email=kb@kevinboone.com Finding Customer EJBs matching name `Boone' Found customer with ID 1On the jBoss server console you should see a huge volume of output, showing all interactions between jBoss and the
Customer EJB. Here is a
typical trace; we will discuss this in more detail later.
[Customer1] Called setEntityContext
[Customer1] Entering ejbFindByPrimaryKey
[Customer1] Executing query: SELECT * FROM BMP_customer WHERE id='1'
[Customer1] Leaving ejbFindByPrimaryKey
[Customer1] Called ejbActivate
[Customer1] Entering ejbLoad
[Customer1] Executing query: SELECT * FROM BMP_customer WHERE id='1'
[Customer1] Leaving ejbLoad
[Customer1] Entering ejbRemove
[Customer1] Executing update: DELETE FROM BMP_customer WHERE id='1'
[Customer1] Leaving ejbRemove
[Customer1] Entering ejbCreate
[Customer1] Executing query: INSERT INTO BMP_customer (id,name,address,email) VALUES ('1','Kevin Boone','Palmers Green, London','kb@kevinboone.com')
[Customer1] Leaving ejbCreate
[Customer1] Called ejbPostCreate
[Customer1] Called getId
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getName
[Customer1] Entering ejbStore
tomer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getName
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getAddress
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getEmail
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Entering ejbFindByNameContaining
[Customer1] Executing query: SELECT * FROM BMP_customer WHERE name like '%Boone%'
[Customer1] Leaving ejbFindByNameContaining
[Customer1] Called getId
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Container factory] Called ejbPassivate
datasource = (DataSource)c.lookup("java:/DefaultDS");
The name `java:DefaultDS' is specific to jBoss, and indeed to a specific
database in the jBoss default set-up. The use of a name like this would make the whole
EJB non-portable. Of course, there are various common-sense ways we could get
around this problem, such as having the EJB extract the datasource name form its
environment, which could be manipulated through the deployment descriptor.
However, the J2EE Specification sets out a standard way of doing name indirection
in J2EE components: the use of `java:comp/env/' names. Names that begin
with this prefix are not treated as plain JNDI names, but as logical names that can
be redirected to real JNDI names at deployment time. So we should really code the
lookup like this:
datasource = (DataSource)c.lookup("java:/comp/env/jdbc/bmp_properly");
The part of the string after the `java:comp/env/' prefix is
technically known as the coded name. This is that part that must
be mapped to the real JNDI name.
<resource-ref> <res-ref-name>jdbc/bmp_properly</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>So where is the real JNDI name (
java:/DefaultDS in this case)?
This is vendor-specific; many developers find this a bit odd, but it's the case
nonetheless. jBoss keeps this information in a separate file, called jboss.xml
which needs to be deployed in the META-INF directory along with the
real ejb-jar.xml. Here is a suitable XML file:
<jboss>
<enterprise-beans>
<entity>
<ejb-name>Customer2</ejb-name>
<resource-ref>
<res-ref-name>jdbc/bmp_properly</res-ref-name>
<resource-name>java:/DefaultDS</resource-name>
</resource-ref>
</entity>
</enterprise-beans>
</jboss>
Since this will accompany a new version of the EJB which I will call Customer2,
the ejb-name references Customer2. This file needs to be
created in the directory [source_root]/servertarget/META-INF in this
example.
ejbStore, etc., at appropriate times with
respect to transaction boundaries. Suppose that the client of an EJB application
calls a number of methods on different EJBs, all as part of the same transaction.
One of these methods may have carried out a database operation which failed, but
the failure is only known to the container at this time. The client will continue to
call other methods on the other EJBs. Now, when it comes to transaction completion time
the container knows that it has to roll back the transaction. This means that any work
done after the original failure is redundant, as it will get rolled back.
if (entityContext.getRollbackOnly())
{
logger.log (1, "Rollback flagged by container: giving up");
return;
}
[Customer1] Called getName [Customer1] Entering ejbStore tomer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1' [Customer1] Leaving ejbStoreThe client has called
getName(), and the container has delegated that
call to the EJB instance. So far, so good. However, the container then goes on to
call ejbStore(), despite the fact the the method getName()
did not do anything the requires a database update. This is normal behaviour.
The EJB container has no (straightforward) way of knowing whether the method updated
any instance variables in such a way that a database update is required.
Therefore it calls ejbStore anyway. This leads to a very
important principle:
The container calls ejbStore() to give the EJB an opportunity to
write its state to the database, not to compel it to do so.
It is the developer's responsibility to author the EJB in such a way that redundant
writes are avoided.
One way to do this is to maintain a flag in an instance
variable that indicates whether a write will be required on the next call
to ejbStore(). For example, we can implement the setXXX
methods like this:
public void setName(final String name)
{
this.name = name;
needWrite = true;
}
The flag needWrite can then be checked on entry to
ejbStore():
if (!needWrite)
{
logger.log (1, "Abandon ejbStore() because needWrite flag not set");
return;
}
and then reset on exit from ejbStore() because, we hope, the instance
variables are synchronized to the database by that point.
needWrite be set and reset? Well, it will need
to be reset after ejbLoad() as, again, the database and the instance
variables are in sync at that point. Similar logic applies to ejbCreate().
Finder methods do not change the state of the instance, so the needWrite
flag is neither set nor reset here.
needWrite flag. In this simple example it
isn't difficult to assure this, but mistakes can easily arise in more complex
EJBs. To reduce the likelihood of such errors, I recommend the following:
all methods that update any persistent instace variable do so by
calling a `set' method, and that method in turn sets the
write flag.
This applies not just be business methods that are called by clients, but to
private methods within the EJB implementation as well. If all modifications
of instance variables takes place through a `set' method, there is little
scope for error.
CustomerBeanVersion1.java).
package com.web_tomorrow.bmp_properly.ejb;
import javax.ejb.*;
import java.rmi.*;
import java.sql.*;
import java.util.*;
import javax.sql.*;
import javax.naming.*;
import com.web_tomorrow.ejb_utils.*;
public class CustomerBeanVersion2 implements EntityBean
{
// Non-persistent instance variables
protected EntityContext entityContext;
protected Logger logger;
protected DataSource datasource;
protected boolean needWrite;
// Persistent instance variables
protected String id;
protected String name;
protected String address;
protected String email;
/**
Save the entity context for future use, and initialize the datasource so we can
use it to get connections in subsequent method calls
*/
public void setEntityContext (EntityContext entityContext)
{
logger = new Logger ("java:comp/env/debug");
logger.log (2, "Called setEntityContext");
this.entityContext = entityContext;
try
{
Context c = new InitialContext();
datasource = (DataSource)c.lookup("java:/comp/env/jdbc/bmp_properly");
}
catch (NamingException e)
{
logger.log (2, "Caught NamingException in setEntityContext: " + e);
throw new EJBException ("Datasource name lookup failed: " + e);
}
}
/**
Does nothing in this example
*/
public void unsetEntityContext ()
{
logger.log (2, "Called unsetEntityContext");
}
/**
Delete an entry from the database
*/
public void ejbRemove () throws RemoveException
{
logger.log (2, "Entering ejbRemove");
if (entityContext.getRollbackOnly())
{
logger.log (1, "Abandon ejbRemove() because rollback flagged by container");
return;
}
try
{
id = (String)entityContext.getPrimaryKey();
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "DELETE FROM BMP_customer WHERE id='" + id + "'";
logger.log (2, "Executing update: " + q);
s.executeUpdate(q);
s.close();
c.close();
}
catch (SQLException e)
{
// If we get an SQL exceptino here, convention dicates that we
// must throw a RemoveException to indicate to the client that
// the deletion probably failed
logger.log (0, "Caught SQLException in ejbRemove: " + e);
throw new RemoveException (e.toString());
}
logger.log (2, "Leaving ejbRemove");
}
/**
Read data from the database. On entry `id' will be set correctly, because it
was set in ejbActivate(). All other persistent instance variables are
meaningless, and must be read from the database
*/
public void ejbLoad ()
{
logger.log (2, "Entering ejbLoad");
// Whether ejbLoad() succeeds or fails, the needWrite flag should be reset.
// After ejbLoad(), the instance variables will either be in sync with the
// database (success) or garbage (failure). In either case, a database update
// will not be required
needWrite = false;
try
{
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
logger.log (2, "Executing query: " + q);
ResultSet rs = s.executeQuery(q);
rs.next(); // SHOULD always succeed: the container should never have to
// call on a non-existent row
name = rs.getString ("name");
address = rs.getString ("address");
email = rs.getString ("email");
rs.close();
s.close();
c.close();
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbLoad: " + e);
throw new EJBException (e);
}
logger.log (2, "Leaving ejbLoad");
}
/**
Write data to the database
*/
public void ejbStore ()
{
logger.log (2, "Entering ejbStore");
if (entityContext.getRollbackOnly())
{
logger.log (1, "Abandon ejbStore() because rollback flagged by container");
return;
}
if (!needWrite)
{
logger.log (1, "Abandon ejbStore() because needWrite flag not set");
return;
}
try
{
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "UPDATE BMP_customer "
+ "SET name='" + name + "',"
+ "address='" + address + "',"
+ "email='" + email+ "' "
+ "WHERE id='" + id + "'";
logger.log (2, "Executing update: " + q);
s.executeUpdate(q);
s.close();
c.close();
needWrite = false;
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbStore: " + e);
throw new EJBException (e);
}
logger.log (2, "Leaving ejbStore");
}
/**
Gets the primary key from the container. In ejbLoad and ejbStore, assume that
the primary key stored in the `id' field is correct
*/
public void ejbActivate ()
{
logger.log (2, "Called ejbActivate");
id = (String)entityContext.getPrimaryKey();
}
/**
Does nothing in this implementation
*/
public void ejbPassivate ()
{
logger.log (2, "Called ejbPassivate");
}
/**
ejbFindByPrimaryKey simply checks whether the data exists in the database; it
does not load it or set instance variables. The container will call ejbLoad
when it wants this to happen. According to the EJB Specification, we must throw
ObjectNotFoundException if the database entry does not exist
*/
public String ejbFindByPrimaryKey(final String id)
throws FinderException
{
try
{
logger.log (2, "Entering ejbFindByPrimaryKey");
boolean ok = false;
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
logger.log (2, "Executing query: " + q);
ResultSet rs = s.executeQuery(q);
if (rs.next()) ok = true;
rs.close();
s.close();
c.close();
if (ok)
{
logger.log (2, "Leaving ejbFindByPrimaryKey");
return id;
}
logger.log (2, "Leaving ejbFindByPrimaryKey, by throwing exception");
throw new ObjectNotFoundException ("No Customer with id=" + id);
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbFindByPrimaryKey: " + e);
throw new EJBException (e);
}
}
/**
ejbFindByNameContaining returns a Collection of primary keys, one for each
row of the customer table whose `name' field contains the supplied string.
If the String is empty, then a collection containing all EJBs is returned
to the client. Clearly this may well be very inefficient if the customer
table has many entries, and this action should be discouraged.
*/
public Collection ejbFindByNameContaining (final String search)
throws FinderException
{
try
{
logger.log (2, "Entering ejbFindByNameContaining");
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "SELECT * FROM BMP_customer WHERE name like '%" + search + "%'";
logger.log (2, "Executing query: " + q);
ResultSet rs = s.executeQuery(q);
ArrayList al = new ArrayList();
while (rs.next())
{
al.add (rs.getString("id"));
}
rs.close();
s.close();
c.close();
logger.log (2, "Leaving ejbFindByNameContaining");
return al;
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbFindByNameContaining: " + e);
throw new FinderException (e.toString());
}
}
/**
ejbCreate must inspect the supplied arguments, create a database entry, and
return the primary key to the container. In this example this is
straightforward, since the primary key is just the `id' field, which is
supplied as a parameter by the client
*/
public String ejbCreate(final String id, final String name,
final String address, final String email)
throws CreateException
{
// We won't need a database update after this method: the instance variables
// and the database will be in sync
needWrite = false;
try
{
logger.log (2, "Entering ejbCreate");
if (entityContext.getRollbackOnly())
{
logger.log (1, "Abandon ejbCreate() because rollback flagged by container");
throw new CreateException("Create abandonned owing to prior rollback");
}
boolean ok = false;
// Set the instance variables from the arguments
this.id = id;
this.name = name;
this.address = address;
this.email = email;
Connection c = datasource.getConnection();
Statement s = c.createStatement();
String q = "INSERT INTO BMP_customer (id,name,address,email) VALUES ("
+ "'" + id + "',"
+ "'" + name + "',"
+ "'" + address + "',"
+ "'" + email + "')";
logger.log (2, "Executing query: " + q);
s.execute(q);
s.close();
c.close();
logger.log (2, "Leaving ejbCreate");
return id;
}
catch (SQLException e)
{
// If we get an SQL exceptino here then something has gone horribly wrong.
// Throw EJB exception to alert container
logger.log (0, "Caught SQLException in ejbCreate: " + e);
throw new EJBException (e);
}
}
public void ejbPostCreate(final String id, final String name,
final String address, final String email)
{
logger.log (2, "Called ejbPostCreate");
}
// Business methods: only `set' and `get' in this example
public String getId()
{
logger.log (2, "Called getId");
return id;
}
// Note no `setId()' : primary key can't be set
public String getName()
{
logger.log (2, "Called getName");
return name;
}
public void setName(final String name)
{
logger.log (2, "Called setName");
this.name = name;
needWrite = true;
}
public String getAddress()
{
logger.log (2, "Called getAddress");
return address;
}
public void setAddress(final String address)
{
logger.log (2, "Called setAddress");
this.address = address;
needWrite = true;
}
public String getEmail()
{
logger.log (2, "Called getEmail");
return email;
}
public void setEmail(final String email)
{
logger.log (2, "Called setEmail");
this.email = email;
needWrite = true;
}
}
To run this new EJB, just specify Customer2 as the argument
to the test client; if you are using Ant, just do
ant runclient2
Customer object, it can call
getXXX and setXXX methods and expect the EJB to
maintain itself in sync with the database.
getName()
is a network operation. It requires the set-up of a TCP/IP connection, calls
through the stub and skeleton, and marshalling and unmarshalling of the return value
from the method, all of which are overheads. The client in the example above
made four similar method calls on EJB instance, and in no case did it attempt to
update any data. Large performance gains can be achieved by recognizing that
often clients merely want to retrieve data from the EJB, and not set it. To support
this mode of operation it makes sense to provide a single method call that returns
all the relevant data items in one method. Typically we will use a separate class
for this purpose. In J2EE jargon, such a class is called a value object.
In the example described in this article we could create a class called, for
example, CustomerData with instance variables id,
name, address, etc. The EJB would then have a
business method getCustomerData() that returns all the instance
variables of the EJB in a single CustomerData instance.
getXXX() methods
on the EJB, and some performance gain could be expected by using value objects
to carry multiple data elements in one method call. However, suppose that the
client carried out a number of operations, some of which updated the EJB and
some of which retrieved values? This is far from being an uncommon situation.
The use of value objects won't help much here. For every setXXX
and getXXX method the container is going to call one or more
of the methods ejbLoad(), ejbStore(),
ejbActivate() and ejbPassivate() in an attempt to keep
the database and the EJB in sync. Or is it? This was the behaviour in the
example because client had not specifically demarcated transaction
boundaries. In other words, the container had to begin a new database transaction
for each business method call, and commit the transaction at the end of the
method call. This was necessary because the container had no better information
about the scope of the current database transaction. Because a single transaction
did not span multiple methods, the container could not assume that any of
the data in the database was locked, so frequent calls to ejbLoad()
and ejbStore() were required. But suppose the client
initiated a new transaction before calling a large number of EJB operations, and
then committed the transaction at the end. A relatively smart container will recognize
that it only has to call ejbLoad() once (after beginning the transaction),
and ejbStore() once (just before committing the transaction).
This discussion should make it evident that:
getting good performance from entity EJBs requires a detailed understanding of the transactional properties of the application, and of techniques that containers use to synchronize EJB load/store operations to transaction boundaries.It is difficult, if not impossible, for a stand-alone client to interact with the EJB container's transaction manager in such a way as to get this effect. However, there is a simple solution:
wrap up multiple entity EJB operations within a session EJB. Ensure that the session EJB begins a transaction that encompasses the entity operations, or has transaction attributes that cause the container to do so.This is one of the reasons that most authorities recommend the use of session EJBs as a facade to an entity EJB system. Other reasons include the ability to cache entity data in the session EJBs, and the provision of a `coarse-grained' interface to clients.
UserTransaction ut = sessionContext.getUserTransaction(); ut.begin() // Entity EJB operation 1... // Entity EJB operation 2... // Entity EJB operation 3... ut.commit();Alternatively we can specify container-managed transactions for the session EJB method, and set the transaction attribute to
Requires or RequiresNew as appropriate.
©1994-2003 Kevin Boone, all rights reserved