使用ControllerLinkBuilder的模板变量

时间:2014-07-29 13:51:17

标签: spring spring-hateoas

我希望我的回复包括:

"keyMaps":{
  "href":"http://localhost/api/keyMaps{/keyMapId}",
  "templated":true
 }

这很容易实现:

add(new Link("http://localhost/api/keyMaps{/keyMapId}", "keyMaps"));

但是,当然,我宁愿使用ControllerLinkBuilder,如下所示:

add(linkTo(methodOn(KeyMapController.class).getKeyMap("{keyMapId}")).withRel("keyMaps"));

问题是,当变量“{keyMapId}”到达UriTemplate构造函数时,它已被包含在编码的URL中:

http://localhost/api/keyMaps/%7BkeyMapId%7D

因此UriTemplate的构造函数不会将其识别为包含变量。

如何说服我想使用模板变量的ControllerLinkBuilder?

7 个答案:

答案 0 :(得分:8)

在我看来,Spring-HATEOAS的当前状态不允许通过ControllerLinkBuilder(我非常希望被证明是错误的),所以我自己使用以下类实现了这个用于模板化查询参数:

public class TemplatedLinkBuilder {

    private static final TemplatedLinkBuilderFactory FACTORY = new TemplatedLinkBuilderFactory();
    public static final String ENCODED_LEFT_BRACE = "%7B";
    public static final String ENCODED_RIGHT_BRACE = "%7D";

    private UriComponentsBuilder uriComponentsBuilder;

    TemplatedLinkBuilder(UriComponentsBuilder builder) {
        uriComponentsBuilder = builder;
    }

    public static TemplatedLinkBuilder linkTo(Object invocationValue) {
        return FACTORY.linkTo(invocationValue);
    }

    public static <T> T methodOn(Class<T> controller, Object... parameters) {
        return DummyInvocationUtils.methodOn(controller, parameters);
    }

    public Link withRel(String rel) {
        return new Link(replaceTemplateMarkers(uriComponentsBuilder.build().toString()), rel);
    }

    public Link withSelfRel() {
        return withRel(Link.REL_SELF);
    }

    private String replaceTemplateMarkers(String encodedUri) {
        return encodedUri.replaceAll(ENCODED_LEFT_BRACE, "{").replaceAll(ENCODED_RIGHT_BRACE, "}");
    }

}

public class TemplatedLinkBuilderFactory {

    private final ControllerLinkBuilderFactory controllerLinkBuilderFactory;

    public TemplatedLinkBuilderFactory() {
        this.controllerLinkBuilderFactory = new ControllerLinkBuilderFactory();
    }

    public TemplatedLinkBuilder linkTo(Object invocationValue) {
        ControllerLinkBuilder controllerLinkBuilder = controllerLinkBuilderFactory.linkTo(invocationValue);
        UriComponentsBuilder uriComponentsBuilder = controllerLinkBuilder.toUriComponentsBuilder();

        Assert.isInstanceOf(DummyInvocationUtils.LastInvocationAware.class, invocationValue);
        DummyInvocationUtils.LastInvocationAware invocations = (DummyInvocationUtils.LastInvocationAware) invocationValue;
        DummyInvocationUtils.MethodInvocation invocation = invocations.getLastInvocation();
        Object[] arguments = invocation.getArguments();
        MethodParameters parameters = new MethodParameters(invocation.getMethod());

        for (MethodParameter requestParameter : parameters.getParametersWith(RequestParam.class)) {
            Object value = arguments[requestParameter.getParameterIndex()];
            if (value == null) {
                uriComponentsBuilder.queryParam(requestParameter.getParameterName(), "{" + requestParameter.getParameterName() + "}");
            }
        }
        return new TemplatedLinkBuilder(uriComponentsBuilder);
    }
}

