|
©1994-2007 Kevin Boone | ||||||||||||||||||||||
|
Home > Computing > Sofware development
First steps with Enterprise JavaBeans using jBoss 3.2
Last modified: Fri Aug 3 08:28:40 2007
This article presensts a step-by-step tutorial on
how to set up jBoss, the open-source
Enterprise JavaBeans (EJB) server, and then create, deploy and test your first
EJB component. It doesn't explain what EJBs
are, or how they are used; there are a number of good introductory articles on
Sun's JavaSoft Web site. And, of course,
I can't resist the temptation to recommend my own book on the subject:
Applied Enterprise JavaBeans technology, published by
Prentice Hall. In addition, this article skips over a lot of complexity,
by making the EJB itself as simple as it could possibly be. To speed things
up, we will allow jBoss to use default settings for lots of things that you
would probably want to change in a real project.
This article is based on a development technique that uses simple, command-line tools and a text editor, nothing more. There are, arguably, easier and faster ways to implement EJB projects. However, I hope that by doing it this way it will be absolutely clear exactly what is going on. Nothing is hidden. In a real project you probably wouldn't want to create your own deployment descriptors using a text editor, but it is a good way to understand the details in a simple example such as this. In practice, you wouldn't want to type all these commands every time you use them, even if you favour this method of development -- you'd probably put them in a shell script or an Ant build script. A note for Microsoft Windows usersThis article focuses on the use of command-line tools. Using such tools probably comes more naturally to a Unix developer than to a Windows developer. The command-lines used below are appropriate for a Unix system. However, they will all work on the Windows DOS prompt as well. You'll just need to remember the following translations.
About the EJBIn this article we will create the simplest of simple EJBs: a stateless session bean with one method. The purpose of this EJB is to calculate compound interest on sums of money, and is therefore calledInterest. The one method provided
has the following signature:
double calcInterest(double principal, double rate, int periods);We will package this EJB into its own JAR file, and deploy that JAR file on jBoss. We'll then write a test client to run the EJB's single method within the jBoss EJB container. Preliminary checksEnsure that you have a modern (1.4.1 or later) java JDK, and you can run its tools at the command line. For example:java -versionShould give a version number, not a `file not found' message. Check that the javac and jar commands can also be invoked
correctly.
Step 1: download and install jBossYou can get jBoss from the jBoss Web site. At the time of writing, the latest version is 3.2.5. It is available in various package formats (tar, zip, bz2, etc); they are identical except for the packaging method. I'll assume that you have downloadedjboss-3.2.5.tar.gz.
Installation consists of unpacking the download into a suitable directory, which I shall call JBOSS_HOME in this article $ cd $JBOSS_HOME $ tar xvfz jboss-3.2.5.tar.gz Step 2: start jBossJust do:$ cd $JBOSS_HOME/bin $ ./run.shor Unix, or > cd %JBOSS_HOME%\bin > run.baton Windows. A successful startup produces about two hundred lines of output, and finishes up something like this: 10:35:26,090 INFO [JkMain] Jk running ID=0 time=1/290 config=nullIf it doesn't get this far, something is wrong. Most likely it will be that an IP port number jBoss uses is already in use by something else, or the Java version is not recent enough. Information on configuring these settings can be found in the documentation on the jBoss Web site.
A particular important directory is the Step 3: create the source treeIf you're an experienced developer you'll no doubt have your own ideas on how to structure a source tree. The structure I use here has worked for me for several years, and is particularly appropriate where the sharp distinction between server-side code and client-side code has to be maintained. If you don't want to create the structure and type in all the code yourself, you can download the source package here. When unpacked, this creates the structure discussed below.At the top level of the source tree we will have the following directories: src for source code, target for compiled classes,
etc for configuration files, and assemble for the
generated JAR files.
The source ( src) directory has three subdirectories:
server, client, and shared.
server is for classes that will only be
deployed onto the server; client is only for the test
client; shared is for classes that are required by both
the server code and the test client. In this case, the shared code will
be the home interface and component interface (what we used to call
the `remote interface') of the EJB. The server needs these interfaces
to build the supporting infrastructure for the EJB; the client needs
them because they expose the EJB's callable methods.
The target directory has two subdirectories:
server and client. The shared code will
appear in both these directories when it is compiled.
The EJB's classes will be in the Java package com.kevinboone.ejb,
so you'll need to create a matching directory structure under
src/server, src/client, and src/shared.
In short, the source tree has the following structure:
src
server
com
kevinboone
ejb
shared
com
kevinboone
ejb
client
com
kevinboone
ejb
etc
target
server
META-INF #see below for why we need this!
client
assemble
Step 4: create the EJB interfaces and implementation classThe home and component interfaces will go into the directorysrc/shared/com/kevinboone/ejb, as they are
shared between the server and the test client. Create
them with your favourite text editor. Here is the
home interface -- note that it exposes only one create()
method. Clients will use this method to tell the EJB container to
create new instances of the EJB. This file should be called
InterestHome.java.
package com.kevinboone.ejb;
import javax.ejb.*;
import java.rmi.*;
/*
(Remote) home interface for the Interest EJB
*/
public interface InterestHome extends EJBHome
{
// All home interface methods must be declared to
// throw CreateException; conventionally the
// implementation will throw this if the EJB
// instance cannot be created.
public Interest create()
throws CreateException, RemoteException;
}
And here is the remote interface -- it also exposes only
one method, the calcInterest() method that clients
will call. This file should be called
Interest.java
package com.kevinboone.ejb;
import javax.ejb.*;
import java.rmi.*;
/*
(Remote) component interface for the Interest EJB
*/
public interface Interest extends EJBObject
{
// Note that all business methods must be declared
// to throw a RemoteException. You'll get this
// exception in your client if there is a failure
// in the EJB infrastructure
public double calcInterest
(double principal, double rate, int periods)
throws RemoteException;
}
The bean implementation class goes in
directory
src/server/com/kevinboone/ejb, as it appears
only on the server, never the client. Call the file
InterestBean.java.
package com.kevinboone.ejb;
import javax.ejb.*;
/*
Implementation class for the Interest EJB
*/
public class InterestBean implements SessionBean
{
// The following methods are just dummies in this
// simple example; they don't have to do anything
public void ejbCreate() {}
public void ejbActivate() {}
public void ejbPassivate() {}
public void ejbRemove() {}
public void setSessionContext(SessionContext sc) {}
// calcInterest()
public double calcInterest (double principal, double rate, int periods)
{
// Write some output to the server log, so we know
// the method has been called
System.out.println ("Someone called calcInterest()");
double result = principal;
// Do the math -- accountants please don't write in :)
for (int i = 0; i < periods ; i++) result = result * (1 + rate);
return result;
}
}
Step 5: compile the server-side codeAt this point, our source tree should look like this:
src
server
com
kevinboone
ejb
InterestBean.java
shared
com
kevinboone
ejb
Interest.java
InterestHome.java
client
com
kevinboone
ejb
etc
target
server
META-INF #see below for why we need this!
client
assemble
We now need to compile the server-side code into the directory
target/server. Here's how:
javac -classpath $LIBDIR/jboss-j2ee.jar \ -d target/server \ src/shared/com/kevinboone/ejb/*.java \ src/server/com/kevinboone/ejb/*.javaRemember to switch the forward slashes for Windows, and put the whole thing on one very long line. The file jboss-j2ee.jar. that we're including
on the classpath contains the API definitions, that is, the code for
interfaces such as javax.ejb.SessionBean. The compiler
won't get very far if you try to compile an EJB without this.
With this command we're compiling the shared and server classes together, and using the -d switch to put the output
in the right place. We should now have a source tree that looks like this:
src
server
com
kevinboone
ejb
InterestBean.java
shared
com
kevinboone
ejb
Interest.java
InterestHome.java
client
com
kevinboone
ejb
etc
target
server
META-INF #see below for why we need this!
com
kevinboone
ejb
Interest.class
InterestHome.class
InterestBean.class
client
assemble
Note that the target/server directory now contains the
interfaces and the implementation class together.
Step 6: create the deployment descriptorThe deployment descriptor must be calledejb-jar.xml
(that what it says in the EJB Spec, not a jBoss restriction).
I suggest creating it in the etc directory of the
source tree. The deployment descriptor should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<!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>Interest</ejb-name>
<home>com.kevinboone.ejb.InterestHome</home>
<remote>com.kevinboone.ejb.Interest</remote>
<ejb-class>com.kevinboone.ejb.InterestBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
</ejb-jar>
The deployment descriptor simply indicates to the server the names of
the classes and interfaces that make up the EJB, the name of the EJB,
and some basic management properties. In a real EJB you could expect
to find transaction properties, security properties, container-managed
relationships, persistence settings, and all sorts of other stuff,
but we don't need any of that today.
Step 7: build the deployable JAR fileWe're now in a position to construct the JAR file for deployment on the server. First, copy the deployment descriptorejb-jar.xml
into target/server/META-INF.
Then:
$ cd target/server $ jar cfv ../../assemble/interest.jar . $ cd ../..The full-stop (.) on the end of the jar command means
`include everything in the current directory'. This command will leave
the JAR file in assemble. If you inspect this JAR,
for example by running
jar tf assemble/interest.jarYou should see that the JAR has the following structure: META-INF/ META-INF/MANIFEST.MF ./ com/ com/kevinboone/ com/kevinboone/ejb/ com/kevinboone/ejb/InterestHome.class com/kevinboone/ejb/Interest.class com/kevinboone/ejb/InterestBean.class META-INF/ META-INF/ejb-jar.xmlIt must have this structure; that is, the directory paths must match the Java package names, and ejb-jar.xml must
be in META-INF. This, again, is a requirement of the
Specification and is intended to make life easier for the EJB
container. It has enough to do as it is, without hunting around for
class files.
Step 8: deployYou're now ready to deploy the JAR on jBoss. This is very easy; simply copy the new JAR file into jBoss's deployment directory. The deployment directory is configurable; if you haven't changed it, it isJBOSS_HOME/server/default/deploy.
So deployment is no more than this:
$ cp assemble/interest.jar $JBOSS_HOME/server/default/deploy/After a second or two, jBoss should spring into life, and you should see messages in its console to the effect that it has detected and deployed a new JAR. 11:36:31,533 INFO [EjbModule] Deploying Interest 11:36:31,824 INFO [EJBDeployer] Deployed: file: interest.jarWhen you deploy, jBoss automatically checks the structural integrity and Specification compliance of the JAR file. If this check fails, the JAR has not been deployed. The error messages are relatively helpful; for example: Bean : Interest Method : public abstract double calcInterest(double, double, int) throws RemoteException Section: 7.10.5 Warning: The methods defined in the remote interface must have a matching method in the bean's class with the same name and same number and types of arguments.Not only has jBoss detected a violation of the EJB Specification, it has indicated which section of the Specification to look at (7.10.5 in this case). If you see messages like this, you need to correct the code. Step 9: writing the test clientThe test client will be a classInterestClient, also
in package com.kevinboone.ejb. The source should
go into the directory src/client, as this code is
client-side only. So, we need a file InterestClient.java, in
src/client/com/kevinboone/ejb.
package com.kevinboone.ejb;
import javax.ejb.*;
import java.rmi.*;
import javax.rmi.*;
import javax.naming.*;
public class InterestClient
{
public static void main (String[] args) throws Exception
{
Context c = new InitialContext();
Object o = c.lookup ("Interest");
InterestHome ih = (InterestHome)
PortableRemoteObject.narrow (o, InterestHome.class);
Interest i = ih.create();
double amount = i.calcInterest(1000, 0.1, 2);
System.out.println ("Result is: " + amount);
i.remove();
}
}
In this client, we do a lookup for the EJB called Interest.
For future reference it is important to realize that the name
Interest here does not have to match the name
you supplied in the deployment descriptor:
<ejb-name>Interest</ejb-name>jBoss defaults to allowing clients of EJBs to use the ejb-name
tag as a lookup name; most EJB products don't. Typically you'll
need to create a separate XML file that denotes the names that clients
will use for EJBs (`JNDI names' in EJB jargon). jBoss does let you do
it this way as well, but in this simple example there is no
potential for name clashes, so we don't need to.
Note also that this horrible construct:
InterestHome ih = (InterestHome)
PortableRemoteObject.narrow (o, InterestHome.class);
is required for compatibility when the server-side EJB infrastructure is
not from the same vendor as the client-side EJB infrastructure.
In practice we could have got away with the much less ugly:
InterestHome ih = (InterestHome)o;but structly speaking this violates the Specification. Step 10: compile the test clientNow we'll compile the test client. Because it uses the EJB's home and component interfaces, which are shared code, we'll need to compile in thesrc/shared directory
as well. So the full command is:
javac -classpath $LIBDIR/jboss-j2ee.jar \ -d target/client \ src/shared/com/kevinboone/ejb/*.java \ src/client/com/kevinboone/ejb/*.javaThis will put the test client compiled class, and the compiled classes for the home and components interfaces, into target/client.
Step 11: create the JNDI properties fileThe test client we are creating is entirely separate from the server. It will run in a separate JVM, perhaps on a different physical host (this is, of course, possible -- EJB is a distributed architecture after all). The client uses JNDI to find the naming server that can give it a reference to the EJB's home object, but the JNDI implementation does not know where the naming server is, or what protocol it uses. In practice, the naming server is likely to be the same physical host as an EJB server, but the JNDI system does not know where the EJB server is either. This complication does not arise when you are building a self-contained application (perhaps with servlets and JSPs as well as EJBs), because if you deploy the whole application in one go, the server always knows where everything is (because you've just deployed the entire system). With a stand-alone client, you have to give the JNDI implementation a bit of help.There are various ways to tell the JNDI system how to talk to the naming server. You can do it in code, but passing arguments to the constructor of InitialContext. Or you could specify
the relevant properties as system properties to the client JVM.
In this example, I will use a jndi.properties file.
Java's standard JNDI implementation will always search the classpath
to find a file with this name. It should contain at least two
properties: the name of a class that it can use to communicate with
the naming server, and the URL of the naming server. Both these
properties are vendor-specific, and you'll need to consult the
server documentation to find out what is required for a specific
product. For jBoss, we need something like this:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.provider.url=jnp://localhost:1099I'd suggest putting this jndi.properties file into
etc along with the deployment descriptor. The
`localhost' in this file indicates that the test client will
look for a name server on the same host as itself; if you want
to try running the EJB across a network, you'll need to substitute
the jBoss server's hostname in here instead. Note that
jnp, it the `jBoss naming protocol'. It is specific
to jBoss. Other products use other protocols, but for cross-platform
compatibility all must be able to use the CORBA IIOP (Internet Intra-Orb
Protocol). JNP is better for this example, because it supports
dynamic stub downloading (more of which later).
Step 12: package the test clientYou don't need to package the test client classes into a separate JAR, but it's convenient to do so, especially if you plan to run the test client on a separate physical host to the server (and this is, of course, possible -- EJB is a distributed architecture after all). So, let's build ainterest-client.jar in assemble like
this:
cp etc/jndi.properties target/client cd target/client jar cfv ../../assemble/interest_client.jar . cd ../..So, finally, our source tree looks like this:
src
server
com
kevinboone
ejb
InterestBean.java
shared
com
kevinboone
ejb
Interest.java
InterestHome.java
client
com
kevinboone
ejb
etc
ejb-jar.xml
jndi.properties
target
server
META-INF #see below for why we need this!
com
kevinboone
ejb
Interest.class
InterestHome.class
InterestBean.class
client
com
kevinboone
ejb
Interest.class
InterestHome.class
InterestClient.class
assemble
interest.jar #server package
interest-client.jar #client package
We've ended up with two JAR files: one containing classes
relevant to the server, plus shared classes; the other containing
the test client, plus shared classes.
Step 13: run the clientSo we come to the moment of truth: can we run the deployed EJB with our test client? The complication here is that the client needs a whole heap of supporting classes on its classpath. We didn't worry about this when deploying the EJB JAR file, because its classpath is the classpath of the jBoss server, which we assume is already correctly set up. But the client is a different matter: it needs to have access to the jBoss JNDI classes, stub downloading mechanism, network protocols, etc., as well as to the EJB API classes. Consequently we need a very long classpath to run the client. The command looks like this:java -classpath assemble/interest_client.jar:\ $LIBDIR/jnp-client.jar:\ $LIBDIR/jboss-common-client.jar:\ $LIBDIR/jboss-j2ee.jar:\ $LIBDIR/jboss-net-client.jar:\ $LIBDIR/jbossall-client.jar \ com.kevinboone.ejb.InterestClientIt should be obvious why I suggested defining an environment variable LIBDIR earlier in this article. If you don't you have an awful lot of typing. If it works, you should see some output from the client showing that it has calculated the amount of interest. More importantly, if you look in the console running the jBoss server, you should see something like this: 12:05:25,513 INFO [STDOUT] Someone called calcInterest()Indicating that the calcInterest() method has been run
on the EJB server.
Step 14: sit back and think about what you've doneIn this example we compiled and deployed a simple distributed application. It was distributed because the Java class called was running in one process, and the Java class doing the calling was in another. You may even have had the caller and the target class on different physical hosts, and made the method call between them. But you did not need to write any networking code, or even take account of networking when coding the EJB. All the network stuff is abstracted away.When the client runs this code: Interest i = ih.create(); double amount = i.calcInterest(1000, 0.1, 2);The entity i must be a Java object, because you can't
call methods on anything except objects. Yet Interest
is an interface, not a class. So what is it that implements
the interface Interest? It isn't InterestBean
(look at the code again, if you don't believe this). So we appear to
have an interface that isn't implemented by anything, but can still
methods called on it. The reality is that Interest is
implemented by a stub, that is, a piece of communications code
generated by the EJB server. jBoss built the stub for your Interest
EJB when you deployed the EJB itself. The stub contains all the same
methods as the EJB itself (or, at least, all those that are exposed
to clients), but no business logic. When the client calls
calcInterest() it is really calling this method on the
stub; the stub then talks to the EJB server, which calls the
calcInterest() method on InterestBean. The
stub and the server organize the passing of arguments and return
values over the wire.
But how did the stub get onto the client in the first place? Remember that the client and the server could be on different hosts. When you deploy the EJB, the stub gets built on the server, but how does it get to the client? Well, either you can copy the stub onto the client yourself, or you can rely on the magic of dynamic stub downloading. When the client uses the jBoss built-in naming protocol, the stub is automatically transferred to the client as part of the name-lookup operation. This means, in effect that the whole of the EJB network infrastructure is completely invisible to the developer.
|
|
|||||||||||||||||||||