Browse Source

Add welcome page support for Spring WebFlux

This commit adds the support for static and templated welcome pages with
Spring WebFlux. The implementation is backed by a `RouterFunction`
that's serving a static `index.html` file or rendering an `index` view.

Closes gh-9785
pull/21545/head
Brian Clozel 6 years ago
parent
commit
525e03d3b5
  1. 18
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java
  2. 90
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java
  3. 200
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java
  4. 2
      spring-boot-project/spring-boot-autoconfigure/src/test/resources/welcome-page/index.html
  5. 8
      spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc

18
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java

@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
@ -42,6 +43,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties @@ -42,6 +43,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -59,6 +61,8 @@ import org.springframework.web.reactive.config.ResourceHandlerRegistry; @@ -59,6 +61,8 @@ import org.springframework.web.reactive.config.ResourceHandlerRegistry;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurationSupport;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
@ -93,6 +97,20 @@ public class WebFluxAutoConfiguration { @@ -93,6 +97,20 @@ public class WebFluxAutoConfiguration {
return new OrderedHiddenHttpMethodFilter();
}
@Configuration(proxyBeanMethods = false)
public static class WelcomePageConfiguration {
@Bean
public RouterFunction<ServerResponse> welcomePageRouterFunction(ApplicationContext applicationContext,
WebFluxProperties webFluxProperties, ResourceProperties resourceProperties) {
WelcomePageRouterFunctionFactory factory = new WelcomePageRouterFunctionFactory(
new TemplateAvailabilityProviders(applicationContext), applicationContext,
resourceProperties.getStaticLocations(), webFluxProperties.getStaticPathPattern());
return factory.createRouterFunction();
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class })
@Import({ EnableWebFluxConfiguration.class })

90
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
/*
* Copyright 2012-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.boot.autoconfigure.web.reactive;
import java.util.Arrays;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
/**
* A {@link RouterFunction} factory for an application's welcome page. Supports both
* static and templated files. If both a static and templated index page are available,
* the static page is preferred.
*
* @author Brian Clozel
*/
final class WelcomePageRouterFunctionFactory {
private final String staticPathPattern;
private final Resource welcomePage;
private final boolean welcomePageTemplateExists;
WelcomePageRouterFunctionFactory(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, String[] staticLocations, String staticPathPattern) {
this.staticPathPattern = staticPathPattern;
this.welcomePage = getWelcomePage(applicationContext, staticLocations);
this.welcomePageTemplateExists = welcomeTemplateExists(templateAvailabilityProviders, applicationContext);
}
private Resource getWelcomePage(ResourceLoader resourceLoader, String[] staticLocations) {
return Arrays.stream(staticLocations).map((location) -> getIndexHtml(resourceLoader, location))
.filter(this::isReadable).findFirst().orElse(null);
}
private Resource getIndexHtml(ResourceLoader resourceLoader, String location) {
return resourceLoader.getResource(location + "index.html");
}
private boolean isReadable(Resource resource) {
try {
return resource.exists() && (resource.getURL() != null);
}
catch (Exception ex) {
return false;
}
}
private boolean welcomeTemplateExists(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext) {
return templateAvailabilityProviders.getProvider("index", applicationContext) != null;
}
RouterFunction<ServerResponse> createRouterFunction() {
if (this.welcomePage != null && "/**".equals(this.staticPathPattern)) {
return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)),
(req) -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(this.welcomePage));
}
else if (this.welcomePageTemplateExists) {
return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)),
(req) -> ServerResponse.ok().render("index"));
}
return null;
}
}

200
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java

@ -0,0 +1,200 @@ @@ -0,0 +1,200 @@
/*
* Copyright 2012-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.boot.autoconfigure.web.reactive;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.result.view.View;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WelcomePageRouterFunctionFactory}
*
* @author Brian Clozel
*/
class WelcomePageRouterFunctionFactoryTests {
private StaticApplicationContext applicationContext;
private final String[] noIndexLocations = { "classpath:/" };
private final String[] indexLocations = { "classpath:/public/", "classpath:/welcome-page/" };
@BeforeEach
void setup() {
this.applicationContext = new StaticApplicationContext();
this.applicationContext.refresh();
}
@Test
void handlesRequestForStaticPageThatAcceptsTextHtml() {
WebTestClient client = withStaticIndex();
client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("welcome-page-static");
}
@Test
void handlesRequestForStaticPageThatAcceptsAll() {
WebTestClient client = withStaticIndex();
client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("welcome-page-static");
}
@Test
void doesNotHandleRequestThatDoesNotAcceptTextHtml() {
WebTestClient client = withStaticIndex();
client.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isNotFound();
}
@Test
void handlesRequestWithNoAcceptHeader() {
WebTestClient client = withStaticIndex();
client.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("welcome-page-static");
}
@Test
void handlesRequestWithEmptyAcceptHeader() {
WebTestClient client = withStaticIndex();
client.get().uri("/").header(HttpHeaders.ACCEPT, "").exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("welcome-page-static");
}
@Test
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.noIndexLocations, "/**");
assertThat(factory.createRouterFunction()).isNull();
}
@Test
void handlesRequestForTemplateThatAcceptsTextHtml() {
WebTestClient client = withTemplateIndex();
client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("welcome-page-template");
}
@Test
void handlesRequestForTemplateThatAcceptsAll() {
WebTestClient client = withTemplateIndex();
client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("welcome-page-template");
}
@Test
void prefersAStaticResourceToATemplate() {
WebTestClient client = withStaticAndTemplateIndex();
client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("welcome-page-static");
}
private WebTestClient createClient(WelcomePageRouterFunctionFactory factory) {
return WebTestClient.bindToRouterFunction(factory.createRouterFunction()).build();
}
private WebTestClient createClient(WelcomePageRouterFunctionFactory factory, ViewResolver viewResolver) {
return WebTestClient.bindToRouterFunction(factory.createRouterFunction())
.handlerStrategies(HandlerStrategies.builder().viewResolver(viewResolver).build()).build();
}
private WebTestClient withStaticIndex() {
WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.indexLocations, "/**");
return WebTestClient.bindToRouterFunction(factory.createRouterFunction()).build();
}
private WebTestClient withTemplateIndex() {
WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.noIndexLocations);
TestViewResolver testViewResolver = new TestViewResolver();
return WebTestClient.bindToRouterFunction(factory.createRouterFunction())
.handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build();
}
private WebTestClient withStaticAndTemplateIndex() {
WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.indexLocations);
TestViewResolver testViewResolver = new TestViewResolver();
return WebTestClient.bindToRouterFunction(factory.createRouterFunction())
.handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build();
}
private WelcomePageRouterFunctionFactory factoryWithoutTemplateSupport(String[] locations,
String staticPathPattern) {
return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders(), this.applicationContext,
locations, staticPathPattern);
}
private WelcomePageRouterFunctionFactory factoryWithTemplateSupport(String[] locations) {
return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders("index"),
this.applicationContext, locations, "/**");
}
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
TestTemplateAvailabilityProviders() {
super(Collections.emptyList());
}
TestTemplateAvailabilityProviders(String viewName) {
this((view, environment, classLoader, resourceLoader) -> view.equals(viewName));
}
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
super(Collections.singletonList(provider));
}
}
static class TestViewResolver implements ViewResolver {
@Override
public Mono<View> resolveViewName(String viewName, Locale locale) {
return Mono.just(new TestView());
}
}
static class TestView implements View {
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Override
public Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange) {
DataBuffer buffer = this.bufferFactory.wrap("welcome-page-template".getBytes(StandardCharsets.UTF_8));
return exchange.getResponse().writeWith(Mono.just(buffer));
}
}
}

2
spring-boot-project/spring-boot-autoconfigure/src/test/resources/welcome-page/index.html

@ -1 +1 @@ @@ -1 +1 @@
welcome-page-static

8
spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc

@ -2796,6 +2796,14 @@ Any resources with a path in `+/webjars/**+` are served from jar files if they a @@ -2796,6 +2796,14 @@ Any resources with a path in `+/webjars/**+` are served from jar files if they a
TIP: Spring WebFlux applications do not strictly depend on the Servlet API, so they cannot be deployed as war files and do not use the `src/main/webapp` directory.
[[boot-features-webflux-welcome-page]]
==== Welcome Page
Spring Boot supports both static and templated welcome pages.
It first looks for an `index.html` file in the configured static content locations.
If one is not found, it then looks for an `index` template.
If either is found, it is automatically used as the welcome page of the application.
[[boot-features-webflux-template-engines]]
==== Template Engines

Loading…
Cancel
Save