JPA 中的 Bean 验证
数据验证是在应用程序的所有层(包括持久性)中进行的普通任务。Java™ Persistence API (JPA) 为 Bean 验证 API 提供支持,以便可以在运行时完成数据验证。本主题包括一个使用方案,在此方案中,在样本数字图像库应用程序的 JPA 环境中使用 Bean 验证。
Bean 验证 API 在 Java Enterprise Edition (Java EE) 和 Java Platform, Standard Edition (JSE) 环境中提供跨技术的无缝验证。除 JPA 以外,这些技术还包括 Java Server Faces (JSF) 和 Java EE Connector Architecture (JCA)。您可以阅读“Bean 验证 API”主题中关于 Bean 验证的更多信息。
有三种 Bean 验证核心概念:约束、约束违例处理和验证器。如果您在类似 JPA 的集成环境中运行应用程序,那么无需直接与验证器连接。
验证约束是添加到 JavaBeans 组件的类、字段或方法的注解或 XML 代码。 约束可以是内置约束或者用户定义的约束。这些注释用来定义常规约束定义以及用于编写约束。内置的约束由 Bean 验证规范定义且每个验证提供程序都可以提供。有关内置约束的列表,请参阅“Bean 验证内置约束”主题。 如果您需要不同于内置约束的约束,可以构建自己的用户定义约束。
约束和 JPA
以下使用方案说明如何在样本数字图像库应用程序的 JPA 体系结构中使用内置约束。
package org.apache.openjpa.example.gallery.model;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Entity
public class Image {
private long id;
private ImageType type;
private String fileName;
private byte[] data;
@Id
@GeneratedValue
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@NotNull(message="Image type must be specified.")
@Enumerated(EnumType.STRING)
public ImageType getType() {
return type;
}
public void setType(ImageType type) {
this.type = type;
}
@Pattern(regexp = ".*\\.jpg|.*\\.jpeg|.*\\.gif",
message="Only images of type JPEG or GIF are supported.")
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}
图像类使用两个内置约束 @NotNull 和 @Pattern。@NotNull 约束确保指定了 ImageType 元素且 @Pattern 约束使用正则表达式模式匹配来确保在图像文件名后添加受支持图像格式后缀。每个约束具有对应的验证逻辑,此逻辑在验证图像实体时在运行时开始。 如果没有满足任一约束,那么 JPA 提供程序抛出 ConstraintViolationException 以及定义的消息。JSR-303 规范还为消息属性内变量的使用作出规定。该变量引用资源束中的键入消息。资源束支持特定于环境的消息以及对消息的全球化、翻译和多文化支持。
package org.apache.openjpa.example.gallery.constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import javax.validation.Constraint;
import javax.validation.Payload;
import org.apache.openjpa.example.gallery.model.ImageType;
@Documented
@Constraint(validatedBy = ImageContentValidator.class)
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface ImageContent {
String message() default "Image data is not a supported format.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
ImageType[] value() default { ImageType.GIF, ImageType.JPEG };
}
下一步,您必须创建验证器类 ImageContentValidator。
在验证约束时,此验证器内的逻辑由验证提供程序实施。验证器类通过 @Constraint 注解上的 validatedBy 属性来绑定到约束注释,如以下代码中所示:package org.apache.openjpa.example.gallery.constraint;
import java.util.Arrays;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.openjpa.example.gallery.model.ImageType;
/**
* Simple check that file format is of a supported type
*/
public class ImageContentValidator implements ConstraintValidator<ImageContent, byte[]> {
private List<ImageType> allowedTypes = null;
/**
* Configure the constraint validator based on the image
* types it should support.
* @param constraint the constraint definition
*/
public void initialize(ImageContent constraint) {
allowedTypes = Arrays.asList(constraint.value());
}
/**
*Validate a specified value.
*/
public boolean isValid(byte[] value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
// Verify the GIF header is either GIF87 or GIF89
if (allowedTypes.contains(ImageType.GIF)) {
String gifHeader = new String(value, 0, 6);
if (value.length >= 6 &&
(gifHeader.equalsIgnoreCase("GIF87a") ||
gifHeader.equalsIgnoreCase("GIF89a"))) {
return true;
}
}
// Verify the JPEG begins with SOI and ends with EOI
if (allowedTypes.contains(ImageType.JPEG)) {
if (value.length >= 4 &&
value[0] == 0xff && value[1] == 0xd8 &&
value[value.length - 2] == 0xff && value[value.length -1] == 0xd9) {
return true;
}
}
// Unknown file format
return false;
}
}
将该新约束应用于图像类的 getData() 方法;例如:@ImageContent
public byte[] getData() {
return data;
}
进行对数据属性的验证时,启动 ImageContentValidator 中的 isValid() 方法。此方法包含用于执行对二进制图像数据格式的简单验证的逻辑。ImageContentValidator 中可能低估的功能是它还可以为特定图像类型进行验证。按定义,它接受 JPEG 或 GIF 格式,但是它还可以为特定格式进行验证。例如,通过将注解更改为以下代码示例,指示验证器只允许具有有效 JPEG 内容的图像数据:@ImageContent(ImageType.JPEG)
public byte[] getData() {
return data;
}
类型级别约束也是注意事项,因为您可能需要验证实体上属性的组合。在先前示例中,在单个属性上使用验证约束。类型级别约束可以提供集体验证。例如,应用于图像实体的约束验证是否已设置图像类型(非空),图像文件名称的扩展名是否为受支持类型,以及数据格式对于指示的类型是否正确。但是,例如,它没有集体验证名为 img0.gif 的文件是否类型为 GIF 而数据格式是否针对有效 GIF 文件图像。
有关类型级别约束的更多信息,请参阅白皮书“OpenJPA Bean Validation Primer”和“类型级别约束”部分。验证组
Bean 验证使用验证组来确定进行什么类型的验证以及何时进行验证。

