Как управлять веб-контекстом Spring связанных с Hateoas ссылок?

Мы создаем API и используем Spring RestController и Spring HATEOAS.
Когда файл войны развертывается в контейнере и запрос 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"
  } ]
}

в том, что веб-контекст представляет собой развернутое приложение (например: placesapi-packaged-war-1.0.0-SNAPSHOT)

В реальной среде выполнения (UAT и далее) контейнер, скорее всего, будет находиться за http-сервером, например Apache, где виртуальный хост или аналогичный фронт веб-приложения. Что-то вроде этого:

<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"
  } ]
}

Неправильно, потому что ссылка в ответе содержит контекст веб-приложения, и если клиент будет следовать этой ссылке, он получит 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-запроса.

Любые мысли об этом будут очень оценены,
Приветствия

Натан

Ответ 1

Во-первых, если вы не знаете, вы можете контролировать контекст веб-приложения (по крайней мере, под Tomcat), создав webapp/META-INF/context.xml, содержащий строку:

<Context path="/" />

..., который заставит контекст приложения быть таким же, как и тот, который вы используете (/).

Однако это был не ваш вопрос. Некоторое время назад я задавал аналогичный вопрос. В результате, из того, что я могу собрать, нет готового механизма для управления созданными ссылками вручную. Вместо этого я создал свою собственную модифицированную версию ControllerLinkBuilder, которая создала базу URL-адреса, используя свойства, определенные в application.properties. Если настройка контекста в вашем приложении сама по себе не является вариантом (т.е. Если вы используете несколько версий под одним и тем же экземпляром Tomcat), то я думаю, что это ваш единственный вариант, если ControllerLinkBuilder не правильно создает ваши URL-адреса.

Ответ 2

Была очень похожая проблема. Мы хотели, чтобы наш общедоступный 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. Вместо этого мы переписываем контекст в сервлет-фильтре. Прежде чем я поделюсь этим кодом, я хочу поднять эту сложную вещь. Мы хотели, чтобы наш api генерировал полезные ссылки при переходе непосредственно на узлы tomcat, где размещалась война, поэтому URL-адреса должны быть host/our-api вместо хоста/хранилища. Чтобы сделать это, обратный прокси должен дать подсказку веб-приложению, что запрос пришел через обратный прокси. Вы можете сделать это с помощью заголовков и т.д. В частности, для нас мы могли бы ТОЛЬКО модифицировать URL-адрес запроса, поэтому мы изменили наш балансировщик нагрузки, чтобы переписать x.com/store на host/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 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 just a standard request
                requestURI = originalRequestURI;
            }


            int endOfContext = requestURI.indexOf("/", 1);
            //If there 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 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 в запросе, это, вероятно, будет ошибкой... но оно прекрасно работает для нас.

Ответ 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/