【问题标题】:How to detect a concurrrent change of value in the data which are modified in a JSF page?如何检测 JSF 页面中修改的数据中值的并发变化?
【发布时间】:2014-12-15 18:07:02
【问题描述】:

JSF 页面包含银行帐户列表。

当用户点击账户ID时,会显示一个新页面,允许从账户中提取一些资金。这个新页面有一个 Account 类型的参数视图,带有一个转换器,用于将 id 转换为 Account。页面显示账户信息并询问提款金额。

当用户提交表单以提取一些钱时,新余额会在数据库中注册一个无状态的 EJB(带有合并)。

我想检测其他用户是否在同一时刻修改了同一个帐户,但这是不可能的,因为从来没有 OptimisticLockException。

解释:如果一开始版本号(@Version注解的属性)是5,如果有其他用户刚刚更改余额并立即提交表单,版本号就递增到6。我以为第一个用户会有一个 OptimisticLockException 但事实并非如此。事实上,当第一个用户提交表单更改余额时,表单在服务器上重新生成,并再次读取帐户,新版本号等于 6,并且在注册新余额时未检测到版本号更改在 EJB 中具有合并(后跟提交)的数据库。

因此,第一个用户不知道其他用户在处理帐户时更改了余额值。这可能是个问题。

如何检测并发更改?我是否应该在代码中保留版本号并与合并前的版本号进行比较(使用悲观锁!)?还有其他更好的方法吗?也许我必须改变我的页面结构?

我的代码(抱歉,是法语):

修改帐户(充值或提款)的 JSF 页面:

<ui:composition template="./template.xhtml">

  <ui:param 
    name="soustitre" 
    value="Ajouter/retirer de l'argent sur le compte de 
           #{mouvementBean.compteBancaire.nom}"/>

  <ui:define name="metadata">
    <f:metadata>
      <f:viewParam name='id' value='#{mouvementBean.compteBancaire}'
                   converter='#{modifierBean.converter}' />
    </f:metadata>
  </ui:define>

  <ui:define name="content">
    <f:phaseListener type="jsf.util.DebugPhaseListener"/>
    <h1>Ajouter/enlever de l'argent sur le compte de  
        #{mouvementBean.compteBancaire.nom}</h1>
    <h:form id="form">
      <h3>Type mouvement :</h3>
      <h:selectOneRadio 
        id='typeMouvement'
        value='#{mouvementBean.typeMouvement}'
        required='true'
        layout='pageDirection'
        requiredMessage="Vous devez dire s'il s'agit d'un ajout ou d'un retrait">
        <f:selectItem itemValue="ajout"
                      itemLabel="Ajout"/>
        <f:selectItem itemValue="retrait"
                      itemLabel="Retrait"/>
      </h:selectOneRadio>
      <h:message for="typeMouvement"/>
      <h3>Montant de la sommme</h3>
      <h:inputText 
        id="montant" value='#{mouvementBean.montant}' required='true'
        requiredMessage="Le montant doit être un nombre entier positif"/>
      <h:message for="montant"/>
      <br/><br/>
      <h:commandButton action="#{mouvementBean.sauvegarder()}" 
                       value="Enregistrer"/>
    </h:form>

  </ui:define>

</ui:composition>

页面的支持bean:

Named(value = "mouvementBean")
@ViewScoped
public class MouvementBean implements Serializable {

  @EJB
  private GestionnaireDeCompteBancaire gestionnaireDeCompteBancaire;

  private CompteBancaire compteBancaire;
  private String typeMouvement;
  @Min(value = 1, message = "Le montant doit être un entier positif")
  private int montant;

  public CompteBancaire getCompteBancaire() {
    return compteBancaire;
  }

  public void setCompteBancaire(CompteBancaire compteBancaire) {
    this.compteBancaire = compteBancaire;
  }

  public String getTypeMouvement() {
    return typeMouvement;
  }

  public void setTypeMouvement(String typeMouvement) {
    this.typeMouvement = typeMouvement;
  }

  public int getMontant() {
    return montant;
  }

  public void setMontant(int montant) {
    this.montant = montant;
  }

  public String sauvegarder() {
    // Enregistre le mouvement dans l'entité
    switch (typeMouvement) {
    case "ajout":
      compteBancaire.deposer(montant);
      break;
    case "retrait":
      try {
        compteBancaire.retirer(montant);
      } catch (CompteException ex) {
        Logger.getLogger(MouvementBean.class.getName()).log(Level.WARNING, null, ex);
        Util.messageErreur("Solde insuffisant sur le compte", 
              "Solde insuffisant sur le compte de " + compteBancaire.getNom(), 
              "form:montant");
        return null;
      }
      break;
    }
    // Enregistre dans la base de données
    gestionnaireDeCompteBancaire.modifierCompte(compteBancaire);
    // Message de succès
    Util.addFlashInfoMessage(typeMouvement + " de " + montant + " effectué pour "
        + compteBancaire.getNom());
    return "listeComptes?faces-redirect=true";
  }
}

