Deliel
Deliel

Reputation: 1

StackOverflowError in Custom Hibernate Validator with Injected Repository

Calling a repository method inside a custom hibernate validator can lead to a StackOverflowError due to infinite recursion.

I am trying to create a custom validator that checks for conflicting entities in the database before saving a new entity. The conflict logic is complex and can change over time, so database constraints (like unique constraints) are not suitable. There can be many entities, but each of them needs its own conflict check before saving.

Entity:

@Data
@Entity
@MyConstraint
public class MyEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    private UUID field1;

    private Integer field2;
}

Repo:

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, UUID> {

    // Dummy query only for demo
    @Query("SELECT e FROM MyEntity e WHERE e.field1 = :#{#origin.field1} AND e.field2 >= :#{#origin.field2}")
    Optional<MyEntity> findConflict(MyEntity origin);
}

Custom constraint:

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { MyConstraintValidator.class })
public @interface MyConstraint {

    String message() default "{MyConstraint.message}";

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

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

Validator implementation:

public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {

    @Autowired
    private MyEntityRepository myEntityRepository;

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // Simple logic for demo
        if (value instanceof MyEntity myEntity) {
            return myEntityRepository.findConflict(myEntity)
                    .isEmpty();
        }

        return false;
    }
}

When I run this code, I encounter a StackOverflowError. The issue arises because invoking the findConflict method in the validator triggers the validation for myEntity again, leading to infinite recursion.

public void test() {
    var entity = new MyEntity();
    entity.setField1(UUID.randomUUID());
    entity.setField2(1);

    myEntityRepository.save(entity);
}

Is there a way to solve this issue? How can I disable validation (including standard validations) for the findConflict method?

stacktrace:

java.lang.StackOverflowError: null
    at org.springframework.data.jpa.repository.query.HqlQueryTransformer.visitSelectClause(HqlQueryTransformer.java:337) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitSelectClause(HqlQueryRenderer.java:1) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlParser$SelectClauseContext.accept(HqlParser.java:3613) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitSelectQuery(HqlQueryRenderer.java:250) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitSelectQuery(HqlQueryRenderer.java:1) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlParser$SelectQueryContext.accept(HqlParser.java:1160) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.springframework.data.jpa.repository.query.HqlQueryTransformer.visitOrderedQuery(HqlQueryTransformer.java:113) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitOrderedQuery(HqlQueryRenderer.java:1) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlParser$OrderedQueryContext.accept(HqlParser.java:1062) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitQueryExpression(HqlQueryRenderer.java:68) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitQueryExpression(HqlQueryRenderer.java:1) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlParser$QueryExpressionContext.accept(HqlParser.java:436) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitSelectStatement(HqlQueryRenderer.java:56) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitSelectStatement(HqlQueryRenderer.java:1) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlParser$SelectStatementContext.accept(HqlParser.java:379) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitQl_statement(HqlQueryRenderer.java:42) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitQl_statement(HqlQueryRenderer.java:1) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlParser$Ql_statementContext.accept(HqlParser.java:302) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitStart(HqlQueryRenderer.java:35) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlQueryRenderer.visitStart(HqlQueryRenderer.java:1) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.HqlParser$StartContext.accept(HqlParser.java:246) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.springframework.data.jpa.repository.query.HqlQueryParser.doFindProjection(HqlQueryParser.java:115) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.JpaQueryParserSupport.projection(JpaQueryParserSupport.java:80) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.JpaQueryEnhancer.getProjection(JpaQueryEnhancer.java:163) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.StringQuery.getProjection(StringQuery.java:102) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.StringQuery.isDefaultProjection(StringQuery.java:147) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery.createJpaQuery(AbstractStringBasedJpaQuery.java:183) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery.doCreateQuery(AbstractStringBasedJpaQuery.java:124) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.createQuery(AbstractJpaQuery.java:243) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:223) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:92) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:152) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:140) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:136) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.1.8.jar:6.1.8]
    at jdk.proxy2/jdk.proxy2.$Proxy117.findConflict(Unknown Source) ~[na:na]
    at com.example.demo.MyConstraintValidator.isValid(MyConstraintValidator.java:16) ~[classes/:na]
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:180) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:66) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:75) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:130) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:123) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:555) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:518) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:488) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:450) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:400) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:172) ~[hibernate-validator-8.0.1.Final.jar:8.0.1.Final]
    at org.hibernate.boot.beanvalidation.BeanValidationEventListener.validate(BeanValidationEventListener.java:128) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.boot.beanvalidation.BeanValidationEventListener.onPreInsert(BeanValidationEventListener.java:81) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.action.internal.EntityInsertAction.preInsert(EntityInsertAction.java:251) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:106) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:632) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:499) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:371) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:66) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1375) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$2(ConcreteSqmSelectQueryPlan.java:138) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:382) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:302) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:526) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:423) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:555) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:223) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:92) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:152) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:140) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70) ~[spring-data-commons-3.3.0.jar:3.3.0]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:136) ~[spring-data-jpa-3.3.0.jar:3.3.0]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.1.8.jar:6.1.8]
    at jdk.proxy2/jdk.proxy2.$Proxy117.findConflict(Unknown Source) ~[na:na]
...

Upvotes: 0

Views: 38

Answers (0)

Related Questions