簡體   English   中英

帶有 JSR 303 的 Spring Data REST 返回 RepositoryConstraintViolationException 而不是 ConstraintViolationException

[英]Spring Data REST with JSR 303 returns RepositoryConstraintViolationException instead of ConstraintViolationException

我正在使用 Spring Boot 2.2.5、Spring DATA REST、Spring HATEOAS、Hibernate。 自從我還在使用 Spring 2.1.x 以來,我就一直在使用驗證 (JSR 303)。 它工作得很好。 遷移后,我注意到了一種奇怪的行為。

當我使用帶有一些驗證錯誤的 SDR 端點(假設我執行實體的 POST 或 PATCH)時,我得到DataIntegrityViolationException而不是ConstraintViolationException

在我的pom中:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

我實現了RepositoryRestConfigurer

@Configuration
@Log4j2
public class GlobalRepositoryRestConfigurer implements RepositoryRestConfigurer {

    @Autowired
    private Validator validator;

    @Override
    public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
        validatingListener.addValidator("beforeCreate", validator);
        validatingListener.addValidator("beforeSave", validator);        
    }

}

以及其他一些自定義消息的配置:

@Configuration
@EnableRetry
public class CustomConfiguration {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:/i18n/messages");
        // messageSource.setDefaultEncoding("UTF-8");
        // set to true only for debugging
        messageSource.setUseCodeAsDefaultMessage(false);
        messageSource.setCacheSeconds((int) TimeUnit.HOURS.toSeconds(1));
        messageSource.setFallbackToSystemLocale(false);
        return messageSource;
    }

    @Bean
    public MessageSourceAccessor messageSourceAccessor() {
        return new MessageSourceAccessor(messageSource());
    }

    /**
     * Enable Spring bean validation https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation
     *
     * @return
     */
    @Bean
    public Validator validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setValidationMessageSource(messageSource());
        return factoryBean;
    }

我的存儲庫是這樣的:

@Transactional
@PreAuthorize("isAuthenticated()")
public interface PrinterRepository extends JpaRepository<Printer, Long>, JpaSpecificationExecutor {

    @Query("FROM Printer p JOIN p.store s")
    List<Printer> findAllJoinStore();


    @RestResource(exported = false)
    @Transactional(readOnly = true)
    @Query("SELECT p FROM Printer p WHERE :allFieldSearch IS NULL OR (p.name LIKE CONCAT('%',:allFieldSearch,'%') OR p.remoteAddress LIKE CONCAT('%',:allFieldSearch,'%'))")
    Page<Printer> search(@Param("allFieldSearch") String allFieldSearch, Pageable pageable);

    @Transactional(readOnly = true)
    @Query("SELECT p FROM Printer p WHERE store.id=:storeId")
    Page<Printer> findByStore(@Param("storeId") long storeId, Pageable pageable);
}

控制器不會覆蓋 POST/PATH 方法來保存實體。

實體是這樣的:

@Entity
@EntityListeners(PrinterListener.class)
@Filter(name = "storeFilter", condition = "store_id = :storeId")
@ScriptAssert.List({
        @ScriptAssert(lang = "javascript", script = "_.serialNumber!=null", alias = "_", reportOn = "serialNumber", message = "{printer.serialnumber.mandatory}"),
        @ScriptAssert(lang = "javascript", script = "_.model!='VIRTUAL' ?_.remoteAddress!=null:true", alias = "_", reportOn = "remoteAddress", message = "{printer.remoteAddress.mandatory}"),
        @ScriptAssert(lang = "javascript", script = "_.zoneId!=null", alias = "_", reportOn = "zoneId", message = "{printer.zoneId.mandatory}"),
        @ScriptAssert(lang = "javascript", script = "_.zoneId!=null?_.isValidTimeZone(_.zoneId):true", alias = "_", reportOn = "zoneId", message = "{printer.zoneId.invalid}")
})
@Data
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
@ToString(callSuper = true)
public class Printer extends AbstractEntity {

    @NotBlank
    @Column(nullable = false)
    private String name;

    // Ip address or hostname
    private String remoteAddress;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 30)
    // @JsonSerialize(using = PrinterModelSerializer.class)
    private PrinterModel model;

這是我的 RestControllerAdvice:

