如何让Eclipselink在映射的超类中触发JSR303约束?

时间:2016-05-18 22:04:23

标签: jpa eclipselink bean-validation

EclipsLink似乎并未在基类中检测或触发JSR303注释约束,该基类是persist()操作期间实体的映射超类。

例如:

public Base
{
    @NotNull
    private Integer id; 

    private String recordName; 

    //other stuff (getters etc) 
}

然后

public class MyObject
    extends Base
{
     //stuff...
}

然后:

<mapped-superclass class="Base">
    <attributes>
         <basic name="recordName">
             <column name = "NAME" />
         </basic>
    </attributes>
</mapped-superclass> 

最后:

<entity class="MyObject">
    <table name="TheTable"/>
        <attributes>
            <id name="id">
                <column name="recordId" />
            </id>
        </attributes>
</entity>     

其他一些相关参数:

  • 使用jpa 2.1 - 特别是eclipslink 2.6.2和2.6.3
  • 我是集成测试 - 所以java se(和spock)
  • JDK 1.8.77
  • 我的classpath中有hibernate验证器(org.hibernate:hibernate-validator:5.2.4.Final)
  • 如果我编写测试夹具并直接使用validitor.validate()(没有jpa或persist),则hibernate验证器按预期工作。
  • 我不使用JPA注释,只使用ORM xml来声明实体映射。
  • 我使用JSR303注释来标记带有约束的attrs和props。
  • persistence.xml标记为验证“AUTO”,并且已尝试使用标记接口的FQDN等javax.persistence.validation.group.pre-persist等属性的许多变体。

如前所述,调用em.persist(myObjectInst)不会触发添加到类“Base”的任何303注释。

*是否有一些调整参数或开关我可以修补这将使这工作? *

注意:我对此进行了深入调试,可以看到org.eclipse.persistence.internal.jpa.metadata.beanvalidation.BeanValidationHelper.detectConstraints()不会查看JSR303注释的任何父类。它似乎只想查看特定的实体类。如果我将JSR303约束移动到具体(或实体类),我可能会猜测;它可能只是工作。但后来我会松开扩展和映射超类的东西。那有什么乐趣?

