Java EE Exercise

Part D: Handle Remote Interface Issues

Controlling the Test Environment for RMI Tests

In the single-JVM environment, we had all resources within reach; database connection, DDL, JDBC, JPA, DAOs, BOs, etc. It was easy to setup and contol the environment for testing. However, we have an issue when testing EJBs and using their @Remote interfaces to do so. Our unit tests no longer have access to the full suite of resources and must rely on the @Remote methods exposed from the application server -or- bring in the data access tier itself. The later approach should be avoided if possible because it couples an RMI Test to too many outside factors. For the later approach, we need to be careful not to add extra test methods to the EJB under test because they would not be appropriate for the intended operational business use.

This example will demonstrate the issue and offer a solution using a separate set of server-side resources deployed as an EJB.

Demonstrate the need for test resources

  1. Add a getPeopleByName() method to the EJB and @Remote interface that returns the collection provided by the business logic method by the same name. The business logic will have gotten this directly from the persistence provider. Therefore, this is the result of an EntityManager.createQuery("...").getResultList();
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarRemote.java
    ...
    import java.util.Collection;
    ...
    @Remote
    public interface RegistrarRemote {
        void ping();
        Person createPerson(Person person)
            throws RegistrarException;
    
        Person getPersonById(long id)
            throws RegistrarException;
    
    
        Collection<Person> getPeopleByName(String firstName, String lastName)
            throws RegistrarException;
    }
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarEJB.java
    ...
    import java.util.Collection;
    ...
        public Collection<Person> getPeopleByName(
            String firstName, String lastName)
            throws RegistrarException {
            log.debug("*** getPeopleByName() ***");
    
            try {
                return registrar.getPeopleByName(firstName, lastName);
            }
            catch (Throwable ex) {
                log.error(ex);
                throw new RegistrarException(ex.toString());
            }
        }
  2. Add a test to the RMI Test that invokes the new method and attempts to inspect the contents of the collection that is returned.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    ...
    import java.util.Collection;
    ...
        @Test
        public void testLazy() throws Exception {
            log.info("*** testLazy ***");
    
            for(int i=0; i<10; i++) {
                Person person = makePerson();
                person.setLastName("smith" + i);
                registrar.createPerson(person);
            }
    
                //the first time we are going to get people straight from the DAO,
                //without cleaning the managed object or creating a new DTO.
            Collection<Person> people = registrar.getPeopleByName("joe", "%");
            assertEquals("unexpected number of lazy people",10, people.size());
        }
  3. Attempt to build and run the application from the root level. Note how the test fails because we have residue left over from the first test when running the second test.
    $ mvn clean install -rf :javaeeExEJB
    
     -*** testLazy ***
    Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 2.704 sec <<< FAILURE!
    
    Results :
    
    Failed tests:   testLazy(myorg.javaeeex.ejbclient.RegistrarIT): unexpected number of lazy people expected:<10> but was:<11>
    
    Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
    
    ...
    [INFO] Java EE Exercise EJB .............................. SUCCESS [7.366s]
    [INFO] Java EE Exercise EAR .............................. SUCCESS [1.416s]
    [INFO] Java EE Exercise Remote Test ...................... FAILURE [15.709s]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD FAILURE
    Test set: myorg.javaeeex.ejbclient.RegistrarIT
     -------------------------------------------------------------------------------
    Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 2.704 sec <<< FAILURE!
    testLazy(myorg.javaeeex.ejbclient.RegistrarIT)  Time elapsed: 0.503 sec  <<< FAILURE!
    java.lang.AssertionError: unexpected number of lazy people expected:<10> but was:<11>
            at org.junit.Assert.fail(Assert.java:93)
            at org.junit.Assert.failNotEquals(Assert.java:647)
            at org.junit.Assert.assertEquals(Assert.java:128)
            at org.junit.Assert.assertEquals(Assert.java:472)
            at myorg.javaeeex.ejbclient.RegistrarIT.testLazy(RegistrarIT.java:85)