有关使用 OpenJPA 的验证组的更多信息,请阅读白皮书《OpenJPA Bean Validation Primer》和“Validation groups”一节。
JPA 域模型
图像实体是相册、创建者和位置持久类型。相册实体包含其图像实体集合的引用。创建者实体包含图像创建者添加到的相册实体的引用以及对创建的图像实体的引用。这提供域中每个实体之间的完整浏览功能。可嵌入的位置已添加到图像来支持随图像一起存储位置信息。
private Location location;
@Valid
@Embedded
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
@Valid 注解提供对 JPA 环境内可嵌入对象的链式验证。因此,验证图像时,也验证其引用的位置上的任何约束。如果没有指定 @Valid,那么未验证该位置。
在 JPA 环境中,通过 @Valid 进行的链式验证仅可用于可嵌入对象。单独地验证引用的实体和实体集合以防止循环验证。Bean 验证和 JPA 环境
JPA 规范简化了 Bean 验证 API 的集成。在 JSE 环境中,您在运行时类路径上提供 Bean 验证 API 和 Bean 验证提供程序时,缺省情况下,已启用 Bean 验证。在 Java EE 环境中,应用程序服务器包括 Bean 验证提供程序,因此无需随应用程序捆绑 Bean 验证提供程序。在这两种环境中,您都必须使用 V2.0 或更高版本的 persistence.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0" >
...
</persistence>
- 自动
如果验证提供程序在类路径内可用,就启用 Bean 验证。自动是缺省值。
- 回调
指定了回调方式时,Bean 验证提供程序必须可供 JPA 提供程序使用。否则,JPA 提供程序在新的 JPA 实体管理器工厂实例化时抛出异常。
- 无
为特定持久性单元禁用 Bean 验证。

