春季HATEOAS HalResourcesSerializer未找到默认构造函数

时间:2019-02-08 10:17:05

标签: java spring spring-boot spring-data-rest spring-hateoas

我正在使用Spring Boot 2,Spring Data REST,Spring HATEOAS创建一个Spring REST应用。

我创建了这个控制器:

@Api(tags = "City Entity")
@RepositoryRestController
@RequestMapping(path = "/api/v1")
@PreAuthorize("isAuthenticated()")
public class CityController {

    @Autowired
    private LocalValidatorFactoryBean validator;

    @Autowired
    private PagedBeanResourceAssembler<City> pagedBeanResourceAssembler;

    @Autowired
    private CityService cityService;


    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(validator);
    }

    @GetMapping(path = "/cities/search/autocomplete")
    public ResponseEntity<?> autocomplete(@RequestParam(name = "city") String city, @RequestParam(name = "country", required = false) String country, Pageable pageable, Locale locale) {
        return new ResponseEntity<>(pagedBeanResourceAssembler.toResource(cityService.autocomplete(city, country, pageable)), HttpStatus.OK);
    }

}

服务方法是:

@Transactional(readOnly = true)
    public Page<City> autocomplete(String text, String country, Pageable pageable) {
        //my logic
        return elasticSearchManager.search(ElasticSearchUtil.getIndexName(City.class), null, City.class, filters, null, pageable);
    }

您可以看到City bean未存储在数据库中。实际上,bean是:

public class City implements Persistable<Long> {

    private Long id;

    @NotBlank
    private String name;

    private String district;

    private String region;

    private String zipCode;

    @NotNull
    @Size(min = 2, max = 2)
    private String country;
}

最后是我的PagedBeanResourceAssembler

@Component
public class PagedBeanResourceAssembler<T> implements ResourceAssembler<Page<T>, PagedResources<T>> {

    @Autowired
    private EntityLinks entityLinks;

    @Override
    public PagedResources<T> toResource(Page<T> page) {
        PagedResources<T> pagedResources = new PagedResources<T>(page.getContent(), asPageMetadata(page));
        return pagedResources;
    }

   private PagedResources.PageMetadata asPageMetadata(Page<?> page) {
        Assert.notNull(page, "Page must not be null!");
        return new PagedResources.PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages());
    }
}

当我进行http呼叫时,我会在控制台中看到一条警告消息:

08/02/2019 11:09:35,526  WARN http-nio-8082-exec-1 RepositoryRestMvcConfiguration$ResourceSupportHttpMessageConverter:205 - Failed to evaluate Jackson serialization for type [class org.springframework.hateoas.PagedResources]: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer.<init>()
08/02/2019 11:09:35,527  WARN http-nio-8082-exec-1 MappingJackson2HttpMessageConverter:205 - Failed to evaluate Jackson serialization for type [class org.springframework.hateoas.PagedResources]: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer.<init>()

不使用PagedResources,错误消失。我不知道我在哪里做错了。我知道HalResourcesSerializer没有默认的构造函数,但是我没有直接使用它,我也不明白为什么Entity持久存在于数据库中的控制器如此有效。 如何解决继续使用PagedResource的问题?

=========更新=========

我添加了配置以提供更详细的视图:

自定义配置:

@Configuration
@EnableRetry
@EnableTransactionManagement
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
public class CustomConfiguration {
    public static CustomConfiguration INSTANCE;

    @PostConstruct
    public void init() {
        INSTANCE = this;
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public static SpringSecurityAuditorAware springSecurityAuditorAware() {
        return new SpringSecurityAuditorAware();
    }

    @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 LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setValidationMessageSource(messageSource());
        return factoryBean;
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator());
        return methodValidationPostProcessor;
    }

    /**
     * Utility class from Google to work with phone numbers {@link https://github.com/googlei18n/libphonenumber}
     *
     * @return
     */
    @Bean
    public PhoneNumberUtil phoneNumberUtil() {
        return PhoneNumberUtil.getInstance();
    }

    /**
     * To enable SpEL expressions
     *
     * @return
     */
    @Bean
    public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
        return new SecurityEvaluationContextExtension();
    }

    /**
     * Define the specific storage manager to use (disk, S3, etc)
     *
     * @return
     */
    @Bean
    public StorageManager storageManager() {
        return new S3StorageManager();
    }

    /**
     * GRACEFUL SHUTDOWN
     */
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    @Bean
    public ConfigurableServletWebServerFactory webServerFactory(final GracefulShutdown gracefulShutdown) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers(gracefulShutdown);
        return factory;
    }

}

GlobalRepositoryRestConfigurer:

@Configuration
public class GlobalRepositoryRestConfigurer implements RepositoryRestConfigurer {
    private Logger log = LogManager.getLogger();

    @Autowired(required = false)
    private Jackson2ObjectMapperBuilder objectMapperBuilder;

    @Autowired
    private Validator validator;

    @Value("${cors.mapping}")
    private String corsMapping;

    @Value("#{'${cors.allowed.headers}'.split(',')}")
    private String[] corsAllowedHeaders;

