Converter from @PathVariable DomainObject to String? (using ControllerLinkBuilder.methodOn)

China☆狼群 提交于 2019-12-01 17:35:25
Franco Gotusso

I had the same issue, it is a bug. If you don't want to do copy & paste on every controller you can try something like this in your WebMvcConfigurationSupport. It works for me.

@Override
public void addFormatters(final FormatterRegistry registry) {
    super.addFormatters(registry);

    try {
        Class<?> clazz = Class.forName("org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor$BoundMethodParameter");
        Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
        field.setAccessible(true);
        DefaultFormattingConversionService service = (DefaultFormattingConversionService) field.get(null);
        for (Converter<?, ?> converter : beanFactory.getBeansOfType(Converter.class).values()) {
            service.addConverter(converter);
        }
    }
    catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

Found a "solution". It requires a lot copy & paste from Spring's classes, but at least it works!

Basically I had to copy org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor and change two lines:

class AnnotatedParametersParameterAccessor {
    ...
    static class BoundMethodParameter {
        // OLD: (with this one you can't call addConverter())
        // private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
        // NEW:
        private static final FormattingConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();

        ...

        public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
            ...
            // ADD:
            CONVERSION_SERVICE.addConverter(new MyNewConverter());
    }

    ...
}

This class get's used by ControllerLinkBuilderFactory. So I had to copy & paste that, too.

And this one get's used by ControllerLinkBuilder. Also copy & paste.

My Converter just does myDomainObject.getId().toString():

public class MyNewConverter implements Converter<Company, String> {
    @Override
    public String convert(Company source) {
        return source.getId().toString();
    }   
}

Now you can use the copy&pasted ControllerLinkBuilder inside the controller and it works as expected!

I developed a framework to render links in spring hateoas and it supports annotated parameters (@PathVariable and @RequestParam) and arbitrary parameters types.

In order to render these arbitrary types you have to create a spring bean that implements com.github.osvaldopina.linkbuilder.argumentresolver.ArgumentResolver interface.

The interface has 3 methods:

  1. public boolean resolveFor(MethodParameter methodParameter)

Is used to determine if the ArgumentResolver can be used to deal with the methodParameter. For example:

public boolean resolveFor(MethodParameter methodParameter) {
    return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
}

Defines that this ArgumentResover will be used for UserDefinedType.

  1. public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter)

Is used to include in the uriTemplate associated with the method the proper template parts. For example:

@Override
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
    uriTemplateAugmenter.addToQuery("value1");
    uriTemplateAugmenter.addToQuery("value2");

}

adds 2 query parameters (value1 and value2) to the uri template.

  1. public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames)

Sets in the template the values for the template variables. For example:

@Override
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
    if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
        template.set("value1", ((UserDefinedType) parameter).getValue1());
    }
    else {
        template.set("value1", "null-value");
    }

    if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
        template.set("value2", ((UserDefinedType) parameter).getValue2());
    }
    else {
        template.set("value2", "null-value");
    }
}

gets the UserDefinedType instance and use it to sets the templates variables value1 and value2 defined in augmentTemplate method.

A ArgumentResolver complete example would be:

@Component
public class UserDefinedTypeArgumentResolver implements ArgumentResolver {

    @Override
    public boolean resolveFor(MethodParameter methodParameter) {
        return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
    }

    @Override
    public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
        uriTemplateAugmenter.addToQuery("value1");
        uriTemplateAugmenter.addToQuery("value2");

    }

    @Override
    public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
        if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
            template.set("value1", ((UserDefinedType) parameter).getValue1());
        }
        else {
            template.set("value1", "null-value");
        }

        if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
            template.set("value2", ((UserDefinedType) parameter).getValue2());
        }
        else {
            template.set("value2", "null-value");
        }
    }
}

and for the following link builder:

   linksBuilder.link()
            .withRel("user-type")
            .fromControllerCall(RootRestController.class)
            .queryParameterForUserDefinedType(new UserDefinedType("v1", "v2"));

to the following method:

@RequestMapping("/user-defined-type")
@EnableSelfFromCurrentCall
public void queryParameterForUserDefinedType(UserDefinedType userDefinedType) {

}

would generate the following link:

{
    ...
    "_links": {
        "user-type": {
        "href": "http://localhost:8080/user-defined-type?value1=v1&value2=v2"
    }
    ...
}

}

full config in spring boot. same as Franco Gotusso's answer just provide more detail. ```

/** * This configuration file is to fix bug of Spring Hateoas. * please check https://github.com/spring-projects/spring-hateoas/issues/118. */

@Component public class MvcConfig extends WebMvcConfigurerAdapter {

@Autowired
private ApplicationContext applicationContext;

@Override
public void addFormatters(final FormatterRegistry registry) {
    super.addFormatters(registry);

    try {
        Class<?> clazz = Class.forName("org.springframework.hateoas.mvc."
                + "AnnotatedParametersParameterAccessor$BoundMethodParameter");
        Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
        field.setAccessible(true);
        DefaultFormattingConversionService service =
                (DefaultFormattingConversionService) field.get(null);
        for (Formatter<?> formatter : applicationContext
                .getBeansOfType(Formatter.class).values()) {
            service.addFormatter(formatter);
        }
        for (Converter<?, ?> converter : applicationContext
                .getBeansOfType(Converter.class).values()) {
            service.addConverter(converter);
        }
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

}

```

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!