自定义验证注释引入了ConcurrentModificationException

时间:2014-10-08 10:12:51

标签: java hibernate jpa seam concurrentmodification

我的任务是为自定义验证创建一个注释。这是由于handling database constraint violations nicely.的一些问题。我对此做的是相对简单。我专门为需要它的一个域类创建了一个类级别的CustomConstraint。我得到的是当前的结果如下:

@UniqueLocation注释:

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = UniqueLocationValidator.class)
@Documented
public @interface UniqueLocation {

    String message() default "must be unique!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

这并不引人注目,实际上它几乎逐字逐句地从hibernate documentation.

中复制

我继续创建我的UniqueLocationValidator并遇到了使用持久化上下文的问题。我想运行一个防御性选择,因此试图将我的应用程序注入@Produces @PersistenceContext EntityManager

因此我将JBoss Seam用于InjectingConstraintValidatorFactory配置我的validation.xml,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<validation-config 
   xmlns="http://jboss.org/xml/ns/javax/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.0.xsd">

   <constraint-validator-factory>
      org.jboss.seam.validation.InjectingConstraintValidatorFactory
   </constraint-validator-factory>

</validation-config>

在遇到Creating Constraint Violations的一些问题之后,这就是我的Validator实际看起来的样子:

@ManagedBean
public class UniqueLocationValidator implements
        ConstraintValidator<UniqueLocation, Location> {
    // must not return a result for name-equality on the same Id
    private final String QUERY_STRING = "SELECT * FROM Location WHERE locationName = :value AND id <> :id";

    @Inject
    EntityManager entityManager;

    private String constraintViolationMessage;

    @Override
    public void initialize(final UniqueLocation annotation) {
        constraintViolationMessage = annotation.message();
    }

    @Override
    public boolean isValid(final Location instance,
            final ConstraintValidatorContext context) {
        if (instance == null) {
            // Recommended, instead use explicit @NotNull Annotation for
            // validating non-nullable instances
            return true;
        }

        if (duplicateLocationExists(instance)) {
            createConstraintViolations(context);
            return false;
        } else {
            return true;
        }
    }

    private void createConstraintViolations(
            final ConstraintValidatorContext context) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(constraintViolationMessage)
                .addNode("locationName").addConstraintViolation();
    }

    private boolean duplicateLocationExists(final Location location) {
        final String checkedValue = location.getLocationName();
        final long id = location.getId();

        Query defensiveSelect = entityManager.createNativeQuery(QUERY_STRING)
                .setParameter("value", checkedValue).setParameter("id", id);

        return !defensiveSelect.getResultList().isEmpty();
    }
}

对于我目前的配置,现在真正的牛肉,问题是:

当我在收到用户的操作后运行以下代码时,该功能非常有效且正确地将重复的位置名称标记为无效。当locationName未重复时,也可以正常工作。

public long add(@Valid final Location location) {
    entityManager.persist(location);
    return location.getId();
}

请注意,此处的entityManager和UniqueLocationValidator中的entityManager都是通过上述@PersistenceContext EntityManager中的Weld CDI注入的。

什么行不通的是:

public long update(@Valid final Location location){
    entityManager.merge(location);
    return location.getId();
}

调用此代码时,我会得到一个相对较短的堆栈跟踪,它有一个ConcurrentModificationException作为根本原因。

我既不明白为什么会这样,也不会理解如何解决这个问题。我没有尝试显式多线程我的应用程序,所以这应该由JBoss 7.1.1-Final我用作应用程序服务器进行管理..

1 个答案:

答案 0 :(得分:4)

EntityManager无法实现您的目标。嗯,通常不会。

在处理更新期间调用您的验证器。通过EntityManager发送的查询会影响内部存储ActionQueue EntityManager。这就是导致ConcurrentModificationException的原因:查询结果改变了EntityManager在刷新更改时迭代的列表。

解决方法是绕过EntityManager

我们怎么做?

嗯,...它有点脏,因为你有效地添加了对hibernate实现的依赖,但你可以通过各种方式get the connection from the Session or EntityManager。一旦你有一个java.sql.Connection对象,你就可以使用像PreparedStatement这样的东西来执行你的查询。


示例修复:

Session session = entityManager.unwrap(Session.class);
SessionFactoryImplementor sessionFactoryImplementation = (SessionFactoryImplementor) session.getSessionFactory();
ConnectionProvider connectionProvider = sessionFactoryImplementation.getConnectionProvider();
try {
       connection = connectionProvider.getConnection();
       PreparedStatement ps = connection.prepareStatement("SELECT 1 FROM Location WHERE id <> ? AND locationName = ?");
       ps.setLong(1, id);
       ps.setString(2, checkedValue);
       ResultSet rs = ps.executeQuery();
       boolean result = rs.next();//found any results? if we can retrieve a row: yes!
       rs.close();
       return result;
}//catch SQLException etc... 
//finally, close resources (only the resultset!)