@RestControllerAdvice
@Log4j2
public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {

 @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<?> handleConflictException(DataIntegrityViolationException ex, HttpServletRequest request, Locale locale) throws Exception {
        /**
         * Keep the default Exception format for Violation exception @see {@link RepositoryRestExceptionHandler}
         */
        if (ex instanceof RepositoryConstraintViolationException) {
            return new ResponseEntity<RepositoryConstraintViolationException>((RepositoryConstraintViolationException) ex, new HttpHeaders(), HttpStatus.BAD_REQUEST);
        }

        /**
         * Custom errors and messages for DataIntegrityViolationException checked against the list of indexes names
         */
        return response(HttpStatus.CONFLICT, new HttpHeaders(), buildIntegrityError(ex, request, HttpStatus.CONFLICT, locale));
    }

 @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<?> handleValidationException(ConstraintViolationException ex, HttpServletRequest request, Locale locale) throws Exception {
        try {
            ResponseEntity<ConstraintViolationException> response = new ResponseEntity<ConstraintViolationException>(ex, new HttpHeaders(), HttpStatus.BAD_REQUEST);
            //return response(HttpStatus.BAD_REQUEST, new HttpHeaders(), ex);
            return response;
        } catch (Exception e) {
            log.error("", e);
        }
        return response(HttpStatus.BAD_REQUEST, new HttpHeaders(), "");
    }

我試圖保存沒有name的打印機,我看到這些日志:

18/03/2020 12:32:35,859 DEBUG http-nio-8082-exec-10 DataSourceUtils:248 - Resetting read-only flag of JDBC Connection [HikariProxyConnection@1578883309 wrapping com.mysql.cj.jdbc.ConnectionImpl@3f194beb]
18/03/2020 12:32:35,896 DEBUG http-nio-8082-exec-10 ValidatingRepositoryEventListener:173 - beforeSave: Printer(super=AbstractEntity(id=2, sid=1374107f-4597-46e5-bc55-3faf98695987, createdBy=9b9ef528-a45e-4ce9-b014-32a157507aef, createdDate=2019-07-17T17:08:15.688Z, lastModifiedDate=2020-03-17T16:05:27.028Z, lastModifiedBy=9b9ef528-a45e-4ce9-b014-32a157507aef, version=18), name=null, remoteAddress=dev1.epson.local, model=RCH_PRINTF, zoneId=Europe/Rome, ssl=true, serialNumber=dfdfdfdf, lotteryCodeSupport=true, url=) with org.springframework.validation.beanvalidation.LocalValidatorFactoryBean@4fe9a396
18/03/2020 12:32:35,896 DEBUG http-nio-8082-exec-10 ValidationUtils:78 - Invoking validator [org.springframework.validation.beanvalidation.LocalValidatorFactoryBean@4fe9a396]
18/03/2020 12:32:35,905 DEBUG http-nio-8082-exec-10 ValidationUtils:94 - Validator found 1 errors
18/03/2020 12:32:35,905 DEBUG http-nio-8082-exec-10 ExceptionHandlerExceptionResolver:398 - Using @ExceptionHandler it.test.server.config.exceptions.ApplicationExceptionHandler#handleConflictException(DataIntegrityViolationException, HttpServletRequest, Locale)
18/03/2020 12:32:35,906 DEBUG http-nio-8082-exec-10 HttpEntityMethodProcessor:265 - Using 'application/json', given [application/json, text/plain, */*] and supported [application/json, application/*+json, application/json, application/*+json, application/x-jackson-smile, application/cbor]
18/03/2020 12:32:35,906 DEBUG http-nio-8082-exec-10 HttpEntityMethodProcessor:91 - Writing [org.springframework.data.rest.core.RepositoryConstraintViolationException: Validation failed]
18/03/2020 12:32:35,906 DEBUG http-nio-8082-exec-10 HstsHeaderWriter:169 - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@5fe7b347
18/03/2020 12:32:35,907 DEBUG http-nio-8082-exec-10 HttpSessionSecurityContextRepository:376 - SecurityContext 'org.springframework.security.core.context.SecurityContextImpl@4af5538a: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@4af5538a: Principal: User(super=AbstractEntity(id=1, sid=9b9ef528-a45e-4ce9-b014-32a157507aef, createdBy=system, createdDate=2019-07-17T16:52:07.194Z, lastModifiedDate=2020-02-14T09:54:28.869Z, lastModifiedBy=9b9ef528-a45e-4ce9-b014-32a157507aef, version=18), fullName=Brusò Andrea, username=admin, password=$2a$10$GWOzC9clkzUf4YgZ7BmOLeiUm9cSi4zLhvjVXeNLahvxJZ2dIdzLq, lastPasswordUpdate=null, enabled=true); Credentials: [PROTECTED]; Authenticated: true; Details: it.test.server.config.security.CustomWebAuthenticationDetails@67692d4c; Granted Authorities: ROLE_ADMIN' stored to HttpSession: 'org.apache.catalina.session.StandardSessionFacade@2f2b2167
18/03/2020 12:32:35,907 DEBUG http-nio-8082-exec-10 ExceptionHandlerExceptionResolver:145 - Resolved [org.springframework.data.rest.core.RepositoryConstraintViolationException: Validation failed]
18/03/2020 12:32:35,908 DEBUG http-nio-8082-exec-10 DispatcherServlet:1131 - Completed 400 BAD_REQUEST
18/03/2020 12:32:35,908 DEBUG http-nio-8082-exec-10 ExceptionTranslationFilter:120 - Chain processed normally
18/03/2020 12:32:35,908 DEBUG http-nio-8082-exec-10 SecurityContextPersistenceFilter:119 - SecurityContextHolder now cleared, as request processing completed

如您所見,驗證已執行,但 Spring 決定調用it.test.server.config.exceptions.ApplicationExceptionHandler#handleConflictException而不是handleValidationException

我注意到的是,如果我使用自定義控制器覆蓋 POST/PATH 方法,我會得到正確的異常。 這樣做日志更改:

18/03/2020 12:38:01,503 DEBUG http-nio-8082-exec-9 ServletInvocableHandlerMethod:174 - Could not resolve parameter [1] in public org.springframework.http.ResponseEntity<?> it.test.server.rest.controllers.accounts.AgentController.update(java.lang.Long,it.test.server.model.accounts.Agent,org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler): lastName: Il campo non può essere vuoto. Inserire un valore valido e ripetere l'operazione.
18/03/2020 12:38:01,504 DEBUG http-nio-8082-exec-9 ExceptionHandlerExceptionResolver:398 - Using @ExceptionHandler it.test.server.config.exceptions.ApplicationExceptionHandler#handleValidationException(ConstraintViolationException, HttpServletRequest, Locale)
18/03/2020 12:38:01,507 DEBUG http-nio-8082-exec-9 HttpEntityMethodProcessor:265 - Using 'application/json', given [application/json, text/plain, */*] and supported [application/json, application/*+json, application/json, application/*+json, application/x-jackson-smile, application/cbor]
18/03/2020 12:38:01,507 DEBUG http-nio-8082-exec-9 HttpEntityMethodProcessor:91 - Writing [javax.validation.ConstraintViolationException: lastName: Il campo non può essere vuoto. Inserire un  (truncated)...]
18/03/2020 12:38:01,508 DEBUG http-nio-8082-exec-9 HstsHeaderWriter:169 - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@5fe7b347
18/03/2020 12:38:01,508 DEBUG http-nio-8082-exec-9 HttpSessionSecurityContextRepository:376 - SecurityContext 'org.springframework.security.core.context.SecurityContextImpl@4af5538a: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@4af5538a: Principal: User(super=AbstractEntity(id=1, sid=9b9ef528-a45e-4ce9-b014-32a157507aef, createdBy=system, createdDate=2019-07-17T16:52:07.194Z, lastModifiedDate=2020-02-14T09:54:28.869Z, lastModifiedBy=9b9ef528-a45e-4ce9-b014-32a157507aef, version=18), fullName=Brusò Andrea, username=admin, password=$2a$10$GWOzC9clkzUf4YgZ7BmOLeiUm9cSi4zLhvjVXeNLahvxJZ2dIdzLq, lastPasswordUpdate=null, enabled=true); Credentials: [PROTECTED]; Authenticated: true; Details: it.test.server.config.security.CustomWebAuthenticationDetails@67692d4c; Granted Authorities: ROLE_ADMIN' stored to HttpSession: 'org.apache.catalina.session.StandardSessionFacade@2bd24cb7
18/03/2020 12:38:01,509 DEBUG http-nio-8082-exec-9 ExceptionHandlerExceptionResolver:145 - Resolved [javax.validation.ConstraintViolationException: lastName: Il campo non può essere vuoto. Inserire un valore valido e ripetere l'operazione.]
18/03/2020 12:38:01,509 DEBUG http-nio-8082-exec-9 DispatcherServlet:1131 - Completed 400 BAD_REQUEST
18/03/2020 12:38:01,509 DEBUG http-nio-8082-exec-9 ExceptionTranslationFilter:120 - Chain processed normally

所以我的猜測是問題是 JSR303 與 SDR 的配置,但不幸的是我沒有在文檔中找到任何提示。 任何建議將不勝感激。

RepositoryConstraintViolationExceptionDataIntegrityViolationException的子類。

查看Spring Data REST中ValidatingRepositoryEventListener的源碼。 ValidatingRepositoryEventListener.validate()方法確實拋出了一個RepositoryConstraintViolationException

private Errors validate(String event, Object entity) {

...
    Errors errors = new ValidationErrors(entity, persistentEntitiesFactory.getObject());

    if (errors.hasErrors()) {
        throw new RepositoryConstraintViolationException(errors);
    }
...
}

當您使用自定義控制器時,此請求與 Spring Data REST 無關,因此會拋出一個普通的ConstraintViolationException

游戲有點晚了,但是您實際上可以通過利用RepositoryConstraintViolationException.errors.allErrors解包到ConstraintViolation實例這一事實,輕松地將您可能為ConstraintViolationException提供的任何@ErrorHandler適應此新異常,只要啟用 JSR 303為您的項目。

問題 Spring Web示例:

public interface RestValidationAdviceTrait extends ValidationAdviceTrait {

    @ExceptionHandler
    default ResponseEntity<Problem> handleConstraintViolation(
            final RepositoryConstraintViolationException exception,
            final NativeWebRequest request) {

        final List<Violation> violations = exception.getErrors().getAllErrors().stream()
                .map(error -> error.unwrap(ConstraintViolation.class))
                .map(this::createViolation)
                .collect(toList());

        return newConstraintViolationProblem(exception, violations, request);

    }

}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM