Skip to content

Commit 525e03d

Browse files
committed
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
1 parent ed4a7d4 commit 525e03d

File tree

5 files changed

+317
-1
lines changed

5 files changed

+317
-1
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3232
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
3333
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
34+
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
3435
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
3536
import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
3637
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
@@ -42,6 +43,7 @@
4243
import org.springframework.boot.convert.ApplicationConversionService;
4344
import org.springframework.boot.web.codec.CodecCustomizer;
4445
import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter;
46+
import org.springframework.context.ApplicationContext;
4547
import org.springframework.context.annotation.Bean;
4648
import org.springframework.context.annotation.Configuration;
4749
import org.springframework.context.annotation.Import;
@@ -59,6 +61,8 @@
5961
import org.springframework.web.reactive.config.ViewResolverRegistry;
6062
import org.springframework.web.reactive.config.WebFluxConfigurationSupport;
6163
import org.springframework.web.reactive.config.WebFluxConfigurer;
64+
import org.springframework.web.reactive.function.server.RouterFunction;
65+
import org.springframework.web.reactive.function.server.ServerResponse;
6266
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
6367
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
6468
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
@@ -93,6 +97,20 @@ public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
9397
return new OrderedHiddenHttpMethodFilter();
9498
}
9599

100+
@Configuration(proxyBeanMethods = false)
101+
public static class WelcomePageConfiguration {
102+
103+
@Bean
104+
public RouterFunction<ServerResponse> welcomePageRouterFunction(ApplicationContext applicationContext,
105+
WebFluxProperties webFluxProperties, ResourceProperties resourceProperties) {
106+
WelcomePageRouterFunctionFactory factory = new WelcomePageRouterFunctionFactory(
107+
new TemplateAvailabilityProviders(applicationContext), applicationContext,
108+
resourceProperties.getStaticLocations(), webFluxProperties.getStaticPathPattern());
109+
return factory.createRouterFunction();
110+
}
111+
112+
}
113+
96114
@Configuration(proxyBeanMethods = false)
97115
@EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class })
98116
@Import({ EnableWebFluxConfiguration.class })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.reactive;
18+
19+
import java.util.Arrays;
20+
21+
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
22+
import org.springframework.context.ApplicationContext;
23+
import org.springframework.core.io.Resource;
24+
import org.springframework.core.io.ResourceLoader;
25+
import org.springframework.http.MediaType;
26+
import org.springframework.web.reactive.function.server.RouterFunction;
27+
import org.springframework.web.reactive.function.server.RouterFunctions;
28+
import org.springframework.web.reactive.function.server.ServerResponse;
29+
30+
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
31+
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
32+
33+
/**
34+
* A {@link RouterFunction} factory for an application's welcome page. Supports both
35+
* static and templated files. If both a static and templated index page are available,
36+
* the static page is preferred.
37+
*
38+
* @author Brian Clozel
39+
*/
40+
final class WelcomePageRouterFunctionFactory {
41+
42+
private final String staticPathPattern;
43+
44+
private final Resource welcomePage;
45+
46+
private final boolean welcomePageTemplateExists;
47+
48+
WelcomePageRouterFunctionFactory(TemplateAvailabilityProviders templateAvailabilityProviders,
49+
ApplicationContext applicationContext, String[] staticLocations, String staticPathPattern) {
50+
this.staticPathPattern = staticPathPattern;
51+
this.welcomePage = getWelcomePage(applicationContext, staticLocations);
52+
this.welcomePageTemplateExists = welcomeTemplateExists(templateAvailabilityProviders, applicationContext);
53+
}
54+
55+
private Resource getWelcomePage(ResourceLoader resourceLoader, String[] staticLocations) {
56+
return Arrays.stream(staticLocations).map((location) -> getIndexHtml(resourceLoader, location))
57+
.filter(this::isReadable).findFirst().orElse(null);
58+
}
59+
60+
private Resource getIndexHtml(ResourceLoader resourceLoader, String location) {
61+
return resourceLoader.getResource(location + "index.html");
62+
}
63+
64+
private boolean isReadable(Resource resource) {
65+
try {
66+
return resource.exists() && (resource.getURL() != null);
67+
}
68+
catch (Exception ex) {
69+
return false;
70+
}
71+
}
72+
73+
private boolean welcomeTemplateExists(TemplateAvailabilityProviders templateAvailabilityProviders,
74+
ApplicationContext applicationContext) {
75+
return templateAvailabilityProviders.getProvider("index", applicationContext) != null;
76+
}
77+
78+
RouterFunction<ServerResponse> createRouterFunction() {
79+
if (this.welcomePage != null && "/**".equals(this.staticPathPattern)) {
80+
return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)),
81+
(req) -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(this.welcomePage));
82+
}
83+
else if (this.welcomePageTemplateExists) {
84+
return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)),
85+
(req) -> ServerResponse.ok().render("index"));
86+
}
87+
return null;
88+
}
89+
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.reactive;
18+
19+
import java.nio.charset.StandardCharsets;
20+
import java.util.Collections;
21+
import java.util.Locale;
22+
import java.util.Map;
23+
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import reactor.core.publisher.Mono;
27+
28+
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
29+
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
30+
import org.springframework.context.support.StaticApplicationContext;
31+
import org.springframework.core.io.buffer.DataBuffer;
32+
import org.springframework.core.io.buffer.DataBufferFactory;
33+
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
34+
import org.springframework.http.HttpHeaders;
35+
import org.springframework.http.MediaType;
36+
import org.springframework.test.web.reactive.server.WebTestClient;
37+
import org.springframework.web.reactive.function.server.HandlerStrategies;
38+
import org.springframework.web.reactive.result.view.View;
39+
import org.springframework.web.reactive.result.view.ViewResolver;
40+
import org.springframework.web.server.ServerWebExchange;
41+
42+
import static org.assertj.core.api.Assertions.assertThat;
43+
44+
/**
45+
* Tests for {@link WelcomePageRouterFunctionFactory}
46+
*
47+
* @author Brian Clozel
48+
*/
49+
class WelcomePageRouterFunctionFactoryTests {
50+
51+
private StaticApplicationContext applicationContext;
52+
53+
private final String[] noIndexLocations = { "classpath:/" };
54+
55+
private final String[] indexLocations = { "classpath:/public/", "classpath:/welcome-page/" };
56+
57+
@BeforeEach
58+
void setup() {
59+
this.applicationContext = new StaticApplicationContext();
60+
this.applicationContext.refresh();
61+
}
62+
63+
@Test
64+
void handlesRequestForStaticPageThatAcceptsTextHtml() {
65+
WebTestClient client = withStaticIndex();
66+
client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class)
67+
.isEqualTo("welcome-page-static");
68+
}
69+
70+
@Test
71+
void handlesRequestForStaticPageThatAcceptsAll() {
72+
WebTestClient client = withStaticIndex();
73+
client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class)
74+
.isEqualTo("welcome-page-static");
75+
}
76+
77+
@Test
78+
void doesNotHandleRequestThatDoesNotAcceptTextHtml() {
79+
WebTestClient client = withStaticIndex();
80+
client.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isNotFound();
81+
}
82+
83+
@Test
84+
void handlesRequestWithNoAcceptHeader() {
85+
WebTestClient client = withStaticIndex();
86+
client.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class)
87+
.isEqualTo("welcome-page-static");
88+
}
89+
90+
@Test
91+
void handlesRequestWithEmptyAcceptHeader() {
92+
WebTestClient client = withStaticIndex();
93+
client.get().uri("/").header(HttpHeaders.ACCEPT, "").exchange().expectStatus().isOk().expectBody(String.class)
94+
.isEqualTo("welcome-page-static");
95+
}
96+
97+
@Test
98+
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
99+
WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.noIndexLocations, "/**");
100+
assertThat(factory.createRouterFunction()).isNull();
101+
}
102+
103+
@Test
104+
void handlesRequestForTemplateThatAcceptsTextHtml() {
105+
WebTestClient client = withTemplateIndex();
106+
client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class)
107+
.isEqualTo("welcome-page-template");
108+
}
109+
110+
@Test
111+
void handlesRequestForTemplateThatAcceptsAll() {
112+
WebTestClient client = withTemplateIndex();
113+
client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class)
114+
.isEqualTo("welcome-page-template");
115+
}
116+
117+
@Test
118+
void prefersAStaticResourceToATemplate() {
119+
WebTestClient client = withStaticAndTemplateIndex();
120+
client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class)
121+
.isEqualTo("welcome-page-static");
122+
}
123+
124+
private WebTestClient createClient(WelcomePageRouterFunctionFactory factory) {
125+
return WebTestClient.bindToRouterFunction(factory.createRouterFunction()).build();
126+
}
127+
128+
private WebTestClient createClient(WelcomePageRouterFunctionFactory factory, ViewResolver viewResolver) {
129+
return WebTestClient.bindToRouterFunction(factory.createRouterFunction())
130+
.handlerStrategies(HandlerStrategies.builder().viewResolver(viewResolver).build()).build();
131+
}
132+
133+
private WebTestClient withStaticIndex() {
134+
WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.indexLocations, "/**");
135+
return WebTestClient.bindToRouterFunction(factory.createRouterFunction()).build();
136+
}
137+
138+
private WebTestClient withTemplateIndex() {
139+
WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.noIndexLocations);
140+
TestViewResolver testViewResolver = new TestViewResolver();
141+
return WebTestClient.bindToRouterFunction(factory.createRouterFunction())
142+
.handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build();
143+
}
144+
145+
private WebTestClient withStaticAndTemplateIndex() {
146+
WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.indexLocations);
147+
TestViewResolver testViewResolver = new TestViewResolver();
148+
return WebTestClient.bindToRouterFunction(factory.createRouterFunction())
149+
.handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build();
150+
}
151+
152+
private WelcomePageRouterFunctionFactory factoryWithoutTemplateSupport(String[] locations,
153+
String staticPathPattern) {
154+
return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders(), this.applicationContext,
155+
locations, staticPathPattern);
156+
}
157+
158+
private WelcomePageRouterFunctionFactory factoryWithTemplateSupport(String[] locations) {
159+
return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders("index"),
160+
this.applicationContext, locations, "/**");
161+
}
162+
163+
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
164+
165+
TestTemplateAvailabilityProviders() {
166+
super(Collections.emptyList());
167+
}
168+
169+
TestTemplateAvailabilityProviders(String viewName) {
170+
this((view, environment, classLoader, resourceLoader) -> view.equals(viewName));
171+
}
172+
173+
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
174+
super(Collections.singletonList(provider));
175+
}
176+
177+
}
178+
179+
static class TestViewResolver implements ViewResolver {
180+
181+
@Override
182+
public Mono<View> resolveViewName(String viewName, Locale locale) {
183+
return Mono.just(new TestView());
184+
}
185+
186+
}
187+
188+
static class TestView implements View {
189+
190+
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
191+
192+
@Override
193+
public Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange) {
194+
DataBuffer buffer = this.bufferFactory.wrap("welcome-page-template".getBytes(StandardCharsets.UTF_8));
195+
return exchange.getResponse().writeWith(Mono.just(buffer));
196+
}
197+
198+
}
199+
200+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
welcome-page-static

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,6 +2796,14 @@ Any resources with a path in `+/webjars/**+` are served from jar files if they a
27962796
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.
27972797

27982798

2799+
[[boot-features-webflux-welcome-page]]
2800+
==== Welcome Page
2801+
Spring Boot supports both static and templated welcome pages.
2802+
It first looks for an `index.html` file in the configured static content locations.
2803+
If one is not found, it then looks for an `index` template.
2804+
If either is found, it is automatically used as the welcome page of the application.
2805+
2806+
27992807

28002808
[[boot-features-webflux-template-engines]]
28012809
==== Template Engines

0 commit comments

Comments
 (0)