I wrote a quick-and-dirty quiz application to check whether my software engineering students do their reading assignments (or at least google quickly). Can an Elvis-level programmer do this in a couple of days with EJB3 and JSF? Here is my experience report.
In my software engineering course, I assign readings from a SafariU bookshelf. Having read the delightful book My Freshman Year: What a Professor Learned by Becoming a Student, I realized that I needed a mechanism for checking that they actually did the reading. At the beginning of each class meeting, I give a 5 minute quiz, and then we all look at the answers:
I wrote a simple EJB3/JSF app for that purpose. (Why JSF? Maybe it's not the world's most wonderful framework, but it's part of GlassFish and supported by NetBeans.) It took about three days of solid work: one productive day, one totally annoying day chasing down a Glassfish bug, and one frustrating day of chasing spurious problems that could be avoided if I, or preferably Netbeans, had been smarter.
Is it really possible to write a quick and dirty web application with EJB3/JSF in a couple of days? What works well and what doesn't? Continuing the "Elvis meets GlassFish" saga, I'd like to report on Elvis' experience.
POJOs and EJB3/JPA persistence worked very well. I had a straightforward OOD model. A quiz has questions. There are two kinds of questions: fill-in-the-blank questions and multiple-choice questions. A multiple-choice question has choices.
Mapping it to the database was extremely straightforward.
@Entity public class Quiz implements Serializable { @Id @GeneratedValue private Long id; private String title; @OneToMany private List<Question> questions; . . . }
I set up the data model in Java and let JPA generate the tables. Easy stuff--Elvis can do this.
I add questions to a quiz
(not necessarily by increasing ID). The ordering needs to be preserved,
but it purely depends on the insertion order, not any intrinsic property
of the element entities.
This could be easily accomplished by storing the index in an additional column in the Quiz_Question join table, like this:
Quiz | Question | Index ----------------------- 1001 | 20552 | 1 1001 | 20207 | 2 . . .
Hibernate can do this, with the @IndexColumn annotation. If you agree that GlassFish should support a true List collection as well, please vote for this enhancement request.
Without explicit support, the data model isn't as clean as it should be. I ended up adding index values to the collected entities, but that's a hack.
I was concerned about this.
Entity beans. Session beans. Managed beans. Would I encounter the evil
distraction of data transfer objects? Was I going to waste a lot of time
chasing through two layers?
But it all worked well. There are no data transfer objects--the entities are POJOs, and they can simply move between the session beans and the JSF managed beans. Ideally, it would have been nice to use something seamless such as Seam, but the session bean/managed bean split wasn't painful. I put all the business logic into the session beans and all the display logic (such as current user, current selection) into the managed beans. Here is an example. Note the injection of the session bean, and note how the button action calls the session bean to persist the current submission.
public class QuizMB { // a JSF managed bean @EJB private EditSB editSB; // a session bean . . . public String nextQuestion() { // a button action if (pastEndDate()) return "past_end_date"; // calling the session bean currentSubmission = quizSB.saveSubmission(currentSubmission); if (currentQuestionIndex == getHighestQuestionIndex()) return "all_questions_taken"; else { currentQuestionIndex++; return currentQuestionType(); } }
I was concerned about lazily
fetched collections. When an entity is exported from a session bean to a
managed bean, it is detached from the container. If it has a lazily
fetched property (by default, any collection-valued property is lazy),
then you can't access it in the managed bean.
Of course, you can't just make all collections eager. The initial page shows a selection box filled with the titles of all quizzes that have elapsed. I simply get a List<Quiz> from the session bean. If a quiz loads all questions, and all questions load all choices, then I pull out a lot more data than I need. So, that collection needs to be lazy.
On the other hand, when I have a multiple choice question, I really want all the choices with it, and there are only a few for each question. I used eager fetching here.
It was easy enough to override the fetch type on a case-by-case basis. Make a query that fetches the parts that you need.
public Quiz getQuiz(Long id) { Query q = em.createQuery("SELECT DISTINCT x FROM Quiz x LEFT JOIN FETCH x.questions WHERE x.id = :id") .setParameter("id", id); return (Quiz) q.getSingleResult(); }
Note the LEFT JOIN FETCH. I first had a JOIN FETCH, and then getSingleResult threw an exception when a quiz had no questions (which only happens when you save an empty quiz, log out, log in again, and then retrieve it, as I found out the hard way when I demoed the app to a colleague.)
Full disclosure: I had a problem with a complex join fetch, but I think that's a bug. I worked around it by making two separate queries.
Overall, the lazy fetch default is good. Override it with eager fetching for small collections, and use LEFT JOIN FETCH otherwise.
I thought it would be a giant pain to use JSF without a GUI builder. (I didn't want to figure out how to build the GUI in Creator and write the remaining code in Netbeans.) But it wasn't too bad. The key is to use CSS as much as possible. Make styles for all functional elements. For example, I tagged prompts (such as User name and Password) as prompt:
<h:outputText styleClass="prompt" value="#{msgs.login_username}"/>
I added a style
.prompt:after { content: ":"; } .prompt { font-weight: bold; }
With a bit of practice, I was able to put most of the styling into CSS, so that the JSF pages only contained the components and the EL wiring.
Ideally, of course, JSF would have many more prefabricated components, but that's a topic for another blog...
I was concerned about
debugging. After all, when something goes wrong with the app server, you
get the Stack Trace from Hell instead of a gentle message "error x
in file y line z".
[#|2006-08-25T08:45:59.290-0800|SEVERE|sun-appserver-ee9.1|javax.enterprise.resource.webcontainer.jsf.application|_ThreadID=21;_ThreadName=httpWorkerThread-8080-1;_RequestID=55780119-4727-462d-b53f-196c600080ba;|javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException javax.faces.el.EvaluationException: javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:91) at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:96) at javax.faces.component.UICommand.broadcast(UICommand.java:383) at javax.faces.component.UIViewRoot.broadcastEvents(UIViewRoot.java:471) at javax.faces.component.UIViewRoot.processApplication(UIViewRoot.java:783) at com.sun.faces.lifecycle.InvokeApplicationPhase.execute(InvokeApplicationPhase.java:97) at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:244) at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:113) at javax.faces.webapp.FacesServlet.service(FacesServlet.java:244) at org.apache.catalina.core.ApplicationFilterChain.servletService(ApplicationFilterChain.java:397) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:278) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:246) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:185) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:73) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:182) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.VirtualServerPipeline.invoke(VirtualServerPipeline.java:120) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:137) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:231) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.invokeAdapter(DefaultProcessorTask.java:619) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.processNonBlocked(DefaultProcessorTask.java:550) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.process(DefaultProcessorTask.java:780) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.executeProcessorTask(DefaultReadTask.java:326) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:251) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:205) at com.sun.enterprise.web.connector.grizzly.TaskBase.run(TaskBase.java:252) at com.sun.enterprise.web.connector.grizzly.WorkerThreadImpl.run(WorkerThreadImpl.java:103) Caused by: javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException at com.horstmann.qq.session._QuizSB_Wrapper.saveSubmission(com.horstmann.qq.session._QuizSB_Wrapper.java) at com.horstmann.qq.web.QuizMB.saveSubmission(QuizMB.java:114) at com.horstmann.qq.web.QuizMB.nextQuestion(QuizMB.java:119) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at com.sun.el.parser.AstValue.invoke(AstValue.java:157) at com.sun.el.MethodExpressionImpl.invoke(MethodExpressionImpl.java:283) at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:71) ... 33 more |#] [#|2006-08-25T08:45:59.294-0800|WARNING|sun-appserver-ee9.1|javax.enterprise.resource.webcontainer.jsf.lifecycle|_ThreadID=21;_ThreadName=httpWorkerThread-8080-1;_RequestID=55780119-4727-462d-b53f-196c600080ba;|#{quizzes.nextQuestion}: javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException javax.faces.FacesException: #{quizzes.nextQuestion}: javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:111) at javax.faces.component.UICommand.broadcast(UICommand.java:383) at javax.faces.component.UIViewRoot.broadcastEvents(UIViewRoot.java:471) at javax.faces.component.UIViewRoot.processApplication(UIViewRoot.java:783) at com.sun.faces.lifecycle.InvokeApplicationPhase.execute(InvokeApplicationPhase.java:97) at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:244) at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:113) at javax.faces.webapp.FacesServlet.service(FacesServlet.java:244) at org.apache.catalina.core.ApplicationFilterChain.servletService(ApplicationFilterChain.java:397) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:278) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:246) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:185) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:73) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:182) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.VirtualServerPipeline.invoke(VirtualServerPipeline.java:120) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:137) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:231) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.invokeAdapter(DefaultProcessorTask.java:619) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.processNonBlocked(DefaultProcessorTask.java:550) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.process(DefaultProcessorTask.java:780) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.executeProcessorTask(DefaultReadTask.java:326) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:251) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:205) at com.sun.enterprise.web.connector.grizzly.TaskBase.run(TaskBase.java:252) at com.sun.enterprise.web.connector.grizzly.WorkerThreadImpl.run(WorkerThreadImpl.java:103) Caused by: javax.faces.el.EvaluationException: javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:91) at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:96) ... 32 more Caused by: javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException at com.horstmann.qq.session._QuizSB_Wrapper.saveSubmission(com.horstmann.qq.session._QuizSB_Wrapper.java) at com.horstmann.qq.web.QuizMB.saveSubmission(QuizMB.java:114) at com.horstmann.qq.web.QuizMB.nextQuestion(QuizMB.java:119) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at com.sun.el.parser.AstValue.invoke(AstValue.java:157) at com.sun.el.MethodExpressionImpl.invoke(MethodExpressionImpl.java:283) at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:71) ... 33 more |#] at com.sun.corba.ee.impl.presentation.rmi.StubInvocationHandlerImpl.privateInvoke(StubInvocationHandlerImpl.java:210) at com.sun.corba.ee.impl.presentation.rmi.StubInvocationHandlerImpl.invoke(StubInvocationHandlerImpl.java:119) at com.sun.corba.ee.impl.presentation.rmi.bcel.BCELStubBase.invoke(BCELStubBase.java:197) at com.horstmann.qq.session.__QuizSB_Remote_DynamicStub.saveSubmission(__QuizSB_Remote_DynamicStub.java) at com.horstmann.qq.session._QuizSB_Wrapper.saveSubmission(com.horstmann.qq.session._QuizSB_Wrapper.java) at com.horstmann.qq.web.QuizMB.saveSubmission(QuizMB.java:114) at com.horstmann.qq.web.QuizMB.nextQuestion(QuizMB.java:119) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at com.sun.el.parser.AstValue.invoke(AstValue.java:157) at com.sun.el.MethodExpressionImpl.invoke(MethodExpressionImpl.java:283) at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:71) at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:96) at javax.faces.component.UICommand.broadcast(UICommand.java:383) at javax.faces.component.UIViewRoot.broadcastEvents(UIViewRoot.java:471) at javax.faces.component.UIViewRoot.processApplication(UIViewRoot.java:783) at com.sun.faces.lifecycle.InvokeApplicationPhase.execute(InvokeApplicationPhase.java:97) at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:244) at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:113) at javax.faces.webapp.FacesServlet.service(FacesServlet.java:244) at org.apache.catalina.core.ApplicationFilterChain.servletService(ApplicationFilterChain.java:397) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:278) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:246) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:185) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:73) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:182) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.VirtualServerPipeline.invoke(VirtualServerPipeline.java:120) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:137) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:231) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.invokeAdapter(DefaultProcessorTask.java:619) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.processNonBlocked(DefaultProcessorTask.java:550) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.process(DefaultProcessorTask.java:780) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.executeProcessorTask(DefaultReadTask.java:326) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:251) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:205) at com.sun.enterprise.web.connector.grizzly.TaskBase.run(TaskBase.java:252) at com.sun.enterprise.web.connector.grizzly.WorkerThreadImpl.run(WorkerThreadImpl.java:103) Caused by: java.rmi.RemoteException javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.RemoteException at com.horstmann.qq.session._QuizSB_Wrapper.saveSubmission(com.horstmann.qq.session._QuizSB_Wrapper.java) at com.horstmann.qq.web.QuizMB.saveSubmission(QuizMB.java:114) at com.horstmann.qq.web.QuizMB.nextQuestion(QuizMB.java:119) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at com.sun.el.parser.AstValue.invoke(AstValue.java:157) at com.sun.el.MethodExpressionImpl.invoke(MethodExpressionImpl.java:283) at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:71) at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:96) at javax.faces.component.UICommand.broadcast(UICommand.java:383) at javax.faces.component.UIViewRoot.broadcastEvents(UIViewRoot.java:471) at javax.faces.component.UIViewRoot.processApplication(UIViewRoot.java:783) at com.sun.faces.lifecycle.InvokeApplicationPhase.execute(InvokeApplicationPhase.java:97) at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:244) at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:113) at javax.faces.webapp.FacesServlet.service(FacesServlet.java:244) at org.apache.catalina.core.ApplicationFilterChain.servletService(ApplicationFilterChain.java:397) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:278) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:246) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:185) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:73) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:182) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at com.sun.enterprise.web.VirtualServerPipeline.invoke(VirtualServerPipeline.java:120) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:137) at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:586) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:556) at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:939) at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:231) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.invokeAdapter(DefaultProcessorTask.java:619) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.processNonBlocked(DefaultProcessorTask.java:550) at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.process(DefaultProcessorTask.java:780) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.executeProcessorTask(DefaultReadTask.java:326) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:251) at com.sun.enterprise.web.connector.grizzly.DefaultReadTask.doTask(DefaultReadTask.java:205) at com.sun.enterprise.web.connector.grizzly.TaskBase.run(TaskBase.java:252) at com.sun.enterprise.web.connector.grizzly.WorkerThreadImpl.run(WorkerThreadImpl.java:103) |#]
This was a serious issue. I learned quickly to recode all my session beans to
public Output mySessionBean(Input input) { try { . . . } finally (RuntimeException ex) { logger.log(Level.SEVERE, "", ex); throw ex; } }
Note to self: Try an interceptor next time.
The NetBeans debugger helped immensely. Without the ability to set breakpoints in the managed bean and session bean code, I would have had a truly miserable time.
Something needs to be done. Glassfish has the wrong view of the world. It thinks that it is running a perfectly debugged application, and if something goes wrong, then it should dump it's life story to the log and sulk. Just imagine if the Java compiler took that approach to error handling. You'd compile, find no class file, and search for elusive clues in the log.
$ javac MyFile.java
$ ls MyFile.class
ls: MyFile.class: No such file or directory
$ vi /usr/local/jdk1.6.0/logs/compiler.log
Fortunately, the compiler doesn't say "don't blame me--it was your fault for feeding me a buggy program". Neither should GlassFish.
In the past, EJB was far too complicated for rapid development. It only made sense for "big iron" apps. But EJB3 is different. A competent developer (Elvis, not Einstein) can use it to put together a simple app quickly. The tools could be better (error handling, visual editing, JSF components), but Netbeans is reasonable and gives you one-stop shopping.