001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.autoconfigure.web.servlet; 018 019import java.time.Duration; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.List; 025import java.util.ListIterator; 026import java.util.Map; 027import java.util.Optional; 028 029import javax.servlet.Servlet; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033 034import org.springframework.beans.factory.BeanFactory; 035import org.springframework.beans.factory.ListableBeanFactory; 036import org.springframework.beans.factory.NoSuchBeanDefinitionException; 037import org.springframework.beans.factory.ObjectProvider; 038import org.springframework.beans.factory.annotation.Autowired; 039import org.springframework.boot.autoconfigure.AutoConfigureAfter; 040import org.springframework.boot.autoconfigure.AutoConfigureOrder; 041import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 042import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 043import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 044import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 045import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 046import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 047import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; 048import org.springframework.boot.autoconfigure.http.HttpMessageConverters; 049import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; 050import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; 051import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; 052import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; 053import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; 054import org.springframework.boot.autoconfigure.web.ResourceProperties; 055import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; 056import org.springframework.boot.autoconfigure.web.format.WebConversionService; 057import org.springframework.boot.context.properties.EnableConfigurationProperties; 058import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter; 059import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; 060import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter; 061import org.springframework.context.ApplicationContext; 062import org.springframework.context.ResourceLoaderAware; 063import org.springframework.context.annotation.Bean; 064import org.springframework.context.annotation.Configuration; 065import org.springframework.context.annotation.Import; 066import org.springframework.context.annotation.Primary; 067import org.springframework.core.Ordered; 068import org.springframework.core.annotation.Order; 069import org.springframework.core.convert.converter.Converter; 070import org.springframework.core.convert.converter.GenericConverter; 071import org.springframework.core.io.ClassPathResource; 072import org.springframework.core.io.Resource; 073import org.springframework.core.io.ResourceLoader; 074import org.springframework.core.task.AsyncTaskExecutor; 075import org.springframework.format.Formatter; 076import org.springframework.format.FormatterRegistry; 077import org.springframework.format.support.FormattingConversionService; 078import org.springframework.http.CacheControl; 079import org.springframework.http.MediaType; 080import org.springframework.http.converter.HttpMessageConverter; 081import org.springframework.util.ClassUtils; 082import org.springframework.validation.DefaultMessageCodesResolver; 083import org.springframework.validation.MessageCodesResolver; 084import org.springframework.validation.Validator; 085import org.springframework.web.HttpMediaTypeNotAcceptableException; 086import org.springframework.web.accept.ContentNegotiationManager; 087import org.springframework.web.accept.ContentNegotiationStrategy; 088import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; 089import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; 090import org.springframework.web.context.request.NativeWebRequest; 091import org.springframework.web.context.request.RequestAttributes; 092import org.springframework.web.context.request.RequestContextListener; 093import org.springframework.web.filter.FormContentFilter; 094import org.springframework.web.filter.HiddenHttpMethodFilter; 095import org.springframework.web.filter.RequestContextFilter; 096import org.springframework.web.servlet.DispatcherServlet; 097import org.springframework.web.servlet.HandlerExceptionResolver; 098import org.springframework.web.servlet.LocaleResolver; 099import org.springframework.web.servlet.View; 100import org.springframework.web.servlet.ViewResolver; 101import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; 102import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; 103import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; 104import org.springframework.web.servlet.config.annotation.EnableWebMvc; 105import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; 106import org.springframework.web.servlet.config.annotation.ResourceChainRegistration; 107import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; 108import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 109import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 110import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 111import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; 112import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; 113import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; 114import org.springframework.web.servlet.i18n.FixedLocaleResolver; 115import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; 116import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; 117import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; 118import org.springframework.web.servlet.resource.AppCacheManifestTransformer; 119import org.springframework.web.servlet.resource.EncodedResourceResolver; 120import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; 121import org.springframework.web.servlet.resource.ResourceResolver; 122import org.springframework.web.servlet.resource.VersionResourceResolver; 123import org.springframework.web.servlet.view.BeanNameViewResolver; 124import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; 125import org.springframework.web.servlet.view.InternalResourceViewResolver; 126 127/** 128 * {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebMvc Web MVC}. 129 * 130 * @author Phillip Webb 131 * @author Dave Syer 132 * @author Andy Wilkinson 133 * @author Sébastien Deleuze 134 * @author Eddú Meléndez 135 * @author Stephane Nicoll 136 * @author Kristine Jetzke 137 * @author Bruce Brouwer 138 * @author Artsiom Yudovin 139 */ 140@Configuration 141@ConditionalOnWebApplication(type = Type.SERVLET) 142@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) 143@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 144@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) 145@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, 146 TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) 147public class WebMvcAutoConfiguration { 148 149 public static final String DEFAULT_PREFIX = ""; 150 151 public static final String DEFAULT_SUFFIX = ""; 152 153 private static final String[] SERVLET_LOCATIONS = { "/" }; 154 155 @Bean 156 @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) 157 @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = true) 158 public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { 159 return new OrderedHiddenHttpMethodFilter(); 160 } 161 162 @Bean 163 @ConditionalOnMissingBean(FormContentFilter.class) 164 @ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true) 165 public OrderedFormContentFilter formContentFilter() { 166 return new OrderedFormContentFilter(); 167 } 168 169 // Defined as a nested config to ensure WebMvcConfigurer is not read when not 170 // on the classpath 171 @Configuration 172 @Import(EnableWebMvcConfiguration.class) 173 @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) 174 @Order(0) 175 public static class WebMvcAutoConfigurationAdapter 176 implements WebMvcConfigurer, ResourceLoaderAware { 177 178 private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); 179 180 private final ResourceProperties resourceProperties; 181 182 private final WebMvcProperties mvcProperties; 183 184 private final ListableBeanFactory beanFactory; 185 186 private final ObjectProvider<HttpMessageConverters> messageConvertersProvider; 187 188 final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; 189 190 private ResourceLoader resourceLoader; 191 192 public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, 193 WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, 194 ObjectProvider<HttpMessageConverters> messageConvertersProvider, 195 ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) { 196 this.resourceProperties = resourceProperties; 197 this.mvcProperties = mvcProperties; 198 this.beanFactory = beanFactory; 199 this.messageConvertersProvider = messageConvertersProvider; 200 this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider 201 .getIfAvailable(); 202 } 203 204 @Override 205 public void setResourceLoader(ResourceLoader resourceLoader) { 206 this.resourceLoader = resourceLoader; 207 } 208 209 @Override 210 public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { 211 this.messageConvertersProvider.ifAvailable((customConverters) -> converters 212 .addAll(customConverters.getConverters())); 213 } 214 215 @Override 216 public void configureAsyncSupport(AsyncSupportConfigurer configurer) { 217 if (this.beanFactory.containsBean( 218 TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) { 219 Object taskExecutor = this.beanFactory.getBean( 220 TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); 221 if (taskExecutor instanceof AsyncTaskExecutor) { 222 configurer.setTaskExecutor(((AsyncTaskExecutor) taskExecutor)); 223 } 224 } 225 Duration timeout = this.mvcProperties.getAsync().getRequestTimeout(); 226 if (timeout != null) { 227 configurer.setDefaultTimeout(timeout.toMillis()); 228 } 229 } 230 231 @Override 232 public void configurePathMatch(PathMatchConfigurer configurer) { 233 configurer.setUseSuffixPatternMatch( 234 this.mvcProperties.getPathmatch().isUseSuffixPattern()); 235 configurer.setUseRegisteredSuffixPatternMatch( 236 this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern()); 237 } 238 239 @Override 240 public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 241 WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties 242 .getContentnegotiation(); 243 configurer.favorPathExtension(contentnegotiation.isFavorPathExtension()); 244 configurer.favorParameter(contentnegotiation.isFavorParameter()); 245 if (contentnegotiation.getParameterName() != null) { 246 configurer.parameterName(contentnegotiation.getParameterName()); 247 } 248 Map<String, MediaType> mediaTypes = this.mvcProperties.getContentnegotiation() 249 .getMediaTypes(); 250 mediaTypes.forEach(configurer::mediaType); 251 } 252 253 @Bean 254 @ConditionalOnMissingBean 255 public InternalResourceViewResolver defaultViewResolver() { 256 InternalResourceViewResolver resolver = new InternalResourceViewResolver(); 257 resolver.setPrefix(this.mvcProperties.getView().getPrefix()); 258 resolver.setSuffix(this.mvcProperties.getView().getSuffix()); 259 return resolver; 260 } 261 262 @Bean 263 @ConditionalOnBean(View.class) 264 @ConditionalOnMissingBean 265 public BeanNameViewResolver beanNameViewResolver() { 266 BeanNameViewResolver resolver = new BeanNameViewResolver(); 267 resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); 268 return resolver; 269 } 270 271 @Bean 272 @ConditionalOnBean(ViewResolver.class) 273 @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class) 274 public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) { 275 ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); 276 resolver.setContentNegotiationManager( 277 beanFactory.getBean(ContentNegotiationManager.class)); 278 // ContentNegotiatingViewResolver uses all the other view resolvers to locate 279 // a view so it should have a high precedence 280 resolver.setOrder(Ordered.HIGHEST_PRECEDENCE); 281 return resolver; 282 } 283 284 @Bean 285 @ConditionalOnMissingBean 286 @ConditionalOnProperty(prefix = "spring.mvc", name = "locale") 287 public LocaleResolver localeResolver() { 288 if (this.mvcProperties 289 .getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { 290 return new FixedLocaleResolver(this.mvcProperties.getLocale()); 291 } 292 AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); 293 localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); 294 return localeResolver; 295 } 296 297 @Override 298 public MessageCodesResolver getMessageCodesResolver() { 299 if (this.mvcProperties.getMessageCodesResolverFormat() != null) { 300 DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver(); 301 resolver.setMessageCodeFormatter( 302 this.mvcProperties.getMessageCodesResolverFormat()); 303 return resolver; 304 } 305 return null; 306 } 307 308 @Override 309 public void addFormatters(FormatterRegistry registry) { 310 for (Converter<?, ?> converter : getBeansOfType(Converter.class)) { 311 registry.addConverter(converter); 312 } 313 for (GenericConverter converter : getBeansOfType(GenericConverter.class)) { 314 registry.addConverter(converter); 315 } 316 for (Formatter<?> formatter : getBeansOfType(Formatter.class)) { 317 registry.addFormatter(formatter); 318 } 319 } 320 321 private <T> Collection<T> getBeansOfType(Class<T> type) { 322 return this.beanFactory.getBeansOfType(type).values(); 323 } 324 325 @Override 326 public void addResourceHandlers(ResourceHandlerRegistry registry) { 327 if (!this.resourceProperties.isAddMappings()) { 328 logger.debug("Default resource handling disabled"); 329 return; 330 } 331 Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); 332 CacheControl cacheControl = this.resourceProperties.getCache() 333 .getCachecontrol().toHttpCacheControl(); 334 if (!registry.hasMappingForPattern("/webjars/**")) { 335 customizeResourceHandlerRegistration(registry 336 .addResourceHandler("/webjars/**") 337 .addResourceLocations("classpath:/META-INF/resources/webjars/") 338 .setCachePeriod(getSeconds(cachePeriod)) 339 .setCacheControl(cacheControl)); 340 } 341 String staticPathPattern = this.mvcProperties.getStaticPathPattern(); 342 if (!registry.hasMappingForPattern(staticPathPattern)) { 343 customizeResourceHandlerRegistration( 344 registry.addResourceHandler(staticPathPattern) 345 .addResourceLocations(getResourceLocations( 346 this.resourceProperties.getStaticLocations())) 347 .setCachePeriod(getSeconds(cachePeriod)) 348 .setCacheControl(cacheControl)); 349 } 350 } 351 352 private Integer getSeconds(Duration cachePeriod) { 353 return (cachePeriod != null) ? (int) cachePeriod.getSeconds() : null; 354 } 355 356 @Bean 357 public WelcomePageHandlerMapping welcomePageHandlerMapping( 358 ApplicationContext applicationContext) { 359 return new WelcomePageHandlerMapping( 360 new TemplateAvailabilityProviders(applicationContext), 361 applicationContext, getWelcomePage(), 362 this.mvcProperties.getStaticPathPattern()); 363 } 364 365 static String[] getResourceLocations(String[] staticLocations) { 366 String[] locations = new String[staticLocations.length 367 + SERVLET_LOCATIONS.length]; 368 System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length); 369 System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, 370 SERVLET_LOCATIONS.length); 371 return locations; 372 } 373 374 private Optional<Resource> getWelcomePage() { 375 String[] locations = getResourceLocations( 376 this.resourceProperties.getStaticLocations()); 377 return Arrays.stream(locations).map(this::getIndexHtml) 378 .filter(this::isReadable).findFirst(); 379 } 380 381 private Resource getIndexHtml(String location) { 382 return this.resourceLoader.getResource(location + "index.html"); 383 } 384 385 private boolean isReadable(Resource resource) { 386 try { 387 return resource.exists() && (resource.getURL() != null); 388 } 389 catch (Exception ex) { 390 return false; 391 } 392 } 393 394 private void customizeResourceHandlerRegistration( 395 ResourceHandlerRegistration registration) { 396 if (this.resourceHandlerRegistrationCustomizer != null) { 397 this.resourceHandlerRegistrationCustomizer.customize(registration); 398 } 399 } 400 401 @Bean 402 @ConditionalOnMissingBean({ RequestContextListener.class, 403 RequestContextFilter.class }) 404 public static RequestContextFilter requestContextFilter() { 405 return new OrderedRequestContextFilter(); 406 } 407 408 @Configuration 409 @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true) 410 public static class FaviconConfiguration implements ResourceLoaderAware { 411 412 private final ResourceProperties resourceProperties; 413 414 private ResourceLoader resourceLoader; 415 416 public FaviconConfiguration(ResourceProperties resourceProperties) { 417 this.resourceProperties = resourceProperties; 418 } 419 420 @Override 421 public void setResourceLoader(ResourceLoader resourceLoader) { 422 this.resourceLoader = resourceLoader; 423 } 424 425 @Bean 426 public SimpleUrlHandlerMapping faviconHandlerMapping() { 427 SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); 428 mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); 429 mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", 430 faviconRequestHandler())); 431 return mapping; 432 } 433 434 @Bean 435 public ResourceHttpRequestHandler faviconRequestHandler() { 436 ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); 437 requestHandler.setLocations(resolveFaviconLocations()); 438 return requestHandler; 439 } 440 441 private List<Resource> resolveFaviconLocations() { 442 String[] staticLocations = getResourceLocations( 443 this.resourceProperties.getStaticLocations()); 444 List<Resource> locations = new ArrayList<>(staticLocations.length + 1); 445 Arrays.stream(staticLocations).map(this.resourceLoader::getResource) 446 .forEach(locations::add); 447 locations.add(new ClassPathResource("/")); 448 return Collections.unmodifiableList(locations); 449 } 450 451 } 452 453 } 454 455 /** 456 * Configuration equivalent to {@code @EnableWebMvc}. 457 */ 458 @Configuration 459 public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { 460 461 private final WebMvcProperties mvcProperties; 462 463 private final ListableBeanFactory beanFactory; 464 465 private final WebMvcRegistrations mvcRegistrations; 466 467 public EnableWebMvcConfiguration( 468 ObjectProvider<WebMvcProperties> mvcPropertiesProvider, 469 ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider, 470 ListableBeanFactory beanFactory) { 471 this.mvcProperties = mvcPropertiesProvider.getIfAvailable(); 472 this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); 473 this.beanFactory = beanFactory; 474 } 475 476 @Bean 477 @Override 478 public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { 479 RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(); 480 adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null 481 || this.mvcProperties.isIgnoreDefaultModelOnRedirect()); 482 return adapter; 483 } 484 485 @Override 486 protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { 487 if (this.mvcRegistrations != null 488 && this.mvcRegistrations.getRequestMappingHandlerAdapter() != null) { 489 return this.mvcRegistrations.getRequestMappingHandlerAdapter(); 490 } 491 return super.createRequestMappingHandlerAdapter(); 492 } 493 494 @Bean 495 @Primary 496 @Override 497 public RequestMappingHandlerMapping requestMappingHandlerMapping() { 498 // Must be @Primary for MvcUriComponentsBuilder to work 499 return super.requestMappingHandlerMapping(); 500 } 501 502 @Bean 503 @Override 504 public FormattingConversionService mvcConversionService() { 505 WebConversionService conversionService = new WebConversionService( 506 this.mvcProperties.getDateFormat()); 507 addFormatters(conversionService); 508 return conversionService; 509 } 510 511 @Bean 512 @Override 513 public Validator mvcValidator() { 514 if (!ClassUtils.isPresent("javax.validation.Validator", 515 getClass().getClassLoader())) { 516 return super.mvcValidator(); 517 } 518 return ValidatorAdapter.get(getApplicationContext(), getValidator()); 519 } 520 521 @Override 522 protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { 523 if (this.mvcRegistrations != null 524 && this.mvcRegistrations.getRequestMappingHandlerMapping() != null) { 525 return this.mvcRegistrations.getRequestMappingHandlerMapping(); 526 } 527 return super.createRequestMappingHandlerMapping(); 528 } 529 530 @Override 531 protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() { 532 try { 533 return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class); 534 } 535 catch (NoSuchBeanDefinitionException ex) { 536 return super.getConfigurableWebBindingInitializer(); 537 } 538 } 539 540 @Override 541 protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() { 542 if (this.mvcRegistrations != null && this.mvcRegistrations 543 .getExceptionHandlerExceptionResolver() != null) { 544 return this.mvcRegistrations.getExceptionHandlerExceptionResolver(); 545 } 546 return super.createExceptionHandlerExceptionResolver(); 547 } 548 549 @Override 550 protected void configureHandlerExceptionResolvers( 551 List<HandlerExceptionResolver> exceptionResolvers) { 552 super.configureHandlerExceptionResolvers(exceptionResolvers); 553 if (exceptionResolvers.isEmpty()) { 554 addDefaultHandlerExceptionResolvers(exceptionResolvers); 555 } 556 if (this.mvcProperties.isLogResolvedException()) { 557 for (HandlerExceptionResolver resolver : exceptionResolvers) { 558 if (resolver instanceof AbstractHandlerExceptionResolver) { 559 ((AbstractHandlerExceptionResolver) resolver) 560 .setWarnLogCategory(resolver.getClass().getName()); 561 } 562 } 563 } 564 } 565 566 @Bean 567 @Override 568 public ContentNegotiationManager mvcContentNegotiationManager() { 569 ContentNegotiationManager manager = super.mvcContentNegotiationManager(); 570 List<ContentNegotiationStrategy> strategies = manager.getStrategies(); 571 ListIterator<ContentNegotiationStrategy> iterator = strategies.listIterator(); 572 while (iterator.hasNext()) { 573 ContentNegotiationStrategy strategy = iterator.next(); 574 if (strategy instanceof PathExtensionContentNegotiationStrategy) { 575 iterator.set(new OptionalPathExtensionContentNegotiationStrategy( 576 strategy)); 577 } 578 } 579 return manager; 580 } 581 582 } 583 584 @Configuration 585 @ConditionalOnEnabledResourceChain 586 static class ResourceChainCustomizerConfiguration { 587 588 @Bean 589 public ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() { 590 return new ResourceChainResourceHandlerRegistrationCustomizer(); 591 } 592 593 } 594 595 interface ResourceHandlerRegistrationCustomizer { 596 597 void customize(ResourceHandlerRegistration registration); 598 599 } 600 601 static class ResourceChainResourceHandlerRegistrationCustomizer 602 implements ResourceHandlerRegistrationCustomizer { 603 604 @Autowired 605 private ResourceProperties resourceProperties = new ResourceProperties(); 606 607 @Override 608 public void customize(ResourceHandlerRegistration registration) { 609 ResourceProperties.Chain properties = this.resourceProperties.getChain(); 610 configureResourceChain(properties, 611 registration.resourceChain(properties.isCache())); 612 } 613 614 private void configureResourceChain(ResourceProperties.Chain properties, 615 ResourceChainRegistration chain) { 616 Strategy strategy = properties.getStrategy(); 617 if (properties.isCompressed()) { 618 chain.addResolver(new EncodedResourceResolver()); 619 } 620 if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) { 621 chain.addResolver(getVersionResourceResolver(strategy)); 622 } 623 if (properties.isHtmlApplicationCache()) { 624 chain.addTransformer(new AppCacheManifestTransformer()); 625 } 626 } 627 628 private ResourceResolver getVersionResourceResolver( 629 ResourceProperties.Strategy properties) { 630 VersionResourceResolver resolver = new VersionResourceResolver(); 631 if (properties.getFixed().isEnabled()) { 632 String version = properties.getFixed().getVersion(); 633 String[] paths = properties.getFixed().getPaths(); 634 resolver.addFixedVersionStrategy(version, paths); 635 } 636 if (properties.getContent().isEnabled()) { 637 String[] paths = properties.getContent().getPaths(); 638 resolver.addContentVersionStrategy(paths); 639 } 640 return resolver; 641 } 642 643 } 644 645 /** 646 * Decorator to make {@link PathExtensionContentNegotiationStrategy} optional 647 * depending on a request attribute. 648 */ 649 static class OptionalPathExtensionContentNegotiationStrategy 650 implements ContentNegotiationStrategy { 651 652 private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class 653 .getName() + ".SKIP"; 654 655 private final ContentNegotiationStrategy delegate; 656 657 OptionalPathExtensionContentNegotiationStrategy( 658 ContentNegotiationStrategy delegate) { 659 this.delegate = delegate; 660 } 661 662 @Override 663 public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) 664 throws HttpMediaTypeNotAcceptableException { 665 Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE, 666 RequestAttributes.SCOPE_REQUEST); 667 if (skip != null && Boolean.parseBoolean(skip.toString())) { 668 return MEDIA_TYPE_ALL_LIST; 669 } 670 return this.delegate.resolveMediaTypes(webRequest); 671 } 672 673 } 674 675}