站内搜索: 请输入搜索关键词
当前页面: 在线文档首页 > JBoss Seam 1.1.0 正式版英文参考手册

Chapter 5. Events, interceptors and exception handling - JBoss Seam 1.1.0 正式版英文参考手册

Chapter 5. Events, interceptors and exception handling

Complementing the contextual component model, there are two further basic concepts that facilitate the extreme loose-coupling that is the distinctive feature of Seam applications. The first is a strong event model where events may be mapped to event listeners via JSF-like method binding expressions. The second is the pervasive use of annotations and interceptors to apply cross-cutting concerns to components which implement business logic.

5.1. Seam events

The Seam component model was developed for use with event-driven applications, specifically to enable the development of fine-grained, loosely-coupled components in a fine-grained eventing model. Events in Seam come in several types, most of which we have already seen:

  • JSF events

  • jBPM transition events

  • Seam page actions

  • Seam component-driven events

  • Seam contextual events

All of these various kinds of events are mapped to Seam components via JSF EL method binding expressions. For a JSF event, this is defined in the JSF template:

<h:commandButton value="Click me!" action="#{helloWorld.sayHello}"/>

For a jBPM transition event, it is specified in the jBPM process definition or pageflow definition:

<start-page name="hello" view-id="/hello.jsp">
    <transition to="hello">
        <action expression="#{helloWorld.sayHello}"/>
    </transition>
</start-page>

You can find out more information about JSF events and jBPM events elsewhere. Lets concentrate for now upon the two additional kinds of events defined by Seam.

5.1.1. Page actions

A Seam page action is an event that occurs just before we render a page. We declare page actions in WEB-INF/pages.xml. We can define a page action for either a particular JSF view id:

<pages>
    <page view-id="/hello.jsp" action="#{helloWorld.sayHello}"/>
<pages>

Or we can use a wildcard to specify an action that applies to all view ids that match the pattern:

<pages>
    <page view-id="/hello/*" action="#{helloWorld.sayHello}"/>
<pages>

If multiple wildcarded page actions match the current view-id, Seam will call all the actions, in order of least-specific to most-specific.

The page action method can return a JSF outcome. If the outcome is non-null, Seam will delegate to the defined JSF navigation rules and a different view may end up being rendered.

Furthermore, the view id mentioned in the <page> element need not correspond to a real JSP or Facelets page! So, we can reproduce the functionality of a traditional action-oriented framework like Struts or WebWork using page actions. For example:

TODO: translate struts action into page action

This is quite useful if you want to do complex things in response to non-faces requests (for example, HTTP GET requests).

5.1.1.1. Page parameters

A JSF faces request (a form submission) encapsulates both an "action" (a method binding) and "parameters" (input value bindings). A page action might also needs parameters!

Since GET requests are bookmarkable, page parameters are passed as human-readable request parameters. (Unlike JSF form inputs, which are anything but!)

Seam lets us provide a value binding that maps a named request parameter to an attribute of a model object.

<pages>
    <page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
        <param name="firstName" value="#{person.firstName}"/>
        <param name="lastName" value="#{person.lastName}"/>
    </page>
<pages>

The <param> declaration is bidirectional, just like a value binding for a JSF input:

  • When a non-faces (GET) request for the view id occurs, Seam sets the value of the named request parameter onto the model object, after performing appropriate type conversions.

  • Any <s:link> or <s:button> transparently includes the request parameter. The value of the parameter is determined by evaluating the value binding during the render phase (when the <s:link> is rendered).

  • Any navigation rule with a <redirect/> to the view id transparently includes the request parameter. The value of the parameter is determined by evaluating the value binding at the end of the invoke application phase.

  • The value is transparently propagated with any JSF form submission for the page with the given view id. (This means that view parameters behave like PAGE-scoped context variables for faces requests.

The essential idea behind all this is that however we get from any other page to /hello.jsp (or from /hello.jsp back to /hello.jsp), the value of the model attribute referred to in the value binding is "remembered", without the need for a conversation (or other server-side state).

This all sounds pretty complex, and you're probably wondering if such an exotic construct is really worth the effort. Actually, the idea is very natural once you "get it". It is definitely worth taking the time to understand this stuff. Page parameters are the most elegant way to propagate state across a non-faces request. They are especially cool for problems like search screens with bookmarkable results pages, where we would like to be able to write our application code to handle both POST and GET requests with the same code. Page parameters eliminate repetitive listing of request parameters in the view definition and make redirects much easier to code.

Note that you don't need an actual page action method binding to use a page parameter. The following is perfectly valid:

<pages>
    <page view-id="/hello.jsp">
        <param name="firstName" value="#{person.firstName}"/>
        <param name="lastName" value="#{person.lastName}"/>
    </page>
<pages>

You can even specify a JSF converter:

<pages>
    <page view-id="/calculator.jsp" action="#{calculator.calculate}">
        <param name="x" value="#{calculator.lhs}"/>
        <param name="y" value="#{calculator.rhs}"/>
        <param name="op" converterId="com.my.calculator.OperatorConverter"/>
    </page>
<pages>
<pages>
    <page view-id="/calculator.jsp" action="#{calculator.calculate}">
        <param name="x" value="#{calculator.lhs}"/>
        <param name="y" value="#{calculator.rhs}"/>
        <param name="op" converter="#{operatorConverter}"/>
    </page>
<pages>

5.1.1.2. Fine-grained files for definition of page actions and parameters

If you have a lot of different page actions and page parameters, you will almost certainly want to split the declarations up over multiple files. You can define actions and parameters for a page with the view id /calc/calculator.jsp in a resource named calc/calculator.page.xml. The root element in this case is the <page> element, and the view id is implied:

<page action="#{calculator.calculate}">
    <param name="x" value="#{calculator.lhs}"/>
    <param name="y" value="#{calculator.rhs}"/>
    <param name="op" converter="#{operatorConverter}"/>
</page>

5.1.2. Component-driven events

Seam components can interact by simply calling each others methods. Stateful components may even implement the observer/observable pattern. But to enable components to interact in a more loosely-coupled fashion than is possible when the components call each others methods directly, Seam provides component-driven events.

We specify event listeners (observers) in WEB-INF/events.xml.

<events>
    <event type="hello">
        <action expression="#{helloListener.sayHelloBack}"/>
        <action expression="#{logger.logHello}"/>
    </event>
<events>

Where the event type is just an arbitrary string.

When an event occurs, the actions registered for that event will be called in the order they appear in events.xml. How does a component raise an event? Seam provides a built-in component for this.

@Name("helloWorld")
public class HelloWorld {
    public void sayHello() {
        FacesMessages.instance().add("Hello World!");
        Events.instance().raiseEvent("hello");
    }
}

Notice that this event producer has no dependency upon event consumers. The event listener may now be implemented with absolutely no dependency upon the producer:

@Name("helloListener")
public class HelloListener {
    public void sayHelloBack() {
        FacesMessages.instance().add("Hello to you too!");
    }
}

If you don't like the events.xml file, we can use an annotation instead:

@Name("helloListener")
public class HelloListener {
    @Observer("hello")
    public void sayHelloBack() {
        FacesMessages.instance().add("Hello to you too!");
    }
}

You might wonder why I've not mentioned anything about event objects in this discussion. In Seam, there is no need for an event object to propagate state between event producer and listener. State is held in the Seam contexts, and is shared between components. However, if you really want to pass an event object, you can:

@Name("helloWorld")
public class HelloWorld {
    private String name;
    public void sayHello() {
        FacesMessages.instance().add("Hello World, my name is #0.", name);
        Events.instance().raiseEvent("hello", name);
    }
}
@Name("helloListener")
public class HelloListener {
    @Observer("hello")
    public void sayHelloBack(String name) {
        FacesMessages.instance().add("Hello #0!", name);
    }
}

5.1.3. Contextual events

Seam defines a number of built-in events that the application can use to perform special kinds of framework integration. The events are:

  • org.jboss.seam.preSetVariable.<name> — called when the context variable <name> is set

  • org.jboss.seam.postSetVariable.<name> — called when the context variable <name> is set

  • org.jboss.seam.preRemoveVariable.<name> — called when the context variable <name> is unset

  • org.jboss.seam.postRemoveVariable.<name> — called when the context variable <name> is unset

  • org.jboss.seam.preDestroyContext.<SCOPE> — called before the <SCOPE> context is destroyed

  • org.jboss.seam.postDestroyContext.<SCOPE> — called after the <SCOPE> context is destroyed

  • org.jboss.seam.beginConversation — called whenever a long-running conversation begins

  • org.jboss.seam.endConversation — called whenever a long-running conversation ends

  • org.jboss.seam.beginPageflow.<name> — called when the pageflow <name> begins

  • org.jboss.seam.endPageflow.<name> — called when the pageflow <name> ends

  • org.jboss.seam.createProcess.<name> — called when the process <name> is created

  • org.jboss.seam.endProcess.<name> — called when the process <name> ends

  • org.jboss.seam.initProcess.<name> — called when the process <name> is associated with the conversation

  • org.jboss.seam.initTask.<name> — called when the task <name> is associated with the conversation

  • org.jboss.seam.startTask.<name> — called when the task <name> is started

  • org.jboss.seam.endTask.<name> — called when the task <name> is ended

  • org.jboss.seam.postCreate.<name> — called when the component <name> is created

  • org.jboss.seam.preDestroy.<name> — called when the component <name> is destroyed

  • org.jboss.seam.beforePhase — called before the start of a JSF phase

  • org.jboss.seam.afterPhase — called after the end of a JSF phase

Seam components may observe any of these events in just the same way they observe component-driven events.

5.2. Seam interceptors

EJB 3.0 introduced a standard interceptor model for session bean components. To add an interceptor to a bean, you need to write a class with a method annotated @AroundInvoke and annotate the bean with an @Interceptors annotation that specifies the name of the interceptor class. For example, the following interceptor checks that the user is logged in before allowing invoking an action listener method:

public class LoggedInInterceptor {

   @AroundInvoke
   public Object checkLoggedIn(InvocationContext invocation) throws Exception {
   
      boolean isLoggedIn = Contexts.getSessionContext().get("loggedIn")!=null;
      if (isLoggedIn) {
         //the user is already logged in
         return invocation.proceed();
      }
      else {
         //the user is not logged in, fwd to login page
         return "login";
      }
   }

}

To apply this interceptor to a session bean which acts as an action listener, we must annotate the session bean @Interceptors(LoggedInInterceptor.class). This is a somewhat ugly annotation. Seam builds upon the interceptor framework in EJB3 by allowing you to use @Interceptors as a meta-annotation. In our example, we would create an @LoggedIn annotation, as follows:

@Target(TYPE)
@Retention(RUNTIME)
@Interceptors(LoggedInInterceptor.class)
public @interface LoggedIn {}

We can now simply annotate our action listener bean with @LoggedIn to apply the interceptor.

@Stateless
@Name("changePasswordAction")
@LoggedIn
@Interceptors(SeamInterceptor.class)
public class ChangePasswordAction implements ChangePassword { 
    
    ...
    
    public String changePassword() { ... }
    
}

If interceptor ordering is important (it usually is), you can add @Interceptor annotations to your interceptor classes to specify a partial order of interceptors.

@Interceptor(around={BijectionInterceptor.class,
                     ValidationInterceptor.class,
                     ConversationInterceptor.class},
             within=RemoveInterceptor.class)
public class LoggedInInterceptor
{
    ...
}

You can even have a "client-side" interceptor, that runs around any of the built-in functionality of EJB3:

@Interceptor(type=CLIENT)
public class LoggedInInterceptor
{
    ...
}

EJB interceptors are stateful, with a lifecycle that is the same as the component they intercept. For interceptors which do not need to maintain state, Seam lets you get a performance optimization by specifying @Interceptor(stateless=true).

Much of the functionality of Seam is implemented as a set of built-in Seam interceptors, including the interceptors named in the previous example. You don't have to explicitly specify these interceptors by annotating your components; they exist for all interceptable Seam components.

You can even use Seam interceptors with JavaBean components, not just EJB3 beans!

EJB defines interception not only for business methods (using @AroundInvoke), but also for the lifecycle methods @PostConstruct, @PreDestroy, @PrePassivate and @PostActive. Seam supports all these lifecycle methods on both component and interceptor not only for EJB3 beans, but also for JavaBean components (except @PreDestroy which is not meaningful for JavaBean components).

5.3. Managing exceptions

JSF is surprisingly limited when it comes to exception handling. As a partial workaround for this problem, Seam lets you define how a particular class of exception is to be treated by annotating the exception class, or declaring the exception class in an XML file. This facility is meant to be combined with the EJB 3.0-standard @ApplicationException annotation which specifies whether the exception should cause a transaction rollback.

Note that Seam applies the EJB 3.0 exception rollback rules also to Seam JavaBean components: system exceptions always cause a transaction rollback, application exceptions do not cause a rollback by default, but do if @ApplicationException(rollback=true) is specified. (An application exception is any checked exception, or any unchecked exception annotated @ApplicationException. A system exception is any unchecked exception without an @ApplicationException annotation.)

This exception results in a HTTP 404 error whenever it propagates out of the Seam component layer. It does not roll back the current transaction.

@HttpError(errorCode=404)
public class ApplicationException extends Exception { ... }

This exception results in a browser redirect whenever it propagates out of the Seam component layer. It also ends the current conversation. It also rolls back the current transaction.

@Redirect(viewId="/failure.xhtml", end=true)
@ApplicationException(rollback=true)
public class UnrecoverableApplicationException extends RuntimeException { ... }

Note that @Redirect does not work for exceptions which occur during the render phase of the JSF lifecycle.

This exception results in immediate rendering of the view, along with a message to the user, when it propagates out of the Seam component layer. It also rolls back the current transaction.

@Render(viewId="/error.xhtml", message="Unexpected error")
public class SystemException extends RuntimeException { ... }

Note that @Render only works when the exception occurs during the INVOKE_APPLICATION phase.

Since we can't add annotations to all the exception classes we are interested in, Seam also lets us specify this functionality in WEB-INF/exceptions.xml.

<exceptions>
   
   <exception class="javax.persistence.EntityNotFoundException">
      <http-error error-code="404"/>
   </exception>
   
   <exception class="javax.persistence.PersistenceException">
      <render>Database access failed</render>
      <end-conversation/>
   </exception>
   
   <exception>
      <redirect view-id="/search.xhtml">Unexpected failure</redirect>
      <end-conversation/>
   </exception>
   
</exceptions>

The last <exception> declaration does not specify a class, and is a catch-all for any exception for which handling is not otherwise specified via annotations or in exceptions.xml.