$SOMETHING
in a Unix shell, are %SOMETHING% at the DOS prompt
cp is copy on Windows.
The other commands used, mkdir, etc., work on Windows
as they are.
Interest. 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.
java -versionShould give a version number, not a `file not found' message. Check that the
javac and jar commands can also be invoked
correctly.
jboss-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
$ 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 client directory
under JBOSS_HOME. In this directory are all the JAR files containing
the EJB API classes, as well as the classes that support EJB test
clients. I will call this directory LIBDIR in the rest of this article.
You might want to set an environment variable to point to this
directory to make the commands quicker to type (some will be very
long).
src for source code, target for compiled classes,
etc for configuration files, and assemble for the
generated JAR files.
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.
target directory has two subdirectories:
server and client. The shared code will
appear in both these directories when it is compiled.
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
src/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;
}
}
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.
-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.
ejb-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.
ejb-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.
JBOSS_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.
InterestClient, 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.
src/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.
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).
interest-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.
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.
12:05:25,513 INFO [STDOUT] Someone called calcInterest()Indicating that the
calcInterest() method has been run
on the EJB server.
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.