RequestScoped bean 能否使用 required="true" 或 @NotNull?
@RequestScoped bean 可以使用 required="true",也可以配合 Bean Validation 使用 @NotNull。当前报错出在主键的恢复方式上:校验失败后,JSF 会跳过模型更新阶段,新建的请求作用域 bean 可能因此拿不到 termsID。
保留 @RequestScoped 时,需要让每次请求都携带主键,并在组件模型更新之外读取它。对于这类编辑页面,使用 @ViewScoped 通常更省代码,无须扩大到 @ConversationScoped。
为什么校验失败后主键没有写入 bean
题目中的普通提交没有设置 immediate="true",相关处理顺序如下:
- 从请求中读取参数,保存到组件中。
- 执行类型转换和校验。
- 将组件值写入 bean 属性,即 Update Model Values。
- 调用
submit()。 - 渲染响应。
某个输入校验失败后,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" 最直接;如果还要拒绝仅含空格的内容,需要另加相应校验。
备注:内容仅供参考。
