Saturday, February 23, 2013

Injecting Stateful Session Bean into servlet

    The advent of DI frameworks makes it natural to annotate a field with @EJB or @Inject and let the framework resolve the dependency for us. It can be that simple but sometimes something can behave incorrectly.
Let's assume that we want to use stateful session bean (SFSB) to store all of the greetings which were sent from each particular user. We can inject SFSB into servlet (front controller) and delegate the requests to that bean:
@Stateful
public class SFSBean {
 private List greetings = new ArrayList<>();
 
 public void addGreeting(String greeting) {
  greetings.add(greeting);
 }
 
 public List getAllGreetings() {
  return new ArrayList<>(greetings);
 }
}

@WebServlet("/send")
public class SFSBServlet extends HttpServlet {
 private static final Logger log = Logger.getLogger(SFSBServlet.class);

 @EJB
 private SFSBean bean;
 
 @Override
 public void doGet(HttpServletRequest req, HttpServletResponse res) {
  bean.addGreeting(req.getParameter("greeting"));
  log.info("This user has already sent greetings: " + bean.getAllGreetings());
 }
}
Note: when you use @WebServlet annotation make sure that you either do not have web.xml descriptor at all (it is possible thanks to Convention Over Configuration promoted by JEE6) or you have the right version of it. I had the following header in web.xml:
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
 version="2.4">
and servlet was not discovered because @WebServlet annotation was introduced in servlet 3.0. The following header did the trick:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0"> 
Let's test our piece of code. The following http GET request:
http://localhost:8080/injection-example/send?greeting=Hello
resulted in:
This user has already sent greetings: [Hello]
Subsequent requests were stored correctly. Next, I opened a new browser to start a new http session and performed the same request. The result was as follows:
This user has already sent greetings: [Hello, Hello, Hello]
The situation is caused by the fact that there is one and only one instance of given servlet per application. All of the incoming requests that match servlet's URL pattern go through the same instance concurrently. Once the servlet instance is created and the dependencies are injected - you are done. The same SFSB will live within the servlet. All of the requests from different clients will be accumulated in the same SFSB. In order to overcome the issue we have to use JNDI lookup to get our SFSB and place it in http session. Each JNDI lookup will result in a fresh instance of SFSB:
@WebServlet("/send")
public class SFSBServlet extends HttpServlet {
 private static final Logger log = Logger.getLogger(SFSBServlet.class);

 @Override
 public void doGet(HttpServletRequest req, HttpServletResponse res) {
  SFSBean bean = getBean(req.getSession());
  bean.addGreeting(req.getParameter("greeting"));
  log.info("This user has already sent greetings: " + bean.getAllGreetings());
 }
 
 private SFSBean getBean(HttpSession session) {
  SFSBean bean = getFromSession(session);
  if (bean == null) {
   bean = performJNDIlookup();
   storeInSession(session, bean);
  }
  return bean;
 }
 
 private SFSBean getFromSession(HttpSession session) {
  return (SFSBean) session.getAttribute("bean");
 }
 
 private SFSBean performJNDIlookup() {
  try {
   return (SFSBean) new InitialContext().lookup("java:global/injection-example/SFSBean!control.SFSBean");
  } catch (NamingException e) {
   throw new RuntimeException(e);
  }
 }
 
 private void storeInSession(HttpSession session, SFSBean bean) {
  session.setAttribute("bean", bean);
 }
}

There are some things that we have to deal with when we use http session for storing attributes. When we are in clustered environment, attributes which are placed in http session should be serializable in case of session replication between JVMs. Moreover, session attributes are not thread safe - the user can open a couple of browser's tabs and perform requests (or even AJAX requests) within tight time slots. Despite the fact that SFSBs are thread safe by definition (it is up to EJB container to serialize concurrent requests which go to the same SFSB), the access to the http session should be thread safe as well. I was working with org.apache.catalina.session.StandardSession which utilizes ConcurrentHashMap for storing user arguments, hence it is thread safe.

This post should make you aware that DI can sometimes work different than you expected. Inject your objects the right way!

3 comments :

  1. Hi,

    thanks for this post. I've been searching some example of servlet and session usage. Just one note. Up at the start of the article you mentiones the @Inject annotation but you don't refer to it later in the article. @Inject is SessionScoped by defaut so you would do not need to use sessioncontext in such case.

    Cheers

    ReplyDelete
    Replies
    1. Hi, thanks for your comment.

      It is quite interesting. I was referring to a vanilla EJB not annotated with @SessionScoped. In such a situation @Inject is pretty similar to @EJB - http://www.adam-bien.com/roller/abien/entry/inject_vs_ejb . However, once you annotate SFSB with @SessionScoped you should be able to get rid of that implicit JNDI lookup - http://www.adam-bien.com/roller/abien/entry/does_cdi_injection_of_sessionscoped

      Delete
    2. I meant: explicit JNDI lookup ;)

      Delete