diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 00000000..0c4b142e --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/README.adoc b/README.adoc index f980ffa7..9d84e36e 100644 --- a/README.adoc +++ b/README.adoc @@ -40,15 +40,11 @@ tracker for issues and merging pull requests into main. If you want to contribute even something trivial please do not hesitate, but follow the guidelines below. -[[sign-the-contributor-license-agreement]] -== Sign the Contributor License Agreement +[[developer-certificate-of-origin]] +== Developer Certificate of Origin (DCO) -Before we accept a non-trivial patch or pull request we will need you to sign the -https://cla.pivotal.io/sign/spring[Contributor License Agreement]. -Signing the contributor's agreement does not grant anyone commit rights to the main -repository, but it does mean that we can accept your contributions, and you will get an -author credit if we do. Active contributors might be asked to join the core team, and -given the ability to merge pull requests. +All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. +For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. [[code-of-conduct]] == Code of Conduct diff --git a/docs/antora.yml b/docs/antora.yml index fad926be..4bfa002b 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -6,7 +6,7 @@ nav: ext: collector: run: - command: ./mvnw --no-transfer-progress -B process-resources -Pdocs -pl docs -Dantora-maven-plugin.phase=none -Dgenerate-docs.phase=none -Dgenerate-readme.phase=none -Dgenerate-cloud-resources.phase=none -Dmaven-dependency-plugin-for-docs.phase=none -Dmaven-dependency-plugin-for-docs-classes.phase=none -DskipTests + command: ./mvnw --no-transfer-progress -B process-resources -Pdocs -pl docs -Dantora-maven-plugin.phase=none -Dgenerate-docs.phase=none -Dgenerate-readme.phase=none -Dgenerate-cloud-resources.phase=none -Dmaven-dependency-plugin-for-docs.phase=none -Dmaven-dependency-plugin-for-docs-classes.phase=none -DskipTests -DdisableConfigurationProperties local: true scan: dir: ./target/classes/antora-resources/ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 947966c4..c8225ada 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,116 +1,116 @@ * xref:index.adoc[Introduction] -// begin Gateway Reactive Server +// begin Gateway Server WebFlux -* xref:spring-cloud-gateway.adoc[] -** xref:spring-cloud-gateway/starter.adoc[] -** xref:spring-cloud-gateway/glossary.adoc[] -** xref:spring-cloud-gateway/how-it-works.adoc[] -** xref:spring-cloud-gateway/configuring-route-predicate-factories-and-filter-factories.adoc[] -** xref:spring-cloud-gateway/request-predicates-factories.adoc[] -** xref:spring-cloud-gateway/gatewayfilter-factories.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/addrequestheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/addrequestheadersifnotpresent-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/addrequestparameter-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/addresponseheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/circuitbreaker-filter-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/cacherequestbody-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/deduperesponseheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/fallback-headers.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/jsontogrpc-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/local-cache-response-filter.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/maprequestheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/modifyrequestbody-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/modifyresponsebody-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/prefixpath-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/preservehostheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/redirectto-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/removejsonattributesresponsebody-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/removerequestheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/removerequestparameter-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/removeresponseheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/requestheadersize-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/requestratelimiter-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/rewritelocationresponseheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/rewritepath-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/rewriterequestparameter-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/rewriteresponseheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/savesession-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/secureheaders-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/setpath-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/setrequestheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/setresponseheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/setstatus-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/stripprefix-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/retry-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/requestsize-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/setrequesthostheader-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/tokenrelay-factory.adoc[] -*** xref:spring-cloud-gateway/gatewayfilter-factories/default-filters.adoc[] -** xref:spring-cloud-gateway/global-filters.adoc[] -** xref:spring-cloud-gateway/httpheadersfilters.adoc[] -** xref:spring-cloud-gateway/tls-and-ssl.adoc[] -** xref:spring-cloud-gateway/http-client.adoc[] -** xref:spring-cloud-gateway/configuration.adoc[] -** xref:spring-cloud-gateway/route-metadata-configuration.adoc[] -** xref:spring-cloud-gateway/http-timeouts-configuration.adoc[] -** xref:spring-cloud-gateway/fluent-java-routes-api.adoc[] -** xref:spring-cloud-gateway/the-discoveryclient-route-definition-locator.adoc[] -** xref:spring-cloud-gateway/reactor-netty-access-logs.adoc[] -** xref:spring-cloud-gateway/cors-configuration.adoc[] -** xref:spring-cloud-gateway/actuator-api.adoc[] -** xref:spring-cloud-gateway/sharing-routes.adoc[] -** xref:spring-cloud-gateway/troubleshooting.adoc[] -** xref:spring-cloud-gateway/developer-guide.adoc[] -** xref:spring-cloud-gateway/aot-and-native-image-support.adoc[] -** xref:spring-cloud-gateway/configuration-properties.adoc[] +* xref:spring-cloud-gateway-server-webflux.adoc[] +** xref:spring-cloud-gateway-server-webflux/starter.adoc[] +** xref:spring-cloud-gateway-server-webflux/glossary.adoc[] +** xref:spring-cloud-gateway-server-webflux/how-it-works.adoc[] +** xref:spring-cloud-gateway-server-webflux/configuring-route-predicate-factories-and-filter-factories.adoc[] +** xref:spring-cloud-gateway-server-webflux/request-predicates-factories.adoc[] +** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestheadersifnotpresent-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestparameter-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addresponseheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/circuitbreaker-filter-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/cacherequestbody-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/deduperesponseheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/fallback-headers.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/jsontogrpc-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/local-cache-response-filter.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/maprequestheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/modifyrequestbody-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/modifyresponsebody-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/prefixpath-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/preservehostheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/redirectto-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/removejsonattributesresponsebody-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/removerequestheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/removerequestparameter-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/removeresponseheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestheadersize-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestratelimiter-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewritelocationresponseheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewritepath-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewriterequestparameter-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewriteresponseheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/savesession-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setpath-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequestheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setresponseheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setstatus-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripprefix-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestsize-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequesthostheader-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/tokenrelay-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/default-filters.adoc[] +** xref:spring-cloud-gateway-server-webflux/global-filters.adoc[] +** xref:spring-cloud-gateway-server-webflux/httpheadersfilters.adoc[] +** xref:spring-cloud-gateway-server-webflux/tls-and-ssl.adoc[] +** xref:spring-cloud-gateway-server-webflux/http-client.adoc[] +** xref:spring-cloud-gateway-server-webflux/configuration.adoc[] +** xref:spring-cloud-gateway-server-webflux/route-metadata-configuration.adoc[] +** xref:spring-cloud-gateway-server-webflux/http-timeouts-configuration.adoc[] +** xref:spring-cloud-gateway-server-webflux/fluent-java-routes-api.adoc[] +** xref:spring-cloud-gateway-server-webflux/the-discoveryclient-route-definition-locator.adoc[] +** xref:spring-cloud-gateway-server-webflux/reactor-netty-access-logs.adoc[] +** xref:spring-cloud-gateway-server-webflux/cors-configuration.adoc[] +** xref:spring-cloud-gateway-server-webflux/actuator-api.adoc[] +** xref:spring-cloud-gateway-server-webflux/sharing-routes.adoc[] +** xref:spring-cloud-gateway-server-webflux/troubleshooting.adoc[] +** xref:spring-cloud-gateway-server-webflux/developer-guide.adoc[] +** xref:spring-cloud-gateway-server-webflux/aot-and-native-image-support.adoc[] +** xref:spring-cloud-gateway-server-webflux/configuration-properties.adoc[] -// begin Gateway Server MVC +// begin Gateway Server Web MVC -* xref:spring-cloud-gateway-server-mvc.adoc[] -** xref:spring-cloud-gateway-server-mvc/starter.adoc[] -** xref:spring-cloud-gateway-server-mvc/glossary.adoc[] -** xref:spring-cloud-gateway-server-mvc/how-it-works.adoc[] -** xref:spring-cloud-gateway-server-mvc/java-routes-api.adoc[] -** xref:spring-cloud-gateway-server-mvc/gateway-request-predicates.adoc[] -** xref:spring-cloud-gateway-server-mvc/gateway-handler-filter-functions.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/addrequestheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/addrequestheadersifnotpresent.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/addrequestparameter.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/addresponseheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/circuitbreaker-filter.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/deduperesponseheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/fallback-headers.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/loadbalancer.adoc[] +* xref:spring-cloud-gateway-server-webmvc.adoc[] +** xref:spring-cloud-gateway-server-webmvc/starter.adoc[] +** xref:spring-cloud-gateway-server-webmvc/glossary.adoc[] +** xref:spring-cloud-gateway-server-webmvc/how-it-works.adoc[] +** xref:spring-cloud-gateway-server-webmvc/java-routes-api.adoc[] +** xref:spring-cloud-gateway-server-webmvc/gateway-request-predicates.adoc[] +** xref:spring-cloud-gateway-server-webmvc/gateway-handler-filter-functions.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/addrequestheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/addrequestheadersifnotpresent.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/addrequestparameter.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/addresponseheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/deduperesponseheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/loadbalancer.adoc[] //*** xref:spring-cloud-gateway-server-mvc/filters/local-cache-response-filter.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/maprequestheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/modifyrequestbody.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/modifyresponsebody.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/prefixpath.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/preservehostheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/redirectto.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/maprequestheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/modifyrequestbody.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/modifyresponsebody.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/prefixpath.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/preservehostheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/redirectto.adoc[] //*** xref:spring-cloud-gateway-server-mvc/filters/removejsonattributesresponsebody.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/removerequestheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/removerequestparameter.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/removeresponseheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/requestheadersize.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/ratelimiter.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/rewritelocationresponseheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/rewritepath.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/removerequestheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/removerequestparameter.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/removeresponseheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/requestheadersize.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/ratelimiter.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/rewritelocationresponseheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/rewritepath.adoc[] //*** xref:spring-cloud-gateway-server-mvc/filters/rewriterequestparameter.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/rewriteresponseheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/rewriteresponseheader.adoc[] //*** xref:spring-cloud-gateway-server-mvc/filters/savesession.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/setpath.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/setrequestheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/setresponseheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/setstatus.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/stripprefix.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/retry.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/requestsize.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/setrequesthostheader.adoc[] -*** xref:spring-cloud-gateway-server-mvc/filters/tokenrelay.adoc[] -** xref:spring-cloud-gateway-server-mvc/writing-custom-predicates-and-filters.adoc[] -** xref:spring-cloud-gateway-server-mvc/working-with-servlets-and-filters.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/setpath.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/setstatus.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/retry.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/requestsize.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc[] +*** xref:spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc[] +** xref:spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc[] +** xref:spring-cloud-gateway-server-webmvc/working-with-servlets-and-filters.adoc[] // begin Gateway Proxy Exchange diff --git a/docs/modules/ROOT/pages/intro.adoc b/docs/modules/ROOT/pages/intro.adoc index 1f285ded..c5c70d56 100644 --- a/docs/modules/ROOT/pages/intro.adoc +++ b/docs/modules/ROOT/pages/intro.adoc @@ -1,10 +1,9 @@ [[introduction]] = Spring Cloud Gateway -// TODO: docs, rework intro for 4 modules This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 6, Spring Boot 3 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency. -There are two distinct flavors of Spring Cloud Gateway: *Server* and *Proxy Exchange*. Each flavor offers WebFlux and MVC compatibility. +There are two distinct flavors of Spring Cloud Gateway: *Server* and *Proxy Exchange*. Each flavor offers WebFlux and Web MVC compatibility. - The Server variant is a full-featured API gateway that can be standalone or embedded in a Spring Boot application. -- The Proxy Exchange variant is exclusively for use in annotation based WebFlux or MVC applications and allows the use of a special `ProxyExchange` object as a parameter to a web handler method. \ No newline at end of file +- The Proxy Exchange variant is exclusively for use in annotation based WebFlux or Web MVC applications and allows the use of a special `ProxyExchange` object as a parameter to a web handler method. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc index 8e39d751..849ce240 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc @@ -1,11 +1,11 @@ [[proxy-exchange-gateway]] -= Proxy Exchange Gateway with Spring MVC or Webflux += Proxy Exchange Gateway with Spring Web MVC or WebFlux WARNING: The following describes an alternative style gateway. None of the Spring Cloud Gateway Server documentation applies to what follows. == How to Include Spring Cloud Gateway Proxy Exchange -To include Spring Cloud Gateway Proxy Exchange in your project, use the artifact with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-gateway-mvc` for the MVC Proxy Exchange. For the WebFlux Proxy Exchange use artifact with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-gateway-webflux`. +To include Spring Cloud Gateway Proxy Exchange in your project, use the artifact with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-gateway-proxyexchange-webmvc` for the MVC Proxy Exchange. For the WebFlux Proxy Exchange use artifact with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-gateway-proxyexchange-webflux`. See the https://projects.spring.io/spring-cloud/[Spring Cloud Project page] for details on setting up your build system with the current Spring Cloud Release Train. @@ -15,7 +15,7 @@ Spring Cloud Gateway provides a utility object called `ProxyExchange`. You can use it inside a regular Spring web handler as a method parameter. It supports basic downstream HTTP exchanges through methods that mirror the HTTP verbs. With MVC, it also supports forwarding to a local handler through the `forward()` method. -To use the `ProxyExchange`, include the right module in your classpath (either `spring-cloud-gateway-mvc` or `spring-cloud-gateway-webflux`). +To use the `ProxyExchange`, include the right module in your classpath (either `spring-cloud-gateway-proxyexchange-webmvc` or `spring-cloud-gateway-proxyexchange-webflux`). The following MVC example proxies a request to `/test` downstream to a remote server: diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc.adoc deleted file mode 100644 index 24ce7b0f..00000000 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc.adoc +++ /dev/null @@ -1,5 +0,0 @@ -[[spring-cloud-gateway-server-mvc]] -= Spring Cloud Gateway Server MVC -:page-section-summary-toc: 1 - -// TODO: diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux.adoc new file mode 100644 index 00000000..abc3ec35 --- /dev/null +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux.adoc @@ -0,0 +1,7 @@ +[[spring-cloud-gateway-server-webflux]] += Spring Cloud Gateway Server WebFlux +:page-section-summary-toc: 1 + +*{spring-cloud-version}* + + diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/actuator-api.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/actuator-api.adoc similarity index 89% rename from docs/modules/ROOT/pages/spring-cloud-gateway/actuator-api.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/actuator-api.adoc index 1f8549cd..5188a214 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/actuator-api.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/actuator-api.adoc @@ -95,13 +95,13 @@ This will default to `true` in a future release. This section details how to retrieve route filters, including: -* xref:spring-cloud-gateway/actuator-api.adoc#gateway-global-filters[Global Filters] +* xref:spring-cloud-gateway-server-webflux/actuator-api.adoc#gateway-global-filters[Global Filters] * <> [[gateway-global-filters]] === Global Filters -To retrieve the xref:spring-cloud-gateway/global-filters.adoc[global filters] applied to all routes, make a `GET` request to `/actuator/gateway/globalfilters`. The resulting response is similar to the following: +To retrieve the xref:spring-cloud-gateway-server-webflux/global-filters.adoc[global filters] applied to all routes, make a `GET` request to `/actuator/gateway/globalfilters`. The resulting response is similar to the following: ---- { @@ -117,11 +117,11 @@ To retrieve the xref:spring-cloud-gateway/global-filters.adoc[global filters] ap ---- The response contains the details of the global filters that are in place. -For each global filter, there is a string representation of the filter object (for example, `org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@77856cc5`) and the corresponding xref:spring-cloud-gateway/global-filters.adoc#gateway-combined-global-filter-and-gatewayfilter-ordering[order] in the filter chain. +For each global filter, there is a string representation of the filter object (for example, `org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@77856cc5`) and the corresponding xref:spring-cloud-gateway-server-webflux/global-filters.adoc#gateway-combined-global-filter-and-gatewayfilter-ordering[order] in the filter chain. [[gateway-route-filters]] === Route Filters -To retrieve the xref:spring-cloud-gateway/gatewayfilter-factories.adoc[`GatewayFilter` factories] applied to routes, make a `GET` request to `/actuator/gateway/routefilters`. +To retrieve the xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories.adoc[`GatewayFilter` factories] applied to routes, make a `GET` request to `/actuator/gateway/routefilters`. The resulting response is similar to the following: ---- @@ -215,7 +215,7 @@ The following table describes the structure of each element (each is a route) of |`route_object.filters` | Array -| The xref:spring-cloud-gateway/gatewayfilter-factories.adoc[`GatewayFilter` factories] applied to the route. +| The xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories.adoc[`GatewayFilter` factories] applied to the route. |`order` | Number @@ -273,14 +273,14 @@ The following table describes the structure of the response: [[creating-and-deleting-a-particular-route-definition]] == Creating and Deleting a Particular Route Definition -To create a route definition, make a `POST` request to `/gateway/routes/\{id_route_to_create}` with a JSON body that specifies the fields of the route (see xref:spring-cloud-gateway/actuator-api.adoc#gateway-retrieving-information-about-a-particular-route[Retrieving Information about a Particular Route]). +To create a route definition, make a `POST` request to `/gateway/routes/\{id_route_to_create}` with a JSON body that specifies the fields of the route (see xref:spring-cloud-gateway-server-webflux/actuator-api.adoc#gateway-retrieving-information-about-a-particular-route[Retrieving Information about a Particular Route]). To delete a route definition, make a `DELETE` request to `/gateway/routes/\{id_route_to_delete}`. [[creating-multiple-route-definitions]] == Creating multiple Route Definitions -To create multiple route definitions in a single request, make a `POST` request to `/gateway/routes` with a JSON body that specifies the fields of the route, including the route id (see xref:spring-cloud-gateway/actuator-api.adoc#gateway-retrieving-information-about-a-particular-route[Retrieving Information about a Particular Route]). +To create multiple route definitions in a single request, make a `POST` request to `/gateway/routes` with a JSON body that specifies the fields of the route, including the route id (see xref:spring-cloud-gateway-server-webflux/actuator-api.adoc#gateway-retrieving-information-about-a-particular-route[Retrieving Information about a Particular Route]). The route definitions will be discarded if any route raises an error during the creation of the routes. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/aot-and-native-image-support.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/aot-and-native-image-support.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/aot-and-native-image-support.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/aot-and-native-image-support.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/configuration-properties.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/configuration-properties.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/configuration-properties.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/configuration-properties.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/configuration.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/configuration.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/configuration.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/configuration.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/configuring-route-predicate-factories-and-filter-factories.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/configuring-route-predicate-factories-and-filter-factories.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/configuring-route-predicate-factories-and-filter-factories.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/configuring-route-predicate-factories-and-filter-factories.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/cors-configuration.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/cors-configuration.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/cors-configuration.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/cors-configuration.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/developer-guide.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/developer-guide.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/developer-guide.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/developer-guide.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/fluent-java-routes-api.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/fluent-java-routes-api.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/fluent-java-routes-api.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/fluent-java-routes-api.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addrequestheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addrequestheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addrequestheadersifnotpresent-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestheadersifnotpresent-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addrequestheadersifnotpresent-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestheadersifnotpresent-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addrequestparameter-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestparameter-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addrequestparameter-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestparameter-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addresponseheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addresponseheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addresponseheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/addresponseheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/cacherequestbody-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/cacherequestbody-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/cacherequestbody-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/cacherequestbody-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/circuitbreaker-filter-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/circuitbreaker-filter-factory.adoc similarity index 98% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/circuitbreaker-filter-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/circuitbreaker-filter-factory.adoc index 5ec70b95..9e50d713 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/circuitbreaker-filter-factory.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/circuitbreaker-filter-factory.adoc @@ -121,7 +121,7 @@ In case of the request being forwarded to fallback, the Spring Cloud CircuitBrea It is added to the `ServerWebExchange` as the `ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR` attribute that can be used when handling the fallback within the gateway application. For the external controller/handler scenario, headers can be added with exception details. -You can find more information on doing so in the xref:spring-cloud-gateway/gatewayfilter-factories/fallback-headers.adoc[FallbackHeaders GatewayFilter Factory section]. +You can find more information on doing so in the xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/fallback-headers.adoc[FallbackHeaders GatewayFilter Factory section]. [[circuit-breaker-status-codes]] == Tripping The Circuit Breaker On Status Codes diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/deduperesponseheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/deduperesponseheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/deduperesponseheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/deduperesponseheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/default-filters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/default-filters.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/default-filters.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/default-filters.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/fallback-headers.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/fallback-headers.adoc similarity index 92% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/fallback-headers.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/fallback-headers.adoc index 02d7fcfb..f3832822 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/fallback-headers.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/fallback-headers.adoc @@ -39,5 +39,5 @@ You can overwrite the names of the headers in the configuration by setting the v * `rootCauseExceptionTypeHeaderName` (`"Root-Cause-Exception-Type"`) * `rootCauseExceptionMessageHeaderName` (`"Root-Cause-Exception-Message"`) -For more information on circuit breakers and the gateway see the xref:spring-cloud-gateway/gatewayfilter-factories/circuitbreaker-filter-factory.adoc[Spring Cloud CircuitBreaker Factory section]. +For more information on circuit breakers and the gateway see the xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/circuitbreaker-filter-factory.adoc[Spring Cloud CircuitBreaker Factory section]. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/jsontogrpc-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/jsontogrpc-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/jsontogrpc-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/jsontogrpc-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/local-cache-response-filter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/local-cache-response-filter.adoc similarity index 94% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/local-cache-response-filter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/local-cache-response-filter.adoc index 27fa083f..090132f4 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/local-cache-response-filter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/local-cache-response-filter.adoc @@ -8,7 +8,7 @@ This filter allows caching the response body and headers to follow these rules: * Response data is not cached if `Cache-Control` header does not allow it (`no-store` present in the request or `no-store` or `private` present in the response). * If the response is already cached and a new request is performed with no-cache value in `Cache-Control` header, it returns a bodiless response with 304 (Not Modified). -This filter configures the local response cache per route and is available only if the `spring.cloud.gateway.filter.local-response-cache.enabled` property is enabled. And a xref:spring-cloud-gateway/global-filters.adoc#local-cache-response-global-filter[local response cache configured globally] is also available as feature. +This filter configures the local response cache per route and is available only if the `spring.cloud.gateway.filter.local-response-cache.enabled` property is enabled. And a xref:spring-cloud-gateway-server-webflux/global-filters.adoc#local-cache-response-global-filter[local response cache configured globally] is also available as feature. It accepts the first parameter to override the time to expire a cache entry (expressed in `s` for seconds, `m` for minutes, and `h` for hours) and a second parameter to set the maximum size of the cache to evict entries for this route (`KB`, `MB`, or `GB`). diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/maprequestheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/maprequestheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/maprequestheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/maprequestheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/modifyrequestbody-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/modifyrequestbody-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/modifyrequestbody-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/modifyrequestbody-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/modifyresponsebody-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/modifyresponsebody-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/modifyresponsebody-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/modifyresponsebody-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/prefixpath-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/prefixpath-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/prefixpath-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/prefixpath-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/preservehostheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/preservehostheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/preservehostheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/preservehostheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/redirectto-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/redirectto-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/redirectto-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/redirectto-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removejsonattributesresponsebody-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removejsonattributesresponsebody-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removejsonattributesresponsebody-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removejsonattributesresponsebody-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removerequestheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removerequestheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removerequestheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removerequestheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removerequestparameter-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removerequestparameter-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removerequestparameter-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removerequestparameter-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removeresponseheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removeresponseheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/removeresponseheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/removeresponseheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestheadersize-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestheadersize-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestheadersize-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestheadersize-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestratelimiter-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestratelimiter-factory.adoc similarity index 59% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestratelimiter-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestratelimiter-factory.adoc index 6f0044d4..9a4dd7d4 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestratelimiter-factory.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestratelimiter-factory.adoc @@ -3,7 +3,7 @@ The `RequestRateLimiter` `GatewayFilter` factory uses a `RateLimiter` implementation to determine if the current request is allowed to proceed. If it is not, a status of `HTTP 429 - Too Many Requests` (by default) is returned. -This filter takes an optional `keyResolver` parameter and parameters specific to the rate limiter (described xref:spring-cloud-gateway/gatewayfilter-factories/requestratelimiter-factory.adoc#key-resolver-section[later in this section]). +This filter takes an optional `keyResolver` parameter and parameters specific to the rate limiter (described xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestratelimiter-factory.adoc#key-resolver-section[later in this section]). `keyResolver` is a bean that implements the `KeyResolver` interface. In configuration, reference the bean by name using SpEL. @@ -27,6 +27,17 @@ The default implementation of `KeyResolver` is the `PrincipalNameKeyResolver`, w By default, if the `KeyResolver` does not find a key, requests are denied. You can adjust this behavior by setting the `spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key` (`true` or `false`) and `spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code` properties. +The following example configures a `KeyResolver` in Java: + +.Config.java +[source,java] +---- +@Bean +KeyResolver userKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); +} +---- + [NOTE] ===== The `RequestRateLimiter` is not configurable with the "shortcut" notation. The following example below is _invalid_: @@ -81,6 +92,7 @@ The following listing configures a `redis-rate-limiter`: Rate limits below `1 request/s` are accomplished by setting `replenishRate` to the wanted number of requests, `requestedTokens` to the timespan in seconds, and `burstCapacity` to the product of `replenishRate` and `requestedTokens`. For example, setting `replenishRate=1`, `requestedTokens=60`, and `burstCapacity=60` results in a limit of `1 request/min`. + .application.yml [source,yaml] ---- @@ -99,21 +111,87 @@ spring: ---- -The following example configures a `KeyResolver` in Java: +This defines a request rate limit of 10 per user. A burst of 20 is allowed, but, in the next second, only 10 requests are available. +The `KeyResolver` is a simple one that gets the `user` request parameter +NOTE: This is not recommended for production + +[[bucket4j-ratelimiter]] +== Bucket4j `RateLimiter` + +This implementation is based on the https://bucket4j.com/[Bucket4j] Java library. +It requires the use of the `com.bucket4j:bucket4j_jdk17-core` dependency as well as one of the https://github.com/bucket4j/bucket4j?tab=readme-ov-file#bucket4j-distributed-features[distributed persistence options]. + +In this example, we will use the Caffeine integration, which is a local cache. This can be added by including the `com.github.ben-manes.caffeine:caffeine` artifact in your dependency management. The `com.bucket4j:bucket4j_jdk17-caffeine` artifact will need to be imported as well. + +.pom.xml +[source,xml] +---- + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.bucket4j + bucket4j_jdk17-caffeine + ${bucket4j.version} + +---- + +First a bean of type `io.github.bucket4j.distributed.proxy.AsyncProxyMananger` needs to be created. .Config.java [source,java] ---- @Bean -KeyResolver userKeyResolver() { - return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); +AsyncProxyManager caffeineProxyManager() { + Caffeine builder = (Caffeine) Caffeine.newBuilder().maximumSize(100); + return new CaffeineProxyManager<>(builder, Duration.ofMinutes(1)).asAsync(); } ---- -This defines a request rate limit of 10 per user. A burst of 20 is allowed, but, in the next second, only 10 requests are available. -The `KeyResolver` is a simple one that gets the `user` request parameter +The `bucket4j-rate-limiter.capacity` property is the maximum number of requests a user is allowed in a single second (without any dropped requests). +This is the number of tokens the token bucket can hold. +Must be greater than zero. + +The `bucket4j-rate-limiter.refillPeriod` property defines the refill period. The bucket refills at a rate of `refillTokens` per `refillPeriod`. This is a required property and uses the https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.typesafe-configuration-properties.conversion.periods[Spring Boot Period format]. + +The `bucket4j-rate-limiter.refillTokens` property defines how many tokens are added to the bucket in during `refillPeriod`. +This defaults to `capacity` and must be greater than or equal to zero. + +The `bucket4j-rate-limiter.requestedTokens` property is how many tokens a request costs. +This is the number of tokens taken from the bucket for each request and defaults to `1`. Must be greater than zero. + +The `bucket4j-rate-limiter.refillStyle` property defines how the bucket is refilled. The 3 options are `GREEDY` (default), `INTERVALLY` and `INTERVALLY_ALIGNED`. +`GREEDY` tries to add the tokens to the bucket as soon as possible. `INTERVALLY`, in opposite to greedy, waits until the whole `refillPeriod` has elapsed before refilling tokens. `INTERVALLY_ALIGNED` is like `INTERVALLY`, but with a specified `timeOfFirstRefill`. + +The `bucket4j-rate-limiter.timeOfFirstRefill` property is an `Instant` only used when `refillStyle` is set to `INTERVALLY_ALIGNED`. + +The following example defines a request rate limit of 10 per user. A burst of 20 is allowed, but, in the next second, only 10 requests are available. NOTE: This is not recommended for production +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + bucket4j-rate-limiter.capacity: 20 + bucket4j-rate-limiter.refillTokens: 10 + bucket4j-rate-limiter.refillPeriod: 1s + bucket4j-rate-limiter.requestedTokens: 1 + +---- + +[[custom-ratelimiter]] +== Custom `RateLimiter` + You can also define a rate limiter as a bean that implements the `RateLimiter` interface. In configuration, you can reference the bean by name using SpEL. `#{@myRateLimiter}` is a SpEL expression that references a bean with named `myRateLimiter`. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestsize-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestsize-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestsize-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestsize-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/retry-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/retry-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewritelocationresponseheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewritelocationresponseheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewritelocationresponseheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewritelocationresponseheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewritepath-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewritepath-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewritepath-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewritepath-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewriterequestparameter-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewriterequestparameter-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewriterequestparameter-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewriterequestparameter-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewriteresponseheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewriteresponseheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/rewriteresponseheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/rewriteresponseheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/savesession-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/savesession-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/savesession-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/savesession-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/secureheaders-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/secureheaders-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setpath-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setpath-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setpath-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setpath-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setrequestheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequestheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setrequestheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequestheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setrequesthostheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequesthostheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setrequesthostheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequesthostheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setresponseheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setresponseheader-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setresponseheader-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setresponseheader-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setstatus-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setstatus-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/setstatus-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/setstatus-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/stripprefix-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripprefix-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/stripprefix-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripprefix-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/tokenrelay-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/tokenrelay-factory.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/tokenrelay-factory.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/tokenrelay-factory.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/global-filters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/global-filters.adoc similarity index 96% rename from docs/modules/ROOT/pages/spring-cloud-gateway/global-filters.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/global-filters.adoc index 5bafd87b..5513a0a1 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/global-filters.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/global-filters.adoc @@ -12,7 +12,7 @@ NOTE: This interface and its usage are subject to change in future milestone rel When a request matches a route, the filtering web handler adds all instances of `GlobalFilter` and all route-specific instances of `GatewayFilter` to a filter chain. This combined filter chain is sorted by the `org.springframework.core.Ordered` interface, which you can set by implementing the `getOrder()` method. -As Spring Cloud Gateway distinguishes between "`pre`" and "`post`" phases for filter logic execution (see xref:spring-cloud-gateway/how-it-works.adoc[How it Works]), the filter with the highest precedence is the first in the "`pre`"-phase and the last in the "`post`"-phase. +As Spring Cloud Gateway distinguishes between "`pre`" and "`post`" phases for filter logic execution (see xref:spring-cloud-gateway-server-webflux/how-it-works.adoc[How it Works]), the filter with the highest precedence is the first in the "`pre`"-phase and the last in the "`post`"-phase. The following listing configures a filter chain: @@ -86,7 +86,7 @@ This filter also implements the automatic calculation of the `max-age` value in If `max-age` is present on the original response, the value is rewritten with the number of seconds set in the `timeToLive` configuration parameter. In subsequent calls, this value is recalculated with the number of seconds left until the response expires. -Setting `spring.cloud.gateway.global-filter.local-response-cache.enabled` to `false` deactivate the local response cache for all routes, the xref:spring-cloud-gateway/gatewayfilter-factories/local-cache-response-filter.adoc[LocalResponseCache filter] allows to use this functionality at route level. +Setting `spring.cloud.gateway.global-filter.local-response-cache.enabled` to `false` deactivate the local response cache for all routes, the xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/local-cache-response-filter.adoc[LocalResponseCache filter] allows to use this functionality at route level. NOTE: To enable this feature, add `com.github.ben-manes.caffeine:caffeine` and `spring-boot-starter-cache` as project dependencies. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/glossary.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/glossary.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/glossary.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/glossary.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/how-it-works.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/how-it-works.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/how-it-works.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/how-it-works.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/http-client.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/http-client.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/http-client.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/http-client.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/http-timeouts-configuration.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/http-timeouts-configuration.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/http-timeouts-configuration.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/http-timeouts-configuration.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/httpheadersfilters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/httpheadersfilters.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/httpheadersfilters.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/httpheadersfilters.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/reactor-netty-access-logs.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/reactor-netty-access-logs.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/reactor-netty-access-logs.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/reactor-netty-access-logs.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/request-predicates-factories.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/request-predicates-factories.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/request-predicates-factories.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/request-predicates-factories.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/route-metadata-configuration.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/route-metadata-configuration.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/route-metadata-configuration.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/route-metadata-configuration.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/sharing-routes.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/sharing-routes.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/sharing-routes.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/sharing-routes.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/starter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/starter.adoc similarity index 96% rename from docs/modules/ROOT/pages/spring-cloud-gateway/starter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/starter.adoc index 7bc6f3f1..ee07b9e3 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/starter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/starter.adoc @@ -2,7 +2,7 @@ = How to Include Spring Cloud Gateway :page-section-summary-toc: 1 -To include Spring Cloud Gateway in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-gateway`. +To include Spring Cloud Gateway in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-gateway-server-webflux`. See the https://projects.spring.io/spring-cloud/[Spring Cloud Project page] for details on setting up your build system with the current Spring Cloud Release Train. If you include the starter, but you do not want the gateway to be enabled, set `spring.cloud.gateway.enabled=false`. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/the-discoveryclient-route-definition-locator.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/the-discoveryclient-route-definition-locator.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/the-discoveryclient-route-definition-locator.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/the-discoveryclient-route-definition-locator.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/tls-and-ssl.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/tls-and-ssl.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/tls-and-ssl.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/tls-and-ssl.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/troubleshooting.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/troubleshooting.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway/troubleshooting.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/troubleshooting.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc.adoc new file mode 100644 index 00000000..567955e0 --- /dev/null +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc.adoc @@ -0,0 +1,5 @@ +[[spring-cloud-gateway-server-webmvc]] += Spring Cloud Gateway Server Web MVC +:page-section-summary-toc: 1 + +// TODO: diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addrequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addrequestheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addrequestheadersifnotpresent.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheadersifnotpresent.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addrequestheadersifnotpresent.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheadersifnotpresent.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addrequestparameter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestparameter.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addrequestparameter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestparameter.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addresponseheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/addresponseheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addresponseheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/circuitbreaker-filter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc similarity index 98% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/circuitbreaker-filter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc index 8ec00ffd..8e16c8f3 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/circuitbreaker-filter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc @@ -155,7 +155,7 @@ In case of the request being forwarded to fallback, the Spring Cloud CircuitBrea It is added to the `ServerRequest` as the `MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR` attribute that can be used when handling the fallback within the gateway application. For the external controller/handler scenario, headers can be added with exception details. -You can find more information on doing so in the xref:spring-cloud-gateway-server-mvc/filters/fallback-headers.adoc[FallbackHeaders Filters section]. +You can find more information on doing so in the xref:spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc[FallbackHeaders Filters section]. [[circuit-breaker-status-codes]] == Tripping The Circuit Breaker On Status Codes diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/deduperesponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/deduperesponseheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/deduperesponseheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/deduperesponseheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/fallback-headers.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc similarity index 96% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/fallback-headers.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc index 22f4114b..3b867001 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/fallback-headers.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc @@ -67,5 +67,5 @@ You can overwrite the names of the headers in the configuration by setting the v * `rootCauseExceptionTypeHeaderName` (`"Root-Cause-Exception-Type"`) * `rootCauseExceptionMessageHeaderName` (`"Root-Cause-Exception-Message"`) -For more information on circuit breakers and the gateway see the xref:spring-cloud-gateway-server-mvc/filters/circuitbreaker-filter.adoc[Spring Cloud CircuitBreaker Filter section]. +For more information on circuit breakers and the gateway see the xref:spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc[Spring Cloud CircuitBreaker Filter section]. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/loadbalancer.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/loadbalancer.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/loadbalancer.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/loadbalancer.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/local-cache-response-filter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/local-cache-response-filter.adoc similarity index 94% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/local-cache-response-filter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/local-cache-response-filter.adoc index 7cde3175..0929e94a 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/local-cache-response-filter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/local-cache-response-filter.adoc @@ -8,7 +8,7 @@ This filter allows caching the response body and headers to follow these rules: * Response data is not cached if `Cache-Control` header does not allow it (`no-store` present in the request or `no-store` or `private` present in the response). * If the response is already cached and a new request is performed with no-cache value in `Cache-Control` header, it returns a bodiless response with 304 (Not Modified). -This filter configures the local response cache per route and is available only if the `spring.cloud.gateway.filter.local-response-cache.enabled` property is enabled. And a xref:spring-cloud-gateway/global-filters.adoc#local-cache-response-global-filter[local response cache configured globally] is also available as feature. +This filter configures the local response cache per route and is available only if the `spring.cloud.gateway.filter.local-response-cache.enabled` property is enabled. And a xref:spring-cloud-gateway-server-webflux/global-filters.adoc#local-cache-response-global-filter[local response cache configured globally] is also available as feature. It accepts the first parameter to override the time to expire a cache entry (expressed in `s` for seconds, `m` for minutes, and `h` for hours) and a second parameter to set the maximum size of the cache to evict entries for this route (`KB`, `MB`, or `GB`). diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/maprequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/maprequestheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/maprequestheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/maprequestheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/modifyrequestbody.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyrequestbody.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/modifyrequestbody.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyrequestbody.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/modifyresponsebody.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyresponsebody.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/modifyresponsebody.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyresponsebody.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/prefixpath.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/prefixpath.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/prefixpath.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/prefixpath.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/preservehostheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/preservehostheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/preservehostheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/preservehostheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/ratelimiter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/ratelimiter.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/ratelimiter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/ratelimiter.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/redirectto.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/redirectto.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/redirectto.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/redirectto.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removejsonattributesresponsebody.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removejsonattributesresponsebody.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removejsonattributesresponsebody.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removejsonattributesresponsebody.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removerequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removerequestheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removerequestparameter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestparameter.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removerequestparameter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestparameter.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removeresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removeresponseheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/removeresponseheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removeresponseheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/requestheadersize.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestheadersize.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/requestheadersize.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestheadersize.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/requestsize.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestsize.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/requestsize.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestsize.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/retry.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc similarity index 88% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/retry.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc index 1a09acbc..9bf2bf99 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/retry.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc @@ -9,6 +9,7 @@ The `Retry` filter supports the following parameters: * `methods`: The HTTP methods that should be retried, represented by using `org.springframework.http.HttpMethod`. * `series`: The series of status codes to be retried, represented by using `org.springframework.http.HttpStatus.Series`. * `exceptions`: A list of thrown exceptions that should be retried. +* `cacheBody`: A flag to signal if the request body should be cached. If set to `true`, the `adaptCacheBody` filter must be used to send the cached body downstream. //* `backoff`: The configured exponential backoff for the retries. //Retries are performed after a backoff interval of `firstBackoff * (factor ^ n)`, where `n` is the iteration. //If `maxBackoff` is configured, the maximum backoff applied is limited to `maxBackoff`. @@ -20,8 +21,11 @@ The following defaults are configured for `Retry` filter, if enabled: * `series`: 5XX series * `methods`: GET method * `exceptions`: `IOException`, `TimeoutException` and `RetryException` +* `cacheBody`: `false` //* `backoff`: disabled +WARNING: Setting `cacheBody` to `true` causes the gateway to read the whole body into memory. This should be used with caution. + The following listing configures a Retry filter: .application.yml @@ -42,11 +46,14 @@ spring: retries: 3 series: SERVER_ERROR methods: GET,POST + cacheBody: true + - name: AdaptCachedBody ---- .GatewaySampleApplication.java [source,java] ---- +import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.adaptCachedBody; import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry; import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; @@ -59,7 +66,8 @@ class RouteConfiguration { public RouterFunction gatewayRouterFunctionsAddReqHeader() { return route("add_request_parameter_route") .route(host("*.retry.com"), http("https://example.org")) - .filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)))) + .filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)).setCacheBody(true))) + .filter(adaptCachedBody()) .build(); } } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewritelocationresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritelocationresponseheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewritelocationresponseheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritelocationresponseheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewritepath.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritepath.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewritepath.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritepath.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewriterequestparameter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriterequestparameter.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewriterequestparameter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriterequestparameter.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewriteresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriteresponseheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/rewriteresponseheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriteresponseheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/savesession.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/savesession.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/savesession.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/savesession.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setpath.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setpath.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setpath.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setpath.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setrequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setrequestheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setrequesthostheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setrequesthostheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setresponseheader.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setstatus.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setstatus.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/setstatus.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setstatus.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/stripprefix.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/stripprefix.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/tokenrelay.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/filters/tokenrelay.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/gateway-handler-filter-functions.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-handler-filter-functions.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/gateway-handler-filter-functions.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-handler-filter-functions.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/gateway-request-predicates.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-request-predicates.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/gateway-request-predicates.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-request-predicates.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/glossary.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/glossary.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/glossary.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/glossary.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/how-it-works.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/how-it-works.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/how-it-works.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/how-it-works.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/java-routes-api.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/java-routes-api.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/java-routes-api.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/java-routes-api.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/starter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/starter.adoc similarity index 76% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/starter.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/starter.adoc index 3c081e91..6d3b7bd8 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/starter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/starter.adoc @@ -1,8 +1,8 @@ [[gateway-starter]] -= How to Include Spring Cloud Gateway Server MVC += How to Include Spring Cloud Gateway Server Web MVC :page-section-summary-toc: 1 -To include Spring Cloud Gateway Server MVC in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-gateway-mvc`. +To include Spring Cloud Gateway Server Web MVC in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-gateway-server-webmvc`. See the https://projects.spring.io/spring-cloud/[Spring Cloud Project page] for details on setting up your build system with the current Spring Cloud Release Train. If you include the starter, but you do not want the gateway to be enabled, set `spring.cloud.gateway.mvc.enabled=false`. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/working-with-servlets-and-filters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/working-with-servlets-and-filters.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/working-with-servlets-and-filters.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/working-with-servlets-and-filters.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/writing-custom-predicates-and-filters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc similarity index 100% rename from docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/writing-custom-predicates-and-filters.adoc rename to docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway.adoc deleted file mode 100644 index 1f896442..00000000 --- a/docs/modules/ROOT/pages/spring-cloud-gateway.adoc +++ /dev/null @@ -1,7 +0,0 @@ -[[spring-cloud-gateway]] -= Spring Cloud Gateway Reactive Server -:page-section-summary-toc: 1 - -*{spring-cloud-version}* - - diff --git a/docs/package.json b/docs/package.json index c3570e2f..567c1f3a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,10 +1,10 @@ { "dependencies": { - "antora": "3.2.0-alpha.4", + "antora": "3.2.0-alpha.8", "@antora/atlas-extension": "1.0.0-alpha.2", - "@antora/collector-extension": "1.0.0-alpha.3", + "@antora/collector-extension": "1.0.1", "@asciidoctor/tabs": "1.0.0-beta.6", - "@springio/antora-extensions": "1.11.1", - "@springio/asciidoctor-extensions": "1.0.0-alpha.10" + "@springio/antora-extensions": "1.14.2", + "@springio/asciidoctor-extensions": "1.0.0-alpha.14" } } diff --git a/docs/pom.xml b/docs/pom.xml index 52ca0764..709dc57e 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT spring-cloud-gateway-docs jar @@ -25,20 +25,28 @@ .* ${maven.multiModuleProjectDirectory}/docs/modules/ROOT/partials/ - src/main/asciidoc + + enable-configuration-properties + + + !disableConfigurationProperties + + + + + ${project.groupId} + spring-cloud-starter-gateway + + + ${project.groupId} + spring-cloud-starter-gateway-mvc + + + docs diff --git a/pom.xml b/pom.xml index 87438173..d1c8473c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT pom Spring Cloud Gateway @@ -15,7 +15,7 @@ org.springframework.cloud spring-cloud-build - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT @@ -51,12 +51,14 @@ UTF-8 UTF-8 - 8.10.1 + 8.14.0 1.0.8.RELEASE 17 2.3.0 - 3.2.1-SNAPSHOT - 4.2.1-SNAPSHOT + 3.3.0-SNAPSHOT + 4.3.0-SNAPSHOT + 4.3.0-SNAPSHOT + 4.3.0-SNAPSHOT @@ -75,6 +77,20 @@ pom import + + org.springframework.cloud + spring-cloud-function-dependencies + ${spring-cloud-function.version} + pom + import + + + org.springframework.cloud + spring-cloud-stream-dependencies + ${spring-cloud-stream.version} + pom + import + org.springframework.cloud spring-cloud-test-support @@ -99,12 +115,12 @@ com.bucket4j - bucket4j-core + bucket4j_jdk17-core ${bucket4j.version} com.bucket4j - bucket4j-caffeine + bucket4j_jdk17-caffeine ${bucket4j.version} @@ -127,11 +143,23 @@ spring-cloud-gateway-dependencies + spring-cloud-gateway-mvc + spring-cloud-gateway-webflux + spring-cloud-gateway-server + spring-cloud-gateway-server-mvc + spring-cloud-gateway-proxyexchange-webflux + spring-cloud-gateway-proxyexchange-webmvc + spring-cloud-gateway-server-webflux + spring-cloud-gateway-server-webmvc + spring-cloud-starter-gateway-server-webflux + spring-cloud-starter-gateway-server-webmvc + spring-cloud-starter-gateway + spring-cloud-starter-gateway-mvc spring-cloud-gateway-sample spring-cloud-gateway-integration-tests diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 6d255037..66103cf7 100644 --- a/spring-cloud-gateway-dependencies/pom.xml +++ b/spring-cloud-gateway-dependencies/pom.xml @@ -6,12 +6,12 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT spring-cloud-gateway-dependencies - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT pom spring-cloud-gateway-dependencies @@ -22,31 +22,67 @@ + org.springframework.cloud spring-cloud-gateway-webflux ${project.version} + org.springframework.cloud spring-cloud-gateway-mvc ${project.version} + org.springframework.cloud spring-cloud-gateway-server ${project.version} + org.springframework.cloud spring-cloud-gateway-server-mvc ${project.version} + + org.springframework.cloud + spring-cloud-gateway-proxyexchange-webflux + ${project.version} + + + org.springframework.cloud + spring-cloud-gateway-proxyexchange-webmvc + ${project.version} + + + org.springframework.cloud + spring-cloud-gateway-server-webflux + ${project.version} + + + org.springframework.cloud + spring-cloud-gateway-server-webmvc + ${project.version} + + + org.springframework.cloud + spring-cloud-starter-gateway-server-webflux + ${project.version} + + + org.springframework.cloud + spring-cloud-starter-gateway-server-webmvc + ${project.version} + + org.springframework.cloud spring-cloud-starter-gateway ${project.version} + org.springframework.cloud spring-cloud-starter-gateway-mvc diff --git a/spring-cloud-gateway-integration-tests/grpc/pom.xml b/spring-cloud-gateway-integration-tests/grpc/pom.xml index 606d6cf6..39aefb18 100644 --- a/spring-cloud-gateway-integration-tests/grpc/pom.xml +++ b/spring-cloud-gateway-integration-tests/grpc/pom.xml @@ -12,13 +12,13 @@ 3.25.1 - 1.68.1 + 1.70.0 org.springframework.cloud spring-cloud-gateway-integration-tests - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. @@ -29,7 +29,7 @@ org.springframework.cloud - spring-cloud-starter-gateway + spring-cloud-starter-gateway-server-webflux org.springframework.boot diff --git a/spring-cloud-gateway-integration-tests/http2/pom.xml b/spring-cloud-gateway-integration-tests/http2/pom.xml index f833c541..49e2de46 100644 --- a/spring-cloud-gateway-integration-tests/http2/pom.xml +++ b/spring-cloud-gateway-integration-tests/http2/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. @@ -27,7 +27,7 @@ org.springframework.cloud - spring-cloud-starter-gateway + spring-cloud-starter-gateway-server-webflux org.springframework.cloud diff --git a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml index c79551c2..810172ab 100644 --- a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml +++ b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. @@ -27,7 +27,7 @@ org.springframework.cloud - spring-cloud-starter-gateway + spring-cloud-starter-gateway-server-webflux org.springframework.cloud diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml index e9bf2a3c..461e4c70 100644 --- a/spring-cloud-gateway-integration-tests/pom.xml +++ b/spring-cloud-gateway-integration-tests/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml index ed68626d..14d0d2aa 100644 --- a/spring-cloud-gateway-mvc/pom.xml +++ b/spring-cloud-gateway-mvc/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java index ba462ba9..0a840d78 100644 --- a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java +++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java @@ -23,9 +23,13 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -108,4 +112,20 @@ public class ProxyResponseAutoConfiguration implements WebMvcConfigurer { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.cloud.gateway.proxyexchange.webmvc.Marker") + protected static class NewModuleConfiguration { + + private static final Log log = LogFactory.getLog(ProxyResponseAutoConfiguration.class); + + private static final String BORDER = "\n\n**********************************************************\n\n"; + + public NewModuleConfiguration() { + log.warn(BORDER + "The artifact spring-cloud-gateway-mvc is deprecated. " + + "It will be removed in the next major release. " + + "Please add spring-cloud-gateway-proxyexchange-webmvc dependency." + BORDER); + } + + } + } diff --git a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java index ddb6c6ba..d426f2d1 100644 --- a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java +++ b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java @@ -25,11 +25,14 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.cloud.gateway.mvc.ProductionConfigurationTests.TestApplication; @@ -59,6 +62,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ContextConfiguration(classes = TestApplication.class) +@ExtendWith(OutputCaptureExtension.class) public class ProductionConfigurationTests { @Autowired @@ -325,6 +329,11 @@ public class ProductionConfigurationTests { assertThat(headers.get("forwarded").get(0)).isEqualTo("host=localhost:" + port); } + @Test + void logsArtifactDeprecatedWarning(CapturedOutput output) { + assertThat(output).contains("spring-cloud-gateway-mvc is deprecated"); + } + @SpringBootApplication static class TestApplication { diff --git a/spring-cloud-gateway-proxyexchange-webflux/pom.xml b/spring-cloud-gateway-proxyexchange-webflux/pom.xml new file mode 100644 index 00000000..3243a645 --- /dev/null +++ b/spring-cloud-gateway-proxyexchange-webflux/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-gateway + 4.3.0-SNAPSHOT + .. + + spring-cloud-gateway-proxyexchange-webflux + spring-cloud-gateway-proxyexchange-webflux + jar + Spring Cloud Gateway Proxy Exchange WebFlux + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-gateway-webflux + + + diff --git a/spring-cloud-gateway-proxyexchange-webflux/src/main/java/org/springframework/cloud/gateway/proxyexchange/webflux/Marker.java b/spring-cloud-gateway-proxyexchange-webflux/src/main/java/org/springframework/cloud/gateway/proxyexchange/webflux/Marker.java new file mode 100644 index 00000000..f54110da --- /dev/null +++ b/spring-cloud-gateway-proxyexchange-webflux/src/main/java/org/springframework/cloud/gateway/proxyexchange/webflux/Marker.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.proxyexchange.webflux; + +@Deprecated +public class Marker { + +} diff --git a/spring-cloud-gateway-proxyexchange-webmvc/pom.xml b/spring-cloud-gateway-proxyexchange-webmvc/pom.xml new file mode 100644 index 00000000..8d04d069 --- /dev/null +++ b/spring-cloud-gateway-proxyexchange-webmvc/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-gateway + 4.3.0-SNAPSHOT + .. + + spring-cloud-gateway-proxyexchange-webmvc + spring-cloud-gateway-proxyexchange-webmvc + jar + Spring Cloud Gateway Proxy Exchange Web MVC + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-gateway-mvc + + + diff --git a/spring-cloud-gateway-proxyexchange-webmvc/src/main/java/org/springframework/cloud/gateway/proxyexchange/webmvc/Marker.java b/spring-cloud-gateway-proxyexchange-webmvc/src/main/java/org/springframework/cloud/gateway/proxyexchange/webmvc/Marker.java new file mode 100644 index 00000000..e9e2b4b5 --- /dev/null +++ b/spring-cloud-gateway-proxyexchange-webmvc/src/main/java/org/springframework/cloud/gateway/proxyexchange/webmvc/Marker.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.proxyexchange.webmvc; + +@Deprecated +public class Marker { + +} diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index 41884109..20671002 100644 --- a/spring-cloud-gateway-sample/pom.xml +++ b/spring-cloud-gateway-sample/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. @@ -31,7 +31,7 @@ org.springframework.cloud - spring-cloud-starter-gateway + spring-cloud-starter-gateway-server-webflux org.isomorphism diff --git a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java index 6cc18e7f..f6dde871 100644 --- a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java +++ b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java @@ -26,10 +26,13 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.gateway.config.GatewayMetricsProperties; @@ -52,6 +55,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen */ @SpringBootTest(classes = { GatewaySampleApplicationTests.TestConfig.class }, webEnvironment = RANDOM_PORT, properties = { "management.endpoint.gateway.enabled=true", "management.server.port=${test.port}" }) +@ExtendWith(OutputCaptureExtension.class) public class GatewaySampleApplicationTests { protected static int managementPort; @@ -259,6 +263,12 @@ public class GatewaySampleApplicationTests { }); } + @Test + void usesNewStarter(CapturedOutput output) { + assertThat(output).doesNotContain("spring-cloud-starter-gateway is deprecated") + .doesNotContain("spring-cloud-gateway-server is deprecated"); + } + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration @LoadBalancerClient(name = "httpbin", configuration = LoadBalancerConfig.class) diff --git a/spring-cloud-gateway-server-mvc/pom.xml b/spring-cloud-gateway-server-mvc/pom.xml index ba17bdf7..6f7ff6b3 100644 --- a/spring-cloud-gateway-server-mvc/pom.xml +++ b/spring-cloud-gateway-server-mvc/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. spring-cloud-gateway-server-mvc @@ -52,6 +52,16 @@ org.springframework.cloud spring-cloud-commons + + org.springframework.cloud + spring-cloud-function-context + true + + + org.springframework.cloud + spring-cloud-stream + true + org.springframework.cloud spring-cloud-loadbalancer @@ -75,7 +85,7 @@ com.bucket4j - bucket4j-core + bucket4j_jdk17-core true @@ -89,10 +99,25 @@ spring-boot-properties-migrator test + + org.springframework.boot + spring-boot-testcontainers + test + + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + test + + + org.awaitility + awaitility + test + com.bucket4j - bucket4j-caffeine + bucket4j_jdk17-caffeine test @@ -105,5 +130,10 @@ junit-jupiter test + + org.testcontainers + rabbitmq + test + \ No newline at end of file diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayMvcClassPathWarningAutoConfiguration.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayMvcClassPathWarningAutoConfiguration.java new file mode 100644 index 00000000..4fef7bc7 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayMvcClassPathWarningAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(GatewayServerMvcAutoConfiguration.class) +@ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true) +public class GatewayMvcClassPathWarningAutoConfiguration { + + private static final Log log = LogFactory.getLog(GatewayMvcClassPathWarningAutoConfiguration.class); + + private static final String BORDER = "\n\n**********************************************************\n\n"; + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") + protected static class WebMvcMissingFromClasspathConfiguration { + + public WebMvcMissingFromClasspathConfiguration() { + log.warn(BORDER + "Spring Web MVC is missing from the classpath, " + + "which is required for Spring Cloud Gateway Server Web MVC. " + + "Please add spring-boot-starter-web dependency." + BORDER); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.cloud.gateway.server.webmvc.Marker") + protected static class NewModuleConfiguration { + + public NewModuleConfiguration() { + log.warn(BORDER + "The artifact spring-cloud-gateway-server-mvc is deprecated. " + + "It will be removed in the next major release. " + + "Please add spring-cloud-gateway-server-webmvc dependency." + BORDER); + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MultipartEnvironmentPostProcessor.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MultipartEnvironmentPostProcessor.java new file mode 100644 index 00000000..ce74a653 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MultipartEnvironmentPostProcessor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.common; + +import java.util.Map; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.StringUtils; + +public class MultipartEnvironmentPostProcessor implements EnvironmentPostProcessor { + + /* for testing */ static final String MULTIPART_ENABLED_PROPERTY = "spring.servlet.multipart.enabled"; + + /* for testing */ static final String MULTIPART_PROPERTY_SOURCE_NAME = "gatewayServerWebmvcMultipartPropertySource"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + String multipartEnabled = environment.getProperty(MULTIPART_ENABLED_PROPERTY); + if (!StringUtils.hasText(multipartEnabled)) { + // no user set property, set it to false. + MapPropertySource propertySource = new MapPropertySource(MULTIPART_PROPERTY_SOURCE_NAME, + Map.of(MULTIPART_ENABLED_PROPERTY, Boolean.FALSE)); + environment.getPropertySources().addFirst(propertySource); + } + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java index 5e635b71..72f30289 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java @@ -116,6 +116,14 @@ public abstract class MvcUtils { } } + public static ByteArrayInputStream getOrCacheBody(ServerRequest request) { + ByteArrayInputStream body = getAttribute(request, MvcUtils.CACHED_REQUEST_BODY_ATTR); + if (body != null) { + return body; + } + return cacheBody(request); + } + public static String expand(ServerRequest request, String template) { Assert.notNull(request, "request may not be null"); Assert.notNull(template, "template may not be null"); diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java index 93a3eb7c..806bdaa4 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java @@ -45,6 +45,7 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriTemplate; +import org.springframework.web.util.UriUtils; import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap; @@ -337,6 +338,27 @@ public abstract class BeforeFilterFunctions { }; } + public static Function rewriteRequestParameter(String name, String replacement) { + return request -> { + MultiValueMap queryParams = new LinkedMultiValueMap<>(request.params()); + if (queryParams.containsKey(name)) { + queryParams.remove(name); + queryParams.add(name, replacement); + } + + MultiValueMap encodedQueryParams = UriUtils.encodeQueryParams(queryParams); + URI rewrittenUri = UriComponentsBuilder.fromUri(request.uri()) + .replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams)) + .build(true) + .toUri(); + + return ServerRequest.from(request).params(params -> { + params.remove(name); + params.add(name, replacement); + }).uri(rewrittenUri).build(); + }; + } + public static Function routeId(String routeId) { return request -> { MvcUtils.setRouteId(request, routeId); diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java index 8a550ce2..272b1e6c 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java @@ -27,6 +27,7 @@ import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import org.springframework.cloud.gateway.server.mvc.common.Configurable; +import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; import org.springframework.cloud.gateway.server.mvc.common.Shortcut; import org.springframework.core.NestedRuntimeException; import org.springframework.http.HttpMethod; @@ -71,6 +72,9 @@ public abstract class RetryFilterFunctions { .setPolicies(Arrays.asList(simpleRetryPolicy, new HttpRetryPolicy(config)).toArray(new RetryPolicy[0])); RetryTemplate retryTemplate = retryTemplateBuilder.customPolicy(compositeRetryPolicy).build(); return (request, next) -> retryTemplate.execute(context -> { + if (config.isCacheBody()) { + MvcUtils.getOrCacheBody(request); + } ServerResponse serverResponse = next.handle(request); if (isRetryableStatusCode(serverResponse.statusCode(), config) @@ -121,6 +125,8 @@ public abstract class RetryFilterFunctions { private Set methods = new HashSet<>(List.of(HttpMethod.GET)); + private boolean cacheBody = false; + // TODO: individual statuses // TODO: backoff // TODO: support more Spring Retry policies @@ -176,6 +182,15 @@ public abstract class RetryFilterFunctions { return this; } + public boolean isCacheBody() { + return cacheBody; + } + + public RetryConfig setCacheBody(boolean cacheBody) { + this.cacheBody = cacheBody; + return this; + } + } private static class RetryException extends NestedRuntimeException { diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerHeaderUtils.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerHeaderUtils.java new file mode 100644 index 00000000..891f05c1 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerHeaderUtils.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.handler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.MessageHeaders; + +/** + * @author Dave Syer + * @author Oleg Zhurakousky + */ +public final class FunctionHandlerHeaderUtils { + + /** + * Message Header name which contains HTTP request parameters. + */ + public static final String HTTP_REQUEST_PARAM = "http_request_param"; + + private static HttpHeaders IGNORED = new HttpHeaders(); + + private static HttpHeaders REQUEST_ONLY = new HttpHeaders(); + + static { + IGNORED.add(MessageHeaders.ID, ""); + IGNORED.add(HttpHeaders.CONTENT_LENGTH, "0"); + // Headers that would typically be added by a downstream client + REQUEST_ONLY.add(HttpHeaders.ACCEPT, ""); + REQUEST_ONLY.add(HttpHeaders.CONTENT_LENGTH, ""); + REQUEST_ONLY.add(HttpHeaders.CONTENT_TYPE, ""); + REQUEST_ONLY.add(HttpHeaders.HOST, ""); + } + + private FunctionHandlerHeaderUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + public static HttpHeaders fromMessage(MessageHeaders headers, List ignoredHeders) { + HttpHeaders result = new HttpHeaders(); + for (String name : headers.keySet()) { + Object value = headers.get(name); + name = name.toLowerCase(Locale.ROOT); + if (!IGNORED.containsKey(name) && !ignoredHeders.contains(name)) { + Collection values = multi(value); + for (Object object : values) { + result.set(name, object.toString()); + } + } + } + return result; + } + + @SuppressWarnings("unchecked") + public static HttpHeaders fromMessage(MessageHeaders headers) { + return fromMessage(headers, Collections.EMPTY_LIST); + } + + public static HttpHeaders sanitize(HttpHeaders request, List ignoredHeders, + List requestOnlyHeaders) { + HttpHeaders result = new HttpHeaders(); + for (String name : request.keySet()) { + List value = request.get(name); + name = name.toLowerCase(Locale.ROOT); + if (!IGNORED.containsKey(name) && !REQUEST_ONLY.containsKey(name) && !ignoredHeders.contains(name) + && !requestOnlyHeaders.contains(name)) { + result.put(name, value); + } + } + return result; + } + + @SuppressWarnings("unchecked") + public static HttpHeaders sanitize(HttpHeaders request) { + return sanitize(request, Collections.EMPTY_LIST, Collections.EMPTY_LIST); + } + + public static MessageHeaders fromHttp(HttpHeaders headers) { + Map map = new LinkedHashMap<>(); + for (String name : headers.keySet()) { + Collection values = multi(headers.get(name)); + name = name.toLowerCase(Locale.ROOT); + Object value = values == null ? null : (values.size() == 1 ? values.iterator().next() : values); + if (name.toLowerCase(Locale.ROOT).equals(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT))) { + name = MessageHeaders.CONTENT_TYPE; + } + map.put(name, value); + } + return new MessageHeaders(map); + } + + private static Collection multi(Object value) { + return value instanceof Collection ? (Collection) value : Arrays.asList(value); + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerRequestProcessingHelper.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerRequestProcessingHelper.java new file mode 100644 index 00000000..1990991b --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerRequestProcessingHelper.java @@ -0,0 +1,130 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.handler; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.CollectionUtils; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.ServerResponse.BodyBuilder; + +import static org.springframework.cloud.gateway.server.mvc.handler.FunctionHandlerHeaderUtils.fromMessage; +import static org.springframework.cloud.gateway.server.mvc.handler.FunctionHandlerHeaderUtils.sanitize; + +/** + * !INTERNAL USE ONLY! + * + * @author Oleg Zhurakousky + * + */ +final class FunctionHandlerRequestProcessingHelper { + + private static Log logger = LogFactory.getLog(FunctionHandlerRequestProcessingHelper.class); + + private FunctionHandlerRequestProcessingHelper() { + + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + static ServerResponse processRequest(ServerRequest request, FunctionInvocationWrapper function, Object argument, + boolean eventStream, List ignoredHeaders, List requestOnlyHeaders) { + if (argument == null) { + argument = ""; + } + + if (function == null) { + return ServerResponse.notFound().build(); + } + + HttpHeaders headers = request.headers().asHttpHeaders(); + + Message inputMessage = null; + + MessageBuilder builder = MessageBuilder.withPayload(argument); + if (!CollectionUtils.isEmpty(request.params())) { + builder = builder.setHeader(FunctionHandlerHeaderUtils.HTTP_REQUEST_PARAM, + request.params().toSingleValueMap()); + } + inputMessage = builder.copyHeaders(headers.toSingleValueMap()).build(); + + if (function.isRoutingFunction()) { + function.setSkipOutputConversion(true); + } + + Object result = function.apply(inputMessage); + if (function.isConsumer()) { + /* + * if (result instanceof Publisher) { Mono.from((Publisher) + * result).subscribe(); } + */ + return HttpMethod.DELETE.equals(request.method()) ? ServerResponse.ok().build() + : ServerResponse.accepted() + .headers(h -> h.addAll(sanitize(headers, ignoredHeaders, requestOnlyHeaders))) + .build(); + // Mono.empty() : + // Mono.just(ResponseEntity.accepted().headers(FunctionHandlerHeaderUtils.sanitize(headers, + // ignoredHeaders, requestOnlyHeaders)).build()); + } + + BodyBuilder responseOkBuilder = ServerResponse.ok() + .headers(h -> h.addAll(sanitize(headers, ignoredHeaders, requestOnlyHeaders))); + + // FIXME: Mono/Flux + /* + * Publisher pResult; if (result instanceof Publisher) { pResult = (Publisher) + * result; if (eventStream) { return Flux.from(pResult); } + * + * if (pResult instanceof Flux) { pResult = ((Flux) pResult).onErrorContinue((e, + * v) -> { logger.error("Failed to process value: " + v, (Throwable) e); + * }).collectList(); } pResult = Mono.from(pResult); } else { pResult = + * Mono.just(result); } + */ + + // return Mono.from(pResult).map(v -> { + if (result instanceof Iterable i) { + List aggregatedResult = (List) StreamSupport.stream(i.spliterator(), false).map(m -> { + return m instanceof Message ? processMessage(responseOkBuilder, (Message) m, ignoredHeaders) : m; + }).collect(Collectors.toList()); + return responseOkBuilder.header("content-type", "application/json").body(aggregatedResult); + } + else if (result instanceof Message message) { + return responseOkBuilder.body(processMessage(responseOkBuilder, message, ignoredHeaders)); + } + else { + return responseOkBuilder.body(result); + } + // }); + } + + private static Object processMessage(BodyBuilder responseOkBuilder, Message message, + List ignoredHeaders) { + responseOkBuilder.headers(h -> h.addAll(fromMessage(message.getHeaders(), ignoredHeaders))); + return message.getPayload(); + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java index 9b3d118b..04cfc1a0 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java @@ -21,22 +21,77 @@ import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; import jakarta.servlet.ServletException; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; import org.springframework.cloud.gateway.server.mvc.config.RouteProperties; +import org.springframework.cloud.stream.function.StreamOperations; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; import org.springframework.web.servlet.function.HandlerFunction; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; +import static org.springframework.cloud.gateway.server.mvc.handler.FunctionHandlerRequestProcessingHelper.processRequest; + public abstract class HandlerFunctions { private HandlerFunctions() { } + // for properties + public static HandlerFunction fn(RouteProperties routeProperties) { + // fn:fnName + return fn(routeProperties.getUri().getSchemeSpecificPart()); + } + + public static HandlerFunction fn(String functionName) { + Assert.hasText(functionName, "'functionName' must not be empty"); + return request -> { + String expandedFunctionName = MvcUtils.expand(request, functionName); + FunctionCatalog functionCatalog = MvcUtils.getApplicationContext(request).getBean(FunctionCatalog.class); + FunctionInvocationWrapper function = functionCatalog.lookup(expandedFunctionName, + request.headers().accept().stream().map(MimeType::toString).toArray(String[]::new)); + if (function != null) { + Object body = function.isSupplier() ? null : request.body(function.getRawInputType()); + return processRequest(request, function, body, false, Collections.emptyList(), Collections.emptyList()); + } + return ServerResponse.notFound().build(); + }; + } + + // for properties + public static HandlerFunction stream(RouteProperties routeProperties) { + // stream:bindingName + return stream(routeProperties.getUri().getSchemeSpecificPart()); + } + + public static HandlerFunction stream(String bindingName) { + Assert.hasText(bindingName, "'bindingName' must not be empty"); + // TODO: validate bindingName + return request -> { + String expandedBindingName = MvcUtils.expand(request, bindingName); + StreamOperations streamOps = MvcUtils.getApplicationContext(request).getBean(StreamOperations.class); + byte[] body = request.body(byte[].class); + MessageHeaders messageHeaders = FunctionHandlerHeaderUtils + .fromHttp(FunctionHandlerHeaderUtils.sanitize(request.headers().asHttpHeaders())); + boolean send = streamOps.send(expandedBindingName, MessageBuilder.createMessage(body, messageHeaders)); + if (send) { + return ServerResponse.accepted().build(); + } + return ServerResponse.badRequest().build(); + }; + } + + // for properties public static HandlerFunction forward(RouteProperties routeProperties) { return forward(routeProperties.getUri().getPath()); } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ProxyExchangeHandlerFunction.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ProxyExchangeHandlerFunction.java index 84ce2018..0ea445fc 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ProxyExchangeHandlerFunction.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ProxyExchangeHandlerFunction.java @@ -123,9 +123,9 @@ public class ProxyExchangeHandlerFunction private HttpHeaders filterHeaders(List filters, HttpHeaders original, REQUEST_OR_RESPONSE requestOrResponse) { HttpHeaders filtered = original; - for (var filter : filters) { + for (Object filter : filters) { @SuppressWarnings("unchecked") - var typed = ((HttpHeadersFilter) filter); + HttpHeadersFilter typed = ((HttpHeadersFilter) filter); filtered = typed.apply(filtered, requestOrResponse); } return filtered; diff --git a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories index 789c0150..ba2cd973 100644 --- a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories @@ -1,3 +1,20 @@ +# +# Copyright 2025 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# + org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\ org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions.FilterSupplier,\ org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.FilterSupplier,\ @@ -14,4 +31,5 @@ org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier=\ org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.PredicateSupplier org.springframework.boot.env.EnvironmentPostProcessor=\ - org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration.GatewayHttpClientEnvironmentPostProcessor \ No newline at end of file + org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration.GatewayHttpClientEnvironmentPostProcessor,\ + org.springframework.cloud.gateway.server.mvc.common.MultipartEnvironmentPostProcessor diff --git a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index bbc80f73..0b209bc3 100644 --- a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration +org.springframework.cloud.gateway.server.mvc.GatewayMvcClassPathWarningAutoConfiguration org.springframework.cloud.gateway.server.mvc.handler.GatewayMultipartAutoConfiguration org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java index 95526486..7eb56405 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java @@ -19,6 +19,8 @@ package org.springframework.cloud.gateway.server.mvc; import java.time.Duration; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -132,6 +134,7 @@ public class GatewayServerMvcAutoConfigurationTests { }); } + @DisabledForJreRange(min = JRE.JAVA_23) @Test void gatewayHttpClientPropertiesWork() { ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class) diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java index d9a7dc7b..1e91f7b6 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java @@ -22,12 +22,11 @@ import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import com.github.benmanes.caffeine.cache.Caffeine; @@ -40,16 +39,17 @@ import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -79,10 +79,8 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StreamUtils; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.function.HandlerFunction; import org.springframework.web.servlet.function.RouterFunction; @@ -117,7 +115,6 @@ import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreaker import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeader; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeadersIfNotPresent; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestParameter; -import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.redirectTo; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.removeRequestHeader; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.rewritePath; @@ -126,7 +123,6 @@ import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunction import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.setRequestHostHeader; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.stripPrefix; import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb; -import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry; import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.forward; import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; @@ -147,6 +143,7 @@ import static org.springframework.web.servlet.function.RequestPredicates.path; @SpringBootTest(properties = { "spring.cloud.gateway.mvc.http-client.type=jdk" }, webEnvironment = WebEnvironment.RANDOM_PORT) @ContextConfiguration(initializers = HttpbinTestcontainers.class) +@ExtendWith(OutputCaptureExtension.class) public class ServerMvcIntegrationTests { static { @@ -393,20 +390,6 @@ public class ServerMvcIntegrationTests { // @formatter:on } - @Test - public void retryWorks() { - restClient.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3"); - // test for: java.lang.IllegalArgumentException: You have already selected another - // retry policy - restClient.get() - .uri("/retry?key=get2") - .exchange() - .expectStatus() - .isOk() - .expectBody(String.class) - .isEqualTo("3"); - } - @Test public void rateLimitWorks() { restClient.get().uri("/anything/ratelimit").exchange().expectStatus().isOk(); @@ -484,7 +467,7 @@ public class ServerMvcIntegrationTests { @Test public void rewritePathPostLocalWorks() { restClient.post() - .uri("/baz/post") + .uri("/baz/localpost") .bodyValue("hello") .header("Host", "www.rewritepathpostlocal.org") .exchange() @@ -636,8 +619,21 @@ public class ServerMvcIntegrationTests { private void assertMultipartData(Map responseBody) { Map files = (Map) responseBody.get("files"); assertThat(files).containsKey("imgpart"); - String file = (String) files.get("imgpart"); - assertThat(file).startsWith("data:").contains(";base64,"); + Object imgpart = files.get("imgpart"); + if (imgpart instanceof List l) { + String file = (String) l.get(0); + assertThat(isPNG(file.getBytes())); + } + else { + String file = (String) imgpart; + assertThat(file).startsWith("data:").contains(";base64,"); + } + } + + private static boolean isPNG(byte[] bytes) { + byte[] pngSignature = { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + byte[] header = Arrays.copyOf(bytes, pngSignature.length); + return Arrays.equals(pngSignature, header); } @Test @@ -981,6 +977,11 @@ public class ServerMvcIntegrationTests { }); } + @Test + void logsArtifactDeprecatedWarning(CapturedOutput output) { + assertThat(output).contains("spring-cloud-gateway-server-mvc is deprecated"); + } + @SpringBootConfiguration @EnableAutoConfiguration @LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class) @@ -991,11 +992,6 @@ public class ServerMvcIntegrationTests { return new TestHandler(); } - @Bean - RetryController retryController() { - return new RetryController(); - } - @Bean EventController eventController() { return new EventController(); @@ -1180,19 +1176,6 @@ public class ServerMvcIntegrationTests { // @formatter:on } - @Bean - public RouterFunction gatewayRouterFunctionsRetry() { - // @formatter:off - return route("testretry") - .route(path("/retry"), http()) - .before(new LocalServerPortUriResolver()) - .filter(retry(3)) - //.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)))) - .filter(prefixPath("/do")) - .build(); - // @formatter:on - } - @Bean public RouterFunction gatewayRouterFunctionsRateLimit() { // @formatter:off @@ -1279,8 +1262,7 @@ public class ServerMvcIntegrationTests { // @formatter:off return route("testform") .POST("/post", host("**.testform.org"), http()) - .before(new LocalServerPortUriResolver()) - .filter(prefixPath("/test")) + .filter(new HttpbinUriResolver()) .filter(addRequestHeader("X-Test", "form")) .build(); // @formatter:on @@ -1677,37 +1659,6 @@ public class ServerMvcIntegrationTests { } - @RestController - protected static class RetryController { - - Log log = LogFactory.getLog(getClass()); - - ConcurrentHashMap map = new ConcurrentHashMap<>(); - - @GetMapping("/do/retry") - public ResponseEntity retry(@RequestParam("key") String key, - @RequestParam(name = "count", defaultValue = "3") int count, - @RequestParam(name = "failStatus", required = false) Integer failStatus) { - AtomicInteger num = getCount(key); - int i = num.incrementAndGet(); - log.warn("Retry count: " + i); - String body = String.valueOf(i); - if (i < count) { - HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; - if (failStatus != null) { - httpStatus = HttpStatus.resolve(failStatus); - } - return ResponseEntity.status(httpStatus).header("X-Retry-Count", body).body("temporarily broken"); - } - return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body); - } - - AtomicInteger getCount(String key) { - return map.computeIfAbsent(key, s -> new AtomicInteger()); - } - - } - protected static class TestHandler implements HandlerFunction { @Override diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/common/MultipartEnvironmentPostProcessorTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/common/MultipartEnvironmentPostProcessorTests.java new file mode 100644 index 00000000..72dd1b3f --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/common/MultipartEnvironmentPostProcessorTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.common; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.gateway.server.mvc.common.MultipartEnvironmentPostProcessor.MULTIPART_ENABLED_PROPERTY; +import static org.springframework.cloud.gateway.server.mvc.common.MultipartEnvironmentPostProcessor.MULTIPART_PROPERTY_SOURCE_NAME; + +public class MultipartEnvironmentPostProcessorTests { + + @Test + void multipartDisabledByDefault() { + MockEnvironment environment = new MockEnvironment(); + MultipartEnvironmentPostProcessor processor = new MultipartEnvironmentPostProcessor(); + processor.postProcessEnvironment(environment, null); + + assertThat(environment.getPropertySources().contains(MULTIPART_PROPERTY_SOURCE_NAME)).isTrue(); + + Boolean multipartEnabled = environment.getProperty(MULTIPART_ENABLED_PROPERTY, Boolean.class); + assertThat(multipartEnabled).isFalse(); + } + + @Test + void multipartEnabledByUser() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty(MULTIPART_ENABLED_PROPERTY, "true"); + MultipartEnvironmentPostProcessor processor = new MultipartEnvironmentPostProcessor(); + processor.postProcessEnvironment(environment, null); + + assertThat(environment.getPropertySources().contains(MULTIPART_PROPERTY_SOURCE_NAME)).isFalse(); + + Boolean multipartEnabled = environment.getProperty(MULTIPART_ENABLED_PROPERTY, Boolean.class); + assertThat(multipartEnabled).isTrue(); + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/FunctionHandlerConfigTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/FunctionHandlerConfigTests.java new file mode 100644 index 00000000..779bc5bd --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/FunctionHandlerConfigTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.config; + +import java.util.Locale; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(properties = {}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("functionhandlerconfigtests") +public class FunctionHandlerConfigTests { + + @Autowired + private TestRestClient restClient; + + @Test + public void testSimpleFunctionWorks() { + restClient.post() + .uri("/simplefunction") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("HELLO"); + } + + @Test + public void testTemplatedFunctionWorks() { + restClient.post() + .uri("/templatedfunction/upper") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("HELLO"); + } + + @Test + public void testSupplierFunctionWorks() { + restClient.get() + .uri("/supplierfunction") + .accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("hello"); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class TestConfiguration { + + @Bean + Function upper() { + return s -> s.toUpperCase(Locale.ROOT); + } + + @Bean + Supplier hello() { + return () -> "hello"; + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/StreamHandlerConfigTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/StreamHandlerConfigTests.java new file mode 100644 index 00000000..5227cc70 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/StreamHandlerConfigTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.config; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest( + properties = { "spring.cloud.function.definition=consumeHello", + "spring.cloud.stream.bindings.consumeHello-in-0.destination=hello-out-0" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("streamhandlerconfigtests") +@Testcontainers +public class StreamHandlerConfigTests { + + @Container + @ServiceConnection + public static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.7.25-management-alpine"); + + @Autowired + private TestRestClient restClient; + + @Autowired + private AtomicBoolean helloConsumed; + + @Test + public void testSimpleStreamWorks() { + helloConsumed.set(false); + restClient.post() + .uri("/simplestream") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isAccepted(); + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> helloConsumed.get()); + assertThat(helloConsumed).isTrue(); + } + + @Test + public void testTemplatedStreamWorks() { + helloConsumed.set(false); + restClient.post() + .uri("/templatedstream/hello") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isAccepted(); + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> helloConsumed.get()); + assertThat(helloConsumed).isTrue(); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class TestConfiguration { + + @Bean + public AtomicBoolean helloConsumed() { + return new AtomicBoolean(false); + } + + @Bean + public Consumer consumeHello(AtomicBoolean helloConsumed) { + return message -> { + helloConsumed.compareAndSet(false, true); + }; + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java new file mode 100644 index 00000000..47101449 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.filter; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.web.servlet.function.ServerRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author raccoonback + */ +class BeforeFilterFunctionsTests { + + @Test + void rewriteRequestParameter() { + MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path") + .param("foo", "bar") + .param("baz", "qux") + .buildRequest(null); + + ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); + + ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo", "replacement").apply(request); + + assertThat(result.param("foo")).isPresent().hasValue("replacement"); + assertThat(result.uri().toString()).hasToString("http://localhost/path?baz=qux&foo=replacement"); + } + + @Test + void rewriteOnlyFirstRequestParameter() { + MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path") + .param("foo", "bar_1") + .param("foo", "bar_2") + .param("foo", "bar_3") + .param("baz", "qux") + .buildRequest(null); + + ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); + + ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo", "replacement").apply(request); + + assertThat(result.param("foo")).isPresent().hasValue("replacement"); + assertThat(result.uri().toString()).hasToString("http://localhost/path?baz=qux&foo=replacement"); + } + + @Test + void rewriteEncodedRequestParameter() { + MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path") + .param("foo[]", "bar") + .param("baz", "qux") + .buildRequest(null); + + ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); + + ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo[]", "replacement[]").apply(request); + + assertThat(result.param("foo[]")).isPresent().hasValue("replacement[]"); + assertThat(result.uri().toString()).hasToString("http://localhost/path?baz=qux&foo%5B%5D=replacement%5B%5D"); + } + + @Test + void rewriteRequestParameterWithEncodedRemainParameters() { + MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path") + .param("foo", "bar") + .param("baz[]", "qux[]") + .buildRequest(null); + + ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); + + ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo", "replacement").apply(request); + + assertThat(result.param("foo")).isPresent().hasValue("replacement"); + assertThat(result.uri().toString()).hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D&foo=replacement"); + } + + @Test + void rewriteRequestParameterWithEncodedPath() { + MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path/é/last") + .param("foo", "bar") + .buildRequest(null); + + ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); + + ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo", "replacement").apply(request); + + assertThat(result.param("foo")).isPresent().hasValue("replacement"); + assertThat(result.uri().toString()).hasToString("http://localhost/path/%C3%A9/last?foo=replacement"); + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java new file mode 100644 index 00000000..edae11e4 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java @@ -0,0 +1,180 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.filter; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers; +import org.springframework.cloud.gateway.server.mvc.test.LocalServerPortUriResolver; +import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig; +import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.core.log.LogMessage; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + +import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.adaptCachedBody; +import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath; +import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry; +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; + +@SuppressWarnings("unchecked") +@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT) +@ContextConfiguration(initializers = HttpbinTestcontainers.class) +public class RetryFilterFunctionTests { + + @LocalServerPort + int port; + + @Autowired + TestRestClient restClient; + + @Test + public void retryWorks() { + restClient.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3"); + // test for: java.lang.IllegalArgumentException: You have already selected another + // retry policy + restClient.get() + .uri("/retry?key=get2") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("3"); + } + + @Test + public void retryBodyWorks() { + restClient.post() + .uri("/retrybody?key=post") + .bodyValue("thebody") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("3"); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class) + protected static class TestConfiguration { + + @Bean + public RouterFunction gatewayRouterFunctionsRetry() { + // @formatter:off + return route("testretry") + .GET("/retry", http()) + .before(new LocalServerPortUriResolver()) + .filter(retry(3)) + .filter(prefixPath("/do")) + .build(); + // @formatter:on + } + + @Bean + public RouterFunction gatewayRouterFunctionsRetryBody() { + // @formatter:off + return route("testretrybody") + .POST("/retrybody", http()) + .before(new LocalServerPortUriResolver()) + .filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)) + .setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)).setCacheBody(true))) + .filter(adaptCachedBody()) + .filter(prefixPath("/do")) + .build(); + // @formatter:on + } + + @RestController + protected static class RetryController { + + Log log = LogFactory.getLog(getClass()); + + ConcurrentHashMap map = new ConcurrentHashMap<>(); + + @GetMapping("/do/retry") + public ResponseEntity retry(@RequestParam("key") String key, + @RequestParam(name = "count", defaultValue = "3") int count, + @RequestParam(name = "failStatus", required = false) Integer failStatus) { + AtomicInteger num = getCount(key); + int i = num.incrementAndGet(); + log.warn("Retry count: " + i); + String body = String.valueOf(i); + if (i < count) { + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + if (failStatus != null) { + httpStatus = HttpStatus.resolve(failStatus); + } + return ResponseEntity.status(httpStatus).header("X-Retry-Count", body).body("temporarily broken"); + } + return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body); + } + + @PostMapping("/do/retrybody") + public ResponseEntity retryBody(@RequestParam("key") String key, + @RequestParam(name = "count", defaultValue = "3") int count, @RequestBody String requestBody) { + AtomicInteger num = getCount(key); + int i = num.incrementAndGet(); + log.warn(LogMessage.format("Retry count: %s, body: %s", i, requestBody)); + String body = String.valueOf(i); + if (!StringUtils.hasText(requestBody)) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .header("X-Retry-Count", body) + .body("missing body"); + } + if (i < count) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .header("X-Retry-Count", body) + .body("temporarily broken"); + } + return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body); + } + + AtomicInteger getCount(String key) { + return map.computeIfAbsent(key, s -> new AtomicInteger()); + } + + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerTests.java new file mode 100644 index 00000000..c2090807 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.handler; + +import java.util.Locale; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.fn; + +@SpringBootTest(properties = {}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class FunctionHandlerTests { + + @Autowired + private TestRestClient restClient; + + @Test + public void testSimpleFunctionWorks() { + restClient.post() + .uri("/simplefunction") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("HELLO"); + } + + @Test + public void testTemplatedFunctionWorks() { + restClient.post() + .uri("/templatedfunction/upper") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("HELLO"); + } + + @Test + public void testSupplierFunctionWorks() { + restClient.get() + .uri("/supplierfunction") + .accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("hello"); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class TestConfiguration { + + @Bean + Function upper() { + return s -> s.toUpperCase(Locale.ROOT); + } + + @Bean + Supplier hello() { + return () -> "hello"; + } + + @Bean + public RouterFunction gatewayRouterFunctionsSimpleFunction() { + // @formatter:off + return route("testsimplefunction") + .POST("/simplefunction", fn("upper")) + .build() + .and(route("testtemplatedfunction") + .POST("/templatedfunction/{fnName}", fn("{fnName}")) + .build()) + .and(route("testsupplierfunction") + .GET("/supplierfunction", fn("hello")) + .build()); + // @formatter:on + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/StreamHandlerTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/StreamHandlerTests.java new file mode 100644 index 00000000..aa89417d --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/StreamHandlerTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.mvc.handler; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.stream; + +@SpringBootTest( + properties = { "spring.cloud.function.definition=consumeHello", + "spring.cloud.stream.bindings.consumeHello-in-0.destination=hello-out-0" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +public class StreamHandlerTests { + + @Container + @ServiceConnection + public static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.7.25-management-alpine"); + + @Autowired + private TestRestClient restClient; + + @Autowired + private AtomicBoolean helloConsumed; + + @Test + public void testSimpleStreamWorks() { + helloConsumed.set(false); + restClient.post() + .uri("/simplestream") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isAccepted(); + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> helloConsumed.get()); + assertThat(helloConsumed).isTrue(); + } + + @Test + public void testTemplatedStreamWorks() { + helloConsumed.set(false); + restClient.post() + .uri("/templatedstream/hello") + .accept(MediaType.TEXT_PLAIN) + .bodyValue("hello") + .exchange() + .expectStatus() + .isAccepted(); + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> helloConsumed.get()); + assertThat(helloConsumed).isTrue(); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class TestConfiguration { + + @Bean + public RouterFunction gatewayRouterFunctionsSimpleStream() { + // @formatter:off + return route("testsimplestream") + .POST("/simplestream", stream("hello-out-0")) + .build() + .and(route("testtemplatedstream") + .POST("/templatedstream/{name}", stream("{name}-out-0")) + .build()); + // @formatter:on + } + + @Bean + public AtomicBoolean helloConsumed() { + return new AtomicBoolean(false); + } + + @Bean + public Consumer consumeHello(AtomicBoolean helloConsumed) { + return message -> helloConsumed.compareAndSet(false, true); + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestController.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestController.java index d65d50f9..5048c042 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestController.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestController.java @@ -16,27 +16,21 @@ package org.springframework.cloud.gateway.server.mvc.test; -import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; -import java.util.List; import java.util.Map; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ServerWebExchange; @RestController @@ -58,35 +52,7 @@ public class TestController { return result; } - @PostMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) - public Map postFormData(HttpServletRequest request, - @RequestParam MultiValueMap parts) throws ServletException, IOException { - HashMap ret = new HashMap<>(); - ret.put("headers", getHeaders(request)); - HashMap files = new HashMap<>(); - ret.put("files", files); - - parts.values().stream().flatMap(List::stream).forEach(part -> { - String contentType = part.getContentType(); - long contentLength = part.getSize(); - // TODO: get part data - files.put(part.getName(), "data:" + contentType + ";base64," + contentLength); - }); - return ret; - } - - @PostMapping(path = "/post", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) - public Map postUrlEncoded(HttpServletRequest request, - @RequestBody(required = false) MultiValueMap form) throws IOException { - HashMap ret = new HashMap<>(); - ret.put("headers", getHeaders(request)); - ret.put("form", form); - return ret; - } - - @PostMapping(path = "/post", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(path = "/localpost", produces = MediaType.APPLICATION_JSON_VALUE) public Map post(HttpServletRequest request, @RequestBody(required = false) String body) { HashMap ret = new HashMap<>(); ret.put("headers", getHeaders(request)); diff --git a/spring-cloud-gateway-server-mvc/src/test/resources/application-functionhandlerconfigtests.yml b/spring-cloud-gateway-server-mvc/src/test/resources/application-functionhandlerconfigtests.yml new file mode 100644 index 00000000..c7430ca4 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/resources/application-functionhandlerconfigtests.yml @@ -0,0 +1,20 @@ +spring.cloud.gateway.mvc: + routesMap: + testsimplefunction: + uri: fn:upper + predicates: + - Path=/simplefunction + - Method=POST + testtemplatedfunction: + uri: fn:{fnName} + predicates: + - Path=/templatedfunction/{fnName} + - Method=POST + testsupplierfunction: + uri: fn:hello + predicates: + - Path=/supplierfunction + - Method=GET +logging: + level: + org.springframework.cloud.gateway.server.mvc: TRACE diff --git a/spring-cloud-gateway-server-mvc/src/test/resources/application-streamhandlerconfigtests.yml b/spring-cloud-gateway-server-mvc/src/test/resources/application-streamhandlerconfigtests.yml new file mode 100644 index 00000000..3e97fc80 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/resources/application-streamhandlerconfigtests.yml @@ -0,0 +1,16 @@ +spring.cloud.gateway.mvc: + routesMap: + testsimplestream: + uri: stream:hello-out-0 + predicates: + - Path=/simplestream + - Method=POST + testtemplatedstream: + uri: stream:{name}-out-0 + predicates: + - Path=/templatedstream/{name} + - Method=POST + +logging: + level: + org.springframework.cloud.gateway.server.mvc: TRACE diff --git a/spring-cloud-gateway-server-webflux/pom.xml b/spring-cloud-gateway-server-webflux/pom.xml new file mode 100644 index 00000000..4f9f40e0 --- /dev/null +++ b/spring-cloud-gateway-server-webflux/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-gateway + 4.3.0-SNAPSHOT + .. + + spring-cloud-gateway-server-webflux + spring-cloud-gateway-server-webflux + jar + Spring Cloud Gateway Server WebFlux + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-gateway-server + + + diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/server/webflux/Marker.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/server/webflux/Marker.java new file mode 100644 index 00000000..3b712903 --- /dev/null +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/server/webflux/Marker.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.webflux; + +@Deprecated +public class Marker { + +} diff --git a/spring-cloud-gateway-server-webmvc/pom.xml b/spring-cloud-gateway-server-webmvc/pom.xml new file mode 100644 index 00000000..9792dfe9 --- /dev/null +++ b/spring-cloud-gateway-server-webmvc/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-gateway + 4.3.0-SNAPSHOT + .. + + spring-cloud-gateway-server-webmvc + spring-cloud-gateway-server-webmvc + jar + Spring Cloud Gateway Server WebMvc + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-gateway-server-mvc + + + diff --git a/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/webmvc/Marker.java b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/webmvc/Marker.java new file mode 100644 index 00000000..b749aa92 --- /dev/null +++ b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/webmvc/Marker.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.webmvc; + +@Deprecated +public class Marker { + +} diff --git a/spring-cloud-gateway-server/pom.xml b/spring-cloud-gateway-server/pom.xml index c365ed4b..b893e88b 100644 --- a/spring-cloud-gateway-server/pom.xml +++ b/spring-cloud-gateway-server/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. spring-cloud-gateway-server @@ -16,7 +16,8 @@ Spring Cloud Gateway Server ${basedir}/.. - 1.68.1 + 1.70.0 + 1.0.0 @@ -58,6 +59,16 @@ spring-cloud-loadbalancer true + + org.springframework.cloud + spring-cloud-function-context + true + + + org.springframework.cloud + spring-cloud-stream + true + org.springframework.boot spring-boot-devtools @@ -135,6 +146,16 @@ caffeine true + + com.bucket4j + bucket4j_jdk17-core + true + + + com.bucket4j + bucket4j_jdk17-caffeine + test + io.micrometer micrometer-observation-test @@ -171,6 +192,16 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-testcontainers + test + + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + test + org.junit-pioneer junit-pioneer @@ -181,6 +212,11 @@ spring-cloud-test-support test + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + test + io.projectreactor reactor-test @@ -206,12 +242,17 @@ junit-jupiter test + + org.testcontainers + rabbitmq + test + org.junit.platform junit-platform-launcher - test - - + test + + org.openjdk.jmh jmh-core 1.20 diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java index 3a32ecb1..e1e27b66 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java @@ -67,10 +67,8 @@ public abstract class AbstractSslConfigurer { } protected SslBundle getBundle() { - if (ssl.getSslBundle() == null || ssl.getSslBundle().length() > 0) { - return null; - } - if (bundles.getBundleNames().contains(ssl.getSslBundle())) { + if (ssl.getSslBundle() != null && ssl.getSslBundle().length() > 0 + && bundles.getBundleNames().contains(ssl.getSslBundle())) { return bundles.getBundle(ssl.getSslBundle()); } return null; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 2b89cc0e..85767b84 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -25,6 +25,7 @@ import java.util.function.Supplier; import javax.net.ssl.TrustManagerFactory; +import io.github.bucket4j.distributed.proxy.AsyncProxyManager; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; @@ -123,6 +124,7 @@ import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter; import org.springframework.cloud.gateway.filter.headers.TransferEncodingNormalizationHeadersFilter; import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter; +import org.springframework.cloud.gateway.filter.ratelimit.Bucket4jRateLimiter; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver; import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter; @@ -736,6 +738,20 @@ public class GatewayAutoConfiguration { return new ConfigurableHintsRegistrationProcessor(); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(AsyncProxyManager.class) + protected static class Bucket4jConfiguration { + + @Bean + @ConditionalOnBean(AsyncProxyManager.class) + @ConditionalOnEnabledFilter(RequestRateLimiterGatewayFilterFactory.class) + public Bucket4jRateLimiter bucket4jRateLimiter(AsyncProxyManager proxyManager, + ConfigurationService configurationService) { + return new Bucket4jRateLimiter(proxyManager, configurationService); + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HttpClient.class) protected static class NettyConfiguration { diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayClassPathWarningAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayClassPathWarningAutoConfiguration.java index 2fd3e441..46507016 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayClassPathWarningAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayClassPathWarningAutoConfiguration.java @@ -59,4 +59,16 @@ public class GatewayClassPathWarningAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.cloud.gateway.server.webflux.Marker") + protected static class NewModuleConfiguration { + + public NewModuleConfiguration() { + log.warn(BORDER + "The artifact spring-cloud-gateway-server is deprecated. " + + "It will be removed in the next major release. " + + "Please add spring-cloud-gateway-server-webflux dependency." + BORDER); + } + + } + } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayFunctionAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayFunctionAutoConfiguration.java new file mode 100644 index 00000000..0afedfbe --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayFunctionAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.config; + +import java.util.Set; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; +import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledGlobalFilter; +import org.springframework.cloud.gateway.filter.FunctionRoutingFilter; +import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.web.reactive.DispatcherHandler; + +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(ContextFunctionCatalogAutoConfiguration.class) +@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, GatewayAutoConfiguration.class }) +@ConditionalOnClass({ FunctionCatalog.class, DispatcherHandler.class }) +@ConditionalOnProperty(name = "spring.cloud.gateway.function.enabled", matchIfMissing = true) +class GatewayFunctionAutoConfiguration { + + @Bean + @ConditionalOnEnabledGlobalFilter + @ConditionalOnBean(FunctionCatalog.class) + public FunctionRoutingFilter functionRoutingFilter(FunctionCatalog functionCatalog, + ServerCodecConfigurer codecConfigurer, Set messageBodyEncoders) { + return new FunctionRoutingFilter(functionCatalog, codecConfigurer.getReaders(), messageBodyEncoders); + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayStreamAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayStreamAutoConfiguration.java new file mode 100644 index 00000000..ba5dcbb4 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayStreamAutoConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.config; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledGlobalFilter; +import org.springframework.cloud.gateway.filter.StreamRoutingFilter; +import org.springframework.cloud.stream.config.BindingServiceConfiguration; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.web.reactive.DispatcherHandler; + +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(BindingServiceConfiguration.class) +@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, GatewayAutoConfiguration.class }) +@ConditionalOnClass({ StreamBridge.class, DispatcherHandler.class }) +@ConditionalOnProperty(name = "spring.cloud.gateway.stream.enabled", matchIfMissing = true) +class GatewayStreamAutoConfiguration { + + @Bean + @ConditionalOnEnabledGlobalFilter + @ConditionalOnBean(StreamBridge.class) + public StreamRoutingFilter streamRoutingFilter(StreamBridge streamBridge, ServerCodecConfigurer codecConfigurer) { + return new StreamRoutingFilter(streamBridge, codecConfigurer.getReaders()); + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java new file mode 100644 index 00000000..e35227dd --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java @@ -0,0 +1,198 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage; +import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncoder; +import org.springframework.cloud.gateway.support.BodyInserterContext; +import org.springframework.cloud.gateway.support.MessageHeaderUtils; +import org.springframework.cloud.gateway.support.NotFoundException; +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MimeType; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ServerWebExchange; + +import static java.util.function.Function.identity; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setAlreadyRouted; + +/** + * @author Spencer Gibb + */ +public class FunctionRoutingFilter implements GlobalFilter, Ordered { + + private static Log logger = LogFactory.getLog(FunctionRoutingFilter.class); + + private final FunctionCatalog functionCatalog; + + private final List> messageReaders; + + private final Map messageBodyEncoders; + + public FunctionRoutingFilter(FunctionCatalog functionCatalog, List> messageReaders, + Set messageBodyEncoders) { + this.functionCatalog = functionCatalog; + this.messageReaders = messageReaders; + this.messageBodyEncoders = messageBodyEncoders.stream() + .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); + } + + @Override + public int getOrder() { + return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 10; + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + String scheme = requestUrl.getScheme(); + + if (isAlreadyRouted(exchange) || !"fn".equals(scheme)) { + return chain.filter(exchange); + } + setAlreadyRouted(exchange); + + FunctionInvocationWrapper function = functionCatalog.lookup(requestUrl.getHost(), + exchange.getRequest().getHeaders().getAccept().stream().map(MimeType::toString).toArray(String[]::new)); + if (function != null) { + return processRequest(exchange, function, messageReaders, messageBodyEncoders).then(chain.filter(exchange)); + } + + return Mono.error(new NotFoundException("No route for uri " + requestUrl)); + } + + protected Mono processRequest(ServerWebExchange exchange, FunctionInvocationWrapper function, + List> messageReaders, Map messageBodyEncoders) { + // 1- convert request body to function input type + // 2- call function + // 3- convert function return to raw data for response + ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); + return serverRequest.bodyToMono(function.getRawInputType()).flatMap(requestBody -> { + ServerHttpRequest request = exchange.getRequest(); + HttpHeaders headers = request.getHeaders(); + + Message inputMessage = null; + + MessageBuilder builder = MessageBuilder.withPayload(requestBody); + if (!CollectionUtils.isEmpty(request.getQueryParams())) { + builder = builder.setHeader(MessageHeaderUtils.HTTP_REQUEST_PARAM, + request.getQueryParams().toSingleValueMap()); + } + inputMessage = builder.copyHeaders(headers.toSingleValueMap()).build(); + + if (function.isRoutingFunction()) { + function.setSkipOutputConversion(true); + } + + List ignoredHeaders = Collections.emptyList(); + HttpHeaders newResponseHeaders = new HttpHeaders(); + + Object functionResult = function.apply(inputMessage); + if (functionResult instanceof Message message) { + newResponseHeaders.addAll(MessageHeaderUtils.fromMessage(message.getHeaders(), ignoredHeaders)); + functionResult = message.getPayload(); + } + Publisher result; + if (functionResult instanceof Publisher publisher) { + // TODO: deal with eventStream + result = publisher; + } + else { + result = Mono.just(functionResult); + } + + Class outClass = byte[].class; + + BodyInserter bodyInserter = BodyInserters.fromPublisher(result, outClass); + CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, + exchange.getResponse().getHeaders()); + + return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { + ServerHttpResponse response = exchange.getResponse(); + Mono messageBody = writeBody(response, outputMessage, outClass); + HttpHeaders responseHeaders = response.getHeaders(); + if (!responseHeaders.containsKey(HttpHeaders.TRANSFER_ENCODING) + || responseHeaders.containsKey(HttpHeaders.CONTENT_LENGTH)) { + messageBody = messageBody.doOnNext(data -> headers.setContentLength(data.readableByteCount())); + } + responseHeaders.addAll(newResponseHeaders); + + // TODO: deal with content type + /* + * if (StringUtils.hasText(config.newContentType)) { + * headers.set(HttpHeaders.CONTENT_TYPE, config.newContentType); } + */ + + // TODO: fail if isStreamingMediaType? + return response.writeWith(messageBody); + })); + }); + } + + private Mono writeBody(ServerHttpResponse httpResponse, CachedBodyOutputMessage message, + Class outClass) { + Mono response = DataBufferUtils.join(message.getBody()); + if (byte[].class.isAssignableFrom(outClass)) { + return response; + } + + List encodingHeaders = httpResponse.getHeaders().getOrEmpty(HttpHeaders.CONTENT_ENCODING); + for (String encoding : encodingHeaders) { + MessageBodyEncoder encoder = messageBodyEncoders.get(encoding); + if (encoder != null) { + DataBufferFactory dataBufferFactory = httpResponse.bufferFactory(); + response = response.publishOn(Schedulers.parallel()).map(buffer -> { + byte[] encodedResponse = encoder.encode(buffer); + DataBufferUtils.release(buffer); + return encodedResponse; + }).map(dataBufferFactory::wrap); + break; + } + } + + return response; + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/StreamRoutingFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/StreamRoutingFilter.java new file mode 100644 index 00000000..8070ba05 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/StreamRoutingFilter.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter; + +import java.net.URI; +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; +import org.springframework.cloud.gateway.support.MessageHeaderUtils; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setAlreadyRouted; + +/** + * @author Spencer Gibb + */ +public class StreamRoutingFilter implements GlobalFilter, Ordered { + + private final StreamBridge streamBridge; + + private final List> messageReaders; + + private final SetStatusGatewayFilterFactory setStatusFilter; + + public StreamRoutingFilter(StreamBridge streamBridge, List> messageReaders) { + this.streamBridge = streamBridge; + this.messageReaders = messageReaders; + // TODO: is this the right place for this? + this.streamBridge.setAsync(true); + setStatusFilter = new SetStatusGatewayFilterFactory(); + } + + @Override + public int getOrder() { + return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 10; + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + String scheme = requestUrl.getScheme(); + + if (isAlreadyRouted(exchange) || !"stream".equals(scheme)) { + return chain.filter(exchange); + } + setAlreadyRouted(exchange); + + ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); + return serverRequest.bodyToMono(byte[].class).flatMap(requestBody -> { + ServerHttpRequest request = exchange.getRequest(); + HttpHeaders headers = request.getHeaders(); + + Message inputMessage = null; + + MessageBuilder builder = MessageBuilder.withPayload(requestBody); + if (!CollectionUtils.isEmpty(request.getQueryParams())) { + // TODO: move HeaderUtils + builder = builder.setHeader(MessageHeaderUtils.HTTP_REQUEST_PARAM, + request.getQueryParams().toSingleValueMap()); + // TODO: sanitize? + } + inputMessage = builder.copyHeaders(headers.toSingleValueMap()).build(); + + // TODO: output content type + boolean send = streamBridge.send(requestUrl.getHost(), inputMessage); + + HttpStatus responseStatus = (send) ? HttpStatus.OK : HttpStatus.BAD_REQUEST; + + SetStatusGatewayFilterFactory.Config config = new SetStatusGatewayFilterFactory.Config(); + config.setStatus(responseStatus.name()); + return setStatusFilter.apply(config).filter(exchange, chain); + }); + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientHttpRoutingFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientHttpRoutingFilter.java index 02dcc349..10892d7d 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientHttpRoutingFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientHttpRoutingFilter.java @@ -43,7 +43,9 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.s /** * @author Spencer Gibb + * @deprecated for removal in 5.0 */ +@Deprecated public class WebClientHttpRoutingFilter implements GlobalFilter, Ordered { private final WebClient webClient; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientWriteResponseFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientWriteResponseFilter.java index 8dab68e4..35009fd7 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientWriteResponseFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/WebClientWriteResponseFilter.java @@ -30,7 +30,9 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.C /** * @author Spencer Gibb + * @deprecated for removal in 5.0 */ +@Deprecated public class WebClientWriteResponseFilter implements GlobalFilter, Ordered { /** diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/ratelimit/Bucket4jRateLimiter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/ratelimit/Bucket4jRateLimiter.java new file mode 100644 index 00000000..fcdc4076 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/ratelimit/Bucket4jRateLimiter.java @@ -0,0 +1,266 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.ratelimit; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.BandwidthBuilder.BandwidthBuilderBuildStage; +import io.github.bucket4j.BandwidthBuilder.BandwidthBuilderRefillStage; +import io.github.bucket4j.BucketConfiguration; +import io.github.bucket4j.ConsumptionProbe; +import io.github.bucket4j.distributed.AsyncBucketProxy; +import io.github.bucket4j.distributed.proxy.AsyncProxyManager; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator; +import org.springframework.cloud.gateway.support.ConfigurationService; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; + +public class Bucket4jRateLimiter extends AbstractRateLimiter { + + /** + * Default Header Name. + */ + public static final String DEFAULT_HEADER_NAME = "X-RateLimit-Remaining"; + + /** + * Redis Rate Limiter property name. + */ + public static final String CONFIGURATION_PROPERTY_NAME = "bucket4j-rate-limiter"; + + private final Log log = LogFactory.getLog(getClass()); + + private final AsyncProxyManager proxyManager; + + private Config defaultConfig = new Config(); + + public Bucket4jRateLimiter(AsyncProxyManager proxyManager, ConfigurationService configurationService) { + super(Config.class, CONFIGURATION_PROPERTY_NAME, configurationService); + this.proxyManager = proxyManager; + } + + @Override + public Mono isAllowed(String routeId, String id) { + Config routeConfig = loadRouteConfiguration(routeId); + + AsyncBucketProxy bucket = proxyManager.builder().build(id, routeConfig.getConfigurationSupplier()); + CompletableFuture bucketFuture = bucket + .tryConsumeAndReturnRemaining(routeConfig.getRequestedTokens()); + return Mono.fromFuture(bucketFuture).onErrorResume(throwable -> { + if (log.isDebugEnabled()) { + log.debug("Error calling Bucket4J rate limiter", throwable); + } + return Mono.just(ConsumptionProbe.rejected(-1, -1, -1)); + }).map(consumptionProbe -> { + boolean allowed = consumptionProbe.isConsumed(); + long remainingTokens = consumptionProbe.getRemainingTokens(); + Response response = new Response(allowed, getHeaders(routeConfig, remainingTokens)); + + if (log.isDebugEnabled()) { + log.debug("response: " + response); + } + return response; + }); + } + + protected Config loadRouteConfiguration(String routeId) { + Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig); + + if (routeConfig == null) { + routeConfig = getConfig().get(RouteDefinitionRouteLocator.DEFAULT_FILTERS); + } + + if (routeConfig == null) { + throw new IllegalArgumentException("No Configuration found for route " + routeId + " or defaultFilters"); + } + return routeConfig; + } + + public Map getHeaders(Config config, Long tokensLeft) { + Map headers = new HashMap<>(); + // TODO: configurable isIncludeHeaders? + // if (isIncludeHeaders()) { + headers.put(config.getHeaderName(), tokensLeft.toString()); + // } + return headers; + } + + public static class Config { + + private static final Function DEFAULT_CONFIGURATION_BUILDER = config -> { + BandwidthBuilderRefillStage bandwidth = Bandwidth.builder().capacity(config.getCapacity()); + + long refillTokens = config.getRefillTokens() == null ? config.getCapacity() : config.getRefillTokens(); + + BandwidthBuilderBuildStage refill = switch (config.getRefillStyle()) { + case GREEDY -> bandwidth.refillGreedy(refillTokens, config.getRefillPeriod()); + case INTERVALLY -> bandwidth.refillIntervally(refillTokens, config.getRefillPeriod()); + case INTERVALLY_ALIGNED -> bandwidth.refillIntervallyAligned(refillTokens, config.getRefillPeriod(), + config.getTimeOfFirstRefill()); + }; + + return BucketConfiguration.builder().addLimit(refill.build()).build(); + }; + + long capacity; + + Function configurationBuilder = DEFAULT_CONFIGURATION_BUILDER; + + Supplier> configurationSupplier; + + String headerName = DEFAULT_HEADER_NAME; + + Duration refillPeriod; + + RefillStyle refillStyle = RefillStyle.GREEDY; + + Long refillTokens; + + long requestedTokens = 1; + + // for RefillStyle.INTERVALLY_ALIGNED + Instant timeOfFirstRefill; + + public long getCapacity() { + return capacity; + } + + public Config setCapacity(long capacity) { + this.capacity = capacity; + return this; + } + + public Function getConfigurationBuilder() { + return configurationBuilder; + } + + public void setConfigurationBuilder(Function configurationBuilder) { + Assert.notNull(configurationBuilder, "configurationBuilder may not be null"); + this.configurationBuilder = configurationBuilder; + } + + public Supplier> getConfigurationSupplier() { + if (configurationSupplier != null) { + return configurationSupplier; + } + return () -> CompletableFuture.completedFuture(getConfigurationBuilder().apply(this)); + } + + public void setConfigurationSupplier(Function configurationBuilder) { + Assert.notNull(configurationBuilder, "configurationBuilder may not be null"); + this.configurationBuilder = configurationBuilder; + } + + public String getHeaderName() { + return headerName; + } + + public Config setHeaderName(String headerName) { + Assert.notNull(headerName, "headerName may not be null"); + this.headerName = headerName; + return this; + } + + public Duration getRefillPeriod() { + return refillPeriod; + } + + public Config setRefillPeriod(Duration refillPeriod) { + this.refillPeriod = refillPeriod; + return this; + } + + public RefillStyle getRefillStyle() { + return refillStyle; + } + + public Config setRefillStyle(RefillStyle refillStyle) { + this.refillStyle = refillStyle; + return this; + } + + public Long getRefillTokens() { + return refillTokens; + } + + public Config setRefillTokens(Long refillTokens) { + this.refillTokens = refillTokens; + return this; + } + + public long getRequestedTokens() { + return requestedTokens; + } + + public Config setRequestedTokens(long requestedTokens) { + this.requestedTokens = requestedTokens; + return this; + } + + public Instant getTimeOfFirstRefill() { + return timeOfFirstRefill; + } + + public Config setTimeOfFirstRefill(Instant timeOfFirstRefill) { + this.timeOfFirstRefill = timeOfFirstRefill; + return this; + } + + public String toString() { + return new ToStringCreator(this).append("capacity", capacity) + .append("headerName", headerName) + .append("refillPeriod", refillPeriod) + .append("refillStyle", refillStyle) + .append("refillTokens", refillTokens) + .append("requestedTokens", requestedTokens) + .append("timeOfFirstRefill", timeOfFirstRefill) + .toString(); + } + + } + + public enum RefillStyle { + + /** + * Greedy tries to add the tokens to the bucket as soon as possible. + */ + GREEDY, + + /** + * Intervally, in opposite to greedy, waits until the whole refillPeriod has + * elapsed before refilling tokens. + */ + INTERVALLY, + + /** + * IntervallyAligned, like Intervally, but with a specified first refill time. + */ + INTERVALLY_ALIGNED; + + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/MessageHeaderUtils.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/MessageHeaderUtils.java new file mode 100644 index 00000000..7916d77d --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/MessageHeaderUtils.java @@ -0,0 +1,116 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.support; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.MessageHeaders; + +/** + * @author Dave Syer + * @author Oleg Zhurakousky + */ +public final class MessageHeaderUtils { + + /** + * Message Header name which contains HTTP request parameters. + */ + public static final String HTTP_REQUEST_PARAM = "http_request_param"; + + private static final HttpHeaders IGNORED = new HttpHeaders(); + + private static final HttpHeaders REQUEST_ONLY = new HttpHeaders(); + + static { + IGNORED.add(MessageHeaders.ID, ""); + IGNORED.add(HttpHeaders.CONTENT_LENGTH, "0"); + // Headers that would typically be added by a downstream client + REQUEST_ONLY.add(HttpHeaders.ACCEPT, ""); + REQUEST_ONLY.add(HttpHeaders.CONTENT_LENGTH, ""); + REQUEST_ONLY.add(HttpHeaders.CONTENT_TYPE, ""); + REQUEST_ONLY.add(HttpHeaders.HOST, ""); + } + + private MessageHeaderUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + public static HttpHeaders fromMessage(MessageHeaders headers, List ignoredHeders) { + HttpHeaders result = new HttpHeaders(); + for (String name : headers.keySet()) { + Object value = headers.get(name); + name = name.toLowerCase(Locale.ROOT); + if (!IGNORED.containsKey(name) && !ignoredHeders.contains(name)) { + Collection values = multi(value); + for (Object object : values) { + result.set(name, object.toString()); + } + } + } + return result; + } + + @SuppressWarnings("unchecked") + public static HttpHeaders fromMessage(MessageHeaders headers) { + return fromMessage(headers, Collections.EMPTY_LIST); + } + + public static HttpHeaders sanitize(HttpHeaders request, List ignoredHeders, + List requestOnlyHeaders) { + HttpHeaders result = new HttpHeaders(); + for (String name : request.keySet()) { + List value = request.get(name); + name = name.toLowerCase(Locale.ROOT); + if (!IGNORED.containsKey(name) && !REQUEST_ONLY.containsKey(name) && !ignoredHeders.contains(name) + && !requestOnlyHeaders.contains(name)) { + result.put(name, value); + } + } + return result; + } + + @SuppressWarnings("unchecked") + public static HttpHeaders sanitize(HttpHeaders request) { + return sanitize(request, Collections.EMPTY_LIST, Collections.EMPTY_LIST); + } + + public static MessageHeaders fromHttp(HttpHeaders headers) { + Map map = new LinkedHashMap<>(); + for (String name : headers.keySet()) { + Collection values = multi(headers.get(name)); + name = name.toLowerCase(Locale.ROOT); + Object value = values == null ? null : (values.size() == 1 ? values.iterator().next() : values); + if (name.toLowerCase(Locale.ROOT).equals(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT))) { + name = MessageHeaders.CONTENT_TYPE; + } + map.put(name, value); + } + return new MessageHeaders(map); + } + + private static Collection multi(Object value) { + return value instanceof Collection ? (Collection) value : Arrays.asList(value); + } + +} diff --git a/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 51f1dfb4..ed8ffb9d 100644 --- a/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -2,8 +2,10 @@ org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguratio org.springframework.cloud.gateway.config.GatewayAutoConfiguration org.springframework.cloud.gateway.config.GatewayResilience4JCircuitBreakerAutoConfiguration org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration +org.springframework.cloud.gateway.config.GatewayFunctionAutoConfiguration org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration +org.springframework.cloud.gateway.config.GatewayStreamAutoConfiguration org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java index 73e389e5..95217bab 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java @@ -49,7 +49,7 @@ public class DisableBuiltInGlobalFiltersTests { @Test public void shouldInjectBuiltInFilters() { - assertThat(globalFilters).hasSizeGreaterThanOrEqualTo(10); + assertThat(globalFilters).hasSizeGreaterThanOrEqualTo(12); } } @@ -86,6 +86,8 @@ public class DisableBuiltInGlobalFiltersTests { "spring.cloud.gateway.global-filter.reactive-load-balancer-client.enabled=false", "spring.cloud.gateway.global-filter.load-balancer-client.enabled=false", "spring.cloud.gateway.global-filter.load-balancer-service-instance-cookie.enabled=false", + "spring.cloud.gateway.global-filter.function-routing.enabled=false", + "spring.cloud.gateway.global-filter.stream-routing.enabled=false", "spring.cloud.gateway.metrics.enabled=false" }) @ActiveProfiles("disable-components") public class DisableAllGlobalFiltersByProperty { diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java new file mode 100644 index 00000000..3e889c44 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter; + +import java.net.URI; +import java.util.Locale; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Spencer Gibb + */ +@SpringBootTest(webEnvironment = RANDOM_PORT, + properties = { "debug=false", "spring.cloud.function.definition=upper", "spring.codec.max-in-memory-size=40" }) +public class FunctionRoutingFilterTests extends BaseWebClientTests { + + @Test + public void functionRoutingFilterWorks() { + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/").build(true).toUri(); + + testClient.post() + .uri(uri) + .bodyValue("hello") + .header("Host", "www.functionroutingfilterjava.org") + .accept(MediaType.TEXT_PLAIN) + .exchange() + .expectBody(String.class) + .consumeWith(res -> assertThat(res.getResponseBody()).isEqualTo("HELLO")); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Bean + Function upper() { + return s -> s.toUpperCase(Locale.ROOT); + } + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("function_routing_filter_java_test", + r -> r.path("/").and().host("www.functionroutingfilterjava.org").uri("fn://upper")) + .build(); + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/StreamRoutingFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/StreamRoutingFilterTests.java new file mode 100644 index 00000000..2c1abdf3 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/StreamRoutingFilterTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter; + +import java.net.URI; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Spencer Gibb + */ +@SpringBootTest(webEnvironment = RANDOM_PORT, + properties = { "debug=false", "spring.cloud.function.definition=consumeHello", + "spring.cloud.stream.bindings.consumeHello-in-0.destination=hello-out-0" }) +@Testcontainers +public class StreamRoutingFilterTests extends BaseWebClientTests { + + @Container + @ServiceConnection + public static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.7.25-management-alpine"); + + @Autowired + private AtomicBoolean helloConsumed; + + @Test + public void streamRoutingFilterWorks() { + helloConsumed.set(false); + + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/").build(true).toUri(); + + testClient.post() + .uri(uri) + .bodyValue("hello") + .header("Host", "www.streamroutingfilterjava.org") + .accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus() + .isOk(); + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> helloConsumed.get()); + assertThat(helloConsumed).isTrue(); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("stream_routing_filter_java_test", + r -> r.path("/").and().host("www.streamroutingfilterjava.org").uri("stream://hello-out-0")) + .build(); + } + + @Bean + public AtomicBoolean helloConsumed() { + return new AtomicBoolean(false); + } + + @Bean + public Consumer consumeHello(AtomicBoolean helloConsumed) { + return message -> helloConsumed.compareAndSet(false, true); + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/Bucket4jRateLimiterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/Bucket4jRateLimiterTests.java new file mode 100644 index 00000000..f262300b --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/Bucket4jRateLimiterTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.ratelimit; + +import java.time.Duration; +import java.util.UUID; + +import com.github.benmanes.caffeine.cache.Caffeine; +import io.github.bucket4j.caffeine.CaffeineProxyManager; +import io.github.bucket4j.distributed.proxy.AsyncProxyManager; +import io.github.bucket4j.distributed.remote.RemoteBucketState; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.RetryingTest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.filter.ratelimit.Bucket4jRateLimiter.RefillStyle; +import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter.Response; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Spencer Gibb + */ +@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "spring.cloud.gateway.redis.enabled=false") +@DirtiesContext +public class Bucket4jRateLimiterTests extends BaseWebClientTests { + + @Autowired + private Bucket4jRateLimiter rateLimiter; + + @RetryingTest(3) + public void bucket4jRateLimiterGreedyWorks() throws Exception { + bucket4jRateLimiterWorks(RefillStyle.GREEDY); + } + + @RetryingTest(3) + public void bucket4jRateLimiterIntervallyWorks() throws Exception { + bucket4jRateLimiterWorks(RefillStyle.INTERVALLY); + } + + public void bucket4jRateLimiterWorks(RefillStyle refillStyle) throws Exception { + String id = UUID.randomUUID().toString(); + + long capacity = 10; + int requestedTokens = 1; + + String routeId = "myroute"; + rateLimiter.getConfig() + .put(routeId, + new Bucket4jRateLimiter.Config().setRefillStyle(refillStyle) + .setHeaderName("X-RateLimit-Custom") + .setCapacity(capacity) + .setRefillPeriod(Duration.ofSeconds(1))); + + checkLimitEnforced(id, capacity, requestedTokens, routeId); + } + + @Test + public void bucket4jRateLimiterIsAllowedFalseWorks() throws Exception { + String id = UUID.randomUUID().toString(); + + int capacity = 1; + int requestedTokens = 2; + + String routeId = "zero_capacity_route"; + rateLimiter.getConfig() + .put(routeId, + new Bucket4jRateLimiter.Config().setCapacity(capacity) + .setRefillPeriod(Duration.ofSeconds(1)) + .setRequestedTokens(requestedTokens)); + + Response response = rateLimiter.isAllowed(routeId, id).block(); + assertThat(response.isAllowed()).isFalse(); + } + + private void checkLimitEnforced(String id, long capacity, int requestedTokens, String routeId) + throws InterruptedException { + // Bursts work + simulateBurst(id, capacity, requestedTokens, routeId); + + checkLimitReached(id, routeId, capacity); + + Thread.sleep(Math.max(1, requestedTokens / capacity) * 1000); + + // # After the burst is done, check the steady state + checkSteadyState(id, capacity, routeId); + } + + private void simulateBurst(String id, long capacity, int requestedTokens, String routeId) { + long previousRemaining = capacity; + for (int i = 0; i < capacity / requestedTokens; i++) { + Response response = rateLimiter.isAllowed(routeId, id).block(); + assertThat(response.isAllowed()).as("Burst # %s is allowed", i).isTrue(); + assertThat(response.getHeaders()).containsKey("X-RateLimit-Custom"); + System.err.println("response headers: " + response.getHeaders()); + long remaining = Long.parseLong(response.getHeaders().get("X-RateLimit-Custom")); + assertThat(remaining).isLessThan(previousRemaining); + previousRemaining = remaining; + // TODO: assert additional headers + } + } + + private void checkLimitReached(String id, String routeId, long capacity) { + Response response = rateLimiter.isAllowed(routeId, id).block(); + if (response.isAllowed()) { // TODO: sometimes there is an off by one error + response = rateLimiter.isAllowed(routeId, id).block(); + } + assertThat(response.isAllowed()).as("capacity # %s is not allowed", capacity).isFalse(); + } + + private void checkSteadyState(String id, long capacity, String routeId) { + Response response; + for (int i = 0; i < capacity; i++) { + response = rateLimiter.isAllowed(routeId, id).block(); + assertThat(response.isAllowed()).as("steady state # %s is allowed", i).isTrue(); + } + + response = rateLimiter.isAllowed(routeId, id).block(); + assertThat(response.isAllowed()).as("steady state # %s is allowed", capacity).isFalse(); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Bean + public AsyncProxyManager caffeineProxyManager() { + Caffeine builder = (Caffeine) Caffeine.newBuilder().maximumSize(100); + return new CaffeineProxyManager<>(builder, Duration.ofMinutes(1)).asAsync(); + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java index 29a914dd..9181ce03 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import org.junitpioneer.jupiter.RetryingTest; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; @@ -100,6 +101,7 @@ public class RedisRateLimiterTests extends BaseWebClientTests { } @Test + @DisabledIfEnvironmentVariable(named = "GITHUB_ACTIONS", matches = "true") public void redisRateLimiterWorksForMultipleRoutes() throws Exception { String id = UUID.randomUUID().toString(); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/CustomBlockHoundIntegrationTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/CustomBlockHoundIntegrationTest.java index 69558c90..228679ac 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/CustomBlockHoundIntegrationTest.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/CustomBlockHoundIntegrationTest.java @@ -29,7 +29,7 @@ import reactor.core.scheduler.Schedulers; public class CustomBlockHoundIntegrationTest { @Test - @DisabledForJreRange(min = JRE.JAVA_16) + @DisabledForJreRange(min = JRE.JAVA_18) public void shouldThrowErrorForBlockingCallWithCustomBlockHoundIntegration() { Assertions.assertThrows(RuntimeException.class, () -> Mono.fromCallable(() -> { Thread.sleep(1); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index e79e8e0a..f998d8e0 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -26,12 +26,15 @@ import org.apache.commons.logging.LogFactory; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.gateway.config.GatewayProperties; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter; @@ -62,6 +65,7 @@ import static org.springframework.cloud.gateway.test.TestUtils.getMap; @SpringBootTest(webEnvironment = RANDOM_PORT) @DirtiesContext @SuppressWarnings("unchecked") +@ExtendWith(OutputCaptureExtension.class) class GatewayIntegrationTests extends BaseWebClientTests { @Autowired @@ -209,6 +213,11 @@ class GatewayIntegrationTests extends BaseWebClientTests { testClient.get().uri("/nocontenttype").exchange().expectStatus().is2xxSuccessful(); } + @Test + void logsArtifactDeprecatedWarning(CapturedOutput output) { + assertThat(output).contains("spring-cloud-gateway-server is deprecated"); + } + @EnableAutoConfiguration @SpringBootConfiguration @Import(DefaultTestConfig.class) diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/ssl/ClientCertAuthSSLBundleTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/ssl/ClientCertAuthSSLBundleTests.java new file mode 100644 index 00000000..e3fa8567 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/ssl/ClientCertAuthSSLBundleTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.test.ssl; + +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.junit.jupiter.api.BeforeEach; +import reactor.netty.http.client.HttpClient; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +@ActiveProfiles("client-auth-ssl-bundle") +public class ClientCertAuthSSLBundleTests extends SingleCertSSLTests { + + @Autowired + private SslBundles sslBundles; + + @BeforeEach + public void setup() throws Exception { + final var sslBundle = sslBundles.getBundle("scg-keystore-with-different-key-password"); + final var sslContext = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .keyManager(sslBundle.getManagers().getKeyManagerFactory()) + .build(); + HttpClient httpClient = HttpClient.create().secure(ssl -> ssl.sslContext(sslContext)); + setup(new ReactorClientHttpConnector(httpClient), "https://localhost:" + port); + } + +} diff --git a/spring-cloud-gateway-server/src/test/resources/application-client-auth-ssl-bundle.yml b/spring-cloud-gateway-server/src/test/resources/application-client-auth-ssl-bundle.yml new file mode 100644 index 00000000..2ca408d5 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/resources/application-client-auth-ssl-bundle.yml @@ -0,0 +1,43 @@ +test: + uri: lb:https://testservice + +server: + ssl: + enabled: true + key-alias: scg + key-store-password: scg1234 + key-password: keyscg1234 + key-store: classpath:scg-keystore-with-different-key-password.jks + trust-store: classpath:scg-truststore.jks + trust-store-password: scg1234 + trust-store-type: JKS + key-store-type: JKS + client-auth: Need +spring: + cloud: + gateway: + httpclient: + ssl: + ssl-bundle: scg-keystore-with-different-key-password + trustedX509Certificates: + - src/test/resources/single-cert-for-different-key-password.pem + default-filters: + - PrefixPath=/httpbin + routes: + - id: default_path_to_httpbin + uri: ${test.uri} + order: 10000 + predicates: + - name: Path + args: + pattern: /** + ssl: + bundle: + jks: + scg-keystore-with-different-key-password: + key: + password: keyscg1234 + keystore: + type: JKS + location: classpath:scg-keystore-with-different-key-password.jks + password: scg1234 \ No newline at end of file diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml index fe8b8ece..5859d52d 100644 --- a/spring-cloud-gateway-webflux/pom.xml +++ b/spring-cloud-gateway-webflux/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. diff --git a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java index d5f79dce..20ff720d 100644 --- a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java +++ b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java @@ -20,9 +20,13 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.gateway.webflux.ProxyExchange; @@ -43,7 +47,7 @@ import org.springframework.web.reactive.result.method.annotation.ArgumentResolve */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication -@ConditionalOnClass({ HandlerMethodReturnValueHandler.class }) +@ConditionalOnClass({ HandlerMethodReturnValueHandler.class, WebClient.class }) @EnableConfigurationProperties(ProxyProperties.class) public class ProxyResponseAutoConfiguration implements WebFluxConfigurer { @@ -76,4 +80,20 @@ public class ProxyResponseAutoConfiguration implements WebFluxConfigurer { configurer.addCustomResolver(context.getBean(ProxyExchangeArgumentResolver.class)); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.cloud.gateway.proxyexchange.webflux.Marker") + protected static class NewModuleConfiguration { + + private static final Log log = LogFactory.getLog(ProxyResponseAutoConfiguration.class); + + private static final String BORDER = "\n\n**********************************************************\n\n"; + + public NewModuleConfiguration() { + log.warn(BORDER + "The artifact spring-cloud-gateway-webflux is deprecated. " + + "It will be removed in the next major release. " + + "Please add spring-cloud-gateway-proxyexchange-webflux dependency." + BORDER); + } + + } + } diff --git a/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java b/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java index e973a8aa..62b26201 100644 --- a/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java +++ b/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java @@ -25,6 +25,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,6 +33,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.cloud.gateway.webflux.ProductionConfigurationTests.TestApplication; @@ -62,6 +65,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(properties = { "spring.cloud.gateway.proxy.skipped=host" }, webEnvironment = WebEnvironment.RANDOM_PORT) @ContextConfiguration(classes = TestApplication.class) @DirtiesContext +@ExtendWith(OutputCaptureExtension.class) public class ProductionConfigurationTests { @Autowired @@ -276,6 +280,11 @@ public class ProductionConfigurationTests { assertThat(deleteResponse.getBody().get("deleted")).usingRecursiveComparison().isEqualTo(foo); } + @Test + void logsArtifactDeprecatedWarning(CapturedOutput output) { + assertThat(output).contains("spring-cloud-gateway-webflux is deprecated"); + } + @SpringBootApplication static class TestApplication { diff --git a/spring-cloud-starter-gateway-mvc/pom.xml b/spring-cloud-starter-gateway-mvc/pom.xml index a2c8eba8..e86d2d1f 100644 --- a/spring-cloud-starter-gateway-mvc/pom.xml +++ b/spring-cloud-starter-gateway-mvc/pom.xml @@ -6,12 +6,13 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. spring-cloud-starter-gateway-mvc spring-cloud-starter-gateway-mvc - Spring Cloud Gateway MVC + jar + Spring Cloud Gateway Starter MVC https://projects.spring.io/spring-cloud Pivotal Software, Inc. diff --git a/spring-cloud-starter-gateway-mvc/src/main/java/org/springframework/cloud/gateway/server/webmvc/starter/StarterDeprecatedAutoConfiguration.java b/spring-cloud-starter-gateway-mvc/src/main/java/org/springframework/cloud/gateway/server/webmvc/starter/StarterDeprecatedAutoConfiguration.java new file mode 100644 index 00000000..6d490391 --- /dev/null +++ b/spring-cloud-starter-gateway-mvc/src/main/java/org/springframework/cloud/gateway/server/webmvc/starter/StarterDeprecatedAutoConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.webmvc.starter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.annotation.Configuration; + +/** + * @author Spencer Gibb + */ +@Configuration(proxyBeanMethods = false) +public class StarterDeprecatedAutoConfiguration { + + private static final Log log = LogFactory.getLog(StarterDeprecatedAutoConfiguration.class); + + private static final String BORDER = "\n\n**********************************************************\n\n"; + + public StarterDeprecatedAutoConfiguration() { + log.warn(BORDER + "spring-cloud-starter-gateway-mvc is deprecated. " + + "It will be removed in the next major release. " + + "Please use spring-cloud-starter-gateway-server-webmvc instead." + BORDER); + } + +} diff --git a/spring-cloud-starter-gateway-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-starter-gateway-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..acc9b59a --- /dev/null +++ b/spring-cloud-starter-gateway-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.cloud.gateway.server.webmvc.starter.StarterDeprecatedAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-starter-gateway-server-webflux/pom.xml b/spring-cloud-starter-gateway-server-webflux/pom.xml new file mode 100644 index 00000000..ab94fcc5 --- /dev/null +++ b/spring-cloud-starter-gateway-server-webflux/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-gateway + 4.3.0-SNAPSHOT + .. + + spring-cloud-starter-gateway-server-webflux + spring-cloud-starter-gateway-server-webflux + Spring Cloud Starter Gateway Server WebFlux + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-starter + + + org.springframework.cloud + spring-cloud-gateway-server-webflux + + + org.springframework.boot + spring-boot-starter-webflux + + + diff --git a/spring-cloud-starter-gateway-server-webmvc/pom.xml b/spring-cloud-starter-gateway-server-webmvc/pom.xml new file mode 100644 index 00000000..00420392 --- /dev/null +++ b/spring-cloud-starter-gateway-server-webmvc/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-gateway + 4.3.0-SNAPSHOT + .. + + spring-cloud-starter-gateway-server-webmvc + spring-cloud-starter-gateway-server-webmvc + Spring Cloud Gateway Server Web MVC + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-starter + + + org.springframework.cloud + spring-cloud-gateway-server-webmvc + + + org.springframework.boot + spring-boot-starter-web + + + diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index f32a7980..8de3f70e 100644 --- a/spring-cloud-starter-gateway/pom.xml +++ b/spring-cloud-starter-gateway/pom.xml @@ -6,12 +6,13 @@ org.springframework.cloud spring-cloud-gateway - 4.2.1-SNAPSHOT + 4.3.0-SNAPSHOT .. spring-cloud-starter-gateway spring-cloud-starter-gateway - Spring Cloud Starter + jar + Spring Cloud Starter Gateway https://projects.spring.io/spring-cloud Pivotal Software, Inc. diff --git a/spring-cloud-starter-gateway/src/main/java/org/springframework/cloud/gateway/server/webflux/starter/StarterDeprecatedAutoConfiguration.java b/spring-cloud-starter-gateway/src/main/java/org/springframework/cloud/gateway/server/webflux/starter/StarterDeprecatedAutoConfiguration.java new file mode 100644 index 00000000..7cff3218 --- /dev/null +++ b/spring-cloud-starter-gateway/src/main/java/org/springframework/cloud/gateway/server/webflux/starter/StarterDeprecatedAutoConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.server.webflux.starter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.annotation.Configuration; + +/** + * @author Spencer Gibb + */ +@Configuration(proxyBeanMethods = false) +public class StarterDeprecatedAutoConfiguration { + + private static final Log log = LogFactory.getLog(StarterDeprecatedAutoConfiguration.class); + + private static final String BORDER = "\n\n**********************************************************\n\n"; + + public StarterDeprecatedAutoConfiguration() { + log.warn(BORDER + "spring-cloud-starter-gateway is deprecated. " + + "It will be removed in the next major release. " + + "Please use spring-cloud-starter-gateway-server-webflux instead." + BORDER); + } + +} diff --git a/spring-cloud-starter-gateway/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-starter-gateway/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..4b340116 --- /dev/null +++ b/spring-cloud-starter-gateway/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.cloud.gateway.server.webflux.starter.StarterDeprecatedAutoConfiguration \ No newline at end of file