In this exercise, you will build and examine a simple EJB that uses bean-managed persistence (BMP) to synchronize the state of the bean with a database. You will also build a client application to test this Ship BMP bean.
As in the CMP examples, the state of the entity beans will be stored in the database that is embedded in JBoss. JBoss was able to create all tables for CMP beans, but it cannot do the same for BMP beans because the deployment descriptors don’t contain any persistence information (object-to-relational mapping, for example). The bean is in fact the only one that knows how to load, store, remove, and find data. The persistence mapping is not described in a configuration file, but embedded in the bean code instead.
One consequence is that the database environment for BMP must always be built explicitly. To make this task easier for the BMP Ship example, Ship’s home interface defines two helpful home methods.
Entity beans can define home methods that perform operations related to the EJB component’s semantics but that are not linked to any particular bean instance. As an analogy, consider the static methods of a class: their semantics are generally closely related to the class’s semantics, but they’re not associated with any particular class instance. Don’t worry if this is not totally clear: Chapter 10 of the EJB book, explains all about home methods.
Here’s a partial view of the Ship EJB’s home interface:
public interface ShipHomeRemote extends javax.ejb.EJBHome { ... public voidmakeDbTable
( ) throws RemoteException; public voiddeleteDbTable
( ) throws RemoteException; }
It defines two home methods. The first creates the table needed by the Ship EJB in the JBoss-embedded database and the second drops it.
The implementation of the
makeDbTable( )
home method is essentially a
CREATE
TABLE
SQL
statement:
public void ejbHomeMakeDbTable ( ) throws SQLException { PreparedStatement ps = null; Connection con = null; try { con = this.getConnection ( ); System.out.println("Creating table SHIP..."); ps = con.prepareStatement ("CREATE TABLE SHIP (
" + "ID INT PRIMARY KEY,
" + "NAME CHAR (30),
" + "TONNAGE DECIMAL (8,2),
" + "CAPACITY INT
" + ")
" ); ps.execute ( ); System.out.println("...done!"); } finally { try { if (ps != null) ps.close ( ); } catch (Exception e) {} try { if (con != null) con.close ( ); } catch (Exception e) {} } }
The
deleteDbTable( )
home method differs only by the
SQL statement it executes:
...
System.out.println("Dropping table SHIP...");
ps = con.prepareStatement ("DROP TABLE SHIP
");
ps.execute ( );
System.out.println("...done!");
...
We explain how to call these methods in a subsequent section.
The Ship EJB source code requires no modification to run in JBoss, so the standard EJB deployment descriptor is very simple.
... <enterprise-beans> <entity> <description> This bean represents a cruise ship. </description> <ejb-name>ShipEJB
</ejb-name> <home>com.titan.ship.ShipHomeRemote</home> <remote>com.titan.ship.ShipRemote</remote> <ejb-class>com.titan.ship.ShipBean</ejb-class> <persistence-type>Bean</persistence-type> <prim-key-class>java.lang.Integer</prim-key-class> <reentrant>False</reentrant> <security-identity><use-caller-identity/></security-identity> <resource-ref> <description>DataSource for the Titan DB</description> <res-ref-name>jdbc/titanDB
</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </entity> </enterprise-beans> ...
This first part of the deployment descriptor essentially tells the container that the Ship bean:
Is named ShipEJB
.
Has a persistence type set to Bean
because
it’s a BMP bean.
Declares a reference to a data source named
jdbc/titanDB
.
Because the bean directly manages the persistence logic, the deployment descriptor does not contain any persistence information. In contrast, this information would have been mandatory for a CMP EJB.
The second part of the deployment descriptor declares the transactional and security attributes of the Ship bean.
...
<assembly-descriptor>
<security-role>
<description>
This role represents everyone who is allowed full
access to the Ship EJB.
</description>
<role-name>everyone</role-name>
</security-role>
<method-permission>
<role-name>everyone</role-name>
<method>
<ejb-name>ShipEJB</ejb-name>
<method-name>*</method-name>
</method>
</method-permission>
<container-transaction>
<method>
<ejb-name>ShipEJB</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required
</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
All methods of the Ship bean require a transaction. If no transaction is active when a method invocation enters the container, a new one will be started.
If you don’t include a
jboss.xml
-specific deployment
descriptor with your bean, JBoss will take the following actions at
deployment time. It will:
Bind the Ship bean in the public JNDI tree under
/ShipEJB
(which is the name given to the bean in
its associated ejb-jar.xml
deployment
descriptor).
Link the jdbc/titanDB
data source expected by the
bean to java:/DefaultDS
, which is a default data
source that represents the embedded database.
Unless you require different settings, you don’t
need to provide a jboss.xml
file. While this
shortcut is generally useful for quick prototyping, it will not
satisfy more complex deployment situations. Furthermore, using a
JBoss-specific deployment descriptor enables you to fine-tune a
container for a particular situation.
If you take a look at the
$JBOSS_HOME/server/default/conf/standardjboss.xml
file, you will find all the default container settings that are
predefined in JBoss (standard BMP, standard CMP, clustered BMP, and
so on). In JBoss, there’s a one-to-one mapping
between a bean and a container, and each container can be configured
independently.
This mapping was a design decision made by the JBoss container developers and has not been dictated by the EJB specification: other application servers may use another mapping.
When you write a JBoss-specific deployment descriptor, you have three options:
Don’t specify any container configuration. JBoss
will use the default configuration found in
standardjboss.xml
.
Create a brand new container configuration. The default settings are
not used at all. JBoss will configure the container only as you
specify in jboss.xml
.
Modify an existing configuration. JBoss loads the default settings
from the existing configuration found in
standardjboss.xml
and overrides them with the
settings you specify in the jboss.xml
deployment
descriptor. This solution allows you to make minor modifications to
the default container with minimal writing in your deployment
descriptor.
The Ship bean uses the last option in order to test its behavior with
different commit options. As outlined below, this new configuration
defines only a single setting
(<commit-option>
). All others are inherited
from the Standard
BMP
EntityBean
configuration declared in the
standardjboss.xml
file. We’ll
discuss commit options in a dedicated section at the end of this
chapter.
<?xml version="1.0"?> <!DOCTYPE jboss PUBLIC "-//JBoss//DTD JBOSS 4.0//EN" "http://www.jboss.org/j2ee/dtd/jboss_4_0.dtd"> <jboss> ... <container-configurations> <container-configuration> <container-name>Standard BMP EntityBean
</container-name> <commit-option>A
</commit-option> </container-configuration> </container-configurations> ...
Because a single deployment descriptor may define multiple EJBs, the
role of the
<ejb-name>
tag is to
link the definitions from the ejb-jar.xml
and
jboss.xml
files. You can consider this tag to be
the bean’s identifier. The
<jndi-name>
tag determines the name under which the
client applications will be able to look up the
EJB’s home interface, in this case
ShipHomeRemote
.
You can also see how the bean refers to a specific configuration,
thanks to the
<configuration-name>
tag.
... <enterprise-beans> <entity> <ejb-name>ShipEJB
</ejb-name> <jndi-name>ShipHomeRemote
</jndi-name> <configuration-name>Standard BMP EntityBean
</configuration-name> <resource-ref> <res-ref-name>jdbc/titanDB
</res-ref-name> <jndi-name>java:/DefaultDS
</jndi-name> </resource-ref> </entity> </enterprise-beans> </jboss>
The Ship bean BMP implementation needs to establish a database
connection explicitly. It’s the
getConnection( )
method that manages the acquisition of
this resource.
private Connection getConnection ( ) throws SQLException
{
try
{
Context jndiCntx = new InitialContext ( );
DataSource ds =
(DataSource)jndiCntx.lookup ("java:comp/env/jdbc/titanDB
");
return ds.getConnection ( );
...
The bean expects to find a data source bound to the
java:comp/env/jdbc/titanDB
JNDI name.
That’s why the ejb-jar.xml
file
contains the following declaration.
Perform the following steps:
Open a command prompt or shell terminal and change to the
ex9_1
directory created by the extraction
process
Set the JAVA_HOME
and
JBOSS_HOME
environment variables to point to where
your JDK and JBoss 4.0 are installed. Examples:
Windows:C:workbookex9_1> set JAVA_HOME=C:jdk1.4.2 C:workbookex9_1> set JBOSS_HOME=C:jboss-4.0
|
Unix:$ export JAVA_HOME=/usr/local/jdk1.4.2 $ export JBOSS_HOME=/usr/local/jboss-4.0
|
Add ant
to your execution path.
Windows:C:workbookex9_1> set PATH=..antin;%PATH%
|
Unix:$ export PATH=../ant/bin:$PATH
|
Perform the build by typing ant
.
As in the last exercise, you will see titan.jar
rebuilt, copied to the JBoss deploy
directory,
and redeployed by the application server.
In the Section 27.1.2 section earlier in this chapter, you saw how the bean implements the home methods that create and drop the table in the database. Now you’ll see how the client application calls these home methods.
public class Client_91 { public static void main (String [] args) { try { Context jndiContext = getInitialContext ( ); Object ref = jndiContext.lookup ("ShipHomeRemote"); ShipHomeRemote home = (ShipHomeRemote) PortableRemoteObject.narrow (ref,ShipHomeRemote.class); // We check if we have to build the database schema... // if ( (args.length > 0) && args[0].equalsIgnoreCase ("CreateDB") ) { System.out.println ("Creating database table..."); home.makeDbTable ( ); } // ... or if we have to drop it... // else if ( (args.length > 0) && args[0].equalsIgnoreCase ("DropDB") ) { System.out.println ("Dropping database table..."); home.deleteDbTable ( ); } else ...
Depending on the first argument found on the command line
(CreateDB
or DropDB
), the
client application calls the corresponding home method.
If nothing is specified on the command line, the client will test our BMP bean:
... else { // ... standard behavior // System.out.println ("Creating Ship 101.."); ShipRemote ship1 = home.create
(new Integer (101),"Edmund Fitzgerald"); ship1.setTonnage (50000.0); ship1.setCapacity (300); Integer pk = new Integer (101); System.out.println ("Finding Ship 101 again.."); ShipRemote ship2 = home.findByPrimaryKey
(pk); System.out.println (ship2.getName ( )); System.out.println (ship2.getTonnage ( )); System.out.println (ship2.getCapacity ( )); System.out.println ("ship1.equals (ship2) == " + ship1.equals
(ship2)); System.out.println ("Removing Ship 101.."); ship2.remove ( ); } ...
The client application first creates a new Ship and calls some of its
remote methods to set its tonnage and capacity. Then it finds the
bean again by calling findByPrimaryKey( )
and
compares the bean references for equality. Because they represent the
same bean instance, they must be equal. We’ve
omitted the exception handling because it deserves no specific
comments.
Testing the BMP bean is a three-step process that involves:
Creating the database table
Testing the bean (possibly many times)
Dropping the database table
For each of these steps, a different Ant target is available.
To create the table, use the createdb_91
Ant
target:
C:workbookex9_1>ant createdb_91 Buildfile: build.xml prepare: compile: createdb_91: [java] Creating database table...
On the JBoss side, the BMP bean displays the following lines:
... 12:31:42,584 INFO [STDOUT] Creating table SHIP... 12:31:42,584 INFO [STDOUT] ...done! ...
Once this step has been performed, the actual testing of the BMP bean can take place.
To test the BMP bean, use the run.client_91
Ant
target:
C:workbookex9_1>ant run.client_91 Buildfile: build.xml prepare: compile: run.client_101: [java] Creating Ship 101.. [java] Finding Ship 101 again.. [java] Edmund Fitzgerald [java] 50000.0 [java] 300 [java] ship1.equals (ship2) == true [java] Removing Ship 101..
Even though it’s not particularly related to BMP beans, let’s focus on an interesting problem that arises when the client first creates and initializes the bean:
ShipRemote ship1 = home.create (new Integer (101),"Edmund Fitzgerald"); ship1.setTonnage (50000.0); ship1.setCapacity (300);
This piece of code generates three different transactions on the server side. The client does not implicitly start any transaction in its code. The transaction starts only when the invocation enters the bean container and commits when the invocation leaves the container. Thus, when the client performs three calls, each one is executed in its own transactional context.
Look at the implications for the BMP bean:
14:36:31,730 INFO [STDOUT] ejbCreate( ) pk=101 name=Edmund Fitzgerald 14:36:31,780 INFO [STDOUT] ejbStore( ) pk=101 14:36:31,840 INFO [STDOUT] setTonnage( ) 14:36:31,840 INFO [STDOUT] ejbStore( ) pk=101 14:36:31,860 INFO [STDOUT] setCapacity( ) 14:36:31,860 INFO [STDOUT] ejbStore( ) pk=101
As you can see, ejbStore( )
is called at the end of
each transaction! Consequently, these three lines of code cause the
bean to be stored three times. Worst of all, after any method
invocation, the container has no way of knowing whether the state of
the bean has been modified, and thus, to be on the safe side, it
triggers storage of the bean. Given that there is no read-only method
concept in EJBs, calls to get methods also trigger calls to
ejbStore( )
:
15:03:19,301 INFO [STDOUT] getName( ) 15:03:19,311 INFO [STDOUT] ejbStore( ) pk=101 15:03:19,331 INFO [STDOUT] getTonnage( ) 15:03:19,331 INFO [STDOUT] ejbStore( ) pk=101 15:03:19,371 INFO [STDOUT] getCapacity( ) 15:03:19,371 INFO [STDOUT] ejbStore( ) pk=101
In the execution of the test program, ejbStore( )
is called seven times.
You can see that transaction boundaries (i.e., where transactions are started and stopped) directly influence the number of callbacks from the container to the Ship bean, and consequently have a direct effect on performance. We’ll now focus on another setting that also affects the set of callback methods the container will invoke on the bean: the commit option . The commit option determines how an entity bean container can make use of its cache. Remember from the container configuration section that the bean is currently using commit option A. Let’s examine all the options and their effects.
If you select commit option A, the entity bean container is allowed to cache any bean that it has loaded. Next time an invocation targets a bean that is already in the application server cache,[74] the container will not have to make a costly database access call to load it again.
If you select commit option B or C, the entity bean container is allowed to cache a bean only if it loads that bean during the lifetime of the currently running transaction. Once the transaction commits or rolls back, the container must remove the bean from the cache. The next time an invocation targets the bean, the container will have to reload it from the database.
That extra reloading is costly—but you must use B or C[75] whenever the data represented by the container can also be modified by other means. Direct database access calls through a console, for example, will cause the container cache to become unsychronized with the database, leading to incorrect computations and other dire results. A container must not use commit option A unless it “owns” the database (or, more accurately, the specific tables it accesses).
Most of the time, this “black or white” approach isn’t satisfactory: in real-world applications, commit option A can be used only very rarely, and commit options B and C will preclude useful cache optimizations. To circumvent these limitations, JBoss provides some proprietary optimizations: an additional commit option, distributed cache invalidations, and even a distributed transactional cache with various locking policies (JBossCache). See the JBoss web site for more information.
The JBoss-proprietary commit option D is a compromise between options A and C: The bean instance can be cached across transactions, but a configurable timeout value indicates when this cached data is stale and must be reloaded from the database. This option is very useful when you want some of the efficiency of commit option A, but want cached entities to be updated periodically to reflect modifications by an external system.
Remember that each EJB deployed in JBoss has its own container. Consequently, for each EJB, you can define the commit option that best fits its specific environment. For example, a Zip code entity bean (with data that will most probably never change) could use commit option A, whereas the Order EJB would use commit option C.
After this introduction to commit options, it becomes possible to guess that the container is currently using commit option A without looking at its configuration. Two pieces of evidence lead us to this conclusion:
The findByPrimaryKey( )
call isn’t
displayed in the log. The container first checks whether the cache
already contains an instance for the given primary key. Because it
does, there is no need to invoke the bean
implementation’s
ejbFindByPrimaryKey( )
method.
ejbLoad( )
isn’t called for the
bean. At the start of each new transaction, it’s
already in cache and there is no need to reload it from the database.
Note that only direct access to a given bean (using its remote
reference) or findByPrimaryKey( )
calls can be
resolved in cache. All other queries (findAll( )
,
findByCapacity( )
, and so on) must be resolved by
the database directly (there is no way to perform queries in the
container cache directly).
To see how different commit options lead to different behavior,
change the commit option in jboss.xml
from A to
C:
...
<container-configurations>
<container-configuration>
<container-name>Standard BMP EntityBean</container-name>
<commit-option>C</commit-option>
</container-configuration>
</container-configurations>
...
Run the tests again. You’ll see:
14:41:29,798 INFO [STDOUT] ejbCreate( ) pk=101 name=Edmund Fitzgerald 14:41:30,449 INFO [STDOUT] ejbStore( ) pk=101 14:41:30,539 INFO [STDOUT] ejbLoad( ) pk=101 14:41:30,599 INFO [STDOUT] setTonnage( ) 14:41:30,609 INFO [STDOUT] ejbStore( ) pk=101 14:41:30,659 INFO [STDOUT] ejbLoad( ) pk=101 14:41:30,669 INFO [STDOUT] setCapacity( ) 14:41:30,679 INFO [STDOUT] ejbStore( ) pk=101 14:41:30,709 INFO [STDOUT] ejbFindByPrimaryKey( ) primaryKey=101 14:41:30,729 INFO [STDOUT] ejbLoad( ) pk=101 14:41:30,750 INFO [STDOUT] getName( ) 14:41:30,750 INFO [STDOUT] ejbStore( ) pk=101 14:41:30,780 INFO [STDOUT] ejbLoad( ) pk=101 14:41:30,790 INFO [STDOUT] getTonnage( ) 14:41:30,800 INFO [STDOUT] ejbStore( ) pk=101 14:41:30,840 INFO [STDOUT] ejbLoad( ) pk=101 14:41:30,850 INFO [STDOUT] getCapacity( ) 14:41:30,860 INFO [STDOUT] ejbStore( ) pk=101 14:41:30,880 INFO [STDOUT] ejbLoad( ) pk=101 14:41:30,900 INFO [STDOUT] ejbStore( ) pk=101 14:41:30,910 INFO [STDOUT] ejbRemove( ) pk=101
Now, in addition to the ejbStore( )
calls
you’ve already seen, you see calls to
ejbLoad( )
at the start of each new transaction,
and the call to ejbFindByPrimaryKey( )
as well,
which reaches the bean implementation because it cannot be resolved
within the cache.
As you have seen during the execution of the client application, the
Ship bean performs many ejbLoad( )
and
ejbStore( )
operations. There are two reasons
behind this behavior:
Many transactions are started.
The Ship bean BMP code is not optimized.
You can reduce the number of transactions in several ways:
Define less fine-grained methods that return all attributes of the bean in a single data object.
Add a new create method with many parameters, so a single call can create and initialize the bean.
Use the Façade pattern: create a stateless session bean that starts a single transaction, then performs all the steps in that one transaction.
Start a transaction in the client application, using a
UserTransaction
object.
BMP code optimization is a wide topic. Here are some tricks that are frequently used:
Use an isModified
flag in your bean. Set it to
true
each time the state of the bean changes (in
set methods, for example). In the implementation of
ejbStore( )
, perform the actual database call only
if isModified
is true
. Think
about the impact on the test application. All the
ejbStore( )
calls resulting from invocations to get
methods will detect that no data has been modified and will not try
to synchronize with the database.
Detect which fields are actually modified during a transaction and
update only those particular fields in the database. This tactic is
especially useful for beans with lots of fields or with fields that
contain large amounts of data. Contrast with the Ship BMP bean as
it’s currently written, where each
setXXX( )
call updates all fields of the database
even though only one actually changes.
Note that any decent CMP engine performs many of these optimizations by default.
Once you’ve run all the tests, clean the database
environment associated with the BMP bean by removing the unused
table. Use the dropdb_91
target:
C:workbookex9_1>ant dropdb_91 Buildfile: build.xml prepare: compile: dropdb_101: [java] Dropping database table...
On the JBoss side, the BMP bean logs the following lines:
... 14:40:34,339 INFO [STDOUT] Dropping table SHIP... 14:40:34,349 INFO [STDOUT] ...done! ...
[74] We are speaking about the application server cache, not the database cache. While database caches are critical to performance, application server caches can improve it even further.
[75] The difference between commit option B and C is very small: when a transaction commits, a container using commit option C must effectively throw away the bean instance while a container using commit option B may keep it and reuse it later. This distinction allows commit option B to be used for very specific container optimizations (such as checking whether the data has really been modified in the database and reusing the instance if no modification has occurred, instead of reloading the whole state).