如何对 jsf 复合组件中的集合属性进行 bean 验证,约束不会触发
Posted
技术标签:
【中文标题】如何对 jsf 复合组件中的集合属性进行 bean 验证,约束不会触发【英文标题】:How to bean-validate a collection property in a jsf composite component, constraints do not fire 【发布时间】:2021-04-24 18:22:28 【问题描述】:如何正确定义 jsf 复合组件,以便在它包含集合的情况下正确地对它的值进行 bean 验证?
我们有一个引用细节集合的实体。两者都使用 bean-validation-constraints 进行注释。请注意details
-property 处的注释。
public class Entity implements Serializable
@NotEmpty
private String name;
@NotEmpty
@UniqueCategory(message="category must be unique")
private List<@Valid Detail> details;
/* getters/setters */
public class Detail implements Serializable
@Pattern(regexp="^[A-Z]+$")
private String text;
@NotNull
private Category category;
/* getters/setters */
public class Category implements Serializable
private final int id;
private final String description;
Category(int id, String description)
this.id = id;
this.description = description;
/* getters/setters */
public class MyConstraints
@Target( ElementType.TYPE, ElementType.FIELD )
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueCategoryValidator.class)
@Documented
public static @interface UniqueCategory
String message();
Class<?>[] groups() default ;
Class<? extends Payload>[] payload() default ;
public static class UniqueCategoryValidator implements ConstraintValidator<UniqueCategory, Collection<Detail>>
@Override
public boolean isValid(Collection<Detail> collection, ConstraintValidatorContext context)
if ( collection==null || collection.isEmpty() )
return true;
Set<Category> set = new HashSet<>();
collection.forEach( d-> set.add( d.getCategory() ));
return set.size() == collection.size();
public void initialize(UniqueCategory constraintAnnotation)
// intentionally empty
private MyConstraints()
// only static stuff
实体可以以jsf形式编辑,其中所有涉及细节的任务都封装在一个复合组件中,例如
<h:form id="entityForm">
<h:panelGrid columns="3">
<p:outputLabel for="@next" value="name"/>
<p:inputText id="name" value="#entityUiController.entity.name"/>
<p:message for="name"/>
<p:outputLabel for="@next" value="details"/>
<my:detailsComponent id="details" details="#entityUiController.entity.details"
addAction="#entityUiController.addAction"/>
<p:message for="details"/>
<f:facet name="footer">
<p:commandButton id="saveBtn" value="save"
action="#entityUiController.saveAction"
update="@form"/>
</f:facet>
</h:panelGrid>
</h:form>
my:detailsComponent
定义为
<ui:component xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:p="http://primefaces.org/ui"
>
<cc:interface>
<cc:attribute name="details" required="true" type="java.lang.Iterable"/>
<cc:attribute name="addAction" required="true" method-signature="void action()"/>
</cc:interface>
<cc:implementation>
<p:outputPanel id="detailsPanel">
<ui:repeat id="detailsContainer" var="detail" value="#cc.attrs.details">
<p:inputText id="text" value="#detail.text" />
<p:message for="text"/>
<p:selectOneMenu id="category" value="#detail.category"
converter="#entityUiController.categoriesConverter"
placeholder="please select" >
<f:selectItem noSelectionOption="true" />
<f:selectItems value="#entityUiController.categoryItems"/>
</p:selectOneMenu>
<p:message for="category"/>
</ui:repeat>
</p:outputPanel>
<p:commandButton id="addDetailBtn" value="add" action="#cc.attrs.addAction"
update="detailsPanel" partialSubmit="true" process="@this detailsPanel"/>
</cc:implementation>
</ui:component>
EntityUiController 是
@Named
@ViewScoped
public class EntityUiController implements Serializable
private static final Logger LOG = Logger.getLogger( EntityUiController.class.getName() );
@Inject
private CategoriesBoundary categoriesBoundary;
@Valid
private Entity entity;
@PostConstruct
public void init()
this.entity = new Entity();
public Entity getEntity()
return entity;
public void saveAction()
LOG.log(Level.INFO, "saved entity: 0", this.entity );
public void addAction()
this.entity.getDetails().add( new Detail() );
public List<SelectItem> getCategoryItems()
return categoriesBoundary.getCategories().stream()
.map( cat -> new SelectItem( cat, cat.getDescription() ) )
.collect( Collectors.toList() );
public Converter<Category> getCategoriesConverter()
return new Converter<Category>()
@Override
public String getAsString(FacesContext context, UIComponent component, Category value)
return value==null ? null : Integer.toString( value.getId() );
@Override
public Category getAsObject(FacesContext context, UIComponent component, String value)
if ( value==null || value.isEmpty() )
return null;
try
return categoriesBoundary.findById( Integer.valueOf(value).intValue() );
catch (NumberFormatException e)
throw new ConverterException(e);
;
当我们现在按下上面h:form
中的保存按钮时,name-inputText 被正确验证,但 details-property 上的 @NotEmpty
- 和 @UniqueCategory
-constraint 被忽略。
我错过了什么?
我们在 java-ee-7, payara 4.
【问题讨论】:
【参考方案1】:在深入研究后,我们最终找到了一个使用支持组件ValidateListComponent
的解决方案。它的灵感来自 UIValidateWholeBean
和 WholeBeanValidator
。
该组件从UIInput
扩展并覆盖验证方法以对上述details
-collection 的克隆进行操作,其中填充了children-UIInput
的已验证值。似乎现在可以工作。
<ui:component ...>
<cc:interface componentType="validatedListComponent">
<cc:attribute name="addAction" required="true" method-signature="void action()"/>
</cc:interface>
<cc:implementation>
... see above ...
</cc:implementation>
</ui:component>
支持组件定义为
@FacesComponent(value = "validatedListComponent")
@SuppressWarnings("unchecked")
public class ValidatedListComponent extends UIInput implements NamingContainer
@Override
public String getFamily()
return "javax.faces.NamingContainer";
/**
* Override @link UIInput#processValidators(FacesContext) to switch the order of
* validation. First validate this components children, then validate this itself.
*/
@Override
public void processValidators(FacesContext context)
// Skip processing if our rendered flag is false
if (!isRendered())
return;
pushComponentToEL(context, this);
for (Iterator<UIComponent> i = getFacetsAndChildren(); i.hasNext(); )
i.next().processValidators(context);
if (!isImmediate())
Application application = context.getApplication();
application.publishEvent(context, PreValidateEvent.class, this);
executeValidate(context);
application.publishEvent(context, PostValidateEvent.class, this);
popComponentFromEL(context);
/**
* Override @link UIInput#validate(FacesContext) to validate a cloned collection
* instead of the submitted value.
*
* Inspired by @link UIValidateWholeBean and @link WholeBeanValidator.
*/
@Override
public void validate(FacesContext context)
AreDetailsValidCallback callback = new AreDetailsValidCallback();
visitTree( VisitContext.createVisitContext(context)
, callback
);
if ( callback.isDetailsValid() )
Collection<?> clonedValue = cloneCollectionAndSetDetailValues( context );
validateValue(context, clonedValue);
/**
* private method copied from @link UIInput#executeValidate(FacesContext).
* @param context
*/
private void executeValidate(FacesContext context)
try
validate(context);
catch (RuntimeException e)
context.renderResponse();
throw e;
if (!isValid())
context.validationFailed();
context.renderResponse();
private Collection<Object> cloneCollectionAndSetDetailValues(FacesContext context)
ValueExpression collectionVE = getValueExpression("value");
Collection<?> baseCollection = (Collection<?>) collectionVE.getValue(context.getELContext());
if ( baseCollection==null )
return null;
// Visit all the components children to find their already validated values.
FindDetailValuesCallback callback = new FindDetailValuesCallback(context);
this.visitTree( VisitContext.createVisitContext(context)
, callback
);
// Clone this components value and put in all cloned details with validated values set.
try
Collection<Object> clonedCollection = baseCollection.getClass().newInstance();
for( Entry<Object,Map<String,Object>> entry : callback.getDetailSubmittedData().entrySet() )
Object clonedDetail = cloneDetailAndSetValues( entry.getKey(), entry.getValue() );
clonedCollection.add( clonedDetail );
return clonedCollection;
catch ( Exception e )
throw new ConverterException(e);
private <T> T cloneDetailAndSetValues(T detail, Map<String, Object> propertyMap) throws Exception
T clonedDetail = clone(detail);
// check the properties we have in the detail
Map<String, PropertyDescriptor> availableProperties = new HashMap<>();
for (PropertyDescriptor propertyDescriptor : getBeanInfo(detail.getClass()).getPropertyDescriptors())
availableProperties.put(propertyDescriptor.getName(), propertyDescriptor);
// put their value (or local value) into our clone
for (Map.Entry<String, Object> propertyToSet : propertyMap.entrySet())
availableProperties.get(propertyToSet.getKey()).getWriteMethod().invoke(clonedDetail,
propertyToSet.getValue());
return clonedDetail;
private static <T> T clone(T object) throws Exception
// clone an object using serialization.
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(object);
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(byteArrayInputStream);
return (T) in.readObject();
private class FindDetailValuesCallback implements VisitCallback
private final FacesContext context;
private final Map<Object, Map<String, Object>> detailSubmittedData = new HashMap<>();
public FindDetailValuesCallback(final FacesContext context)
this.context = context;
final Map<Object, Map<String, Object>> getDetailSubmittedData()
return detailSubmittedData;
@Override
public VisitResult visit(VisitContext visitContext, UIComponent component)
if ( isVisitorTarget(component) )
ValueExpression ve = component.getValueExpression("value");
Object value = ((EditableValueHolder)component).getValue();
if (ve != null)
ValueReference vr = ve.getValueReference(context.getELContext());
String prop = (String)vr.getProperty();
Object base = vr.getBase();
Map<String, Object> propertyMap
= Optional.ofNullable( detailSubmittedData.get(base) )
.orElseGet( HashMap::new );
propertyMap.put(prop, value );
detailSubmittedData.putIfAbsent( base, propertyMap);
return ACCEPT;
private class AreDetailsValidCallback implements VisitCallback
private boolean detailsValid;
public AreDetailsValidCallback()
this.detailsValid = true;
public boolean isDetailsValid()
return detailsValid;
@Override
public VisitResult visit(VisitContext visitContext, UIComponent component)
if ( isVisitorTarget(component) )
if ( !((EditableValueHolder)component).isValid() )
this.detailsValid = false;
return ACCEPT;
private boolean isVisitorTarget( UIComponent component )
return component instanceof EditableValueHolder && component.isRendered()
&& component!=ValidatedListComponent.this;
更新:有时在FindDetailValuesCallback#visit
中获取ValueReference
是个问题。 Michele here 给出的答案解决了这个问题。
【讨论】:
以上是关于如何对 jsf 复合组件中的集合属性进行 bean 验证,约束不会触发的主要内容,如果未能解决你的问题,请参考以下文章
JSF拒绝处理深层嵌套的复合组件。不,真的:“JSF1098:[...]这浪费了处理器时间[...]”