Add the ability to control testing environment
  1. Add a TestUtilEJB and @Remote interface that can be used to reset the database to a clean state. In a production system, we would place this in a separate EJB module so we could prevent it from being deployed in the operational system. However, for simplicity, lets place it in our existing EJB module right next to the RegistrarEJB. Since the business interface has a simple interface that is safe for use for a remote client, have the @Remote interface just extend the business logic interface -- rather than refine it. The business interface is located in the Impl module.
    $ cat javaeeExEJB/src/main/java/myorg/javaeeex/ejb/TestUtilRemote.java
    
    package myorg.javaeeex.ejb;
    
    import javax.ejb.Remote;
    
    import myorg.javaeeex.bl.TestUtil;
    
    @Remote
    public interface TestUtilRemote extends TestUtil {
    }
    $ cat javaeeExEJB/src/main/java/myorg/javaeeex/ejb/TestUtilEJB.java
    
    package myorg.javaeeex.ejb;
    
    import javax.annotation.PostConstruct;
    import javax.ejb.Stateless;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    import myorg.javaeeex.bl.TestUtil;
    import myorg.javaeeex.blimpl.TestUtilImpl;
    
    @Stateless
    public class TestUtilEJB implements TestUtilRemote {
        private static Log log = LogFactory.getLog(TestUtilEJB.class);
    
        @PersistenceContext(unitName="javaeeEx")
        private EntityManager em;
    
        private TestUtil testUtil;
    
        @PostConstruct
        public void init() {
            log.info(" *** TestUtilEJB:init() ***");
            testUtil = new TestUtilImpl();
            ((TestUtilImpl)testUtil).setEntityManager(em);
        }
    
        public void resetAll() throws Exception {
            try {
                testUtil.resetAll();
            }
            catch (Exception ex) {
                log.warn("error in resetAll", ex);
                throw ex;
            }
        }
    }
  2. This is what your EJB should look like at this point.
    javaeeEx
    |-- javaeeExEJB                         
    |   |-- pom.xml                         
    |   `-- src                             
    |       `-- main                        
    |           |-- java                    
    |           |   `-- myorg               
    |           |       `-- javaeeex        
    |           |           `-- ejb         
    |           |               |-- RegistrarEJB.java
    |           |               |-- RegistrarLocal.java
    |           |               |-- RegistrarRemote.java
    |           |               |-- TestUtilEJB.java    
    |           |               `-- TestUtilRemote.java 
    |           `-- resources                           
    |               `-- META-INF                        
    |                   `-- persistence.xml       
  3. Lookup the JNDI name TestUtilRemote inside the unit test.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    
    ...
    import myorg.javaeeex.bl.TestUtil;
    import myorg.javaeeex.ejb.TestUtilRemote;
    ...
        private static final String testUtilJNDI = System.getProperty("jndi.name.testUtil",
            "javaeeExEAR/javaeeExEJB/TestUtilEJB!myorg.javaeeex.ejb.TestUtilRemote");
        private TestUtil testUtil;
    
        public void setUp() throws Exception {
            ...
            log.debug("jndi name:" + testUtilJNDI);
            testUtil = (TestUtilRemote)jndi.lookup(testUtilJNDI);
            log.debug("testUtil=" + testUtil);
        }
  4. Create a cleanup() method that is invoked from setUp() and uses the resetAll() from the TestUtilEJB to reset the database to an emoty state.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    
    ...
        public void setUp() throws Exception {
            ...
            testUtil = (TestUtilRemote)jndi.lookup(testUtilJNDI);
    
            cleanup();
        }
    
        protected void cleanup() throws Exception {
            log.info("calling testUtil.resetAll()");
            testUtil.resetAll();
            log.info("testUtil.resetAll() complete");
        }
  5. Re-run the build from the root. Notice how the TestUtilEJB is now being used to reset the database and the test now passes.
    ...
     -testUtil=Proxy for remote EJB StatelessEJBLocator{appName='javaeeExEAR', moduleName='javaeeExEJB', distinctName='', beanName='TestUtilEJB', view='interface myorg.javaeeex.ejb.TestUtilRemote'}
     -calling testUtil.resetAll()
     -testUtil.resetAll() complete
     -*** testLazy ***
    Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.419 sec
    
    Results :
    
    Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
    
    ...
    [INFO] Java EE Exercise EJB .............................. SUCCESS [6.074s]
    [INFO] Java EE Exercise EAR .............................. SUCCESS [1.098s]
    [INFO] Java EE Exercise Remote Test ...................... SUCCESS [12.943s]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    //SERVER LOG
    02:48:09,757 DEBUG [myorg.javaeeex.ejb.RegistrarEJB] (EJB default - 6) **** init ****
    02:48:09,758 DEBUG [myorg.javaeeex.ejb.RegistrarEJB] (EJB default - 6) em=org.jboss.as.jpa.container.TransactionScopedEntityManager@19e2011
    02:48:09,761 DEBUG [myorg.javaeeex.ejb.RegistrarEJB] (EJB default - 6) init complete, registrar=myorg.javaeeex.blimpl.RegistrarImpl@1734643
    02:48:09,762 DEBUG [myorg.javaeeex.ejb.RegistrarEJB] (EJB default - 6) ping called
    02:48:09,860 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) found 4 statements
    02:48:09,861 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
        alter table JAVAEEEX_ADDRESS 
            drop constraint FKEB70B40A6E18CE38
    02:48:09,864 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
    
        drop table JAVAEEEX_ADDRESS if exists
    02:48:09,890 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
    
        drop table JAVAEEEX_PERSON if exists
    02:48:09,892 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
    
    02:48:09,895 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) found 4 statements
    02:48:09,898 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
        create table JAVAEEEX_ADDRESS (
            id bigint generated by default as identity (start with 1),
            city varchar(255),
            state varchar(255),
            street varchar(255),
            zip varchar(255),
            PERSON_ID bigint,
            primary key (id)
        )
    02:48:09,902 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
    
        create table JAVAEEEX_PERSON (
            id bigint generated by default as identity (start with 1),
            firstName varchar(255),
            lastName varchar(255),
            ssn varchar(255),
            primary key (id)
        )
    02:48:09,913 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
    
        alter table JAVAEEEX_ADDRESS 
            add constraint FKEB70B40A6E18CE38 
            foreign key (PERSON_ID) 
            references JAVAEEEX_PERSON
    02:48:09,924 DEBUG [myorg.javaeeex.jpa.DBUtil] (EJB default - 7) executing:
    
    
    02:48:09,948 DEBUG [myorg.javaeeex.ejb.RegistrarEJB] (EJB default - 8) *** createPerson() ***
    ...

Address Lazy load issues in EJB interfaces

It is temptingly easy to directly return the managed entities returned from the entity manager from the interface of the EJB. However, there is an issue doing so. The data within the database can have a rich set of relationships. If we walked all relationships, we might end up traversing a significant amount of the total database. The provide accounts for this by using lazy loading. In this approach, many of your related objects are initially loaded in as stand-in references and only loaded (hydrated) when needed. This is not a problem when the accessing code is within the same JVM as the EntityManager and the resources to access the database are still in place when a lazily loaded object is inspected. This is a problem when you pass one of these objects outside of the scope of the entity manager -- aka RMI clients.

Demonstrate Lazy Load Problem

  1. Go a little deeper into the Collection returned by the EJB. Attempt to invoke a method on each member in the collection.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    
    ...
        public void testLazy() throws Exception {
    
            ...
    
            Collection<Person> people = registrar.getPeopleByName("joe", "%");
            assertEquals("unexpected number of lazy people",10, people.size());
            for (Person p: people) {
                p.getAddresses().iterator().next().getZip();
            }
  2. Attempt to rebuild the application from the root and note the exception. Due to LazyLoading, the information for each Address was never actually retrieved.
     -*** testLazy ***
    Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.693 sec <<< FAILURE!
    
    Results :
    
    Tests in error: 
      testLazy(myorg.javaeeex.ejbclient.RegistrarIT): failed to lazily initialize a collection of role: 
            myorg.javaeeex.bo.Person.addresses, no session or session was closed
    
    Tests run: 3, Failures: 0, Errors: 1, Skipped: 0
    
    ...
    [INFO] Java EE Exercise EJB .............................. SUCCESS [6.687s]
    [INFO] Java EE Exercise EAR .............................. SUCCESS [0.837s]
    [INFO] Java EE Exercise Remote Test ...................... FAILURE [15.746s]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD FAILURE
    ./javaeeExTest/target/surefire-reports/myorg.javaeeex.ejbclient.RegistrarIT.txt
    ::::::::::::::
    Test set: myorg.javaeeex.ejbclient.RegistrarIT
     -------------------------------------------------------------------------------
    Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.692 sec <<< FAILURE!
    testLazy(myorg.javaeeex.ejbclient.RegistrarIT)  Time elapsed: 0.484 sec  <<< ERROR!
    org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: myorg.javaeeex.bo.Person.addresses, no session or session 
    was closed
        ...
            at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:266)
            at myorg.javaeeex.ejbclient.RegistrarIT.testLazy(RegistrarIT.java:105)

Demonstrate a solution to Lazy Load Problem

  1. Create a new method for the EJB that will hydrate the Address objects before returning the collection. This is unqiue to the @Remote interface and need not be on the business logic interface.
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarRemote.java
    
    ...
    public interface RegistrarRemote {
        Collection<Person> getPeopleByNameHydrated(String firstName, String lastName)
            throws RegistrarException;
    }
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarEJB.java
    
    ...
    import myorg.javaeeex.bo.Address;
    ...
        public Collection<Person> getPeopleByNameHydrated(
                String firstName, String lastName)
                throws RegistrarException {
            log.debug("*** getPeopleByNameHydrated() ***");
    
            try {
                Collection<Person> people =
                    registrar.getPeopleByName(firstName, lastName);
                for (Person p: people) {
                    hydratePerson(p);
                }
                return people;
            }
            catch (Throwable ex) {
                log.error(ex);
                throw new RegistrarException(ex.toString());
            }
        }
    
        private void hydratePerson(Person person) {
            for (Address address : person.getAddresses()) {
                address.getZip();
            }
        }
  2. Update the test method to account for the LazyLoadException and now go on to test the hydrated form of the method.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    
    ...
    import org.hibernate.LazyInitializationException;
    ...
        @Test
        public void testLazy() throws Exception {
    
            ...
    
            Collection<Person> people = registrar.getPeopleByName("joe", "%");
            assertEquals("unexpected number of lazy people",10, people.size());
            try {
                for (Person p: people) {
                    p.getAddresses().iterator().next().getZip();
                }
                fail("no lazy instantiation exception thrown");
            }
            catch (LazyInitializationException expected) {
                log.info("got expected lazy instantiation exception:" + expected);
            }
    
                //this time, the EJB will be asked to walk the graph returned
            people = registrar.getPeopleByNameHydrated("joe", "%");
            assertEquals("unexpected number of loaded people",10, people.size());
            for (Person p: people) {
                p.getAddresses().iterator().next().getZip();
            }
  3. Rebuild from the root of the application.
    $ mvn clean install
    
    ...
    
     -*** testLazy ***
     -got expected lazy instantiation exception:org.hibernate.LazyInitializationException: 
        failed to lazily initialize a collection of role: myorg.javaeeex.bo.Person.addresses, no session or session was closed
    Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.477 sec
    
    Results :
    
    Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
    
    ...
    [INFO] Java EE Exercise .................................. SUCCESS [0.658s]
    [INFO] Java EE Exercise Impl ............................. SUCCESS [15.167s]
    [INFO] Java EE Exercise EJB .............................. SUCCESS [3.415s]
    [INFO] Java EE Exercise EAR .............................. SUCCESS [2.034s]
    [INFO] Java EE Exercise Remote Test ...................... SUCCESS [14.965s]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS

Address provider classes in EJB remote interfaces

With lazy loading issues addressed, it again becomes tempingly easy to directly return (hydrated) entities returned from the entity manager directly from our EJB interface. However, once managed, an entity may have residue objects associated with it to implement the monitoring required during the managed state.

This exercise will demonstrate how our vanilla POJOs where poluted during their managed state and how they present provider-specific classes to the RMI Client. This issue with this is that it may require the RMI Client to add provider-specific classes to their classpath to handle these "not so plain" POJOs.

Demonstrate the detached, but once-managed POJOs have provider classes associated with them

  1. Add a test method that returns a collection of Persons and their associated Address. Inspect the class of the collection that holds the Addresses. Note that it is a persistence provider class. If you do not have the provider's classes in your classpath you will suffer a demarshalling/class-not-found exception. With JBoss, it is version-dependent whether they provide these classes as part of their RMI client .jar file. Take a look. Your Person is not quite a "plain" POJO.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    
    ...
    
        @Test
        public void testPOJO() throws Exception {
            log.info("*** testPOJO ***");
    
            for(int i=0; i<10; i++) {
                Person person = makePerson();
                person.setLastName("smith" + i);
                registrar.createPerson(person);
            }
            //the objects returned will be fully loaded, but...
            Collection<Person> people =
                registrar.getPeopleByNameHydrated("joe", "%");
            assertEquals("unexpected number of managed people",10, people.size());
    
    
                //the collection class requires hibernate to be in the path
            Class<?> clazz = people.iterator().next().getAddresses().getClass();
            log.debug("collection class=" + clazz);
            assertTrue("unexpected collection class",
                    clazz.getPackage().getName().contains("hibernate"));
    
        }
  2. Rebuild your application from the root and note the output as well as the results of the above JUnit assert.
      -*** testPOJO ***
       -collection class=class org.hibernate.collection.PersistentBag

Use a cleaning approach to return plain POJOs

In this first approach we will reuse our POJO class of the entity to construct a "plain" POJO. This will involve some tedious copying of data from the provider entity object to the plain POJO object.

  1. Create a new getPeopleByNameCleaned() method in the EJB. Have this method instantiate clean POJOs and populate them with the data from the managed entities. Be careful to copy data with a collection and not the entire collection itself.
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarRemote.java
    
    ...
    public interface RegistrarRemote {
        ...
    
        Collection<Person> getPeopleByName(String firstName, String lastName)
            throws RegistrarException;
        Collection<Person> getPeopleByNameHydrated(String firstName, String lastName)
            throws RegistrarException;
        Collection<Person> getPeopleByNameCleaned(String firstName, String lastName)
            throws RegistrarException;
    }
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarEJB.java
    
    ...
    import java.util.ArrayList;
    ...
        public Collection<Person> getPeopleByNameCleaned(
                String firstName, String lastName)
                throws RegistrarException {
            log.debug("*** getPeopleByNameCleaned() ***");
    
            try {
                Collection<Person> people = new ArrayList<Person>();
                for (Person personBO :
                    registrar.getPeopleByName(firstName, lastName)) {
    
                    Person personPOJO = new Person(personBO.getId());
                    personPOJO.setFirstName(personBO.getFirstName());
                    personPOJO.setLastName(personBO.getLastName());
                    personPOJO.setSsn(personBO.getSsn());
                    for (Address addressBO : personBO.getAddresses()) {
                        Address addressPOJO = new Address(
                                addressBO.getId(),
                                addressBO.getStreet(),
                                addressBO.getCity(),
                                addressBO.getState(),
                                addressBO.getZip());
                        personPOJO.getAddresses().add(addressPOJO);
                    }
                    people.add(personPOJO);
                }
                return people;
            }
            catch (Throwable ex) {
                log.error(ex);
                throw new RegistrarException(ex.toString());
            }
        }
  2. Add a test of the cleaned approach and verify the collection and rest of the POJO is, in fact, "plain" and free of provider classes.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    
    ...
        public void testPOJO() throws Exception {
    
            ...
    
                //now get a graph of objects that contain pure POJO classes. The
                //server will create fresh POJOs for DTOs and pass information from
                //the business object POJO to the data transfer object POJO.
            people = registrar.getPeopleByNameCleaned("joe", "%");
            assertEquals("unexpected number of clean people",10, people.size());
            for (Person p: people) {
                p.getAddresses().iterator().next().getZip();
            }
    
                //the POJOs are cleansed of their hibernate types
            clazz = people.iterator().next().getAddresses().getClass();
            log.debug("collection class=" + clazz);
            assertFalse("unexpected collection class",
                    clazz.getPackage().getName().contains("hibernate"));
        }
  3. Rebuild the application from the root level. Note the collection class types produced and the results of the assertions included.
     -*** testPOJO ***
     -collection class=class org.hibernate.collection.PersistentBag
     -collection class=class java.util.ArrayList

Use a DTO approach to return plain POJOs

With the POJO cleaning approach, we manually instantiated and populated POJOs to be returned and used our entity classes as the implementation class for the POJOs. However, we could have also used DTOs just as easily, except for the effort of creating separate classes. See the next section for a coverage of DTOs.

Leverage DTOs for interface data transfer

Entity classes are meant to implement the data-centric business rules of an application. Because of their POJO design, it is tempting to resuse them to transfer data to/from the client. However, there are many times when the business rules applied on the server side and are not the same as when used on the client side. There are also cases where the entity represents more than the client wants or can handle. Many times, we are just looking to transfer data to/from the client and will want to create a set of transient classes specifically for this purpose.

Create DTOs

  1. Create a set of Person and Address DTO classes to transfer information to/from the client. Have the PersonDTO not carry the Person's SSN. Be sure to make these classes Serializable. These classes can go in either the EJB module with the rest of the @Remote interfaces or in a lower-level module if they happen to get referenced (i.e., created by a DAO query) by lower-level packages. For the class project, the BO project is a safe and easy place to put them, but we will place them in the EJB module for this exercise since they are not used anywhere in the lower levels.
    $ mkdir -p javaeeExEJB/src/main/java/myorg/javaeeex/dto
    $ cat javaeeExEJB/src/main/java/myorg/javaeeex/dto/PersonDTO.java
    
    package myorg.javaeeex.dto;
    
    import java.io.Serializable;
    import java.util.ArrayList;
    import java.util.Collection;
    
    public class PersonDTO implements Serializable {
        private static final long serialVersionUID = 1L;
        private long id;
        private String firstName;
        private String lastName;
        private Collection<AddressDTO> addresses = new ArrayList<AddressDTO>();
    
        public PersonDTO() {}
        public PersonDTO(long id) { setId(id); }
    
        public long getId() {
            return id;
        }
        private void setId(long id) {
            this.id = id;
        }
        public String getFirstName() {
            return firstName;
        }
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
        public String getLastName() {
            return lastName;
        }
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
        public Collection<AddressDTO> getAddresses() {
            return addresses;
        }
        public void setAddresses(Collection<AddressDTO> addresses) {
            this.addresses = addresses;
        }
        public String toString() {
            StringBuffer text = new StringBuffer();
            text.append("id=" + id);
            text.append(":" + firstName);
            text.append(" " + lastName);
            text.append(", addresses={");
            for (AddressDTO address : addresses) {
                text.append("{" + address.toString() + "},");
            }
            text.append("}");
            return text.toString();
        }
    }
    $ cat javaeeExEJB/src/main/java/myorg/javaeeex/dto/AddressDTO.java
    
    package myorg.javaeeex.dto;
    
    import java.io.Serializable;
    
    public class AddressDTO implements Serializable {
        private static final long serialVersionUID = 1L;
        private long id;
        private String street;
        private String city;
        private String state;
        private String zip;
    
        public AddressDTO() {}
        public AddressDTO(
                long id, String street, String city, String state, String zip) {
            this.id = id;
            this.city = city;
            this.state = state;
            this.street = street;
            this.zip = zip;
        }
    
        public long getId() {
            return id;
        }
        @SuppressWarnings("unused")
        private void setId(long id) {
            this.id = id;
        }
    
        public String getStreet() {
            return street;
        }
        public void setStreet(String street) {
            this.street = street;
        }
        public String getCity() {
            return city;
        }
        public void setCity(String city) {
            this.city = city;
        }
        public String getState() {
            return state;
        }
        public void setState(String state) {
            this.state = state;
        }
        public String getZip() {
            return zip;
        }
        public void setZip(String zip) {
            this.zip = zip;
        }
    
        public String toString() {
            StringBuilder text = new StringBuilder();
    
            text.append(street + " ");
            text.append(city + ", ");
            text.append(state + " ");
            text.append(zip);
            return text.toString();
        }
    }
  2. Your EJB should now look like the following
    javaeeEx                                                                                         
    |-- javaeeExEJB                                                                               
    |   |-- pom.xml                                                                               
    |   `-- src                                                                                   
    |       `-- main                                                                              
    |           |-- java                                                                          
    |           |   `-- myorg                                                                     
    |           |       `-- javaeeex                                                              
    |           |           |-- dto                                                               
    |           |           |   |-- AddressDTO.java                                               
    |           |           |   `-- PersonDTO.java                                                
    |           |           `-- ejb                                                               
    |           |               |-- RegistrarEJB.java                                             
    |           |               |-- RegistrarLocal.java                                           
    |           |               |-- RegistrarRemote.java                                          
    |           |               |-- TestUtilEJB.java                                              
    |           |               `-- TestUtilRemote.java                                           
    |           `-- resources                                                                     
    |               `-- META-INF                                                                  
    |                   `-- persistence.xml      

Add a method to the EJB

  1. Add a new getPeopleByNameDTO() method that will return the results using the DTO classes created above.
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarRemote.java
    
    ...
    import myorg.javaeeex.dto.PersonDTO;
    
    public interface RegistrarRemote {
        ...
    
        Collection<Person> getPeopleByName(String firstName, String lastName)
            throws RegistrarException;
        Collection<Person> getPeopleByNameHydrated(String firstName, String lastName)
            throws RegistrarException;
        Collection<Person> getPeopleByNameCleaned(String firstName, String lastName)
            throws RegistrarException;
        Collection<PersonDTO> getPeopleByNameDTO(String firstName, String lastName)
            throws RegistrarException;
    }
    $ cat ./javaeeExEJB/src/main/java/myorg/javaeeex/ejb/RegistrarEJB.java
    
    ...
    import myorg.javaeeex.dto.AddressDTO;
    import myorg.javaeeex.dto.PersonDTO;
    ...
       public Collection<PersonDTO> getPeopleByNameDTO(
               String firstName, String lastName)
               throws RegistrarException {
           log.debug("*** getPeopleByNameDTO() ***");
    
           try {
               Collection<PersonDTO> people = new ArrayList<PersonDTO>();
               for (Person personBO :
                   registrar.getPeopleByName(firstName, lastName)) {
    
                   PersonDTO personDTO = makeDTO(personBO);
                   people.add(personDTO);
               }
               return people;
           }
           catch (Throwable ex) {
               log.error(ex);
               throw new RegistrarException(ex.toString());
           }
       }
    
       private PersonDTO makeDTO(Person personBO) {
           PersonDTO personDTO = new PersonDTO(personBO.getId());
           personDTO.setFirstName(personBO.getFirstName());
           personDTO.setLastName(personBO.getLastName());
           //note that there is no SSN in the DTO
           for (Address addressBO : personBO.getAddresses()) {
               AddressDTO addressDTO = new AddressDTO(
                       addressBO.getId(),
                       addressBO.getStreet(),
                       addressBO.getCity(),
                       addressBO.getState(),
                       addressBO.getZip());
               personDTO.getAddresses().add(addressDTO);
           }
           return personDTO;
       }

Use the new EJB method from the RMI Test

  1. Add a test of the DTO capability from the RMI Test.
    $ cat ./javaeeExTest/src/test/java/myorg/javaeeex/ejbclient/RegistrarIT.java
    
    ...
    import myorg.javaeeex.dto.AddressDTO;
    import myorg.javaeeex.dto.PersonDTO;
    ...
    
        @Test
        public void testDTOs() throws Exception {
            log.info("*** testDTOs ***");
    
            for(int i=0; i<10; i++) {
                Person person = makePerson();
                person.setLastName("smith" + i);
                registrar.createPerson(person);
            }
    
            //now get a graph of objects that contain pure DTOs versus BOs
            Collection<PersonDTO>peopleDTO =
                registrar.getPeopleByNameDTO("joe", "%");
            assertEquals("unexpected number of DTO people",10, peopleDTO.size());
            for (PersonDTO p: peopleDTO) {
                    Collection<AddressDTO> a = p.getAddresses();
                a.iterator().next().getZip();
            }
            //the DTOs are POJOs that are designed to only contain what the
            //clients needs to see. It contains no server-side behavior and
            //could be represented as an XML document. In this example, we
            //have excluded the SSN from the PersonDTO.
        }
  2. Rebuild and test your application from the root. We now are passing information to the RMI client using DTO classes instead of the persisted entity classes.
    $ mvn clean install -rf :javaeeExEJB
    
    ...
     -*** testDTOs ***
    Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.817 sec
    
    Results :
    
    Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
    
    ...
    [INFO] Java EE Exercise EJB .............................. SUCCESS [5.273s]
    [INFO] Java EE Exercise EAR .............................. SUCCESS [1.059s]
    [INFO] Java EE Exercise Remote Test ...................... SUCCESS [13.384s]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS

Summary

  • In this exercise, we demonstrated and worked through a couple issues with the remote interface for the EJB.
    • The RMI Test needs a way to reset the application back to a known state prior to running a test. Rather than interfacing directly to the database, the @Remote interface of a new TestUtilEJB was used to do the work on the server side. Once testing is complete and the application is placed into production, the TestUtilEJB would not be part of the operational deployment.
    • The collection returned had some lazy-loaded objects in them and we had to specifically design a method that could be called by external clients. This new method made sure the object references were fully loaded from the database prior to passing back to the client.
    • The once-managed entities contain some provider-specific classes that may need to be accounted for in the classpaths of the RMI client or within the EJB, acting as a remote facade. We copied data from the not-so-plain managed entity instances into fresh POJO entity instances to clean up the unwanted classes from the remote interface.
    • We then tackled the issue where certain behavior, properties, and classes may not be wanted, understood, or be able to handle from the client perspective. We brought in the use of some data transfer object (DTO) classes to represent information we wanted expressed to the client. These objects are transient -- thus they are not directly mapped into the database.