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.web.embedded.tomcat; 018 019import java.io.File; 020import java.io.InputStream; 021import java.lang.reflect.Method; 022import java.net.URL; 023import java.nio.charset.Charset; 024import java.nio.charset.StandardCharsets; 025import java.time.Duration; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.LinkedHashSet; 031import java.util.List; 032import java.util.Locale; 033import java.util.Set; 034 035import javax.servlet.ServletContainerInitializer; 036 037import org.apache.catalina.Context; 038import org.apache.catalina.Engine; 039import org.apache.catalina.Host; 040import org.apache.catalina.Lifecycle; 041import org.apache.catalina.LifecycleEvent; 042import org.apache.catalina.LifecycleException; 043import org.apache.catalina.LifecycleListener; 044import org.apache.catalina.Manager; 045import org.apache.catalina.Valve; 046import org.apache.catalina.WebResource; 047import org.apache.catalina.WebResourceRoot.ResourceSetType; 048import org.apache.catalina.WebResourceSet; 049import org.apache.catalina.Wrapper; 050import org.apache.catalina.connector.Connector; 051import org.apache.catalina.core.AprLifecycleListener; 052import org.apache.catalina.loader.WebappLoader; 053import org.apache.catalina.session.StandardManager; 054import org.apache.catalina.startup.Tomcat; 055import org.apache.catalina.startup.Tomcat.FixContextListener; 056import org.apache.catalina.util.LifecycleBase; 057import org.apache.catalina.webresources.AbstractResourceSet; 058import org.apache.catalina.webresources.EmptyResource; 059import org.apache.catalina.webresources.StandardRoot; 060import org.apache.coyote.AbstractProtocol; 061import org.apache.coyote.http2.Http2Protocol; 062import org.apache.tomcat.util.scan.StandardJarScanFilter; 063 064import org.springframework.boot.web.server.ErrorPage; 065import org.springframework.boot.web.server.MimeMappings; 066import org.springframework.boot.web.server.WebServer; 067import org.springframework.boot.web.servlet.ServletContextInitializer; 068import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; 069import org.springframework.context.ResourceLoaderAware; 070import org.springframework.core.io.ResourceLoader; 071import org.springframework.util.Assert; 072import org.springframework.util.ClassUtils; 073import org.springframework.util.ReflectionUtils; 074import org.springframework.util.StringUtils; 075 076/** 077 * {@link AbstractServletWebServerFactory} that can be used to create 078 * {@link TomcatWebServer}s. Can be initialized using Spring's 079 * {@link ServletContextInitializer}s or Tomcat {@link LifecycleListener}s. 080 * <p> 081 * Unless explicitly configured otherwise this factory will create containers that listen 082 * for HTTP requests on port 8080. 083 * 084 * @author Phillip Webb 085 * @author Dave Syer 086 * @author Brock Mills 087 * @author Stephane Nicoll 088 * @author Andy Wilkinson 089 * @author EddĂș MelĂ©ndez 090 * @author Christoffer Sawicki 091 * @since 2.0.0 092 * @see #setPort(int) 093 * @see #setContextLifecycleListeners(Collection) 094 * @see TomcatWebServer 095 */ 096public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory 097 implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware { 098 099 private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 100 101 private static final Set<Class<?>> NO_CLASSES = Collections.emptySet(); 102 103 /** 104 * The class name of default protocol used. 105 */ 106 public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol"; 107 108 private File baseDirectory; 109 110 private List<Valve> engineValves = new ArrayList<>(); 111 112 private List<Valve> contextValves = new ArrayList<>(); 113 114 private List<LifecycleListener> contextLifecycleListeners = new ArrayList<>( 115 Collections.singleton(new AprLifecycleListener())); 116 117 private List<TomcatContextCustomizer> tomcatContextCustomizers = new ArrayList<>(); 118 119 private List<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new ArrayList<>(); 120 121 private List<Connector> additionalTomcatConnectors = new ArrayList<>(); 122 123 private ResourceLoader resourceLoader; 124 125 private String protocol = DEFAULT_PROTOCOL; 126 127 private Set<String> tldSkipPatterns = new LinkedHashSet<>(TldSkipPatterns.DEFAULT); 128 129 private Charset uriEncoding = DEFAULT_CHARSET; 130 131 private int backgroundProcessorDelay; 132 133 /** 134 * Create a new {@link TomcatServletWebServerFactory} instance. 135 */ 136 public TomcatServletWebServerFactory() { 137 } 138 139 /** 140 * Create a new {@link TomcatServletWebServerFactory} that listens for requests using 141 * the specified port. 142 * @param port the port to listen on 143 */ 144 public TomcatServletWebServerFactory(int port) { 145 super(port); 146 } 147 148 /** 149 * Create a new {@link TomcatServletWebServerFactory} with the specified context path 150 * and port. 151 * @param contextPath the root context path 152 * @param port the port to listen on 153 */ 154 public TomcatServletWebServerFactory(String contextPath, int port) { 155 super(contextPath, port); 156 } 157 158 @Override 159 public WebServer getWebServer(ServletContextInitializer... initializers) { 160 Tomcat tomcat = new Tomcat(); 161 File baseDir = (this.baseDirectory != null) ? this.baseDirectory 162 : createTempDir("tomcat"); 163 tomcat.setBaseDir(baseDir.getAbsolutePath()); 164 Connector connector = new Connector(this.protocol); 165 tomcat.getService().addConnector(connector); 166 customizeConnector(connector); 167 tomcat.setConnector(connector); 168 tomcat.getHost().setAutoDeploy(false); 169 configureEngine(tomcat.getEngine()); 170 for (Connector additionalConnector : this.additionalTomcatConnectors) { 171 tomcat.getService().addConnector(additionalConnector); 172 } 173 prepareContext(tomcat.getHost(), initializers); 174 return getTomcatWebServer(tomcat); 175 } 176 177 private void configureEngine(Engine engine) { 178 engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay); 179 for (Valve valve : this.engineValves) { 180 engine.getPipeline().addValve(valve); 181 } 182 } 183 184 protected void prepareContext(Host host, ServletContextInitializer[] initializers) { 185 File documentRoot = getValidDocumentRoot(); 186 TomcatEmbeddedContext context = new TomcatEmbeddedContext(); 187 if (documentRoot != null) { 188 context.setResources(new LoaderHidingResourceRoot(context)); 189 } 190 context.setName(getContextPath()); 191 context.setDisplayName(getDisplayName()); 192 context.setPath(getContextPath()); 193 File docBase = (documentRoot != null) ? documentRoot 194 : createTempDir("tomcat-docbase"); 195 context.setDocBase(docBase.getAbsolutePath()); 196 context.addLifecycleListener(new FixContextListener()); 197 context.setParentClassLoader( 198 (this.resourceLoader != null) ? this.resourceLoader.getClassLoader() 199 : ClassUtils.getDefaultClassLoader()); 200 resetDefaultLocaleMapping(context); 201 addLocaleMappings(context); 202 context.setUseRelativeRedirects(false); 203 configureTldSkipPatterns(context); 204 WebappLoader loader = new WebappLoader(context.getParentClassLoader()); 205 loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName()); 206 loader.setDelegate(true); 207 context.setLoader(loader); 208 if (isRegisterDefaultServlet()) { 209 addDefaultServlet(context); 210 } 211 if (shouldRegisterJspServlet()) { 212 addJspServlet(context); 213 addJasperInitializer(context); 214 } 215 context.addLifecycleListener(new StaticResourceConfigurer(context)); 216 ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); 217 host.addChild(context); 218 configureContext(context, initializersToUse); 219 postProcessContext(context); 220 } 221 222 /** 223 * Override Tomcat's default locale mappings to align with other servers. See 224 * {@code org.apache.catalina.util.CharsetMapperDefault.properties}. 225 * @param context the context to reset 226 */ 227 private void resetDefaultLocaleMapping(TomcatEmbeddedContext context) { 228 context.addLocaleEncodingMappingParameter(Locale.ENGLISH.toString(), 229 DEFAULT_CHARSET.displayName()); 230 context.addLocaleEncodingMappingParameter(Locale.FRENCH.toString(), 231 DEFAULT_CHARSET.displayName()); 232 } 233 234 private void addLocaleMappings(TomcatEmbeddedContext context) { 235 getLocaleCharsetMappings() 236 .forEach((locale, charset) -> context.addLocaleEncodingMappingParameter( 237 locale.toString(), charset.toString())); 238 } 239 240 private void configureTldSkipPatterns(TomcatEmbeddedContext context) { 241 StandardJarScanFilter filter = new StandardJarScanFilter(); 242 filter.setTldSkip( 243 StringUtils.collectionToCommaDelimitedString(this.tldSkipPatterns)); 244 context.getJarScanner().setJarScanFilter(filter); 245 } 246 247 private void addDefaultServlet(Context context) { 248 Wrapper defaultServlet = context.createWrapper(); 249 defaultServlet.setName("default"); 250 defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); 251 defaultServlet.addInitParameter("debug", "0"); 252 defaultServlet.addInitParameter("listings", "false"); 253 defaultServlet.setLoadOnStartup(1); 254 // Otherwise the default location of a Spring DispatcherServlet cannot be set 255 defaultServlet.setOverridable(true); 256 context.addChild(defaultServlet); 257 context.addServletMappingDecoded("/", "default"); 258 } 259 260 private void addJspServlet(Context context) { 261 Wrapper jspServlet = context.createWrapper(); 262 jspServlet.setName("jsp"); 263 jspServlet.setServletClass(getJsp().getClassName()); 264 jspServlet.addInitParameter("fork", "false"); 265 getJsp().getInitParameters().forEach(jspServlet::addInitParameter); 266 jspServlet.setLoadOnStartup(3); 267 context.addChild(jspServlet); 268 context.addServletMappingDecoded("*.jsp", "jsp"); 269 context.addServletMappingDecoded("*.jspx", "jsp"); 270 } 271 272 private void addJasperInitializer(TomcatEmbeddedContext context) { 273 try { 274 ServletContainerInitializer initializer = (ServletContainerInitializer) ClassUtils 275 .forName("org.apache.jasper.servlet.JasperInitializer", null) 276 .newInstance(); 277 context.addServletContainerInitializer(initializer, null); 278 } 279 catch (Exception ex) { 280 // Probably not Tomcat 8 281 } 282 } 283 284 // Needs to be protected so it can be used by subclasses 285 protected void customizeConnector(Connector connector) { 286 int port = (getPort() >= 0) ? getPort() : 0; 287 connector.setPort(port); 288 if (StringUtils.hasText(this.getServerHeader())) { 289 connector.setAttribute("server", this.getServerHeader()); 290 } 291 if (connector.getProtocolHandler() instanceof AbstractProtocol) { 292 customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler()); 293 } 294 if (getUriEncoding() != null) { 295 connector.setURIEncoding(getUriEncoding().name()); 296 } 297 // Don't bind to the socket prematurely if ApplicationContext is slow to start 298 connector.setProperty("bindOnInit", "false"); 299 if (getSsl() != null && getSsl().isEnabled()) { 300 customizeSsl(connector); 301 } 302 TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer( 303 getCompression()); 304 compression.customize(connector); 305 for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) { 306 customizer.customize(connector); 307 } 308 } 309 310 private void customizeProtocol(AbstractProtocol<?> protocol) { 311 if (getAddress() != null) { 312 protocol.setAddress(getAddress()); 313 } 314 } 315 316 private void customizeSsl(Connector connector) { 317 new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector); 318 if (getHttp2() != null && getHttp2().isEnabled()) { 319 connector.addUpgradeProtocol(new Http2Protocol()); 320 } 321 } 322 323 /** 324 * Configure the Tomcat {@link Context}. 325 * @param context the Tomcat context 326 * @param initializers initializers to apply 327 */ 328 protected void configureContext(Context context, 329 ServletContextInitializer[] initializers) { 330 TomcatStarter starter = new TomcatStarter(initializers); 331 if (context instanceof TomcatEmbeddedContext) { 332 TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context; 333 embeddedContext.setStarter(starter); 334 embeddedContext.setFailCtxIfServletStartFails(true); 335 } 336 context.addServletContainerInitializer(starter, NO_CLASSES); 337 for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) { 338 context.addLifecycleListener(lifecycleListener); 339 } 340 for (Valve valve : this.contextValves) { 341 context.getPipeline().addValve(valve); 342 } 343 for (ErrorPage errorPage : getErrorPages()) { 344 new TomcatErrorPage(errorPage).addToContext(context); 345 } 346 for (MimeMappings.Mapping mapping : getMimeMappings()) { 347 context.addMimeMapping(mapping.getExtension(), mapping.getMimeType()); 348 } 349 configureSession(context); 350 for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) { 351 customizer.customize(context); 352 } 353 } 354 355 private void configureSession(Context context) { 356 long sessionTimeout = getSessionTimeoutInMinutes(); 357 context.setSessionTimeout((int) sessionTimeout); 358 Boolean httpOnly = getSession().getCookie().getHttpOnly(); 359 if (httpOnly != null) { 360 context.setUseHttpOnly(httpOnly); 361 } 362 if (getSession().isPersistent()) { 363 Manager manager = context.getManager(); 364 if (manager == null) { 365 manager = new StandardManager(); 366 context.setManager(manager); 367 } 368 configurePersistSession(manager); 369 } 370 else { 371 context.addLifecycleListener(new DisablePersistSessionListener()); 372 } 373 } 374 375 private void configurePersistSession(Manager manager) { 376 Assert.state(manager instanceof StandardManager, 377 () -> "Unable to persist HTTP session state using manager type " 378 + manager.getClass().getName()); 379 File dir = getValidSessionStoreDir(); 380 File file = new File(dir, "SESSIONS.ser"); 381 ((StandardManager) manager).setPathname(file.getAbsolutePath()); 382 } 383 384 private long getSessionTimeoutInMinutes() { 385 Duration sessionTimeout = getSession().getTimeout(); 386 if (isZeroOrLess(sessionTimeout)) { 387 return 0; 388 } 389 return Math.max(sessionTimeout.toMinutes(), 1); 390 } 391 392 private boolean isZeroOrLess(Duration sessionTimeout) { 393 return sessionTimeout == null || sessionTimeout.isNegative() 394 || sessionTimeout.isZero(); 395 } 396 397 /** 398 * Post process the Tomcat {@link Context} before it's used with the Tomcat Server. 399 * Subclasses can override this method to apply additional processing to the 400 * {@link Context}. 401 * @param context the Tomcat {@link Context} 402 */ 403 protected void postProcessContext(Context context) { 404 } 405 406 /** 407 * Factory method called to create the {@link TomcatWebServer}. Subclasses can 408 * override this method to return a different {@link TomcatWebServer} or apply 409 * additional processing to the Tomcat server. 410 * @param tomcat the Tomcat server. 411 * @return a new {@link TomcatWebServer} instance 412 */ 413 protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { 414 return new TomcatWebServer(tomcat, getPort() >= 0); 415 } 416 417 @Override 418 public void setResourceLoader(ResourceLoader resourceLoader) { 419 this.resourceLoader = resourceLoader; 420 } 421 422 @Override 423 public void setBaseDirectory(File baseDirectory) { 424 this.baseDirectory = baseDirectory; 425 } 426 427 /** 428 * Returns a mutable set of the patterns that match jars to ignore for TLD scanning. 429 * @return the list of jars to ignore for TLD scanning 430 */ 431 public Set<String> getTldSkipPatterns() { 432 return this.tldSkipPatterns; 433 } 434 435 /** 436 * Set the patterns that match jars to ignore for TLD scanning. See Tomcat's 437 * catalina.properties for typical values. Defaults to a list drawn from that source. 438 * @param patterns the jar patterns to skip when scanning for TLDs etc 439 */ 440 public void setTldSkipPatterns(Collection<String> patterns) { 441 Assert.notNull(patterns, "Patterns must not be null"); 442 this.tldSkipPatterns = new LinkedHashSet<>(patterns); 443 } 444 445 /** 446 * Add patterns that match jars to ignore for TLD scanning. See Tomcat's 447 * catalina.properties for typical values. 448 * @param patterns the additional jar patterns to skip when scanning for TLDs etc 449 */ 450 public void addTldSkipPatterns(String... patterns) { 451 Assert.notNull(patterns, "Patterns must not be null"); 452 this.tldSkipPatterns.addAll(Arrays.asList(patterns)); 453 } 454 455 /** 456 * The Tomcat protocol to use when create the {@link Connector}. 457 * @param protocol the protocol 458 * @see Connector#Connector(String) 459 */ 460 public void setProtocol(String protocol) { 461 Assert.hasLength(protocol, "Protocol must not be empty"); 462 this.protocol = protocol; 463 } 464 465 /** 466 * Set {@link Valve}s that should be applied to the Tomcat {@link Engine}. Calling 467 * this method will replace any existing valves. 468 * @param engineValves the valves to set 469 */ 470 public void setEngineValves(Collection<? extends Valve> engineValves) { 471 Assert.notNull(engineValves, "Valves must not be null"); 472 this.engineValves = new ArrayList<>(engineValves); 473 } 474 475 /** 476 * Returns a mutable collection of the {@link Valve}s that will be applied to the 477 * Tomcat {@link Engine}. 478 * @return the engine valves that will be applied 479 */ 480 public Collection<Valve> getEngineValves() { 481 return this.engineValves; 482 } 483 484 @Override 485 public void addEngineValves(Valve... engineValves) { 486 Assert.notNull(engineValves, "Valves must not be null"); 487 this.engineValves.addAll(Arrays.asList(engineValves)); 488 } 489 490 /** 491 * Set {@link Valve}s that should be applied to the Tomcat {@link Context}. Calling 492 * this method will replace any existing valves. 493 * @param contextValves the valves to set 494 */ 495 public void setContextValves(Collection<? extends Valve> contextValves) { 496 Assert.notNull(contextValves, "Valves must not be null"); 497 this.contextValves = new ArrayList<>(contextValves); 498 } 499 500 /** 501 * Returns a mutable collection of the {@link Valve}s that will be applied to the 502 * Tomcat {@link Context}. 503 * @return the context valves that will be applied 504 * @see #getEngineValves() 505 */ 506 public Collection<Valve> getContextValves() { 507 return this.contextValves; 508 } 509 510 /** 511 * Add {@link Valve}s that should be applied to the Tomcat {@link Context}. 512 * @param contextValves the valves to add 513 */ 514 public void addContextValves(Valve... contextValves) { 515 Assert.notNull(contextValves, "Valves must not be null"); 516 this.contextValves.addAll(Arrays.asList(contextValves)); 517 } 518 519 /** 520 * Set {@link LifecycleListener}s that should be applied to the Tomcat 521 * {@link Context}. Calling this method will replace any existing listeners. 522 * @param contextLifecycleListeners the listeners to set 523 */ 524 public void setContextLifecycleListeners( 525 Collection<? extends LifecycleListener> contextLifecycleListeners) { 526 Assert.notNull(contextLifecycleListeners, 527 "ContextLifecycleListeners must not be null"); 528 this.contextLifecycleListeners = new ArrayList<>(contextLifecycleListeners); 529 } 530 531 /** 532 * Returns a mutable collection of the {@link LifecycleListener}s that will be applied 533 * to the Tomcat {@link Context}. 534 * @return the context lifecycle listeners that will be applied 535 */ 536 public Collection<LifecycleListener> getContextLifecycleListeners() { 537 return this.contextLifecycleListeners; 538 } 539 540 /** 541 * Add {@link LifecycleListener}s that should be added to the Tomcat {@link Context}. 542 * @param contextLifecycleListeners the listeners to add 543 */ 544 public void addContextLifecycleListeners( 545 LifecycleListener... contextLifecycleListeners) { 546 Assert.notNull(contextLifecycleListeners, 547 "ContextLifecycleListeners must not be null"); 548 this.contextLifecycleListeners.addAll(Arrays.asList(contextLifecycleListeners)); 549 } 550 551 /** 552 * Set {@link TomcatContextCustomizer}s that should be applied to the Tomcat 553 * {@link Context}. Calling this method will replace any existing customizers. 554 * @param tomcatContextCustomizers the customizers to set 555 */ 556 public void setTomcatContextCustomizers( 557 Collection<? extends TomcatContextCustomizer> tomcatContextCustomizers) { 558 Assert.notNull(tomcatContextCustomizers, 559 "TomcatContextCustomizers must not be null"); 560 this.tomcatContextCustomizers = new ArrayList<>(tomcatContextCustomizers); 561 } 562 563 /** 564 * Returns a mutable collection of the {@link TomcatContextCustomizer}s that will be 565 * applied to the Tomcat {@link Context}. 566 * @return the listeners that will be applied 567 */ 568 public Collection<TomcatContextCustomizer> getTomcatContextCustomizers() { 569 return this.tomcatContextCustomizers; 570 } 571 572 @Override 573 public void addContextCustomizers( 574 TomcatContextCustomizer... tomcatContextCustomizers) { 575 Assert.notNull(tomcatContextCustomizers, 576 "TomcatContextCustomizers must not be null"); 577 this.tomcatContextCustomizers.addAll(Arrays.asList(tomcatContextCustomizers)); 578 } 579 580 /** 581 * Set {@link TomcatConnectorCustomizer}s that should be applied to the Tomcat 582 * {@link Connector}. Calling this method will replace any existing customizers. 583 * @param tomcatConnectorCustomizers the customizers to set 584 */ 585 public void setTomcatConnectorCustomizers( 586 Collection<? extends TomcatConnectorCustomizer> tomcatConnectorCustomizers) { 587 Assert.notNull(tomcatConnectorCustomizers, 588 "TomcatConnectorCustomizers must not be null"); 589 this.tomcatConnectorCustomizers = new ArrayList<>(tomcatConnectorCustomizers); 590 } 591 592 @Override 593 public void addConnectorCustomizers( 594 TomcatConnectorCustomizer... tomcatConnectorCustomizers) { 595 Assert.notNull(tomcatConnectorCustomizers, 596 "TomcatConnectorCustomizers must not be null"); 597 this.tomcatConnectorCustomizers.addAll(Arrays.asList(tomcatConnectorCustomizers)); 598 } 599 600 /** 601 * Returns a mutable collection of the {@link TomcatConnectorCustomizer}s that will be 602 * applied to the Tomcat {@link Connector}. 603 * @return the customizers that will be applied 604 */ 605 public Collection<TomcatConnectorCustomizer> getTomcatConnectorCustomizers() { 606 return this.tomcatConnectorCustomizers; 607 } 608 609 /** 610 * Add {@link Connector}s in addition to the default connector, e.g. for SSL or AJP 611 * @param connectors the connectors to add 612 */ 613 public void addAdditionalTomcatConnectors(Connector... connectors) { 614 Assert.notNull(connectors, "Connectors must not be null"); 615 this.additionalTomcatConnectors.addAll(Arrays.asList(connectors)); 616 } 617 618 /** 619 * Returns a mutable collection of the {@link Connector}s that will be added to the 620 * Tomcat. 621 * @return the additionalTomcatConnectors 622 */ 623 public List<Connector> getAdditionalTomcatConnectors() { 624 return this.additionalTomcatConnectors; 625 } 626 627 @Override 628 public void setUriEncoding(Charset uriEncoding) { 629 this.uriEncoding = uriEncoding; 630 } 631 632 /** 633 * Returns the character encoding to use for URL decoding. 634 * @return the URI encoding 635 */ 636 public Charset getUriEncoding() { 637 return this.uriEncoding; 638 } 639 640 @Override 641 public void setBackgroundProcessorDelay(int delay) { 642 this.backgroundProcessorDelay = delay; 643 } 644 645 /** 646 * {@link LifecycleListener} to disable persistence in the {@link StandardManager}. A 647 * {@link LifecycleListener} is used so not to interfere with Tomcat's default manager 648 * creation logic. 649 */ 650 private static class DisablePersistSessionListener implements LifecycleListener { 651 652 @Override 653 public void lifecycleEvent(LifecycleEvent event) { 654 if (event.getType().equals(Lifecycle.START_EVENT)) { 655 Context context = (Context) event.getLifecycle(); 656 Manager manager = context.getManager(); 657 if (manager != null && manager instanceof StandardManager) { 658 ((StandardManager) manager).setPathname(null); 659 } 660 } 661 } 662 663 } 664 665 private final class StaticResourceConfigurer implements LifecycleListener { 666 667 private final Context context; 668 669 private StaticResourceConfigurer(Context context) { 670 this.context = context; 671 } 672 673 @Override 674 public void lifecycleEvent(LifecycleEvent event) { 675 if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { 676 addResourceJars(getUrlsOfJarsWithMetaInfResources()); 677 } 678 } 679 680 private void addResourceJars(List<URL> resourceJarUrls) { 681 for (URL url : resourceJarUrls) { 682 String path = url.getPath(); 683 if (path.endsWith(".jar") || path.endsWith(".jar!/")) { 684 String jar = url.toString(); 685 if (!jar.startsWith("jar:")) { 686 // A jar file in the file system. Convert to Jar URL. 687 jar = "jar:" + jar + "!/"; 688 } 689 addResourceSet(jar); 690 } 691 else { 692 addResourceSet(url.toString()); 693 } 694 } 695 } 696 697 private void addResourceSet(String resource) { 698 try { 699 if (isInsideNestedJar(resource)) { 700 // It's a nested jar but we now don't want the suffix because Tomcat 701 // is going to try and locate it as a root URL (not the resource 702 // inside it) 703 resource = resource.substring(0, resource.length() - 2); 704 } 705 URL url = new URL(resource); 706 String path = "/META-INF/resources"; 707 this.context.getResources().createWebResourceSet( 708 ResourceSetType.RESOURCE_JAR, "/", url, path); 709 } 710 catch (Exception ex) { 711 // Ignore (probably not a directory) 712 } 713 } 714 715 private boolean isInsideNestedJar(String dir) { 716 return dir.indexOf("!/") < dir.lastIndexOf("!/"); 717 } 718 719 } 720 721 private static final class LoaderHidingResourceRoot extends StandardRoot { 722 723 private LoaderHidingResourceRoot(TomcatEmbeddedContext context) { 724 super(context); 725 } 726 727 @Override 728 protected WebResourceSet createMainResourceSet() { 729 return new LoaderHidingWebResourceSet(super.createMainResourceSet()); 730 } 731 732 } 733 734 private static final class LoaderHidingWebResourceSet extends AbstractResourceSet { 735 736 private final WebResourceSet delegate; 737 738 private final Method initInternal; 739 740 private LoaderHidingWebResourceSet(WebResourceSet delegate) { 741 this.delegate = delegate; 742 try { 743 this.initInternal = LifecycleBase.class.getDeclaredMethod("initInternal"); 744 this.initInternal.setAccessible(true); 745 } 746 catch (Exception ex) { 747 throw new IllegalStateException(ex); 748 } 749 } 750 751 @Override 752 public WebResource getResource(String path) { 753 if (path.startsWith("/org/springframework/boot")) { 754 return new EmptyResource(getRoot(), path); 755 } 756 return this.delegate.getResource(path); 757 } 758 759 @Override 760 public String[] list(String path) { 761 return this.delegate.list(path); 762 } 763 764 @Override 765 public Set<String> listWebAppPaths(String path) { 766 return this.delegate.listWebAppPaths(path); 767 } 768 769 @Override 770 public boolean mkdir(String path) { 771 return this.delegate.mkdir(path); 772 } 773 774 @Override 775 public boolean write(String path, InputStream is, boolean overwrite) { 776 return this.delegate.write(path, is, overwrite); 777 } 778 779 @Override 780 public URL getBaseUrl() { 781 return this.delegate.getBaseUrl(); 782 } 783 784 @Override 785 public void setReadOnly(boolean readOnly) { 786 this.delegate.setReadOnly(readOnly); 787 } 788 789 @Override 790 public boolean isReadOnly() { 791 return this.delegate.isReadOnly(); 792 } 793 794 @Override 795 public void gc() { 796 this.delegate.gc(); 797 } 798 799 @Override 800 protected void initInternal() throws LifecycleException { 801 if (this.delegate instanceof LifecycleBase) { 802 try { 803 ReflectionUtils.invokeMethod(this.initInternal, this.delegate); 804 } 805 catch (Exception ex) { 806 throw new LifecycleException(ex); 807 } 808 } 809 } 810 811 } 812 813}