    @Value("#{'${cors.exposed.headers}'.split(',')}")
    private String[] corsExposedHeaders;

    @Value("#{'${cors.allowed.methods}'.split(',')}")
    private String[] corsAllowedMethod;

    @Value("#{'${cors.allowed.origins}'.split(',')}")
    private String[] corsAllowedOrigins;

    @Value("${cors.max.age}")
    private int corsMaxAge;

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.getCorsRegistry().addMapping(corsMapping).exposedHeaders(corsExposedHeaders).allowedOrigins(corsAllowedOrigins)
                .allowedHeaders(corsAllowedHeaders).allowedMethods(corsAllowedMethod).maxAge(corsMaxAge);

    }

    @Override
    public void configureConversionService(ConfigurableConversionService conversionService) {

    }

    /**
     * ValidationException serialiazer
     *
     * @return
     */
    @Bean
    public ValidationExceptionSerializer validationExceptionSerializer() {
        return new ValidationExceptionSerializer();
    }

    @Bean
    public CustomValidationExceptionSerializer customValidationExceptionSerializer() {
        return new CustomValidationExceptionSerializer();
    }

    @Bean
    public ConstraintViolationExceptionSerializer constraintViolationExceptionSerializer() {
        return new ConstraintViolationExceptionSerializer();
    }

    /**
     * Customize Object Mapper
     */
    @Override
    public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
        if (this.objectMapperBuilder != null) {
            /**
             * Custom serializer for ConstraintViolationException
             * (https://jira.spring.io/browse/DATAREST-593)
             */
            try {
                SimpleModule constraintExceptionModule = new SimpleModule();
                constraintExceptionModule.addSerializer(ConstraintViolationException.class, constraintViolationExceptionSerializer());

                constraintExceptionModule.addSerializer(ValidationException.class, validationExceptionSerializer());
                constraintExceptionModule.addSerializer(cloud.optix.server.exceptions.ValidationException.class, customValidationExceptionSerializer());
                objectMapper.registerModule(constraintExceptionModule);
                this.objectMapperBuilder.configure(objectMapper);
            } catch (Exception e) {
                log.error("", e);
            }
        }
    }

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

    @Override
    public void configureExceptionHandlerExceptionResolver(ExceptionHandlerExceptionResolver exceptionResolver) {

    }

    /**
     * Adding converter to donwload files in{@link org.springframework.web.bind.annotation.RestController}
     *
     * @param messageConverters
     */
    @Override
    public void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        // super.configureHttpMessageConverters(messageConverters);
        messageConverters.add(new ResourceHttpMessageConverter());
    }
}

WebMvcConfiguration:

@Configuration
// Enable entity links for Spring HATEOAS
@EnableHypermediaSupport(type = {HypermediaType.HAL})
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private TenantRestClient tenantRestClient;

    @Value("${cors.mapping}")
    private String corsMapping;

    @Value("#{'${cors.allowed.headers}'.split(',')}")
    private String[] corsAllowedHeaders;

    @Value("#{'${cors.exposed.headers}'.split(',')}")
    private String[] corsExposedHeaders;

    @Value("#{'${cors.allowed.methods}'.split(',')}")
    private String[] corsAllowedMethod;

    @Value("#{'${cors.allowed.origins}'.split(',')}")
    private String[] corsAllowedOrigins;

    @Value("${cors.max.age}")
    private int corsMaxAge;

    @Autowired
    public WebMvcConfiguration() {
    }

    @Bean
    public LocaleResolver localeResolver() {
        return new SmartLocaleResolver();
    }

    public class SmartLocaleResolver extends CookieLocaleResolver {
        @Override
        public Locale resolveLocale(HttpServletRequest request) {
            String acceptLanguage = request.getHeader("Accept-Language");
            if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) {
                return super.determineDefaultLocale(request);
            }
            return request.getLocale();
        }
    }

    /**
     * Custom exception in WEB MVC
     *
     * @return
     */
    @Bean
    public CustomErrorAttributes myCustomErrorAttributes() {
        return new CustomErrorAttributes();
    }

    /**
     * Global CORS security configuration
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(corsMapping).exposedHeaders(corsExposedHeaders).allowedOrigins(corsAllowedOrigins).allowedHeaders(corsAllowedHeaders)
                .allowedMethods(corsAllowedMethod).maxAge(corsMaxAge);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TenantInterceptor());
    }

}

2 个答案:

答案 0 :(得分:1)

尝试注释掉您配置中的这一行:

this.objectMapperBuilder.configure(objectMapper);

我认为RepositoryRestConfigurer可以很好地为自己配置objectMapper。

如果您需要它来自动从类路径中添加更多模块,请手动添加/配置这些模块。

答案 1 :(得分:0)

当使用RepresentationModelAssembler<Object, PersistentEntityResource> resourceAssembler作为控制器方法的参数时,您将得到相同的错误。当您使用PersistentEntityResourceAssembler resourceAssembler时,spring将为您创建合适的实例。

我建议您尝试检查PagedBeanResourceAssembler类的类层次结构,并找到一些更具体的类/实现。