The following Code-Snippet works correctly with Spring MVC:
@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
private static final String[] ANGULAR_RESOURCES = {
"/favicon.ico",
"/main.*.js",
"/polyfills.*.js",
"/runtime.*.js",
"/styles.*.css",
"/deeppurple-amber.css",
"/indigo-pink.css",
"/pink-bluegrey.css",
"/purple-green.css",
"/3rdpartylicenses.txt"
};
private static final List<Locale> SUPPORTED_LANGUAGES = List.of(Locale.GERMAN, Locale.ENGLISH);
private static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
private final String prefix;
private final Collection<HttpMessageConverter<?>> messageConverters;
private final AsyncTaskExecutor asyncTaskExecutor;
public WebMvcConfiguration(
@Value("${spring.thymeleaf.prefix:" + ThymeleafProperties.DEFAULT_PREFIX + "}") String prefix,
Collection<HttpMessageConverter<?>> messageConverters,
AsyncTaskExecutor asyncTaskExecutor) {
this.prefix = StringUtils.appendIfMissing(prefix, "/");
this.messageConverters = messageConverters;
this.asyncTaskExecutor = asyncTaskExecutor;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.setOrder(1);
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.resourceChain(true);
SUPPORTED_LANGUAGES.forEach(registerLocalizedAngularResourcesTo(registry));
}
private Consumer<Locale> registerLocalizedAngularResourcesTo(ResourceHandlerRegistry registry) {
return language -> {
final var relativeAngularResources = Stream.of(ANGULAR_RESOURCES)
.filter(resource -> StringUtils.contains(resource, "*"))
.map(resource -> "/" + language.getLanguage() + resource)
.toArray(String[]::new);
registry.addResourceHandler(relativeAngularResources)
.addResourceLocations(prefix + language.getLanguage() + "/");
final var fixedAngularResources = Stream.of(ANGULAR_RESOURCES)
.filter(resource -> !StringUtils.contains(resource, "*"))
.map(resource -> "/" + language.getLanguage() + resource)
.toArray(String[]::new);
registry.addResourceHandler(fixedAngularResources)
.addResourceLocations(prefix);
registry.addResourceHandler("/" + language.getLanguage() + "/assets/**")
.addResourceLocations(prefix + language.getLanguage() + "/assets/");
};
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.setOrder(2);
SUPPORTED_LANGUAGES.forEach(language -> registry.addViewController("/" + language.getLanguage() + "/**").setViewName(language.getLanguage() + "/index"));
}
@Bean
public RouterFunction<ServerResponse> routerFunction() {
return route(GET("/"), this::defaultLandingPage);
}
private ServerResponse defaultLandingPage(ServerRequest request) {
final var locale = Optional.ofNullable(Locale.lookup(request.headers().acceptLanguage(), SUPPORTED_LANGUAGES))
.orElse(DEFAULT_LOCALE);
return ServerResponse.status(HttpStatus.TEMPORARY_REDIRECT).render("redirect:/" + locale.getLanguage());
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(APPLICATION_JSON);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.addAll(messageConverters);
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(5000).setTaskExecutor(asyncTaskExecutor);
}
}
I convert the Application to use Spring Web-Flux and rewrite the Configuration:
@Configuration
@EnableWebFlux
public class WebFluxConfiguration implements WebFluxConfigurer {
private static final String[] ANGULAR_RESOURCES = {
"/favicon.ico",
"/main.*.js",
"/polyfills.*.js",
"/runtime.*.js",
"/styles.*.css",
"/deeppurple-amber.css",
"/indigo-pink.css",
"/pink-bluegrey.css",
"/purple-green.css",
"/3rdpartylicenses.txt"
};
private static final List<Locale> SUPPORTED_LANGUAGES = List.of(Locale.GERMAN, Locale.ENGLISH);
private static final Locale DEFAULT_LANGUAGE = Locale.ENGLISH;
private final String prefix;
private final ThymeleafReactiveViewResolver thymeleafReactiveViewResolver;
public WebFluxConfiguration(@Value("${spring.thymeleaf.prefix:" + ThymeleafProperties.DEFAULT_PREFIX + "}") String prefix, ThymeleafReactiveViewResolver thymeleafReactiveViewResolver) {
this.prefix = prefix;
this.thymeleafReactiveViewResolver = thymeleafReactiveViewResolver;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.setOrder(1);
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.resourceChain(true);
SUPPORTED_LANGUAGES.forEach(addLocalizedAngularResourcesTo(registry));
}
private Consumer<Locale> addLocalizedAngularResourcesTo(ResourceHandlerRegistry registry) {
return language -> {
final var relativeAngularResources = Stream.of(ANGULAR_RESOURCES)
.filter(resource -> StringUtils.contains(resource, "*"))
.map(resource -> "/" + language.getLanguage() + resource)
.toArray(String[]::new);
registry.addResourceHandler(relativeAngularResources)
.addResourceLocations(prefix + language.getLanguage() + "/");
final var fixedAngularResources = Stream.of(ANGULAR_RESOURCES)
.filter(resource -> !StringUtils.contains(resource, "*"))
.map(resource -> "/" + language.getLanguage() + resource)
.toArray(String[]::new);
registry.addResourceHandler(fixedAngularResources)
.addResourceLocations(prefix);
registry.addResourceHandler("/" + language.getLanguage() + "/assets/**")
.addResourceLocations(prefix + language.getLanguage() + "/assets/");
};
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(thymeleafReactiveViewResolver);
}
@Bean
public RouterFunction<ServerResponse> routerFunction() {
final var routerFunctionBuilder = route().GET("/", this::defaultLandingPage);
SUPPORTED_LANGUAGES.forEach(addLocalizedLandingPageTo(routerFunctionBuilder));
return routerFunctionBuilder.build();
}
private Mono<ServerResponse> defaultLandingPage(ServerRequest request) {
final var locale = Optional.ofNullable(Locale.lookup(request.headers().acceptLanguage(), SUPPORTED_LANGUAGES))
.orElse(DEFAULT_LANGUAGE);
return ServerResponse.temporaryRedirect(request.uriBuilder().path(locale.getLanguage()).build()).build();
}
private Consumer<Locale> addLocalizedLandingPageTo(RouterFunctions.Builder routerFunctionBuilder) {
return language -> {
var requestPredicate = Stream.of(ANGULAR_RESOURCES)
.map(angularResource -> "/" + language.getLanguage() + angularResource)
.reduce(GET("/" + language.getLanguage() + "/**"), (requestPredicte, route) -> requestPredicte.and(GET(route).negate()), RequestPredicate::and);
requestPredicate = requestPredicate.and(GET("/" + language.getLanguage() + "/assets/**").negate());
routerFunctionBuilder.route(requestPredicate, request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).render(language.getLanguage() + "/index"));
};
}
}
The rewritten Application is available via Github
When trying to access the Resource /en/favicon.ico
I will receive a HTTP 404 Error while accessing the Resource /en/main.6187e65880c98290.js
will give me a HTTP 200 with the correct Content.
@Test
@DisplayName("GET /en/main.6187e65880c98290.js and expect the Content of en/main.6187e65880c98290.js")
void relativeResource() {
web.get()
.uri("/en/main.6187e65880c98290.js")
.exchange()
.expectStatus().isOk()
.expectBody().consumeWith(response -> assertThat(response.getResponseBody())
.asString()
.isNotEmpty()
.isEqualTo(getContent("/en/main.6187e65880c98290.js")));
}
private String getContent(String location) {
final var resource = resourceLoader.getResource(String.join("/", thymeleafPrefix, StringUtils.stripStart(location, "/")));
if (!resource.exists() || !resource.isFile() || !resource.isReadable()) {
return null;
}
try (final Reader reader = new InputStreamReader(resource.getInputStream(), UTF_8)) {
return FileCopyUtils.copyToString(reader);
} catch (IOException e) {
log.error("Error while reading Test-Resource {}", location, e);
return null;
}
}
@Test
@DisplayName("GET /en/favicon.ico and expect the Content of en/favicon.ico")
void staticResource() {
web.get()
.uri("/en/favicon.ico")
.exchange()
.expectStatus().isOk()
.expectBody().consumeWith(response -> assertThat(response.getResponseBody())
.asString()
.isNotEmpty()
.isEqualTo(getContent("/en/favicon.ico")));
}
Comment From: drgnchan
As the pic shows,the reason is that the attribute of org.springframework.web.reactive.HandlerMapping.pathWithinHandlerMapping
is blank.I think there should be checking another attribute org.springframework.web.reactive.HandlerMapping.bestMatchingPattern
or org.springframework.web.reactive.HandlerMapping.bestMatchingHandler
is existing.
Comment From: bclozel
This case can be reduced to a favicon file located in src/main/resources/static/en/favicon.ico
and the following configuration:
@Configuration
public class WebConfiguration implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/en/favicon.ico").addResourceLocations("classpath:/static/");
}
}
The ResourceWebHandler
is only using the path within the handler mapping to resolve the resource in the configured locations. In this case, the pattern has no dynamic part (no *
, no regexp elements) and will always have an empty path within handler mapping value.
This is not a problem on the MVC side, where the HTTP handling itself is slightly different and can use the full path.
We're going to handle this as an enhancement, as backporting this change to 5.3.x has not been a problem so far and we want to avoid serving static resources that were ignored in the past.
@mufasa1976 Note that your resource handling setup is quite inefficient, as it's registering many handlers for few resources; this might cause performance inefficiencies when handling HTTP requests. I'm not familiar with your constraints, but I would try in general to use a front-end build tool to pack and clean those resources, only leaving the ones you're expecting to be public.
You could then try and reduce registrations just for supported languages:
@Configuration
public class WebConfiguration implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/en/**").addResourceLocations("classpath:/static/en/");
registry.addResourceHandler("/de/**").addResourceLocations("classpath:/static/de/");
}
}
Comment From: mufasa1976
@bclozel: you are completely right. I've copied the Block from the WebMvcConfiguration
and reworked it to work as WebFluxConfiguration
. But in this case it is best to reduce to
SUPPORTED_LANGUAGE.forEach(locale -> registry.addResourceHandler("/" + locale.getLanguage() + "/**")
.addResourceLocations(prefix + locale.getLanguage() + "/"));
Thank you for your Tip!