<persistence-unit name="auto-validation">
...
<!-- Validation modes: AUTO, CALLBACK, NONE -->
<validation-mode>AUTO</validation-mode>
...
</persistence-unit>
另一方式是通过在新建 JPA 实体管理器工厂时指定值为“自动”、“回调”或“无”的 javax.persistence.validation.mode 属性来以编程方式配置验证方式,如以下示例中所示:Map<String, String> props = new HashMap<String, String>();
props.put("javax.persistence.validation.mode", "callback");
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("validation", props);
<persistence-unit name="non-default-validation-groups">
<class>my.Entity</class>
<validation-mode>CALLBACK</validation-mode>
<properties>
<property name="javax.persistence.validation.group.pre-persist"
value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
<property name="javax.persistence.validation.group.pre-update"
value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
<property name="javax.persistence.validation.group.pre-remove"
value="javax.validation.groups.Default"/>
</property>
</persistence-unit>
以下示例显示 JPA 生命周期的各种阶段,包括保存、更新和移除:EntityManagerFactory emf =
Persistence.createEntityManagerFactory("BeanValidation");
EntityManager em = emf.createEntityManager();
Location loc = new Location();
loc.setCity("Rochester");
loc.setState("MN");
loc.setZipCode("55901");
loc.setCountry("USA");
// Create an Image with non-matching type and file extension
Image img = new Image();
img.setType(ImageType.JPEG);
img.setFileName("Winter_01.gif");
loadImage(img);
img.setLocation(loc);
// *** PERSIST ***
try {
em.getTransaction().begin();
// Persist the entity with non-matching extension and type
em.persist(img);
} catch (ConstraintViolationException cve) {
// Transaction was marked for rollback, roll it back and
// start a new one
em.getTransaction().rollback();
em.getTransaction().begin();
// Fix the file type and re-try the persist.
img.setType(ImageType.GIF);
em.persist(img);
em.getTransaction().commit();
}
// *** UPDATE ***
try {
em.getTransaction().begin();
// Modify the file name to a non-matching file name
// and commit to trigger an update
img.setFileName("Winter_01.jpg");
em.getTransaction().commit();
} catch (ConstraintViolationException cve) {
// Handle the exception. The commit failed so the transaction
// was already rolled back.
handleConstraintViolation(cve);
}
// The update failure caused img to be detached. It must be merged back
// into the persistence context.
img = em.merge(img);
// *** REMOVE ***
em.getTransaction().begin();
try {
// Remove the type and commit to trigger removal
img.setType(ImageType.GIF);
em.remove(img);
} catch (ConstraintViolationException cve) {
// Rollback the active transaction and handle the exception
em.getTransaction().rollback();
handleConstraintViolation(cve);
}
em.close();
emf.close();
异常
在 JPA 生命周期的任何部分中都可能发生验证错误。
private void handleConstraintViolation(ConstraintViolationException cve) {
Set<ConstraintViolation<?>> cvs = cve.getConstraintViolations();
for (ConstraintViolation<?> cv : cvs) {
System.out.println("------------------------------------------------");
System.out.println("Violation: " + cv.getMessage());
System.out.println("Entity: " + cv.getRootBeanClass().getSimpleName());
// The violation occurred on a leaf bean (embeddable)
if (cv.getLeafBean() != null && cv.getRootBean() != cv.getLeafBean()) {
System.out.println("Embeddable: " +
cv.getLeafBean().getClass().getSimpleName());
}
System.out.println("Attribute: " + cv.getPropertyPath());
System.out.println("Invalid value: " + cv.getInvalidValue());
}
}
在使用属性级别约束时,约束违例处理一般很简单。如果您将类型级别验证器用于类型级别约束,那么可能更难以确定哪个属性或属性组合验证失败。
而且,将整个对象返回为无效值,而不是单个属性。在需要具体故障信息的情况下,可能如 Bean 验证规范中所述,提供对属性级别约束的使用或定制约束违例。样本
可以通过在白皮书 OpenJPA Bean Validation primer 中提供的样本来实施本主题中提供的 JPA 模型和图像库应用程序使用方案。