Spring Data REST с JSR 303 возвращает RepositoryConstraintViolationException вместо ConstraintViolationException - PullRequest
0 голосов
/ 18 марта 2020

Я использую Spring Boot 2.2.5, Spring DATA REST, Spring HATEOAS, Hibernate. Я использовал валидацию (JSR 303) с тех пор, как я все еще использовал Spring 2.1.x. Работало нормально. После миграции я заметил странное поведение.

Когда я использую конечную точку SDR (скажем, я делаю POST или PATCH объекта) с некоторыми ошибками проверки, я получаю DataIntegrityViolationException вместо ConstraintViolationException.

В моем пом:

<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, но, к сожалению, я не нашел никаких подсказок на do c. Любой совет будет очень признателен.

...