动作和动作监听器之间的区别

412
actionactionListener 有什么区别?我应该在什么情况下使用 action,而不是 actionListener
4个回答

618

actionListener

如果您想在真正的业务操作执行之前有一个钩子,例如记录它和/或设置附加属性(通过<f:setPropertyActionListener>),并/或访问调用操作的组件(可通过ActionEvent参数获得),请使用actionListener。因此,仅用于在真正的业务操作被调用之前进行准备。

actionListener方法默认具有以下签名:

import javax.faces.event.ActionEvent;
// ...

public void actionListener(ActionEvent event) {
    // ...
}

应该这样声明,不需要任何方法括号:

<h:commandXxx ... actionListener="#{bean.actionListener}" />

请注意,EL 2.2无法传递{{additional}}参数。但是,您可以通过传递和指定自定义参数来完全覆盖{{ActionEvent}}参数。以下示例是有效的:
<h:commandXxx ... actionListener="#{bean.methodWithoutArguments()}" />
<h:commandXxx ... actionListener="#{bean.methodWithOneArgument(arg1)}" />
<h:commandXxx ... actionListener="#{bean.methodWithTwoArguments(arg1, arg2)}" />

public void methodWithoutArguments() {}
public void methodWithOneArgument(Object arg1) {}
public void methodWithTwoArguments(Object arg1, Object arg2) {}

请注意在无参数方法表达式中括号的重要性。如果它们不存在,JSF仍将期望带有ActionEvent参数的方法。
如果您使用EL 2.2+,则可以通过声明多个操作侦听器方法。
<h:commandXxx ... actionListener="#{bean.actionListener1}">
    <f:actionListener binding="#{bean.actionListener2()}" />
    <f:actionListener binding="#{bean.actionListener3()}" />
</h:commandXxx>

public void actionListener1(ActionEvent event) {}
public void actionListener2() {}
public void actionListener3() {}

请注意binding属性中括号的重要性。如果它们不存在,EL会混淆地抛出一个错误:javax.el.PropertyNotFoundException: Property 'actionListener1' not found on type com.example.Bean,因为默认情况下binding属性被解释为值表达式,而不是方法表达式。添加EL 2.2+风格的括号可以将值表达式透明地转换为方法表达式。另请参见为什么我能绑定<f:actionListener>到任意方法而不受JSF支持的限制?

动作

如果您想执行业务操作并必要时处理导航,请使用actionaction方法可以(因此不必)返回一个String,该字符串将用作导航案例结果(目标视图)。 返回值为nullvoid将使其返回到同一页并保持当前视图范围活动。 空字符串或相同的视图ID的返回值也将返回到同一页,但重新创建视图范围,并因此销毁任何当前活动的视图范围bean(如果适用),并在可能的情况下重新创建它们。

action方法可以是任何有效的MethodExpression,也可以使用EL 2.2参数的方法,如下所示:

<h:commandXxx value="submit" action="#{bean.edit(item)}" />

使用这种方法:
public void edit(Item item) {
    // ...
}

注意,当你的操作方法仅返回一个字符串时,你也可以在action属性中直接指定该字符串。因此,这样做非常笨拙:
<h:commandLink value="Go to next page" action="#{bean.goToNextpage}" />

使用这种毫无意义的方法返回硬编码字符串:

public String goToNextpage() {
    return "nextpage";
}

相反,直接将硬编码的字符串放入属性中:

<h:commandLink value="Go to next page" action="nextpage" />

请注意,这表明了一个糟糕的设计:通过POST导航。这不利于用户和SEO。这一切都在何时应该使用h:outputLink而不是h:commandLink?中解释,并且应该得到解决。
<h:link value="Go to next page" outcome="nextpage" />

另请参阅如何在JSF中导航?如何使URL反映当前页面(而不是上一个页面)?


f:ajax监听器

自从JSF 2.x以来,有第三种方法,即<f:ajax监听器>

<h:commandXxx ...>
    <f:ajax listener="#{bean.ajaxListener}" />
</h:commandXxx>

ajaxListener方法默认的签名如下:

import javax.faces.event.AjaxBehaviorEvent;
// ...

public void ajaxListener(AjaxBehaviorEvent event) {
    // ...
}

在Mojarra中,AjaxBehaviorEvent参数是可选的,以下内容同样有效。
public void ajaxListener() {
    // ...
}

但在MyFaces中,它会抛出一个MethodNotFoundException异常。当你想省略参数时,以下内容在两个JSF实现中都有效。

<h:commandXxx ...>
    <f:ajax execute="@form" listener="#{bean.ajaxListener()}" render="@form" />
</h:commandXxx>

在命令组件上,Ajax监听器并不是非常有用。它们在输入和选择组件<h:inputXxx>/<h:selectXxx>上更有用。在命令组件中,只需使用action和/或actionListener以提高代码的清晰度和自我记录性。此外,像actionListener一样,f:ajax listener也不支持返回导航结果。

<h:commandXxx ... action="#{bean.action}">
    <f:ajax execute="@form" render="@form" />
</h:commandXxx>

关于executerender属性的解释,请参阅了解PrimeFaces过程/更新和JSF f:ajax execute/render属性


调用顺序

actionListener总是在与其绑定的组件中按照声明顺序被调用,且在action之前被执行。f:ajax listener总是在任何actionListener之前被执行。因此,以下示例:

<h:commandButton value="submit" actionListener="#{bean.actionListener}" action="#{bean.action}">
    <f:actionListener type="com.example.ActionListenerType" />
    <f:actionListener binding="#{bean.actionListenerBinding()}" />
    <f:setPropertyActionListener target="#{bean.property}" value="some" />
    <f:ajax listener="#{bean.ajaxListener}" />
