Monday, September 28, 2009

How to Use EJB 3.0 Session Bean in the Web Application

A sample that uses EJB 3.0 stateless session bean is described here. It is from the ejb3 example in weblogic 10.

Overview of the Whole Picture

Scenario #1: Remote Session Bean
The web.xml file has the following:
<ejb-ref>
    <ejb-ref-name>ejb/ReviewManager</ejb-ref-name>
    <ejb-ref-type>Session</ejb-ref-type>
    <remote>examples.ejb.ejb30.service.ReviewService</remote>
    <ejb-link>domain.jar#ReviewManagerBean</ejb-link>
    </ejb-ref>
Here domain.jar is the jar file that contains the ReviewManagerBean.class and the file persistence.xml. The jar is the ejb module. The jsp and servlet files below are in a war file that is the web module. The domain.jar and the web module are in the same ear file. The ejb module domain.jar does not have the descriptor file ejb-jar.xml because it uses ejb annotation. The ReviewManagerBean class is the following:
package examples.ejb.ejb30.session;
import examples.ejb.ejb30.mdb.ReviewListener;
import examples.ejb.ejb30.service.ReviewServiceImpl;
import examples.ejb.ejb30.service.ReviewService;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

/**
 * A Stateless Session Bean operating on a persistent domain model.
 * The EJB container registers the bean in JNDI global namespace.
 * The JNDI name for the bean is the simple name (i.e.
 * stripped of package name) of the business interface it implements.
 * For this example, the JNDI name will be ReviewService
 * which is specified by the annotation element mappedName="ReviewService".

* NOTE:This policy is ambiguous when two beans implements the same * business interface.

* The EJB contain also injects the bean with a instance of * {@link javax.persistence.EntityManager EntityManager} (or rather a proxy). * This EntityManager is used to interact with the persistent EJB domain * model. */ @Stateless(mappedName="ReviewService") @Remote(ReviewService.class) public class ReviewManagerBean extends ReviewServiceImpl implements ReviewService { @PersistenceContext(unitName = "reviewSession") private EntityManager em; @EJB ReviewListener listener; @PostConstruct public void init(){ // inject the EnityManager. injectEntityManager(em); addReviewAddedListener(listener); } }


In the jsp file, the bean is used as follows:
<%
private ReviewService serviceImpl;
String SESSION_BEAN_JNDI_NAME = "java:comp/env/ejb/ReviewManager";
InitialContext ctx = new InitialContext();
serviceImpl = (ReviewService) ctx.lookup(SESSION_BEAN_JNDI_NAME);

Artist artist = serviceImpl.newArtist(name);
%>  
In summary, the web component uses JNDI and InitialContext to get the session bean. A session bean is a service. It is injected an EntityManager instance by the J2EE container using the unitName. Each business method call is a transaction unit. The business method can just use the EntityManager to perform database activity without using any transaction maintence code such as beginTransaction, commit, or rollback. The container will take care of that.
Scenario #2: Local Session Bean
In this scenario, the session bean is local. No configuration is needed in web.xml. The session bean is the following:
@Stateless
@Local(Process.class)
public class ProcessBean {
//reference name and type inferred from variable.
@EJB ReviewListener listener;
@PersistenceContext(unitName = "reviewSession")
private EntityManager em;

public Book getRandomBook() {
List books = em.createQuery("select i from Book i").getResultList();
int size = books.size();
return size == 0 ? null : books.get(new Random().nextInt(size));
}

public void addReview(int uid, String reviewerName, int rating, String comments) {
Book book = em.find(Book.class, uid);
if (book == null)
throw new ObjectNotFoundException("Can't find book with id ["+uid+"]");
Reviewer reviewer = em.find(Reviewer.class, reviewerName);
if (reviewer == null) {
reviewer = new Reviewer(reviewerName, Person.Gender.MALE);
}
Review review = reviewer.review(book, rating, comments);
em.persist(reviewer);
listener.reviewAdded(review);
}
It is injected an EntityManager. In the web module, the servlet will use it as in the follows:
public class EJBRefSampleClientServlet extends HttpServlet {
@EJB
private examples.ejb.ejb30.ejbref.Process service_;
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
......
service_.addReview(uid, "guest", rate, comments);
......
}
We can see that here the servlet uses dependency injection to get the EJB. Local clients can use JNDI lookup as well as in the using remote EJB case, but dependency injection results in simpler code. See[4] for some examples.

Integration with JPA

In both of the scenarios above, the JSP or the Servlet does not use the JPA directly. Instead, the JSP/Servlet just uses a session bean and calls the business mehtod of the bean. The session bean implements the service by using the injected EntityManager from the J2EE container. But can the JSP/Servlet use the JPA directly? The answer is yes. But be very careful here. Since EntityManager is not thread-safe, the servlet should be injected an EntityManager and use it directly. There are two ways.
  1. Use @PersistenceUnit to inject an EntityManagerFactory into the servlet. This is to use application managed entity manager. In the service() method, you can then use the code like the following:
    EntityManager em = emf.createEntityManager();
    try{
      beginTransaction();
      em.persist(......)
      commitTransaction();
    }catch(Exception e){
      //rollback code
    }finally{
    em.close();
    }
    
  2. Use @PersistenceContext to declare a dependency on an EntityManager in the servlet and use JNDI to look up the EntityManager. You'll also need to write code to begin/commit/rollback the transaction.

How to Write a Session Bean

To create a remote session bean, use one of the following:
  1. Decorate the business interface with @Remote:
    @Remote
    public interface InterfaceName{...}
  2. Decorate the bean class with @Remote, specifying the business interface or interfaces:
    @Remote(InterfaceName.class)
    public class BeanName implements InterfaceName{...}

To create a local session bean, use one of the following:
  1. Create the business interface and the bean class without the @Remote or @Local annotation.
  2. Decorate the business interface with @Local:
    @Local
    public interface InterfaceName{...}
  3. Decorate the bean class with @Local, specifying the business interface or interfaces:
    @Local(InterfaceName.class)
    public class BeanName implements InterfaceName{...}
Although it is uncommon, it is possible for a session bean to allow both remote and local access. In this case, you can do one of the following:
  1. Use two business interfaces, one is decorated with @Remote and the other is decorated with @Local. Let the bean implement both these two interfaces
  2. The bean class explicitly designates the business interfaces by using @Remote and @Local.
Note that the same business interface cannot be both a local and remote business interface. See [5]. To create a stateless session bean for web service client, there are a few requirements. The bean is now an endpoint implementation class. Some requirements are:
  • The class must be annotated with either the javax.jws.WebService or javax.jws.WebServiceProvider.
  • The endpoint class must be annotated @Stateless.
  • The class must have a default public constructor.
  • Business methods that are exposed to web service clients must be annotated @WebMethod
The following is a sample from Java EE5 tutorial:
@Stateless
  @WebService
  public class HelloServiceBean{
    private String message = "Hello";
    public void HelloServiceBean(){}

    @WebMethod
    public String sayHello(String name){ 
      return message + name + ".";
    }
  }
There are some requirements on business methods.
  • If the bean allows remote access through a remote business interface, the arguemnts and return types must be legal types for the Java RMI API.
  • If the bean is a web service endpoint, the arguments and return types for the methods annotated @WebMethod must be legal types for JAX-WS.
  • The modifier must not be static or final.
The code of the ReviewManagerBean is the following:
@Stateless(mappedName="ReviewService")
@Remote(ReviewService.class)
public class ReviewManagerBean extends ReviewServiceImpl
implements ReviewService {
@PersistenceContext(unitName = "reviewSession")
private EntityManager em;
@EJB ReviewListener listener;

@PostConstruct
public void init(){
  // inject the EnityManager.
  injectEntityManager(em);
  addReviewAddedListener(listener);
}
The code contains the ReviewListener. This is not relevent to our topic now. It is used for other purpose. The important thing is how the session bean uses the JPA. The ReviewManagerBean lets the container to manage the transaction. The unitName is from persistence.xml. Notice that the transaction-type is JTA.
<?xml version="1.0"?>

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">

<persistence-unit name="reviewSession" transaction-type="JTA">
<class>examples.ejb.ejb30.domain.Artist</class>
......
<properties>
<property name="kodo.ConnectionURL" value="jdbc:pointbase:server://localhost:9092/demo"/>
<property name="kodo.ConnectionDriverName" value="com.pointbase.jdbc.jdbcUniversalDriver"/>
<property name="kodo.ConnectionUserName" value="examples"/>
<property name="kodo.ConnectionPassword" value="examples"/>
<property name="kodo.jdbc.SynchronizeMappings" value="refresh"/>
</properties>
</persistence-unit>
</persistence>
ReviewManagerBean is injected an EntityManager instead of an EntityManagerFactory. The init() method calls injectEntityManager(em). But this is actually not needed. The reason why the ejb3 example code does this is that it wants to utilize the code ReviewServiceImpl which is for both container managed and application managed service. If you look at the code, you can see that the injected private instance variable em of ReviewManagerBean is not used directly. It will first get injected when the bean is created. And then the injectEntityManager method passes this instance to the parent class ReviewServiceImpl where the em is assigned to the private instance variable _em of ReviewServiceImpl. So if ReviewServiceImpl is not used, then the injection of em is enough. There would be no need to call injectEntityManager.
public void injectEntityManager(EntityManager em) {
_em = em;
}
The business method newArtist can be the following:
public Artist newArtist(String name) {
  if (name == null || name.trim().length() == 0)
   throw new RuntimeException("[" + name + "] can not be an Artist's name");
  EntityManager em = _em
  Artist artist = getPerson(Artist.class, name);
  if (artist == null) {
    artist = new Artist(name);
    em.persist(artist);
  } else {
    throw new ObjectExistedException("Artist named [" + name + "] exists");
  }
  return artist;
}

public  T getPerson(Class personType, String name) {
  return _em.find(personType, name);
}

Notes on Different Name Attributes in Anotation

  1. In @Stateless, @name() is the annotation equivalent of <ejb-name> in ejb-jar.xml. If no @Stateless name() is specified, it defaults to the unqualified bean class name.
  2. In @Stateless, mappedName() is used to assign a global JNDI name to the bean's remote interface.
  3. The mappedName only applies to Remote interface. If you're resolving a local interface the caller must be in the same application. For that, the beanName()/ejb-link should be used.
  4. There is a portability issue about mappedName. If a vendor doesn't support is you can resolve the EJB dependency using the vendor's config file without changing the source code of the standard .xml.
  5. If the .war file is not in the same .ear file as the ejb-jar, you have to use the Remote interface.
  6. beanName() and ejb-link only apply if the target remote ejb is defined within the same application as the referencing component.
See references [1] and [2] for more details.

References

[1] http://forums.sun.com/thread.jspa?threadID=5332558
[2] https://glassfish.dev.java.net/javaee5/ejb/EJB_FAQ.html#EJB_ejb-ref_ejb_local_ref
[3] http://weblogs.java.net/blog/ss141213/archive/2005/12/dont_use_persis_1.html
[4] http://www.developer.com/java/ejb/article.php/10931_3650661_5/EJB-3-Session-Beans.htm
[5] The Java EE5 Tutorial from Sun Microsystems.

No comments:

Post a Comment