转换器(在另一个支持 bean ModifierBean 中):

public Converter getConverter() {
  return new Converter() {
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, 
          String value) {
      // Pour faire des tests pour la concurrence
      CompteBancaire c = 
            gestionnaireDeCompteBancaire.getById(Long.parseLong(value));
      System.out.println("Dans le convertisseur à la fin de getAsObject " + c);
    return c;
//        return gestionnaireDeCompteBancaire.getById(Long.parseLong(value));
  }

    @Override
    public String getAsString(FacesContext context, UIComponent component, 
          Object value) {
      return ((CompteBancaire)value).getId().toString();
    }
  };
}

指向所有帐户列表中页面的链接:

        <p:dataTable value="#{listeManagedBean.comptes}" var="item"
                     paginator="true" rows="5"
                     rowsPerPageTemplate="2,5,10,20">
          <p:column filterBy="#{item.id}" filterMatchMode="exact"
                    sortBy="#{item.id}"
                    style="width: 5%; text-align: center;">
            <f:facet name="header">
              <h:outputText value="Id"/>
            </f:facet>
            <h:link outcome="mouvement" value="#{item.id}">
              <f:param name="id" value="#{item.id}"/>
            </h:link>
          </p:column>

【问题讨论】:

  • 请提供更多细节。您使用什么 JPA 提供程序?您如何以及在何处实施乐观锁定?
  • 我不强制乐观锁定;合并完成时,JPA 具有默认的乐观锁定。如果不会为第一个用户再次读取实体,则版本号将与数据库的版本号不同(因为另一个用户进行了更新)并且合并将抛出 OptimisticLockException。
  • 默认情况下 Eclipselink 不强制乐观锁定。相反,它假设应用程序本身管理并发问题。所以你最终没有锁定策略。正如Understanding Eclipselink 中所说,“默认情况下,EclipseLink 持久性提供程序假定应用程序负责数据一致性”

标签: jsf jakarta-ee concurrency


【解决方案1】:

问题是:

当第一个用户提交表单更改余额时,表单在服务器上重建并再次读取帐户,新版本号等于6

应该只加载/重新加载帐户

当用户点击账户id时,会显示一个新页面,允许从账户中提取一些钱

而不是什么时候

用户提交表单更改余额

您应该至少使用一个@ViewScoped bean 来维护在回发请求之间加载(和分离)的帐户。

您不应在回发请求时重新加载(或刷新)帐户。

这些应该足以引发 OptimisticLockException。

如果您希望加载时互斥而不是保存时,请使用悲观锁。在这种情况下,只有第一个用户可以访问帐户页面,第二个用户将获得 PessimisticLockException。


根据您的 cmets,使用 ListConverter / ListIndexConverter 或让您自己的转换器扩展 ValueChangeConverter 以避免转换器重新加载。


更新

首先,确保这一点:

@Named(value = "mouvementBean")
@ViewScoped
public class MouvementBean implements Serializable 
{
    public class MouvementBean()
    {
        // log your bean creation to verify that it's not re-instantiated on postback:
        // in some situation CDI annotations like @javax.inject.Named
        // are not compatible with @javax.faces.bean.ViewScoped and your bean becomes
        // RequestScoped. I don't think this is the case, but double check it. And if you
        // don't need CDI just stick with @javax.faces.ManagedBean
    }
}

其次,f:viewParam 也在回发时执行!因此,要么使用o:viewParam(OmniFaces,再次:D),要么尝试使用简单的@PostConstruct 解决方案(并从页面删除 f:viewParam):

@Named(value = "mouvementBean")
@ViewScoped
public class MouvementBean implements Serializable 
{
    @EJB
    private GestionnaireDeCompteBancaire gestionnaireDeCompteBancaire;

    private CompteBancaire compteBancaire;

    @PostConstruct
    public void init()
    {
        FacesContext facesContext = FacesContext.getCurrentInstance();

        // OmniFaces, again and again :)
        // String stringId = Faces.getRequestParameter("id");
        String stringId = facesContext.getExternalContext().getRequestParameterMap().get("id");

        long id = NumberUtils.toLong(stringId);

        compteBancaire = gestionnaireDeCompteBancaire.getById(id);

        // or you can do it with converter. But, generally, it's not a good practice to instance converters on your own, like you do
        // in ModifierBean.getConverter(). Create a standalone converter class that implements javax.faces.convert.Converter and 
        // register it with @FacesConverter("compteConverter"), so you can delegate instantiation to Application
        //
        // Application application = facesContext.getApplication();
        //
        // compteBancaire = (CompteBancaire) application.createConverter("compteConverter").getAsObject(facesContext, null, stringId);
    }
}