</h:commandButton>

将按以下顺序调用方法:

  1. Bean#ajaxListener()
  2. Bean#actionListener()
  3. ActionListenerType#processAction()
  4. Bean#actionListenerBinding()
  5. Bean#setProperty()
  6. Bean#action()

异常处理

actionListener支持一种特殊的异常:AbortProcessingException。如果从actionListener方法中抛出此异常,则JSF将跳过任何剩余的action listeners和action方法,并直接进行呈现响应。您不会看到错误/异常页面,但JSF将记录它。每当从actionListener抛出任何其他异常时,这也将隐式地完成。因此,如果您打算通过业务异常的结果来阻止页面的块,则应在action方法中执行该作业。

如果使用actionListener的唯一原因是具有返回到同一页的void方法,则这是一个不好的选择。与某些IDE通过EL验证所让您相信的相反,action方法也可以完全返回void。请注意PrimeFaces展示示例中充斥着这种类型的actionListener。这确实是错误的。不要以此为借口去效仿。

在ajax请求中,需要使用特殊的异常处理程序。无论您是否使用<f:ajax>listener属性都是如此。有关说明和示例,请前往JSF ajax请求中的异常处理

1
你说得对,在默认情况下,actionListeners中的异常会被吞噬,但在JSF 2.0中,这种行为可以被改变。请参见下面的答案以了解详情。 - Arjan Tijms
3
@arjan:你的观点是正确的,JSF 2.0允许你更改actionListener抛出异常时的默认处理方式,但这仍然不是滥用actionListener执行业务操作的好借口。 - BalusC
1
实际上,业务操作主要在请求/响应周期的“流”中,只有action与之对应。actionListener用于次要事项。只是想澄清,如果需要,可以传播来自actionListener的异常 ;) - Arjan Tijms
2
@Kawy:当在 actionListener 属性中使用时,方法名称可自由选择,并且必须为 public。只有当您正在使用 <f:actionListener type> 时,processAction 名称才是强制性的,因为该类型必须实现 ActionListener 接口,该接口恰好定义了该方法名称 processAction - BalusC
2
@Muhammed:在所有常规动作监听器之前调用ajax动作监听器。请注意,即使使用<f:ajax>,在命令组件的情况下,您也应该优先使用“action”属性进行业务操作。例如:<h:commandButton action="#{bean.businessAction}"><f:ajax/></h:commandButton>。 - BalusC
显示剩余12条评论

48

正如BalusC所指出的那样,在默认情况下,actionListener会吞噬异常,但在JSF 2.0中还有一些细节需要注意。也就是说,它不仅会将异常记录下来并输出日志,而且实际上会发布该异常。

这是通过以下方式进行调用的:

context.getApplication().publishEvent(context, ExceptionQueuedEvent.class,                                                          
    new ExceptionQueuedEventContext(context, exception, source, phaseId)
);

该事件的默认监听器是ExceptionHandler,对于Mojarra而言,其设置为com.sun.faces.context.ExceptionHandlerImpl。此实现基本上会重新抛出任何异常,但当它涉及到AbortProcessingException时,则会记录日志。ActionListeners会将由客户端代码引发的异常包装在这样的AbortProcessingException中,这就是为什么总是记录这些异常的原因。

然而,在faces-config.xml中可以使用自定义实现替换此ExceptionHandler

<exception-handlerfactory>
   com.foo.myExceptionHandler
</exception-handlerfactory>

除了全局监听事件之外,单个bean也可以监听这些事件。以下是此概念的验证:

@ManagedBean
@RequestScoped
public class MyBean {

    public void actionMethod(ActionEvent event) {

        FacesContext.getCurrentInstance().getApplication().subscribeToEvent(ExceptionQueuedEvent.class, new SystemEventListener() {

        @Override
        public void processEvent(SystemEvent event) throws AbortProcessingException {
            ExceptionQueuedEventContext content = (ExceptionQueuedEventContext)event.getSource();
            throw new RuntimeException(content.getException());
        }

        @Override
        public boolean isListenerForSource(Object source) {
            return true;
        }
        });

        throw new RuntimeException("test");
    }

}

(注意,这不是编写监听器的正常方法,仅供演示!)

像这样从 Facelet 调用:

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core">
    <h:body>
        <h:form>
            <h:commandButton value="test" actionListener="#{myBean.actionMethod}"/>
        </h:form>
    </h:body>
</html>

会导致显示错误页面。


44

ActionListener首先被触发,可以修改响应选项,然后Action被调用并确定下一页的位置。

如果您在同一页上有多个按钮,这些按钮应该前往相同的位置,但执行略有不同的操作,则可以使用相同的Action来处理每个按钮,但使用不同的ActionListener来处理略有不同的功能。

以下链接描述了它们之间的关系:

http://www.java-samples.com/showtutorial.php?tutorialid=605


4
加一,黑体字已经说了几乎一切。 - Farhan stands with Palestine

0

TL;DR:

ActionListener(可以有多个)按照注册的顺序在action之前执行。

长答案:

一个业务action通常会调用一个EJB服务,如果需要的话还会设置最终结果和/或导航到不同的视图。如果你不是在做这些事情,那么使用actionListener更合适,例如当用户与组件交互时,比如h:commandButtonh:link,可以通过将托管bean方法的名称传递给UI组件的actionListener属性来处理它们,或者实现一个ActionListener接口并将实现类名称传递给UI组件的actionListener属性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接