I'm trying to call Spring's ControllerLinkBuilder.methodOn()
with a non-String type, which always fails. And I don't know which kind of Converter
to use and where to register it.
Here's my Controller:
@RestController
@RequestMapping("/companies")
class CompanyController {
@RequestMapping(value="/{c}", method=RequestMethod.GET)
void getIt(@PathVariable Company c) {
System.out.println(c);
Link link = linkTo(methodOn(getClass()).getIt(c));
}
}
The System.out.println(c)
works well. My Company
Domain object get's fetched from DB. (I'm using DomainClassConverter
)
But the other way doesn't work: ConverterNotFoundException: No converter found capable of converting from type @PathVariable Company to type String
Do I just need a Converter<Company, String>
? And where should I register it? I tried something within the addFormatters(FormatterRegistry registry)
method of WebMvcConfigurationSupport
, but it did just display the same error. But after all I'm not sure what exactly I tried...
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:
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
.
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.
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);
}
}
}
```
来源:https://stackoverflow.com/questions/22240155/converter-from-pathvariable-domainobject-to-string-using-controllerlinkbuilde