Developer Tools
JDeveloper
| Oracle JDeveloper Tip
Creating a CMT Stateless SessionBean Facade for an Application Module Author: Steve Muench, BC4J Development TeamDate: July 15, 2004 Contents
For our example, we will create a stateless EJB session bean that uses a container-managed transaction. To keep things simple, let's assume our service has a single business method named createOrUpdateDepartment() with the following signature: public boolean createOrUpdateDepartment(int id, String name, String loc)The goal of this article is to illustrate how to use the BC4J application module named com.example.hr.HRApp as part of the implementation of this createOrUpdateDepartment method on our stateless enterprise bean. Let's assume that the HRApp application module has a view object member named Departments, based on the com.example.hr.DeptView view object, based on the familiar DEPT table and related to the com.example.hr.Dept entity object so our view can be updateable. Our Application Module We start with a BC4J ApplicationModule component named HRApp in the com.example.hr package. We've written a custom createOrUpdateDepartment() method in our HRAppImpl.java class and we've visited the ApplicationModule editor's Client Methods panel and shuttled createOrUpdateDepartment method into the list of selected methods to include on our application module's client interface. The method implementation tries to find an existing row in the Departments view object having the id passed in as its primary key, then either updates that existing row or creates a new row appropriately. It returns boolean true if a new row gets created. The code looks like this:
/*
* Returns true if new row was created.
*/
public boolean createOrUpdateDepartment(int id, String name, String loc) {
boolean createdNewRow = false;
/*
* Use the "Departments" view object instance of this AM
*/
ViewObject departments = getDepartments();
/*
* Check to see if the Department with this ID already exists
*/
Number deptId = new Number(id);
Key k = new Key(new Object[]{deptId});
Row[] found = departments.findByKey(k,1);
if (found.length > 0) {
/*
* If we found it, then update it's Dname and Loc
*/
Row existingDept = found[0];
existingDept.setAttribute("Dname", name);
existingDept.setAttribute("Loc", loc);
}
else {
createdNewRow = true;
/*
* Otherwise create a new Dept row
*/
Row newDept = departments.createRow();
/*
* Add the new row to the view object's default rowset
*/
departments.insertRow(newDept);
/*
* Populate the attributes from the parameter arguments.
*/
newDept.setAttribute("Deptno", new Number(id));
newDept.setAttribute("Dname", name);
newDept.setAttribute("Loc", loc);
}
return createdNewRow;
}
By writing our custom code inside the lightweight application module component, we gain two benefits:
We can start by using the JDeveloper Enterprise Bean wizard to create a new stateless session bean called StatelessSample implemented by:
package com.example;
import javax.ejb.EJBObject;
import java.rmi.RemoteException;
public interface StatelessSample extends EJBObject {
boolean createOrUpdateDepartment(int id, String name, String loc)
throws RemoteException, AppException;
}
The
AppException is an example of an application-specific exception that our method will throw if any problems arise during its execution. Its code looks like this:
package com.example;
public class AppException extends Exception {
public AppException(String msg) {super(msg);}
}
Before we start adding BC4J into the picture for our Stateless SessionBean wrapper implementation, our
StatelessSampleBean class looks like this:
package com.example;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
public class StatelessSampleBean implements SessionBean {
public void ejbCreate(){}
public void ejbActivate(){}
public void ejbPassivate(){}
public void ejbRemove(){}
public void setSessionContext(SessionContext ctx){
}
public boolean createOrUpdateDepartment(int id, String name, String loc)
throws AppException {
// TODO: Implement method here
}
}
We need to mark the createOrUpdateDepartment method as requiring a transaction. To do this, we can use the EJB Module editor to click on the StatelessSample bean, select the createOrUpdateDepartment method, and then use the Container Transaction Attribute drop-down list on the Remote Properties tab to pick the value Required from the list.
If you double-click on the ejb-jar.xml file in the sample project you can see the results of setting the transaction attribute in the EJB Module Editor. Look for the <transaction-attribute> tag in the <container-transaction> sections in the file.
<ejb-jar>
<enterprise-beans>
<session>
<description>Session Bean ( Stateless )</description>
<display-name>StatelessSample</display-name>
<ejb-name>StatelessSample</ejb-name>
<home>com.example.StatelessSampleHome</home>
<remote>com.example.StatelessSample</remote>
<local-home>com.example.StatelessSampleLocalHome</local-home>
<local>com.example.StatelessSampleLocal</local>
<ejb-class>com.example.StatelessSampleBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>StatelessSample</ejb-name>
<method-intf>Local</method-intf>
<method-name>createOrUpdateDepartment</method-name>
<method-params>
<method-param>int</method-param>
<method-param>java.lang.String</method-param>
<method-param>java.lang.String</method-param>
</method-params>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
<container-transaction>
<method>
<ejb-name>StatelessSample</ejb-name>
<method-intf>Remote</method-intf>
<method-name>createOrUpdateDepartment</method-name>
<method-params>
<method-param>int</method-param>
<method-param>java.lang.String</method-param>
<method-param>java.lang.String</method-param>
</method-params>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
Aggregating a BC4J Application Module
With the EJB aspects of our bean setup, we can proceed to implementing the BC4J application module aggregation. The first thing we do is add private variables to hold the EJB SessionContext and the instance of the aggregated BC4J ApplicationModule, like this: /* * Place to hold onto the aggregated appmodule instance. * * NOTE: We're using the custom interface com.example.hr.common.HRApp * (which extends oracle.jbo.ApplicationModule) that the BC4J * design time creates for us when we exposed the * createOrUpdateDepartment() method on our application module's * client interface. */ transient private HRApp _am = null; /* * Remember the SessionContext that the EJB container provides us */ private SessionContext _ctx = null;and we modify the default, empty implementation of the setSessionContext() method to remember the session context like this:
public void setSessionContext(SessionContext ctx){ _ctx = ctx; }
We add additional constants that hold the names of the transactional and non-transactional J2EE datasources that we want BC4J to use, as well as the fully-qualified name of the BC4J application module that we'll be aggregating:
/*
* JNDI resource name for the J2EE datasource to use for application work
* For OC4J, this is the name of the 'ejb-location' name from the OC4J
* <data-source> element in the data-source.xml file.
*/
private static final String TRANSACTIONAL_DATASOURCE = "jdbc/scottDS";
/*
* JNDI resource name for the non-transactional 2EE datasource to be
* used for AM snapshot passivation if your bean uses that feature.
* This datasource will be used lazily by BC4J, but it must be set.
*
* For OC4J, this is the name of the 'location' name from the OC4J
* <data-source> element in the data-source.xml file.
*/
private static final String NONTRANSACTIONAL_DATASOURCE = "jdbc/scottCoreDS";
/*
* Fully-qualified BC4J application module name to aggregate
*/
private static final String APPMODNAME = "com.example.hr.HRApp";
We expand the now-empty ejbCreate() and ejbRemove() methods to create and destroy the aggregated instance of the BC4J application module that we'll use for the lifetime of the stateless session bean. When we're done, ejbCreate() it looks like this:
public void ejbCreate() throws CreateException {
try {
/*
* Setup a hashtable of environment parameters for JNDI initial context
*/
Hashtable env = new Hashtable();
env.put(JboContext.INITIAL_CONTEXT_FACTORY,JboContext.JBO_CONTEXT_FACTORY);
/*
* NOTE: we want to use the BC4J app module in local mode as a simple Java class!
*/
env.put(JboContext.DEPLOY_PLATFORM, JboContext.PLATFORM_LOCAL);
/*
* We set the internal datasource to be some convenient,
* non-transactional datasource name.
*/
env.put(PropertyConstants.INTERNAL_CONNECTION_PARAMS,NONTRANSACTIONAL_DATASOURCE);
/*
* Create an initial context, using this hashtable of environment params
*/
InitialContext ic = new InitialContext(env);
/*
* Lookup a home interface for the application module
*/
ApplicationModuleHome home = (ApplicationModuleHome)ic.lookup(APPMODNAME);
/*
* Using the home, create the instance of the appmodule we'll use
*/
_am = (HRApp)home.create();
/*
* Register the BC4J factory to handle EJB container-managed transactions
*/
registerContainerManagedTransactionHandlerFactory();
}
catch(Exception ex) {
ex.printStackTrace();
throw new CreateException(ex.getMessage());
}
}
and
ejbRemove() looks like this:
public void ejbRemove() {
try {
// Cleanup any appmodule resources before getting shutdown
_am.remove();
}
catch(JboException ex) { /* Ignore */ }
}
The helper method named
reigsterContainerManagedTransactionHandlerFactory() looks like this:
/*
* Handle setting up the correct BC4J transaction handler
* for participating in CMT transactions.
*/
private void registerContainerManagedTransactionHandlerFactory() {
SessionImpl session = (SessionImpl)_am.getSession();
session.setTransactionHandlerFactory(
new TransactionHandlerFactory() {
public TransactionHandler createTransactionHandler() {
return new ContainerManagedTxnHandlerImpl();
}
public JTATransactionHandler createJTATransactionHandler() {
return new ContainerManagedTxnHandlerImpl();
}
}
);
}
It utilizes some built-in classes in the BC4J framework to handle the interaction between the EJB container managed transaction and the BC4J application module's transaction. The last detail is to implement the
createOrUpdateDepartment() method in the SessionBean. The implementation needs to:
_ctx.setRollbackOnly();on the session context to signal to the container that something went wrong in posting our pending changes so that the overall container transaction should not proceed. The final createOrUpdateDepartment() wrapper code to perform these steps looks like this:
public boolean createOrUpdateDepartment(int id,
String name,
String loc)
throws AppException {
boolean createdNewRow = false;
try {
/*
* Connect the AM to the datasource we want to use for
* the duration of this single method call.
*/
_am.getTransaction().connectToDataSource(null,
TRANSACTIONAL_DATASOURCE,
false);
/*
* Delegate the method call to the implementation of
* the custom HRApp interface of our HRApp application
* module.
*/
createdNewRow = _am.createOrUpdateDepartment(id,name,loc);
/*
* Post all changes in the AM, but we don't commit them.
* The EJB container managed transaction handles
* the commit.
*/
_am.getTransaction().postChanges();
return createdNewRow;
}
catch(JboException ex) {
/*
* To be good EJB Container-Managed Transaction "citizens"
* we have to mark the transaction as needing a rollback
* if there are problems
*/
_ctx.setRollbackOnly();
throw new AppException("Error creating dept "+ id
+"\n"+ex.getMessage());
}
finally {
try {
/*
* Disconnect the AM from the datasource we're using
*/
_am.getTransaction().disconnect();
}
catch(Exception ex) { /* Ignore */ }
}
}
With the EJB-Tier work done, we can build a sample client program to test this new stateless EJB Session Bean by selecting the bean in the JDeveloper IDE and choosing "Create Sample Java Client" from the right-mouse menu.
When the "Sample EJB Client Details" dialog appears, we take the defaults of connecting to embedded OC4J container. Clicking the (OK) button generates the following test class:
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import com.example.StatelessSampleEJB;
import com.example.StatelessSampleEJBHome;
public class SampleStatelessSampleEJBClient {
public static void main(String [] args) {
SampleStatelessSampleEJBClient sampleStatelessSampleEJBClient =
new SampleStatelessSampleEJBClient();
try {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.evermind.server.rmi.RMIInitialContextFactory");
env.put(Context.SECURITY_PRINCIPAL, "admin");
env.put(Context.SECURITY_CREDENTIALS, "welcome");
env.put(Context.PROVIDER_URL,
"ormi://localhost:23891/current-workspace-app");
Context ctx = new InitialContext(env);
StatelessSampleEJBHome statelessSampleEJBHome =
(StatelessSampleEJBHome)ctx.lookup("StatelessSampleEJB");
StatelessSampleEJB statelessSampleEJB;
// Use one of the create() methods below to create a new instance
// statelessSampleEJB = statelessSampleEJBHome.create();
// Call any of the Remote methods below to access the EJB
// statelessSampleEJB.createDepartment( int id,
// String name,
// String loc );
}
catch(Throwable ex) {
ex.printStackTrace();
}
}
}
I modified the sample client slightly to produce one that attempts to test our createOrUpdateDepartment method in a few different ways, and which includes the appropriate PROVIDER_URL values for connecting to the SessionBean running on the embbed OC4J container as well as an external OC4J container:
package com.example;
import com.example.StatelessSample;
import com.example.StatelessSampleHome;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
public class StatelessSampleClient {
public static void main(String[] args) throws Throwable {
StatelessSampleClient statelessSampleClient =
new StatelessSampleClient();
Context context = getInitialContext();
/*
* Lookup the Home Interface
*/
StatelessSampleHome statelessSampleHome =
(StatelessSampleHome) PortableRemoteObject.narrow(
context.lookup("StatelessSample"),
StatelessSampleHome.class);
/*
* Use one of the create() methods below to create a new instance
*/
StatelessSample statelessSampleEJB = statelessSampleHome.create();
/*
* Call any of the Remote methods below to access the EJB
*/
boolean created = false;
try {
created = statelessSampleEJB.createOrUpdateDepartment(13, "Test1", "Loc1");
System.out.println((created ? "Created" : "Updated")+" department 13");
}
catch (AppException ax) {
System.err.println("AppException: " + ax.getMessage());
}
try {
statelessSampleEJB.createOrUpdateDepartment(14, "Test2", "Loc2");
System.out.println((created ? "Created" : "Updated")+" department 14");
}
catch (AppException ax) {
System.err.println("AppException: " + ax.getMessage());
}
try {
/*
* Try setting a department id that is too large!
*/
statelessSampleEJB.createOrUpdateDepartment(23456, "Test3", "Loc3");
System.out.println((created ? "Created" : "Updated")+" department 23456");
}
catch (AppException ax) {
System.err.println("AppException: " + ax.getMessage());
}
try {
/*
* Try setting a location of "Loc5" which will fail during
* post-time if you have run the ExampleTriggerToMakeUpdateFail.sql
* script in this project.
*/
statelessSampleEJB.createOrUpdateDepartment(14, "Test2", "Loc5");
System.out.println((created ? "Created" : "Updated")+" department 14");
}
catch (AppException ax) {
System.err.println("AppException: " + ax.getMessage());
}
}
private static Context getInitialContext() throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.evermind.server.rmi.RMIInitialContextFactory");
env.put(Context.SECURITY_PRINCIPAL, "admin");
env.put(Context.SECURITY_CREDENTIALS, "welcome");
/*
* Use this URL if you want to test against the stateless session
* bean running on the embedded OC4J container.
*
* NOTE: Remember to "Run" the session bean first to start the
* ---- embedded container before running this client, or you'll
* get an expected "Cannot connect" error since the server
* bean won't be running and available for client connections.
*/
String provider_url = "ormi://localhost:23891/current-workspace-app";
/*
* Use this URL instead if you want to test on an externally deployed
* OC4J instance.
*/
// String provider_url = "ormi://localhost/bc4jcmtslsb";
env.put(Context.PROVIDER_URL,provider_url);
return new InitialContext(env);
}
}
Running the Sample
To run the example inside the JDeveloper IDE (9.0.3 or later) you need to:
To run the example on the embedded OC4J container:
To run the example on the external, standalone OC4J container:
When you run the StatelessSampleClient the first time, you'll see output like: Created department 13 Created department 14 AppException: Error creating dept 23456 JBO-27010: Attribute set with value 23456 for Deptno in Dept has invalid precision/scale Updated department 14If you want to experiment with failures that can occur during an update, you might experiment by running the ExampleTriggerToMakeUpdateFail.sql script against your SCOTT account and then try running the StatelessSampleClient again. This scripts creates the simple database trigger below which rejects any attempt to insert or update a row in the DEPT table with a LOC value of "Loc5" (case-insensitive):
create trigger reject_dept_loc5
before insert or update on dept for each row
begin
if (upper(:new.loc) = 'LOC5') then
raise_application_error(-20001,'Cannot have location of LOC5');
end if;
end;
If you create this trigger and rerun the
StatelessSampleClient, you'll see output like this:
Updated department 13
Updated department 14
AppException: Error creating dept 23456
JBO-27010: Attribute set with value 23456 for Deptno
in Dept has invalid precision/scale
AppException: Error creating dept 14
JBO-26041: Failed to post data to database during "Update":
SQL Statement " UPDATE DEPT Dept SET DNAME=:1,LOC=:2 WHERE DEPTNO=:3".
Conclusion
Hopefully this article has illustrated that it is straightforward to utilize the full power of BC4J in local mode as part of your EJB Stateless Session Beans using container-managed transaction. This example illustrated delegating to the single createOrUpdateDepartment method in the application module, but by replicating the same code pattern above, we could easily expose many methods on the Stateless SessionBean in the same way.
Code Listing The full code listing for the StatelessSampleBean bean implementation class looks like this:
/**
* StatelessSampleBean
*
* Illustrates how to use an aggregated BC4J application module
* in local mode as part of the implementation of a stateless
* EJB session bean using container-managed transaction.
*
* HISTORY
* smuench 15-JUL-2004 Updated
* smuench/dmutreja 14-FEB-2002 Created
*/package com.example;
import com.example.hr.common.*;
import java.util.Hashtable;
import javax.ejb.CreateException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.naming.InitialContext;
import oracle.jbo.ApplicationModuleHome;
import oracle.jbo.JboContext;
import oracle.jbo.JboException;
import oracle.jbo.common.PropertyConstants;
import oracle.jbo.server.JTATransactionHandler;
import oracle.jbo.server.SessionImpl;
import oracle.jbo.server.TransactionHandler;
import oracle.jbo.server.TransactionHandlerFactory;
import oracle.jbo.server.ejb.ContainerManagedTxnHandlerImpl;
public class StatelessSampleBean implements SessionBean {
/*
* Place to hold onto the aggregated appmodule instance.
*
* NOTE: We're using the custom interface com.example.hr.common.HRApp
* (which extends oracle.jbo.ApplicationModule) that the BC4J
* design time creates for us when we exposed the
* createOrUpdateDepartment() method on our application module's
* client interface.
*/
transient private HRApp _am = null;
/*
* Remember the SessionContext that the EJB container provides us
*/
private SessionContext _ctx = null;
/*
* JNDI resource name for the J2EE datasource to use for application work
* For OC4J, this is the name of the 'ejb-location' name from the OC4J
* <data-source> element in the data-source.xml file.
*/
private static final String TRANSACTIONAL_DATASOURCE = "jdbc/scottDS";
/*
* JNDI resource name for the non-transactional 2EE datasource to be
* used for AM snapshot passivation if your bean uses that feature.
* This datasource will be used lazily by BC4J, but it must be set.
*
* For OC4J, this is the name of the 'location' name from the OC4J
* <data-source> element in the data-source.xml file.
*/
private static final String NONTRANSACTIONAL_DATASOURCE = "jdbc/scottCoreDS";
/*
* Fully-qualified BC4J application module name to aggregate
*/
private static final String APPMODNAME = "com.example.hr.HRApp";
public void ejbCreate() throws CreateException {
try {
/*
* Setup a hashtable of environment parameters for JNDI initial context
*/
Hashtable env = new Hashtable();
env.put(JboContext.INITIAL_CONTEXT_FACTORY,JboContext.JBO_CONTEXT_FACTORY);
/*
* NOTE: we want to use the BC4J app module in local mode as a simple Java class!
*/
env.put(JboContext.DEPLOY_PLATFORM, JboContext.PLATFORM_LOCAL);
/*
* We set the internal datasource to be some convenient,
* non-transactional datasource name.
*/
env.put(PropertyConstants.INTERNAL_CONNECTION_PARAMS,NONTRANSACTIONAL_DATASOURCE);
/*
* Create an initial context, using this hashtable of environment params
*/
InitialContext ic = new InitialContext(env);
/*
* Lookup a home interface for the application module
*/
ApplicationModuleHome home = (ApplicationModuleHome)ic.lookup(APPMODNAME);
/*
* Using the home, create the instance of the appmodule we'll use
*/
_am = (HRApp)home.create();
/*
* Register the BC4J factory to handle EJB container-managed transactions
*/
registerContainerManagedTransactionHandlerFactory();
}
catch(Exception ex) {
ex.printStackTrace();
throw new CreateException(ex.getMessage());
}
}
public void ejbActivate() {}
public void ejbPassivate() {}
public void ejbRemove() {
try {
/*
* Cleanup any appmodule resources before getting shutdown
*/
_am.remove();
}
catch(JboException ex) { /* Ignore */ }
}
public void setSessionContext(SessionContext ctx){ _ctx = ctx; }
/*
* Returns true if new row was created.
*/
public boolean createOrUpdateDepartment(int id, String name, String loc)
throws AppException {
boolean createdNewRow = false;
try {
/*
* Connect the AM to the datasource we want to use for the duration
* of this single method call.
*/
_am.getTransaction().connectToDataSource(null,TRANSACTIONAL_DATASOURCE,false);
/*
* Delegate the method call to the implementation of the custom
* HRApp interface of our HRApp application module.
*/
createdNewRow = _am.createOrUpdateDepartment(id,name,loc);
/*
* Post all changes in the AM, but we don't commit them.
* The EJB container managed transaction handles the commit.
*/
_am.getTransaction().postChanges();
return createdNewRow;
}
catch(JboException ex) {
/*
* To be good EJB Container-Managed Transaction "citizens" we have
* to mark the transaction as needing a rollback if there are problems
*/
_ctx.setRollbackOnly();
throw new AppException("Error creating dept "+ id +"\n"+ex.getMessage());
}
finally {
try {
/*
* Disconnect the AM from the datasource we're using
*/
_am.getTransaction().disconnect();
}
catch(Exception ex) { /* Ignore */ }
}
}
/*
* Handle setting up the correct BC4J transaction handler
* for participating in CMT transactions.
*/
private void registerContainerManagedTransactionHandlerFactory() {
SessionImpl session = (SessionImpl)_am.getSession();
session.setTransactionHandlerFactory(
new TransactionHandlerFactory() {
public TransactionHandler createTransactionHandler() {
return new ContainerManagedTxnHandlerImpl();
}
public JTATransactionHandler createJTATransactionHandler() {
return new ContainerManagedTxnHandlerImpl();
}
}
);
}
}
|