13

我们正在构建一个 API 并使用Spring RestControllers 和Spring HATEOAS.
当 war 文件被部署到一个容器并且一个 GET 请求被发送到http://localhost:8080/placesapi-packaged-war-1.0.0-SNAPSHOT/places时,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虚拟主机或类似服务器位于 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>

使用上述方法,当我们向 发出 GET 请求时http://nathans-macbook-pro.local/places,结果响应如下所示:

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

这是错误的,因为响应中的链接包含 Web 应用程序上下文,并且如果客户端要遵循该链接,他们将得到 404

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

我做了一些探索,可以看到使用自定义标头X-Forwarded-Host可以控制主机和端口,但我看不到任何类似的东西能够控制上下文。

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

对此的任何想法将不胜感激,
干杯

弥敦道

4

3 回答 3

3

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

<Context path="/" />

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

然而,这不是你的问题。不久前我提出了一个类似的问题。因此,据我所知,没有现成的机制来手动控制生成的链接。相反,我创建了自己的修改版本ControllerLinkBuilder,它使用定义在application.properties. 如果在您的应用程序本身上设置上下文不是一个选项(即,如果您在同一个 Tomcat 实例下运行多个版本),那么我认为这是您唯一的选择,如果ControllerLinkBuilder没有正确构建您的 URL。

于 2014-09-29T08:50:04.300 回答
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 应用程序提示请求来自反向代理。您可以使用标头等来执行此操作。特别是对我们而言,我们只能修改请求 url,因此我们将负载均衡器更改为将 x.com/store 重写为 host/our-api/store 这个额外的 /store 让我们知道该请求来自反向代理,因此需要使用公共上下文根。同样,您可以使用另一个标识符(自定义标头,

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

    }
}

这是一个 hack,如果有任何事情取决于知道请求中的 REAL 上下文路径,它可能会出错......但它对我们来说工作得很好。

于 2014-10-07T22:34:13.663 回答
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/
于 2015-05-07T12:02:21.183 回答