嵌入普通的ControllerLinkBuilder然后使用类似的逻辑来解析为空的@RequestParam带注释的参数,并将这些参数添加到查询参数中。此外,我们的客户端重新使用这些模板化的URI来执行对服务器的进一步请求。为了实现这一点而不需要担心剥离未使用的模板化参数,我必须执行相反的操作(使用{params}交换null),我正在使用自定义Spring {{1如下

RequestParamMethodArgumentResolver

此外,这需要替换我使用的现有public class TemplatedRequestParamResolver extends RequestParamMethodArgumentResolver { public TemplatedRequestParamResolver() { super(false); } @Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception { Object value = super.resolveName(name, parameter, webRequest); if (value instanceof Object[]) { Object[] valueAsCollection = (Object[])value; List<Object> resultList = new LinkedList<Object>(); for (Object collectionEntry : valueAsCollection) { if (nullifyTemplatedValue(collectionEntry) != null) { resultList.add(collectionEntry); } } if (resultList.isEmpty()) { value = null; } else { value = resultList.toArray(); } } else{ value = nullifyTemplatedValue(value); } return value; } private Object nullifyTemplatedValue(Object value) { if (value != null && value.toString().startsWith("{") && value.toString().endsWith("}")) { value = null; } return value; } }

RequestParamMethodArgumentResolver

很遗憾,虽然@Configuration public class ConfigureTemplatedRequestParamResolver { private @Autowired RequestMappingHandlerAdapter adapter; @PostConstruct public void replaceArgumentMethodHandlers() { List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(adapter.getArgumentResolvers()); for (int cursor = 0; cursor < argumentResolvers.size(); ++cursor) { HandlerMethodArgumentResolver handlerMethodArgumentResolver = argumentResolvers.get(cursor); if (handlerMethodArgumentResolver instanceof RequestParamMethodArgumentResolver) { argumentResolvers.remove(cursor); argumentResolvers.add(cursor, new TemplatedRequestParamResolver()); break; } } adapter.setArgumentResolvers(argumentResolvers); } } {模板化 URI中的有效字符,但它们在URI中无效,这可能是您的客户端代码依赖的问题它有多严格。我更喜欢Spring-HATEOAS内置的整洁解决方案!

答案 1 :(得分:6)

使用spring-hateoas的最新版本,您可以执行以下操作:

UriComponents uriComponents = UriComponentsBuilder.fromUri(linkBuilder.toUri()).build();
UriTemplate template = new UriTemplate(uriComponents.toUriString())
   .with("keyMapId", TemplateVariable.SEGMENT);

会给你:http://localhost:8080/bla{/keyMapId}",

答案 2 :(得分:5)

我们遇到了同样的问题。一般的解决方法是我们有自己的LinkBuilder类和一堆静态助手。模板化的看起来像这样:

public static Link linkToSubcategoriesTemplated(String categoryId){

    return new Link(
        new UriTemplate(
            linkTo(methodOn(CategoryController.class).subcategories(null, null, categoryId))
                .toUriComponentsBuilder().build().toUriString(),
            // register it as variable
            getBaseTemplateVariables()
        ),
        REL_SUBCATEGORIES
    );
}

private static TemplateVariables getBaseTemplateVariables() {
    return new TemplateVariables(
        new TemplateVariable("page", TemplateVariable.VariableType.REQUEST_PARAM),
        new TemplateVariable("sort", TemplateVariable.VariableType.REQUEST_PARAM),
        new TemplateVariable("size", TemplateVariable.VariableType.REQUEST_PARAM)
    );
}

这是为了公开PagedResource的控制器响应的参数。

然后在控制器中我们称之为根据需要添加一个withRel。

答案 3 :(得分:4)

根据this issue comment,这将在即将发布的spring-hateoas中得到解决。

目前,Maven Central中的ControllerLinkBuilder可以直接替换de.escalon.hypermedia:spring-hateoas-ext

我现在可以这样做:

import static de.escalon.hypermedia.spring.AffordanceBuilder.*

...

add(linkTo(methodOn(KeyMapController.class).getKeyMap(null)).withRel("keyMaps"));

我传入null作为参数值,表示我想使用模板变量。变量名称将自动从控制器中提取。

答案 4 :(得分:3)

从此提交开始:

https://github.com/spring-projects/spring-hateoas/commit/2daf8aabfb78b6767bf27ac3e473832c872302c7

您现在可以传递null预期路径变量的位置。它对我有用,没有解决方法。

resource.add(linkTo(methodOn(UsersController.class).someMethod(null)).withRel("someMethod"));

结果:

    "someMethod": {
        "href": "http://localhost:8080/api/v1/users/{userId}",
        "templated": true
    },

同时检查相关问题:https://github.com/spring-projects/spring-hateoas/issues/545

答案 5 :(得分:2)

我需要在spring数据休息应用程序的根目录中包含一个带有模板变量的链接,以便通过traverson访问oauth2令牌。这很好用,也许很有用:

@Component
class RepositoryLinksResourceProcessor implements ResourceProcessor<RepositoryLinksResource> {

    @Override
    RepositoryLinksResource process(RepositoryLinksResource resource) {

        UriTemplate uriTemplate =  new UriTemplate(
                ControllerLinkBuilder.
                        linkTo(
                                TokenEndpoint,
                                TokenEndpoint.getDeclaredMethod("postAccessToken", java.security.Principal, Map )).
                        toUriComponentsBuilder().
                        build().
                        toString(),
                new TemplateVariables([
                        new TemplateVariable("username", TemplateVariable.VariableType.REQUEST_PARAM),
                        new TemplateVariable("password", TemplateVariable.VariableType.REQUEST_PARAM),
                        new TemplateVariable("clientId", TemplateVariable.VariableType.REQUEST_PARAM),
                        new TemplateVariable("clientSecret", TemplateVariable.VariableType.REQUEST_PARAM)
                ])
        )

        resource.add(
                new Link( uriTemplate,
                        "token"
                )
        )

        return resource
    }
}

答案 6 :(得分:0)

基于之前的评论,我已经实现了一个通用的帮助方法(针对spring-hateoas-0.20.0)作为“临时”解决方法。该实现仅考虑RequestParameters,并且远未经过优化或经过充分测试。对于沿着同一个兔子洞行进的其他一些可怜的灵魂来说,它可能会派上用场:

public static Link getTemplatedLink(final Method m, final String rel) {
    DefaultParameterNameDiscoverer disco = new DefaultParameterNameDiscoverer();

    ControllerLinkBuilder builder = ControllerLinkBuilder.linkTo(m.getDeclaringClass(), m);
    UriTemplate uriTemplate = new UriTemplate(UriComponentsBuilder.fromUri(builder.toUri()).build().toUriString());
    Annotation[][] parameterAnnotations = m.getParameterAnnotations();

    int param = 0;
    for (Annotation[] parameterAnnotation : parameterAnnotations) {
        for (Annotation annotation : parameterAnnotation) {
            if (annotation.annotationType().equals(RequestParam.class)) {
                RequestParam rpa = (RequestParam) annotation;
                String parameterName = rpa.name();
                if (StringUtils.isEmpty(parameterName)) parameterName = disco.getParameterNames(m)[param];
                uriTemplate = uriTemplate.with(parameterName, TemplateVariable.VariableType.REQUEST_PARAM);
            }
        }
        param++;
    }
    return new Link(uriTemplate, rel);
}