Spring HATEOAS HalResourcesSerializer No default constructor found
Asked Answered
I

3

6

I'm creating a Spring REST app using Spring Boot 2, Spring Data REST, Spring HATEOAS.

I created this controller:

@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);
    }

}

The service method is:

@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);
    }

As you can see City bean is not stored in the DB. In fact the bean is:

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;
}

and finally this is my 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());
    }
}

When I make a http call I see a WARNING message in the console:

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>()

Not using a PagedResources the error goes away. I don't understand where I'm doing something wrong. I got that HalResourcesSerializer has not a default constructor, but I don't use it directly and I don't understand either why with Entity persisted in the db a controller such this works fine. How can I fix this problem continuing to use a PagedResource?

======== UPDATE ==========

I add my configuration to give a more detailed view:

CustomConfiguration:

@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());
    }

}
Infrastructure answered 8/2, 2019 at 10:17 Comment(3)
Something is messed up in your configuration. Pls insert all of configurations where you alter the RepositoryRestMvcConfiguraition (Especially modifying messageConverters or Objectmappers)Calcimine
@Calcimine I added all relevant configuration files.Infrastructure
I see that you are using field injection (@Autowired on fields). Best practice is to use constructor one. i.e. you declare private final fields and make constructor to fill them. This constructor will be marked as @Autowired if needed. Project lombok can help you to remove need for constructor creation. Just use RequiredArgsConstructor, see projectlombok.org/features/constructorBeefburger
C
1

Try commenting out this line in your configuration:

this.objectMapperBuilder.configure(objectMapper);

RepositoryRestConfigurer configures the objectMapper for itself quite well I assume.

If you need it for automatically adding more modules from your classpath, then add/configure those modules manually.

Calcimine answered 9/2, 2019 at 15:26 Comment(1)
Actually it seems updating Spring the warning disappeared.Infrastructure
S
1

I had the same error with different setup. I've migrated from Spring Boot 1.5 to Spring Boot 2.7 and had this line in my config:

objectMapper.registerModule(new Jackson2HalModule());

Apparently, sprint boot starter configures jackson well enough. Removing this line solved this same exception for me.

Stabile answered 15/5 at 16:10 Comment(0)
B
0

You will get same error when you use RepresentationModelAssembler<Object, PersistentEntityResource> resourceAssembler as parameter to your controller method. When you use PersistentEntityResourceAssembler resourceAssembler, spring will create right instance for you.

I suggest you to try to examine class hierarchy of PagedBeanResourceAssembler class and find some more specific class/implementation.

Beefburger answered 20/2, 2020 at 5:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.