更新 看起来像EclipseLink 2.6.x中的问题。有关详细信息,请参见此处(https://www.eclipse.org/forums/index.php?t=msg&th=1077658&goto=1732842&#msg_1732842)。

1 个答案:

答案 0 :(得分:2)

从我所看到的,eclipse链接2.6.X到2.6.4似乎在维护触发JSR 303 bean验证的合同方面存在Massive错误。 现在,eclipselink 2.6.4只会在您的子实体被标记为约束的右侧时触发这些验证。

我的集成测试在JEE 6库版本下完美运行(例如eclipselink 2.4.x)。

当我将库升级到JEE 7版本时,在ecliselink的特定情况下,这意味着版本:2.6.1到2.6.4,它们都表现出相同的错误。

到目前为止我分析过的单元测试已经过验证,必须触发ConstraintViolationExceptions,例如not null。

因此,如果您使用扩展抽象实体B的实体A.而抽象实体B是@MappedSuperClass。 如果在抽象实体B上找到@NotNull或任何其他此类约束,您将遇到问题... 在这种情况下,事情会进展顺利。

eclipselink不会触发约束违规。 相反,如果您在测试中发出commit()或flush(),则DB会阻止您。 Eclipse-link将在db异常上回滚。

然而,只要你去实体A然后你就把它抽成一个虚拟字段:     @NotNull     private String dummy;

这足以使Validator(例如hibernate验证器)被调用。

在这种情况下,我的测试仍然失败,因为现在我得到了两个@NotNull约束验证而不是一个。

在下面的代码片段中,我将介绍eclipselink 2.6.1上堆栈跟踪的相关信息。

Caused by: javax.validation.ConstraintViolationException: 
Bean Validation constraint(s) violated while executing Automatic Bean Validation on callback event:'prePersist'. 
Please refer to embedded ConstraintViolations for details.
    at org.eclipse.persistence.internal.jpa.metadata.listeners.BeanValidationListener.validateOnCallbackEvent(BeanValidationListener.java:108)
    at org.eclipse.persistence.internal.jpa.metadata.listeners.BeanValidationListener.prePersist(BeanValidationListener.java:77)
    at org.eclipse.persistence.descriptors.DescriptorEventManager.notifyListener(DescriptorEventManager.java:748)
    at org.eclipse.persistence.descriptors.DescriptorEventManager.notifyEJB30Listeners(DescriptorEventManager.java:691)
    at org.eclipse.persistence.descriptors.DescriptorEventManager.executeEvent(DescriptorEventManager.java:229)
    at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.registerNewObjectClone(UnitOfWorkImpl.java:4314)
    at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.registerNotRegisteredNewObjectForPersist(UnitOfWorkImpl.java:4291)
    at org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork.registerNotRegisteredNewObjectForPersist(RepeatableWriteUnitOfWork.java:521)
    at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.registerNewObjectForPersist(UnitOfWorkImpl.java:4233)
    at org.eclipse.persistence.internal.jpa.EntityManagerImpl.persist(EntityManagerImpl.java:507)
    at TEST_THAT_IS_PROBLEMATIC
    ... 25 more

在上面的堆栈跟踪中,您有单元测试在实体A上执行em.persist(),并且此实例A具有虚拟@NotNull字段。因此调用验证。

eclipselink中的错误似乎是当BeanValidationListener向BeanValidationHelper询问类是否受约束时:

Eclipselink的代码如下:

private void validateOnCallbackEvent(DescriptorEvent event, String callbackEventName, Class[] validationGroup) {
        Object source = event.getSource();
        boolean noOptimization = "true".equalsIgnoreCase((String) event.getSession().getProperty(PersistenceUnitProperties.BEAN_VALIDATION_NO_OPTIMISATION));
        boolean shouldValidate = noOptimization || beanValidationHelper.isConstrained(source.getClass());
        if (shouldValidate) {
            Set<ConstraintViolation<Object>> constraintViolations = getValidator(event).validate(source, validationGroup);
            if (constraintViolations.size() > 0) {
                // There were errors while call to validate above.
                // Throw a ConstrainViolationException as required by the spec.
                // The transaction would be rolled back automatically
                // TODO need to I18N this.
                throw new ConstraintViolationException(
                        "Bean Validation constraint(s) violated while executing Automatic Bean Validation on callback event:'" +
                                callbackEventName + "'. Please refer to embedded ConstraintViolations for details.",
                        (Set<ConstraintViolation<?>>) (Object) constraintViolations); /* Do not remove the explicit
                        cast. This issue is related to capture#a not being instance of capture#b. */
            }
        }
    }

问题在于查询:

beanValidationHelper.isConstrained(source.getClass());

返回false,这是完全错误的。

最后,如果检查BeanValidationHelper的实现,代码的初始部分如下所示:

 private Boolean detectConstraints(Class<?> clazz) {
        for (Field f : ReflectionUtils.getDeclaredFields(clazz)) {
            for (Annotation a : f.getDeclaredAnnotations()) {
                final Class<? extends Annotation> type = a.annotationType();
                if (KNOWN_CONSTRAINTS.contains(type.getName())){
                    return true;
                }
                // Check for custom annotations on the field (+ check inheritance on class annotations).
                // Custom bean validation annotation is defined by having @Constraint annotation on its class.
                for (Annotation typesClassAnnotation : type.getAnnotations()) {
                    final Class<? extends Annotation> classAnnotationType = typesClassAnnotation.annotationType();
                    if (Constraint.class == classAnnotationType) {
                        KNOWN_CONSTRAINTS.add(type.getName());
                        return true;
                    }
                }
            }
        }

上面的实现显然是错误的,因为整个方法是非递归的,并且它从反射中使用的apis本身是非递归的。 他们只关注当前的实例。 如果您看到以下堆栈溢出线程:

What is the difference between getFields and getDeclaredFields in Java reflection

排名靠前的答案清楚地解释了:

Field f : ReflectionUtils.getDeclaredFields(clazz)

仅返回当前类的字段,但不返回父项。

我正在努力做的事情是同时实施这个解决方法,以便在BeanValidationHelper中破解算法,以检测需要验证的类:

 @Transient
    @NotNull
    private final char waitForEclipseLinkToFixTheVersion264 = 'a';

通过执行上述操作,您可以清楚地将代码标记为可以在将来删除的块。 而且由于该领域是短暂的...嘿,它不会改变你的数据库。

请注意,eclipselink论坛现在还有其他信息。 这个错误比只是不正确地跟踪“beanValidation&#34;在BeanValidationListner.class中需要。 这个bug有第二个深度。 随eclipse-link提供的BeanValidationListner.class也没有为以下内容注册任何实现: PreWriteEvent和DescriptorEventManager.PreInsertEvent。

因此当&#34; DeferredCachedDetectionPolocy.class是calculatinChanges()时,如果你的实体A有JSR字段,它仍然没有得到JSR 303验证。 这很有可能发生在你身上,因为你的enitya是: T0:通过确定持久和有效 T1:您在同一事务中修改peristed实体,并且在calculateChanges调用事件litenters时。 BeanValidationListner.class不关心preInsertEvent。 它只是假设验证是在prePersist中完成的,并且根本不会调用验证。

解决这个问题,我还不确定。 我将研究如何在PreInserPhase期间注册事件列表器,它与BeanValidationListner相同。 或者,我将在本地修补BeanValidationListner.class以订阅PreINsert事件。

我讨厌修改其他人维护的图书馆代码,所以我会首先找到我们自己的eventListner的方法作为这个bug的临时工作。

添加允许验证这两个错误的存储库。 https://github.com/99sono/EclipseLink_2_6_4_JSR_303Bug

对于第2个错误,以下EventListner可以为临时解决方案提供服务,直到eclipse链接2.6.4修复了它们的bean验证编排逻辑。

package jpa.eclipselink.test.bug2workaround;

import java.util.Map;

import javax.validation.Validation;
import javax.validation.ValidatorFactory;

import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.descriptors.changetracking.DeferredChangeDetectionPolicy;
import org.eclipse.persistence.internal.jpa.deployment.BeanValidationInitializationHelper;
import org.eclipse.persistence.internal.jpa.metadata.listeners.BeanValidationListener;

/**
 * Temporary work-around for JSR 303 bean validation flow in eclipselink.
 *
 * <P>
 * Problem: <br>
 * The
 * {@link DeferredChangeDetectionPolicy#calculateChanges(Object, Object, boolean, org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet, org.eclipse.persistence.internal.sessions.UnitOfWorkImpl, org.eclipse.persistence.descriptors.ClassDescriptor, boolean)}
 * during a flush will do one of the following: <br>
 * {@code descriptor.getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PreInsertEvent, writeQuery)); }
 * or <br>
 *
 * {@code descriptor.getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PreUpdateEvent, writeQuery)); }
 *
 * <P>
 * WHe it does
 * {@code descriptor.getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PreInsertEvent, writeQuery)); }
 * the {@link BeanValidationListener} will not do anything. We want it to do bean validation.
 */
public class ForceBeanManagerValidationOnPreInsert extends DescriptorEventAdapter {

    private static final Class[] DUMMY_GROUP_PARAMETER = null;

    /**
     * This is is the EJB validator that eclipselink uses to do JSR 303 validations during pre-update, pre-delete,
     * pre-persist, but not pre-insert.
     *
     * Do not access this field directly. Use the {@link #getBeanValidationListener(DescriptorEvent)} api to get it, as
     * this api will initialize the tool if necessary.
     */
    BeanValidationListener beanValidationListener = null;

    final Object beanValidationListenerLock = new Object();

    /**
     *
     */
    public ForceBeanManagerValidationOnPreInsert() {
        super();

    }

    /**
     * As a work-around we want to do bean validation that the container is currently not doing.
     */
    @Override
    public void preInsert(DescriptorEvent event) {
        // (a) get for ourselves an instances of the eclipse link " Step 4 - Notify internal listeners."
        // that knows how to run JSR 303 validations on beans associated to descriptor events
        BeanValidationListener eclipseLinkBeanValidationListenerTool = getBeanValidationListener(event);

        // (b) let the validation listener run its pre-update logic on a preInsert it serves our purpose
        eclipseLinkBeanValidationListenerTool.preUpdate(event);

    }

    /**
     * Returns the BeanValidationListener that knows how to do JSR 303 validation. Creates a new instance if needed,
     * otherwise return the already created listener.
     *
     * <P>
     * We can only initialize our {@link BeanValidationListener} during runtime, to get access to the JPA persistence
     * unit properties. (e.g. to the validation factory).
     *
     * @param event
     *            This event describes an ongoing insert, updetae, delete event on an entity and for which we may want
     *            to force eclipselink to kill the transaction if a JSR bean validation fails.
     * @return the BeanValidationListener that knows how to do JSR 303 validation.
     */
    protected BeanValidationListener getBeanValidationListener(DescriptorEvent event) {
        synchronized (beanValidationListenerLock) {
            // (a) initializae our BeanValidationListener if needed
            boolean initializationNeeded = beanValidationListener == null;
            if (initializationNeeded) {
                beanValidationListener = createBeanValidationListener(event);
            }

            // (b) return the validation listerner that is normally used by eclipse link
            // for pre-persist, pre-update and pre-delete so that we can force it run on pre-insert
            return beanValidationListener;
        }

    }

    /**
     * Creates a new instance of the {@link BeanValidationListener} that comes with eclipse link.
     *
     * @param event
     *            the ongoing db event (e.g. pre-insert) where we want to trigger JSR 303 bean validation.
     *
     * @return A new a new instance of the {@link BeanValidationListener} .
     */
    protected BeanValidationListener createBeanValidationListener(DescriptorEvent event) {
        Map peristenceUnitProperties = event.getSession().getProperties();
        ValidatorFactory validatorFactory = getValidatorFactory(peristenceUnitProperties);
        return new BeanValidationListener(validatorFactory, DUMMY_GROUP_PARAMETER, DUMMY_GROUP_PARAMETER,
                DUMMY_GROUP_PARAMETER);
    }

    /**
     * Snippet of code taken out of {@link BeanValidationInitializationHelper}
     *
     * @param puProperties
     *            the persistence unit properties that may be specifying the JSR 303 validation factory.
     * @return the validation factory that can check if a bean is violating business rules. Almost everyone uses
     *         hirbernate JSR 303 validation.
     */
    protected ValidatorFactory getValidatorFactory(Map puProperties) {
        ValidatorFactory validatorFactory = (ValidatorFactory) puProperties
                .get(PersistenceUnitProperties.VALIDATOR_FACTORY);

        if (validatorFactory == null) {
            validatorFactory = Validation.buildDefaultValidatorFactory();
        }
        return validatorFactory;
    }

}

只需将此bean验证器添加到类中,最好是基本抽象类,以确保在插入前进行JSR 303验证。 这应该可以解决这个漏洞,它允许我们将违反业务规则的dirtyu实体提交给db。

以下是一个具有解决方法的实体示例。

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DESCRIMINATOR", length = 32)
@DiscriminatorValue("Bug2WorkAround")
@Entity
@EntityListeners({ ForceBeanManagerValidationOnPreInsert.class })
public class Bug2Entity2WithWorkAround extends GenericEntity {

亲切的问候。