PAYATHON 2026

RequestScoped bean 能否使用 required="true" 或 @NotNull?

排障赵师傅

@RequestScoped bean 可以使用 required="true",也可以配合 Bean Validation 使用 @NotNull。当前报错出在主键的恢复方式上:校验失败后,JSF 会跳过模型更新阶段,新建的请求作用域 bean 可能因此拿不到 termsID。

保留 @RequestScoped 时,需要让每次请求都携带主键,并在组件模型更新之外读取它。对于这类编辑页面,使用 @ViewScoped 通常更省代码,无须扩大到 @ConversationScoped。

为什么校验失败后主键没有写入 bean

题目中的普通提交没有设置 immediate="true",相关处理顺序如下:

  1. 从请求中读取参数,保存到组件中。
  2. 执行类型转换和校验。
  3. 将组件值写入 bean 属性,即 Update Model Values。
  4. 调用 submit()。
  5. 渲染响应。

某个输入校验失败后,JSF 会转到 Render Response,跳过模型更新和普通 action 调用。所以,即使请求已经提交了主键,setTermsID() 也不一定会执行。JSF 生命周期说明

<f:viewParam> 对应的 UIViewParameter 继承自 UIInput,同样参与转换、校验和模型更新,并不会在请求开始时无条件给 bean 属性赋值。UIViewParameter API

此时,preRenderView 仍可能调用 initializeExisting()。当前 bean 是新创建的,termsID 又尚未写入,初始化代码便会因缺少主键而出错。

把校验移到 submit() 中看起来有效,是因为执行到这里时,模型更新已经完成。而组件校验一旦失败,请求就不会执行这个普通 action。

更省代码的方案:使用 @ViewScoped

编辑同一条记录时,通常需要在多次提交之间保留实体主键和编辑状态。@ViewScoped 适合这个范围,校验失败后仍会使用当前视图中的 bean。ViewScoped API

对于使用传统 JSF managed bean 的 JSF 2 项目,可以保留原有字段和保存逻辑,调整作用域,并在初始化方法中增加判断:

import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import javax.faces.context.FacesContext;

@ManagedBean(name = "ARTermsEditBean")
@ViewScoped
public class ARTermsEditBean implements Serializable {

    private static final long serialVersionUID = 1L;

    public void initializeExisting() {
        FacesContext context = FacesContext.getCurrentInstance();

        if (context.isPostback() || context.isValidationFailed()) {
            return;
        }

        // 原有逻辑:检查 termsID,查询实体并填充表单属性。
    }

    // 保留原有字段、访问器和 submit()。
}

页面中的 <f:viewParam>、preRenderView 监听以及 required="true" 可以继续使用。这样,初始化只会在首次请求且参数校验通过时执行,避免提交后重新查询实体、覆盖编辑状态。

如果项目使用 CDI,JSF 2.2 及之后的版本应使用 @Named 配合 javax.faces.view.ViewScoped;Jakarta 项目则使用对应的 jakarta.* 包。不要混用传统 managed bean 与 CDI 的注解组合。CDI 视图作用域还要求 bean 满足可序列化和可代理条件。

绑定 <f:viewParam> 的属性通常要到模型更新阶段才有值,因此不要直接把依赖该属性的加载逻辑搬进视图作用域 bean 的 @PostConstruct。

保留 @RequestScoped 的方案

保留请求作用域时,需要同时处理主键的传递和读取时机。先让 Save 请求明确携带主键:

<p:commandButton value="Save"
                 ajax="false"
                 action="#{ARTermsEditBean.submit}">
    <f:param name="termsid" value="#{ARTermsEditBean.termsID}" />
</p:commandButton>

再在 bean 初始化时直接读取请求参数,不等待 <f:viewParam> 更新模型。可以将下面的方法合并到现有的 @RequestScoped bean 中:

import javax.annotation.PostConstruct;
import javax.faces.context.FacesContext;

@PostConstruct
public void init() {
    String rawId = FacesContext.getCurrentInstance()
            .getExternalContext()
            .getRequestParameterMap()
            .get("termsid");

    if (rawId == null || rawId.trim().isEmpty()) {
        throw new IllegalArgumentException("缺少 termsid");
    }

    try {
        termsID = Long.valueOf(rawId);
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("termsid 格式不正确", e);
    }

    initializeExisting();
}

采用这个方案时,需要做以下调整:

  • 移除原来的 preRenderView 初始化监听,避免重复加载。
  • 主键改由请求参数初始化,可以移除原来的 <f:viewParam>。
  • initializeExisting() 每次请求都要加载实体,不要加入上一方案中通过 isPostback() 提前返回的判断。
  • 保留输入框的 required="true" 和现有保存逻辑。

首次 GET 从 URL 获取 termsid,后续通过该按钮提交时,由 <f:param> 携带。这样,即使必填校验失败,当前请求的 bean 也已经取得主键。页面如果还有其他提交入口,也需要传递同一参数。

代码假设主键类型为 Long。无效参数的异常应接入项目已有的错误处理;加载和保存实体时,仍须检查当前用户是否有权编辑该记录。

只读字段与 @NotNull 的注意事项

在只读字段中显示主键,并不能保证它会写回 bean。浏览器提交参数、JSF 组件解码参数和模型更新是不同的环节。排查时,可以分别检查浏览器请求中的参数,以及 bean 的 setter 是否被调用。

单独改用 <h:inputHidden> 也解决不了这个问题。隐藏输入同样依赖模型更新阶段,其他字段校验失败时,其值仍可能无法写回新 bean。

@NotNull 只拒绝 null,不会拒绝空字符串 ""。NotNull API 它能否拦住空白提交,还取决于空字符串处理配置及 Bean Validation 集成。页面必填项保留 required="true" 最直接;如果还要拒绝仅含空格的内容,需要另加相应校验。

备注:内容仅供参考。