如何控制Spring Hateoas生成链接的Web上下文?

时间:2014-09-29 08:33:37

标签: spring spring-hateoas webcontext

我们正在构建API并正在使用Spring RestControllerSpring HATEOAS 将war文件部署到容器并向http://localhost:8080/placesapi-packaged-war-1.0.0-SNAPSHOT/places发出GET请求时,HATEOAS链接如下所示:

{
  "links" : [ {
    "rel" : "self",
    "href" : "http://localhost:8080/placesapi-packaged-war-1.0.0-SNAPSHOT/places",
    "lastModified" : "292269055-12-02T16:47:04Z"
  } ]
}

因为Web上下文是已部署应用程序的上下文(例如:placesapi-packaged-war-1.0.0-SNAPSHOT

在真实的运行时环境(UAT及更高版本)中,容器可能位于http Apache等http服务器后面,其中虚拟主机或类似的Web应用程序前端。像这样:

<VirtualHost Nathans-MacBook-Pro.local>
   ServerName Nathans-MacBook-Pro.local

   <Proxy *>
     AddDefaultCharset Off
     Order deny,allow
     Allow from all
   </Proxy>

   ProxyPass / ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/
   ProxyPassReverse / ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/

</VirtualHost>

使用上述内容,当我们向http://nathans-macbook-pro.local/places发出GET请求时,生成的响应如下所示:

{
  "links": [ {
    "rel": "self",
    "href": "http://nathans-macbook-pro.local/placesapi-packaged-war-1.0.0-SNAPSHOT/places",
    "lastModified": "292269055-12-02T16:47:04Z"
  } ]
}

这是错误的,因为响应中的链接包含网络应用上下文,如果客户端要关注该链接,则会获得404

有谁知道如何在这方面控制Spring HATEOAS的行为?基本上我需要能够控制它在链接中生成的Web上下文名称。

我做了一些讨论,可以看到使用自定义标题X-Forwarded-Host,您可以控制主机和端口,但我看不到任何类似的能够控制上下文。< / p>

我们考虑的其他选项涉及将应用程序部署到ROOT上下文或固定的命名上下文,然后相应地设置我们的虚拟主机。然而,这些感觉就像妥协而不是解决方案,因为理想情况下我们希望在同一容器上托管应用程序的多个版本(例如:placesapi-packaged-war-1.0.0-RELEASE,placesapi-packaged-war-1.0.1- RELEASE,placesapi-packaged-war-2.0.0-RELEASE等)并根据http请求标题将虚拟主机转发到正确的应用程序。

对此的任何想法都将非常感激,
干杯

3 个答案:

答案 0 :(得分:3)

首先,如果您不了解,您可以通过创建包含该行的webapp/META-INF/context.xml来控制Web应用程序的上下文(至少在Tomcat下):

<Context path="/" />

...这将使应用程序上下文与您正在使用的内容相同(/)。

但是,这不是你的问题。我提出了一个similar question a little while back。因此,从我可以收集到的内容来看,没有用于手动控制生成的链接的开箱即用机制。相反,我创建了自己的ControllerLinkBuilder修改版本,它使用application.properties中定义的属性构建了URL的基础。如果在应用程序本身上设置上下文不是一个选项(即如果您在同一个Tomcat实例下运行多个版本),那么我认为这是您唯一的选择,如果{{1没有正确构建您的网址。

答案 1 :(得分:3)

有一个非常相似的问题。我们希望我们的公共URL是x.com/store,在内部我们的集群中主机的上下文路径是host / our-api。生成的所有URL都包含x.com/our-api而不是x.com/store,并且无法从公共脏网上解析。

首先只是一个注释,我们得到x.com的原因是因为我们的反向代理不会重写HOST头。如果确实如此,我们需要将X-Forwarded-Host标头集添加到x.com,以便HATEOAS链接构建器生成正确的主机。这是我们的反向代理特有的。

至于让路径工作......我们不想使用自定义的ControllerLinkBuilder。相反,我们在servlet过滤器中重写上下文。在我分享该代码之前,我想提出最棘手的事情。我们希望我们的api在直接进入托管战争的tomcat节点时生成可用的链接,因此url应该是host / our-api而不是host / store。为了做到这一点,反向代理需要通过反向代理向Web应用程序提供请求。您可以使用标题等执行此操作。具体对我们来说,我们只能修改请求网址,因此我们更改了负载均衡器以重写x.com/store到主机/ our-api /存储此额外/存储让我们知道请求来自反向代理,因此需要使用公共上下文根。您可以再次使用另一个标识符(自定义标头,存在X-Forwared-Host等)来检测情况。或者您可能不关心让单个节点返回可用的URL(但它非常适合测试)。

public class ContextRewriteFilter extends GenericFilterBean {


    @Override
    public void doFilter(ServletRequest req, ServletResponse res, final FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest)req;

        //There's no cleanup to perform so no need for try/finally
        chain.doFilter(new ContextRewriterHttpServletRequestWrapper(request), res);

    }


    private static class ContextRewriterHttpServletRequestWrapper extends HttpServletRequestWrapper {

        //I'm not totally certain storing/caching these once is ok..but i can't think of a situation
        //where the data would be changed in the wrapped request
        private final String context;
        private final String requestURI;
        private final String servletPath;



        public ContextRewriterHttpServletRequestWrapper(HttpServletRequest request){
            super(request);

            String originalRequestURI = super.getRequestURI();
            //If this came from the load balancer which we know BECAUSE of the our-api/store root, rewrite it to just be from /store which is the public facing context root
            if(originalRequestURI.startsWith("/our-api/store")){
                requestURI = "/store" + originalRequestURI.substring(25);
            }
            else {
                //otherwise it's just a standard request
                requestURI = originalRequestURI;
            }


            int endOfContext = requestURI.indexOf("/", 1);
            //If there's no / after the first one..then the request didn't contain it (ie /store vs /store/)
            //in such a case the context is the request is the context so just use that
            context = endOfContext == -1 ? requestURI : requestURI.substring(0, endOfContext);

            String sPath = super.getServletPath();
            //If the servlet path starts with /store then this request came from the load balancer
            //so we need to pull out the /store as that's the context root...not part of the servlet path
            if(sPath.startsWith("/store")) {
                sPath = sPath.substring(6);
            }

            //I think this complies with the spec
            servletPath = StringUtils.isEmpty(sPath) ? "/" : sPath;


        }


        @Override
        public String getContextPath(){
            return context;
        }

        @Override
        public String getRequestURI(){

            return requestURI;
        }

        @Override
        public String getServletPath(){
            return servletPath;
        }

    }
}

这是一个黑客,如果有什么依赖于知道请求中的REAL上下文路径它可能会错误...但它一直很好地为我们工作。

答案 2 :(得分:3)

ProxyPass /placesapi-packaged-war-1.0.0-SNAPSHOT
ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/   
ProxyPassReverse /placesapi-packaged-war-1.0.0-SNAPSHOT
ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/