This article takes a simple-minded stand-alone application, and incrementally evolves it to gently introduce the most interesting APIs of J2EE. The coverage includes:
// Select from (0 when done): // 1 - Design Patterns by GOF // 2 - Applying UML and Patterns // 3 - C++ for Deadend Careers // 4 - Java for Resume Inflation // 5 - Practical Java // Choice: 1 // Choice: 3 // Choice: 5 // Choice: 0 // The following items were ordered: // Design Patterns by GOF // C++ for Deadend Careers // Practical JavaA fundamental OO design principle is: never "name" a concrete class, always use an interface type when declaring an argument or return type. [ 2 ] [ 3 ] An additional step of decoupling commonly practiced is the use of factory methods for instantiating objects. [ 4 ] Whenever the new operator is used to create an object, the exact implementation class must be hard-wired in the code. Factory methods provide the indirection necessary to break this coupling.
"All constructors should be private or protected. The only way to create instances of a class should be through factory methods. It's nobody's business but the class' whether a class manufactures a new object or recycles an old one." [ 5 ]
The use of factory methods becomes even more desirable in distributed systems. Factory methods can encapsulate the logic for identifying the desirable concrete class to instantiate, and where that object should be hosted on the network. They can also be used to hide the use of back-end persistence mechanisms. [ 6 ]
Given the design heuristics of interfaces and factory methods, and anticipating the desirable demonstration points of future increments, the implementation in below produces the previous output.
interface IStore { ArrayList getItems(); String placeOrder( ArrayList list ); } class Factory { public static IStore createStore() { return new StoreImpl(); } } public class StoreClient { public static void main( String[] args ) throws IOException { BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) ); ArrayList theChoices = new ArrayList(); int choice; // Get a Store object somehow (create, load, look up) IStore theStore = Factory.createStore(); // Download and present the list of // current titles ArrayList theItems = theStore.getItems(); System.out.println( "Select from " + "(0 when done):" ); Iterator it = theItems.iterator(); for (int i=1; it.hasNext(); i++) System.out.println( " " + i + " - " + it.next() ); // Solicit the customer's choices while (true) { System.out.print( "Choice: " ); if ((choice = Integer.parseInt( in.readLine() )) == 0) break; theChoices.add(new Integer(choice-1)); } // Place the order with the Store, AND, // print the returned confirmation // statement System.out.println( theStore.placeOrder( theChoices ) ); } }The implementation of the IStore interface above is spelled out below. The Item class doesn't do much yet, but when we get to Entity Beans, it will take on some fairly interesting responsibilities. The placeOrder() method indexes into an array of Item objects and implicitly calls toString() on each one as it formats a confirmation String.
class Item { private String name; public Item( String na ) { name = na; } public String toString() { return name; } } class StoreImpl implements IStore { private ArrayList itemList = new ArrayList(); public StoreImpl() { itemList.add( new Item( "Design Patterns by GOF" ) ); itemList.add( new Item( "Applying UML and Patterns" ) ); ... } public ArrayList getItems() { return itemList; } public String placeOrder( ArrayList list ) { StringBuffer sb = new StringBuffer(); sb.append( "The following items were " + "ordered:\n" ); for (Iterator it = list.iterator(); it.hasNext(); ) { sb.append( " " ); sb.append( itemList.get( ((Integer)it.next()).intValue() ) ); sb.append( '\n' ); } return sb.toString(); } }[The complete application can be found in listing 1.]
Additionally, the methods defined in a "remote" class must all be amended to throw java.rmi.RemoteException. This is a proactive convention that anticipates the inhospitable environment presented by the network.
class Item implements java.io.Serializable { ... } interface RIStore extends java.rmi.Remote { ArrayList getItems() throws RemoteException; String placeOrder( ArrayList list ) throws RemoteException; }The ability to project method invocations across the network requires a significant amount of effort. This includes: marshalling arguments and return values, segmenting each request into packets, guaranteeing the delivery of these packets, reconstructing packets back into a "virtual function call" on the receiving end, and going through the inverse processing to return the results to the caller. That functionality has been implemented in the class java.rmi.server.UnicastRemoteObject, and is simply reused by inheriting from this framework class.
In order for servers and clients to find each other, a third party is needed called a "naming service". Sun provides a standard Naming class front-end and the free "rmiregistry" executable back-end. A physical server object is registered in the naming service under a logical string using the static method rebind(). Clients can then request a remote proxy to the server object by invoking the static method lookup() with the same logical string. Because the remote server object may exist on a different machine, Sun has chosen to use the form of a standard URL for communicating the location of the distributed object to the naming service.
// Publishing and finding remote RMI servers. // Server implementation class StoreImpl extends UnicastRemoteObject implements RIStore { ... } // Server main() try { Naming.rebind( "StoreServer", new StoreImpl() ); System.out.println( "StoreServer bound in " + "rmiregistry" ); } catch (MalformedURLException ex ) { ex.printStackTrace(); } catch (RemoteException ex ) { ex.printStackTrace(); } // Client main() RIStore theStore = (RIStore) Naming.lookup( "rmi://127.0.0.1/StoreServer" );Because of the magic of client-side "stub" code and server-side "skeleton" code, the remainder of the StoreImpl and StoreClient implementations remain the same. This "stub" and "skeleton" code is boiler-plate in form, and can be mechanically generated. That is the role of the Sun-supplied "rmic" utility.
The output below demonstrates the requisite build and execute steps. Notice that three different Command Prompt windows are required: the "rmiregistry" window, the server window, and the client window.
// RMI build and execute. D:\j2ee\rmi> rmiregistry D:\j2ee\rmi> javac StoreImpl.java D:\j2ee\rmi> rmic StoreImpl D:\j2ee\rmi> java StoreImpl StoreServer bound in rmiregistry Design Patterns by GOF Applying UML and Patterns Design Patterns by GOF D:\j2ee\rmi> javac StoreClient.java D:\j2ee\rmi> java StoreClient Select from (0 when done): 1 - Design Patterns by GOF 2 - Applying UML and Patterns 3 - C++ for Deadend Careers 4 - Java for Resume Inflation 5 - Practical Java Choice: 1 Choice: 2 Choice: 1 Choice: 0 The following items were ordered: Design Patterns by GOF Applying UML and Patterns Design Patterns by GOF[The complete application can be found in listing 2.]
Sun's original RMI naming service does not support: a hierarchical naming space, federation of existing name spaces, or integration of legacy naming and directory services. JNDI handles all of these.
To use JNDI, the remote class replaces its inheritance relationship to the class UnicastRemoteObject with the class PortableRemoteObject. Additionally, it replaces its invocation of the static method rebind(), with a call to the instance method rebind() defined on the InitialContext class. This class implements the Context interface and provides the starting point for resolution of names. A "context" object is like a directory - it consists of a set of name-to-object bindings.
You will notice that RMI's use of a URL "name" argument has been retired. Instead, a tree-like naming hierarchy can be built up by using simple Strings, and binding many child "context" objects to a parent "context" object.
The "stub" and "skeleton" remote proxy code is still generated by the "rmic" utility. But the argument "-iiop" is required, because JNDI uses CORBA's Internet Inter-ORB Protocol as its transport mechanism.
// JNDI on the server. // Server implementation import java.rmi.*; // Remote, RemoteException import javax.rmi.*; // PortableRemoteObject import javax.naming.*; // InitialContext class StoreImpl extends PortableRemoteObject implements RIStore { ... } // Server main() try { InitialContext ic = new InitialContext(); ic.rebind( "StoreServer", new StoreImpl() ); System.out.println( "StoreServer bound" + " in JNDI" ); } catch (NamingException ex ) { ex.printStackTrace(); } catch (RemoteException ex ) { ex.printStackTrace(); } // D:\j2ee\jndi> javac StoreImpl.java // D:\j2ee\jndi> rmic -iiop StoreImpl // D:\j2ee\jndi> java StoreImpl // StoreServer bound in JNDIThe transformation of the client application is very similar. One additional wrinkle is replacing the previous "brute force" downcast, with a much safer static method invocation. The narrow() method will ensure that the object being passed can indeed be cast to the desired type.
// JNDI on the client. // Client main() try { InitialContext ic = new InitialContext(); Object obj = ic.lookup( "StoreServer" ); RIStore theStore = (RIStore) PortableRemoteObject.narrow( obj, RIStore.class ); } catch (NamingException ex ) { ex.printStackTrace(); }One final detail is the new naming server. With RMI, the naming server is "rmiregistry". With JNDI, the server is the J2EE reference implementation server.
[The complete application can be found in listing 3.]
The servlet runs as a single instance hosted by a Web "container" that is running on the server machine. Each client that wants to use the services of a servlet is vectored to a servlet instance by the Web container, and it shares the services of this servlet object with possibly many other clients. This architectural introduction is necessary because servlets exhibit the same default "stateless" behavior as the HTTP protocol.
In our application, we would like "stateful" behavior. As the user selects books, we would like to keep a per-user record of these selections, and then when '0' is entered, the list of accumulated selections is passed in the placeOrder() invocation. Fortunately, servlets provide a per-user "session" object that programmers can use as a general purpose "state" repository. We'll return to this issue in just a bit.
Like applets, servlets have a base class that performs all their "heavy lifting". Each servlet simply inherits from this base class. The servlet state that can be "shared" by all the multiplexed clients can be implemented as attributes of the servlet class. In this application, that consists of the reference to the remote Store, and the list of current titles (listing 1). Servlets are not initialized with constructors, they are initialized with an init() method that is invoked automatically by the Web container. All of our JNDI code, and our getItems() request can go in this method.
LISTING 1 - Servlet attributes and initialization. public class StoreClientServlet extends javax.servlet.http.HttpServlet { private RIStore theStore; private ArrayList theItems; public void init( ServletConfig sc ) { try { super.init( sc ); InitialContext ic = new InitialContext(); Object obj = ic.lookup( "StoreServer" ); theStore = (RIStore) PortableRemoteObject.narrow( obj, RIStore.class ); theItems = theStore.getItems(); } catch (ServletException ex) { ex.printStackTrace(); } catch (NamingException ex ) { ex.printStackTrace(); } catch (java.rmi.RemoteException ex ) { ex.printStackTrace(); } }HTML pages and forms generate two kinds of requests: get, and post. Servlets have an entry point (i.e. method) for each: doGet(), and doPost(). For most purposes, they can be treated interchangeably; so we'll have one call the other, and we won't have to duplicate any functionality (listing 2).
A servlet needs to access the input coming from the user, and generate output destined for the user. That is the purpose of the two arguments specified in the doGet() and doPost() methods. The "request" object encapsulates the input parameters and the "session" capability. The "response" object encapsulates the output stream for all the dynamic HTML the servlet chooses to send back to the user.
LISTING 2 - Servlet's HTTP entry points. public void doPost( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { doGet( request, response ); } public void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { ... }To communicate with the user, the servlet must generate HTML. The conduit for getting that HTML to the right socket on the Internet is the PrintWriter object encapsulated in the "response" object. All the Strings that are output to the PrintWriter object will be interpreted in the user's browser as HTML. A discussion of HTML is beyond the scope of this article. The code in listing 3 will produce the screen dump in figure 1. How a URL maps to a particular servlet is vendor specific, and is not discussed here.
LISTING 3 - Servlet HTML code. public void doGet( HttpServletRequest request, HttpServletResponse response ) { PrintWriter pw = response.getWriter(); pw.println( "<html><head><title>StoreClientServlet</title></head><body>" ); pw.println( "<form method=post action=StoreAlias>" ); pw.println( "Select from (0 when done):<ol>" ); Iterator it = theItems.iterator(); for (int i=1; it.hasNext(); i++) pw.println( "<li>" + it.next() ); pw.println( "</ol>Choice:" ); pw.println( "<input type=text name=choice>" ); pw.println( "<input type=submit value=Select>" ); pw.println( "</form></body></html>" );
When this is not the case, we know there is user input waiting to be retrieved in the HTML text input component called "choice". The contents of HTML input components can be retrieved by invoking getParameter() on the "request" object, and supplying the "name" assigned to the input component when it was created in HTML. If the user entered a non-zero value, then the designated selection needs to be remembered in the ArrayList object that is stored in the HttpSession object.
LISTING 4 - Servlet user input processing. HttpSession session = req.getSession(); // get the array of choices stored in the session object ArrayList theChoices = (ArrayList) session.getAttribute( "theChoices" ); // get the text input element named "choice" String choice = req.getParameter("choice"); // if this is the first req for this page if (theChoices == null) { theChoices = new ArrayList(); session.setAttribute( "theChoices", theChoices ); // remember the current selection } else if ( ! choice.equals("0")) { theChoices.add( new Integer( Integer.parseInt(choice) - 1 ) ); session.setAttribute( "theChoices", theChoices );If the user entered a zero value, then it is time to send the ArrayList of choices to the remote Store object by invoking the placeOrder() method (listing 5). The formatted confirmation string that is returned will have its white-space "mangled" if it is sent directly to a Web browser. For this reason, the private method formatOrderConfirmation() has been written. It converts: embedded carriage returns into "<br>" tags, and indentation into non-collapsible blank characters (figure 2).
When the user is done with this page, selecting the hyperlink called "Top" will cause flow-of-control to return to the very first page. At this point, we would like a new session to be instantiated for this user - so that the previous choices will be discarded. We can make this happen by "invalidating" the current session.
LISTING 5 - Servlet "place order" processing. // time to place the order } else { StringBuffer response = new StringBuffer( theStore.placeOrder( theChoices ) ); formatOrderConfirmation( response ); pw.println( response.toString() ); pw.println( "<p><a href=StoreAlias>Top</a></body>" ); session.invalidate(); return; }
LISTING 6 - Servlet "history" processing. // add an extra <UL> to display the history of previous choices if (theChoices.size() > 0) { pw.println( "<p>Selections are:<ul>" ); it = theChoices.iterator(); for (int i=1; it.hasNext(); i++) pw.println( "<li>" + theItems.get( ((Integer)it.next()).intValue()) ); pw.println( "</ul></form></body>" ); }
The HTML file in listing 7 demonstrates the basics of JSPs. It is standard HTML with Java code placed inside "<%" and "%>" delimiters. An additional feature demonstrated is the JSP expression. These are prefaced with a "<%=" delimiter, and allow the result of a Java expression to be "inlined" with HTML text. This file produces the screen dump in figure 4. [How a URL maps to a particular JSP is vendor specific, and is not discussed here.]
LISTING 7 - JSP simple example. <html><head><title>DemoJsp</title></head><body> The date is <%= new java.util.Date() %> <p> <% StringBuffer sb = new StringBuffer(); for (int i=1; i < 11; i++) { sb.append( i ); sb.append( ' ' ); } %> Some numbers are: <%= sb.toString() %> </body></html>
The first two lines in listing 8 are the beginning of our Store JSP file (the remainder of the code belongs to a .java source file). The second line demonstrates the syntax for creating an instance of a Java Bean. The name of the object will be "store", the class that will be instantiated is "article.jsp.StoreBean", and the scope of the object will be the same as in the previous servlet. The JSP engine (that is supplied with, or bolted on to, the Web server) will generate all the servlet code that developers used to have to write themselves.
LISTING 8 - JSP Store initialization. <head><title>StoreClientJsp</title></head><body> <jsp:useBean id="store" class="article.jsp.StoreBean" scope="session" /> package article.jsp; public class StoreBean implements java.io.Serializable { private RIStore theStore; private ArrayList theItems; private ArrayList theChoices; private String selection; public StoreBean() { theChoices = new ArrayList(); try { InitialContext ic = new InitialContext(); Object object = ic.lookup( "StoreServer" ); theStore = (RIStore) PortableRemoteObject.narrow( object, RIStore.class ); theItems = theStore.getItems(); } catch (NamingException ex ) { ex.printStackTrace(); } catch (java.rmi.RemoteException ex) { ex.printStackTrace(); } } }The code for the first screen dump in the servlet discussion is shown in listing 9. The top nine lines appear in the JSP file. The "jsp:getProperty" tag is new. This invokes a getItemList() method [note the "get" prefix and the use of camel-case capitalization] on the "store" object. The signature of a "get" method should accept no arguments, and return a type that will be inserted into the HTML stream.
The bottom seven lines implement getItemList() in the StoreBean class. Normally, presentation code is confined to an HTML file, and application processing functionality is partitioned in a Java Bean. This convention was bent in listing 9 - the "<li>" tag was put in the Java Bean getItemList() method to simplify the HTML file.
LISTING 9 - JSP user input prompting. <form method=post action=StoreJspAlias> Select from (0 when done): <ol> <jsp:getProperty name="store" property="itemList" /> </ol> Choice: <input type=text name=choice> <input type=submit value=Select> </form></body> public String getItemList() { StringBuffer sb = new StringBuffer(); Iterator it = theItems.iterator(); for (int i=1; it.hasNext(); i++) sb.append( "<li>" + it.next() ); return sb.toString(); }The next block in the previous servlet discussion deals with handling "choice" input from the user. The refactoring to a JSP implementation is given in listing 10. Because we chose to define a Java Bean in the previous step, we no longer need an HttpSession object to support retained state.
The "jsp:setProperty" tag is new. It is another point of leverage offered by JSP. We no longer need to invoke getParameter() methods on a servlet "request" object to retrieve URL and form parameters. We specify an HTML "widget" that should be mapped to a "set" method in our Java Bean [note the "set" prefix and the use of camel-case capitalization]. The signature of the "set" method should accept one String argument and return void. The top line in listing 10 goes in the JSP file, and causes the form parameter called "choice" to be mapped to the "str" argument of the setSelection() method of the "store" object.
LISTING 10 - JSP user input processing. <jsp:setProperty name="store" property="selection" param="choice" /> public void setSelection( String str ) { selection = str; if ( ! selection.equals("0")) theChoices.add( new Integer( Integer.parseInt(selection) - 1 )); }Continuing to march through the previous servlet discussion, the next chunk of functionality is placing the order with the remote Store object and displaying the confirmation string returned. In order to simplify the code that would otherwise be required in the JSP file, an isDone() abstraction was added to the Java Bean (see listing 11). If isDone() returns true, the next five lines in the JSP file are interpreted.
The first of those lines is another "jsp:getProperty" tag. This causes the getResponse() method to be invoked on the "store" Java Bean, and the method's return value to be inserted into the HTML stream. The final two instructions in the JSP file are embedded Java code. The JSP framework automatically creates an HttpSession object with the variable name "session". This is where the Java Bean object that was created with the "jsp:useBean" tag was stored. [You will recall it was declared to have "session scope" above.] The invalidate() request will cause the Bean to be released, so that its retained state is jettisoned.
LISTING 11 - JSP "place order" processing. <% if (store.isDone()) { %> <jsp:getProperty name="store" property="response" /> <p><a href=StoreJspAlias>Top</a></body> <% session.invalidate(); return; } %> public boolean isDone() { if (selection == null) return false; return selection.equals( "0" ); } public String getResponse() { StringBuffer response = null; try { response = new StringBuffer( theStore.placeOrder( theChoices ) ); } catch (java.rmi.RemoteException ex ) { ex.printStackTrace(); } // massage the returned String to work with HTML conventions formatOrderConfirmation( response ); return response.toString(); }The last remaining piece of the servlet to be converted is appending the "history" of previous selections. This was implemented with a JSP expression ("<%=" delimiter) strictly for demonstration purposes (listing 12). It could have just as well been implemented with a "jsp:getProperty" tag. The Java code that appears after the "<%=" is expected to return a value that can be inserted into the HTML stream. Again, HTML was embedded in the Java Bean to simplify the JSP file.
LISTING 12 - JSP "history" processing. <%= store.displayCurrentSelections() %> </form></body> public String displayCurrentSelections() { // add an extra <UL> to display the history of previous choices StringBuffer sb = new StringBuffer(); if (theChoices.size() > 0) { sb.append( "<p>Selections are:<ul>" ); for (Iterator it = theChoices.iterator(); it.hasNext(); ) sb.append( "<li>" + theItems.get( ((Integer)it.next()).intValue()) ); sb.append( "</ul>" ); } return sb.toString(); }[The complete application can be found in listing 5.]
EJB is the "component model" for the J2EE framework. Its goal is to offload Enterprise implementation issues like transaction processing, security, persistence, resource pooling, and scalability from the EJB component developer. The EJB app server (with its "container" abstraction) will manage these details for all its Enterprise Beans. But in order to do that, the container totally decouples each bean from the clients requesting its services. This decoupling (or "insulation") is the primary factor in all the architectural elements discussed in this section; and, significantly affects what EJBs will be allowed to do for themselves. For example, EJBs cannot:
The current implementation of our Store server maps nicely to a stateless Session Bean (SLSB) - the client maintains knowledge of previously selected Items, and the Store is only responsible for responding to getItems() and placeOrder() requests. In subsequent increments, we will: transform the Store into a stateful Session Bean (SFSB), and refactor the Item class into an Entity Bean.
Reworking the JNDI Store server as a SLSB will require remarkably little massaging. Every EJB has three architectural elements: a remote (or "business") interface, a home (or "factory") interface, and a bean implementation. The first of these is exactly what we defined for our original RMI demonstration. The only difference is the requirement to inherit from the javax.ejb.EJBObject interface, instead of the java.rmi.Remote interface.
interface RIStore extends Remote { --- becomes --- interface RIStore extends EJBObject {The "home" interface encapsulates what could be described as "life cycle" services: create, and find methods. Because the EJB framework will be responsible for generically creating, initializing, caching, and reusing any and all bean instances; an architecture had to be set up that the framework could use at deploy time (in conjunction with the Java reflection mechanism) to discover and characterize all "create" and "find" entry points.
"The home interface of all SLSBs contains one create() method with no arguments. This is a requirement of the EJB specification. It is illegal to define create() methods with arguments, because SLSBs don't maintain conversational state that needs to be initialized. There are no find() methods in Session Beans because Session Beans ... do not represent data in a database." [ 7 ] The following interface was added to our Store component.
// --- added --- interface HIStore extends EJBHome { public RIStore create() throws java.rmi.RemoteException, javax.ejb.CreateException; }The Store server no longer needs to inherit implementation from javax.rmi.PortableRemoteObject. That class' role was to provide the "remote procedure call" framework for distributed servers. With EJB, each bean is not the distributed server. Each bean is the incarnation of the business functionality the client is seeking, but the client never actually references the bean (or a bean proxy) directly. The app server's container implementation totally insulates the bean and the client from one another. The inheritance relationship in the JNDI implementation has been replaced with an aggregation-delegation relationship in the EJB architecture.
In order to realize this "collaboration at arm's length" between a client and the insulated bean, an interface is required - SessionBean in this case. This interface specifies the "technical contract" that the container requires of the bean, in order for the container to accomplish all its scalability and decoupling magic.
The Store server does not actually implement the remote interface anymore. This is because the previous "is a" relationship has now been replaced with a "has a" relationship inside the EJB app server's container. The programmer implementing the bean (not the compiler) is responsible for ensuring that all business methods are defined. As a final step in the development process, a vendor-specific "deployment tool" will ensure that all contracts and conventions have been satisfied.
class StoreImpl extends PortableRemoteObject implements RIStore { --- becomes --- public class StoreImpl implements SessionBean {Because the app server's container needs to be able to create, share, and reuse bean objects at will (in order to achieve its scalability goals), initialization of a bean instance is now the responsibility of the ejbCreate() method. This method is called by the create() method declared in the home interface above. The EJB framework is able to map the one to the other through the aggregation-delegation relationship introduced earlier. Note that the return type for create() is the remote interface, but the return type for ejbCreate() is void. This is the convention for Session Beans.
public StoreImpl() throws RemoteException { --- becomes --- public void ejbCreate() {All that remains on the server side is to provide implementations of the four methods mandated by the SessionBean contract. The first three are associated with "swapping" a potentially large number of virtual beans in and out of some scalable number of real objects. The last provides an access point for referencing "environment" information. These methods provide the mechanism for handling "state" information, so we'll revisit them when we discuss Entity Beans.
// Session Bean "technical" contract. // --- added --- from SessionBean interface public void ejbActivate() { System.out.println( "ejbActivate:" ); } public void ejbPassivate() { System.out.println( "ejbPassivate:" ); } public void ejbRemove() { System.out.println( "ejbRemove:" ); } public void setSessionContext( SessionContext ctx ) { System.out.println("setSessionContext:"); context = ctx; }We are now ready to discuss what changes are necessary in the client application. With RMI and JNDI, a server object either had to be explicitly started on the server, or "activated" as the result of a client request. For scalability reasons, we would rather hide these kinds of implementation details behind a "factory" interface. The factory abstraction is solely responsible for choosing when and where Session Bean objects are created, and how these objects are shared, swapped, and retired.
For this reason, the client of a Session Bean does not "look up" the server object. Instead, the client looks up the factory object. This object is generated by the EJB framework, but the JNDI name that the object is bound under is specified during the deployment process. After finding the factory object, the client is required to explicitly ask for a server object to be "created". This notion of "create" is entirely abstract. What the factory object does in response to this request is "nobody's business" but its own. [ 5 ]
// Remote server look up. Object obj = ic.lookup( "StoreServer" ); RIStore theStore = (RIStore) PortableRemoteObject.narrow( obj, RIStore.class ); --- becomes --- Object obj = ic.lookup( "StoreHome" ); HIStore storeHome = (HIStore) PortableRemoteObject.narrow( obj, HIStore.class ); RIStore theStore = storeHome.create();No other changes are required in the client application - the "remote store" abstraction functions just as it did previously.
[The complete application can be found in listing 6.]
The "business" interface will need to change. This is generally discouraged because of the impact it will have on the client community. But, considering the remarkable reduction in effort that will be afforded those clients, they will be only too happy to accept that relatively minor inconvenience. The changes to the remote interface include: add a selectItem() method, and remove the parameter to the placeOrder() method.
String placeOrder( ArrayList list ) throws RemoteException; --- becomes --- void selectItem( int number ) throws RemoteException; String placeOrder() throws RemoteException;Because the Session Bean now holds "state" information, there are several differences from the previous section that need to be highlighted.
// Migrating "state" to the server. public String placeOrder( ArrayList list) { for (Iterator it = list.iterator(); it.hasNext(); ) { // --- becomes --- public String placeOrder() { for (Iterator it = theChoices.iterator(); it.hasNext(); ) { // --- added --- public void selectItem( int number ) { theChoices.add( new Integer( number ) ); }In the client application, instead of calling add() on the ArrayList when a selection is made; selectItem() will be called on the remote Store server. Additionally, the call to placeOrder() no longer requires an argument.
// Migrating "state" off the client. theChoices.add( new Integer( choice-1 ) ); --- becomes --- theStore.selectItem( choice-1 ); System.out.println( theStore.placeOrder( theChoices ) ); --- becomes --- System.out.println( theStore.placeOrder() );There are quite a few subjects that have not been discussed here. These include: passivation, activation, transactions, concurrency, reentrancy. For the sake of limiting the length of this article, that information has not been included.
[The complete application can be found in listing 7.]
LISTING 13 - Client interaction, persistent data. D:\j2ee\cmp> java StoreClient Select from (0 when done): 1 - Design Patterns by GOF ..... $35 ... 1 2 - Applying UML and Patterns .. $30 ... 1 3 - C++ for Deadend Careers .... $5 ... 1 4 - Java for Resume Inflation .. $10 ... 0 5 - Practical Java ............. $20 ... 0 Choice: 1 Choice: 2 Choice: 1 Choice: 0 The following items were ordered: Design Patterns by GOF ..... $35 ... 2 Applying UML and Patterns .. $30 ... 2 Design Patterns by GOF ..... $35 ... 3 Total bill is $100 D:\j2ee\cmp> java StoreClient Select from (0 when done): 1 - Design Patterns by GOF ..... $35 ... 3 2 - Applying UML and Patterns .. $30 ... 2 3 - C++ for Deadend Careers .... $5 ... 1 4 - Java for Resume Inflation .. $10 ... 0 5 - Practical Java ............. $20 ... 0 Choice:The purpose of Entity Beans is to manage concurrent access to shared persistent data. There are two ways of handling the persistence: container managed persistence (CMP), and bean managed persistence (BMP). With the former, the app server's "container" capability writes all the SQL, and does all the work to store and retrieve Bean state from a database. With the latter, the Bean developer is responsible for all the SQL code. We will discuss CMP first.
An Entity Bean requires the same three architectural elements as a Session Bean: a remote (or "business") interface, a home (or "factory") interface, and a bean implementation. In listing 14, the remote interface declares a getSummary() method that will replace the previous toString(). This is to emphasize that the class will now be reporting the name, price, and totalOrdered for each object. An order() method has been added that will increment the totalOrdered attribute, and, will also return the price of the Item object.
The home interface's purpose is to provide a level of indirection capable of providing the same quality of decoupling that a factory affords (see the discussion in the "Starting point" section at the top of the article). Because Entity Beans represent persistent data, the client is expected to first search for the desired object, and if the object does not exist, then ask for the object to be created.
Each Entity Bean home interface must declare at least one "find" method - findByPrimaryKey(). This method shall take one argument - the type of the primary key that has been designed to uniquely identify each instance of the Entity Bean. At a minimum, the primary key must be a class that implements java.io.Serializable. If the type of the primary key is a primitive type (as is the case here), then that type must be "promoted" to its java.lang wrapper type equivalent. The method shall return the remote interface type.
Any number of "find" methods may be declared. The only restrictions are: the name of the method must provide the EJB framework sufficient insight so that it can infer a mapping from the find method's name to its corresponding implementation class attribute, and, each method must return either the remote interface type or the java.util.Collection type. An example would be - Collection findByPrice( Integer price ). The findAllInstances() method declared in listing 14 is a common (but optional) convention.
LISTING 14 - Item bean remote and home interfaces. interface RIItem extends EJBObject { public String getSummary() throws RemoteException; public int order() throws RemoteException; } interface HIItem extends EJBHome { RIItem create( Integer primaryKey, String name, int price ) throws RemoteException, CreateException; Collection findAllInstances() throws RemoteException, FinderException; RIItem findByPrimaryKey( Integer pk ) throws RemoteException, FinderException; }As with Session Beans, the bean implementation class does not directly implement the remote and home interfaces, it implements the EntityBean interface (listing 15). For container managed persistence, the attributes to be persisted must be public. This is because the EJB framework is doing the work of writing the attributes to persistent storage, and it does not enjoy any priviledged visibility into the bean implementation class.
LISTING 15 - Item bean implementation. public class ItemImpl implements EntityBean { transient private EntityContext context; public int id; public String name; public int price; public int numSold; public String getSummary() { return name + " $" + price + "..." + numSold; } public int order() { numSold++; return price; }The SLSB section introduced the requirement that there must be one ejbCreate() method in the bean implementation class for each create() method in the home interface. By convention: the return type of ejbCreate() is the type of the primary key, the argument list must match that of its corresponding create(), and the value returned is always null (listing 16).
When ejbCreate() is invoked by the container, the Entity Bean does not have a fully formed identity. If the Bean needed to reference that identity, or pass it to some other component, that should not be done in ejbCreate(). For these situations, an ejbPostCreate() method has been specified. Its argument list should match that of the corresponding ejbCreate(). Its return type is void. Its implementation is routinely empty, unless, the developer needs to do one the things just described.
LISTING 16 - Item bean initialization. public Integer ejbCreate( Integer pk, String nm, int pr ) { id = pk.intValue(); name = nm; price = pr; numSold = 0; System.out.println( "ItemImpl " + "ejbCreate: " + id ); return null; } public void ejbPostCreate( Integer pk, String nm, int pr ) { System.out.println( "ItemImpl " + "ejbPostCreate: " + id ); }The EntityBean interface (that the bean implementation class affiliates itself with) mandates the same four methods that were briefly discussed at the end of the SLSB section. In addition, it introduces three more methods. The first two, ejbLoad() and ejbStore() (listing 17) are associated with keeping each bean instance "synchronized" with its corresponding record in the underlying database table. Normally, the CMP implementation will automatically map and maintain the entire persistence mechanism. Only in cases where the complexity of the bean's attributes outstrip the CMP capability of your app server vendor, will the developer be obliged to define implementations for these methods.
The final "new" method is unsetEntityContext(). This is called by the EJB framework as the final response to an invocation of the remove() method. The developer is encouraged to assign null to the context attribute, for reasons similar to the practice of assigning null to a deallocated pointer in C. Returning to the methods shared with the SessionBean interface, ejbActivate() and ejbPassivate() are used to support "swapping" logical bean instances in and out of physical bean hosts (not unlike the symbiont-host relationship of the Trill race in Star Trek). These entry points should be used to close (and subsequently reopen) resources (e.g. a JDBC connection) that should not remain active while the bean is in "hibernation". Additionally, ejbActivate() should always initialize the bean class' primary key attribute.
The SLSB discussion briefly mentioned the SessionContext object. Entity Beans have a comparable EntityContext object. This object is created by the EJB framework before ejbCreate() is invoked. It provides access to elements of each bean's "environment" like: the associated EJBObject wrapper object, the associated EJBHome factory object, the primary key, transaction elements, and security information.
LISTING 17 - Item bean "technical" methods. // from SB and EB interface public void ejbActivate() { id = ((Integer)context.getPrimaryKey()).intValue(); System.out.println( "ItemImpl " + "ejbActivate: " + id ); } public void ejbPassivate() ... public void ejbRemove() ... public void setEntityContext( ... // from EntityBean interface public void ejbLoad() { System.out.println( "ItemImpl " + "ejbLoad: " + id ); } public void ejbStore() { System.out.println( "ItemImpl " + "ejbStore: " + id ); } public void unsetEntityContext() { System.out.println( "ItemImpl " + "setEntityContext: " + id ); context = null; }We are through implementing the "server" side of Item Entity Beans. It is now time to consider the "client" side. Entity Beans are fairly "fine grain" components. Allowing clients to access them directly across the network can quickly bring application and network performance to its knees. For this reason, it is highly recommended that clients should only interact with Session Beans, which in turn manipulate Entity Beans as appropriate.
The "client" of Item Entity Beans is the Store Session Bean. Its first order of business is to retrieve all current Item beans. This will involve acquiring a reference to an implementation of the bean's home interface, and requesting findAllInstances() (listing 18). If the Collection that is returned is empty, then no instances exist in the persistent store yet. The only option available at this point is to create a set of default instances.
LISTING 18 - Refactoring the Store bean. public void ejbCreate() { System.out.println( "ejbCreate:" ); try { InitialContext ic = new InitialContext(); Object obj = ic.lookup( "ItemHome" ); HIItem itemHome = (HIItem) PortableRemoteObject.narrow( obj, HIItem.class ); Collection coll = itemHome.findAllInstances(); if (coll.size() > 0) { for (Iterator it = coll.iterator(); it.hasNext(); ) { RIItem item = (RIItem) PortableRemoteObject.narrow( it.next(), RIItem.class ); itemList.add( item ); } } else { itemList.add( itemHome.create( new Integer(1), "Design Patterns by GOF .....", 35 )); itemList.add( itemHome.create( new Integer(2), "Applying UML and Patterns ..", 30 )); ... } } catch ...Store's itemList attribute no longer contains lightweight, serializable objects. Instead, it contains proxies to remote Entity Beans. We don't want the getItems() method returning this ArrayList object directly to its client, because that would entail the client making remote method invocations across the network every time it needed a price or total check.
We can address this issue by having getItems() return a Collection of "summary objects" - an abstraction that provides just enough state and behavior for the client to accomplish its responsibilities (without having to make round-trip requests across the network). [See listing 19.] In this simple design, the summary object is nothing more than a String. As a result, the client of the Store Session Bean doesn't change at all. Before the Item class became an Entity Bean, the collection of objects returned from getItems() each responded to an implicit invocation of toString() when it was passed to System.out.println(). Now, the collection of String objects returned are output directly to the client's standard out.
LISTING 19 - Returning "summary objects" to the client. public Collection getItems() { Collection summary = new ArrayList(); try { for (Iterator it = itemList.iterator(); it.hasNext(); ) summary.add( ((RIItem)it.next()).getSummary() ); } catch (RemoteException ex) { ex.printStackTrace(); } System.out.println( "\nreturning " + summary.size() + " items to client"); return summary; }The placeOrder() method in the Store Session Bean has changed slightly to accomodate the expanded functionality of Item objects (listing 20). Each Item Entity Bean maintains a persistent record of the total orders placed against it. This attribute is incremented by calling the order() method. Strictly for demonstration purposes, order() was also designed to simultaneously provide a "getPrice" capability.
LISTING 20 - Exercising Item bean's new leverage. public String placeOrder() { StringBuffer sb = new StringBuffer(); RIItem element; String summary; int totalBill = 0; System.out.println(); sb.append( "The following items were ordered:\n" ); try { for (Iterator it = theChoices.iterator(); it.hasNext(); ) { element = (RIItem) itemList.get( ((Integer)it.next()).intValue() ); totalBill += element.order(); summary = element.getSummary(); System.out.println( summary ); sb.append( " " ); sb.append( summary ); sb.append( '\n' ); } } catch (RemoteException ex) { ex.printStackTrace(); } sb.append( "Total bill is $" + totalBill + '\n' ); System.out.println( "returning " + theChoices.size() + " orders to client, bill is $" + totalBill + '\n' ); return sb.toString(); }[The complete application can be found in listing 8.]
Each of the entry points mandated and called by the EJB framework generally map to a single SQL statement -
ejbFindByPrimaryKey() | SELECT |
ejbFindAllInstances() | SELECT |
ejbCreate() | INSERT |
ejbLoad() | SELECT |
ejbStore() | UPDATE |
ejbRemove() | DELETE |
ejbActivate() | application specific |
ejbPassivate() | application specific |
[The complete application can be found in listing 9.]
All these architectural elements contribute to the scalability and maintainability of enterprise information systems. They provide a "component model" capable of supporting deploy-time specification of transactions, security, and collaborations. Additionally, they promote flexible multi- tier, distributed designs that encourage the segmentation of responsibilities into: model, view, and controller layers.
J2EE will continue to grow. The discussion offered here was intended to
introduce a significant subset of its current state.
References