第三,即使它确实有效,您也将f:metadata 声明在错误的位置。正确的位置是这样的(f:metadata 不需要“包含”,它不是常规组件):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" 
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      ...>

    <f:metadata>
        <f:viewParam name="id" value="#{mouvementBean.compteBancaire}"
            converter="#{modifierBean.converter}" />
    </f:metadata>

    <ui:composition template="./template.xhtml">
        <ui:param name="soustitre" 
            value="Ajouter/retirer de l'argent sur le compte de #{mouvementBean.compteBancaire.nom}"/>

        ...
    </ui:composition>
</html>

【讨论】:

  • 是的,我错了;实际上转换器是在验证阶段启动的(我添加了一个 phaseListener 来了解这一点)。在 JSF 页面中,我使用 #{bean.amount} 之类的 EL 表达式,其中未提及帐户所有者,因此我不明白为什么在验证阶段启动转换器(针对帐户所有者)。在此转换器中,有代码加载所有者以将 id 转换为所有者。在 JSF 页面中有一个使用转换器的 viewParam,它将所有者放入支持 bean(其范围是视图)的属性中。
  • 问题来自重新加载转换器以将 id 转换为帐户的所有者。如果其他用户修改了帐号,此时转换器会加载帐号的新版本号。
  • 感谢您的帮助米歇尔。我会尝试 OmniFaces 看看它是否能解决问题,但我想找到没有它的解决方案。我找到了另一种方法,即删除转换器并将其替换为 ,该 使用帐户的 id 加载帐户(与转换器的代码相同)。抛出 OptimisticLockException。但是,只有当我了解转换器在验证阶段启动的原因时,我才会感到满意。如果我有页面和 bean 的代码,你能快速阅读并尝试理解吗?
  • 我强烈推荐 OmniFaces,它是一部杰作。我非常广泛地使用它。在验证期间调用转换器以允许验证器处理对象而不是字符串。请参阅this article 进行全面分析,尤其是图像。贴出你的代码,我试试看:)
  • 是的,我知道这是一个由非常优秀的程序员编写的非常好的库,但我有一个原则:如果可能,请遵守标准。在这种特殊情况下,我的解决方案很简单并且有效。也许我会在另一个场合使用那个库。
【解决方案2】:

感谢米歇尔的更新。 “f:viewParam 也在回发时执行”是我错过的。由于&lt;f:viewParam&gt;被视为输入组件,其值在JSF生命周期的验证阶段进行转换和验证。

我将您的答案标记为一个很好的答案,但我写这个另一个答案是为了为未来的读者总结我们的讨论,并告诉他们还有另一个解决方案。

所以,有两种方法可以解决我的问题:

  • 解决方案1:您在更新答案时解释的解决方案(我还没有测试过)。
  • 解决方案 2:使用 &lt;f:viewAction&gt; 代替转换器,正如我之前解释的那样。在我在问题中给出的代码中,您只需删除&lt;f:converter&gt; 并在元数据中添加&lt;f:viewAction&gt; 即可调用将id(长)转换为CompteBancaire 的代码。您还必须为支持 bean 中的 id 添加一个属性。

解决方案 2 需要一个用于支持 bean 的视图范围来保持变量 compteBancaire 的值。我不知道您的解决方案是否有必要。使用我的第一个代码(我的问题中存在并发问题的代码),请求范围就足够了(在我的代码中,您可以将 @ViewScoped 替换为 @RequestScoped,因为变量 compteBancaire 由转换器初始化)。

关于你的评论:

我不认为 f:metadata 处于错误的位置。我使用一个包含元数据位置 (&lt;ui:insert&gt;) 的模板。在使用模板的页面中,f:metadata 将处于最佳位置(就像您在示例中给出的代码一样)。

我不使用@javax.faces.bean.ViewScoped;我使用 CDI 的@javax.faces.view.ViewScoped,所以我认为@Named 不会有问题。

为什么说用方法返回转换器不好呢?这种方式我会遇到什么类型的问题?我经常为转换器使用独立类,但在方法中返回转换器有助于注入 EJB(使用@EJB)。

【讨论】:

    【解决方案3】:

    由于这是银行账户资金,您需要有一个非常可靠的解决方案。也许最好的解决方案是实现长时间运行的事务并锁定数据库中的行,这样任何人都无法选择该行进行更新。我的意思是在 ui 上,如果您实施乐观锁定方法,可能会出现“有人更新数据 blabla”之类的消息,这看起来很糟糕。您不应允许用户在其他人更新此数据时看到他可以更新数据的页面。 或者,如果您选择乐观锁定,那么您将不得不将对象的状态仅存储在有状态 bean(会话)中,并实现重试逻辑以进行支付等所需的所有检查,并使用新版本再次重试更新对象数字。另外不要忘记所有数据库更新都应该在一个事务中,否则您可能会得到错误的数据。使用 spring 事务注解。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-16
      • 2010-09-26
      • 2011-07-13
      • 1970-01-01
      • 1970-01-01
      • 2016-11-29
      相关资源
      最近更新 更多