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.util.Arrays; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.concurrent.atomic.AtomicInteger; 023import java.util.stream.Collectors; 024 025import javax.naming.NamingException; 026 027import org.apache.catalina.Container; 028import org.apache.catalina.Context; 029import org.apache.catalina.Engine; 030import org.apache.catalina.Lifecycle; 031import org.apache.catalina.LifecycleException; 032import org.apache.catalina.LifecycleState; 033import org.apache.catalina.Service; 034import org.apache.catalina.connector.Connector; 035import org.apache.catalina.startup.Tomcat; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.apache.naming.ContextBindings; 039 040import org.springframework.boot.web.server.WebServer; 041import org.springframework.boot.web.server.WebServerException; 042import org.springframework.util.Assert; 043 044/** 045 * {@link WebServer} that can be used to control a Tomcat web server. Usually this class 046 * should be created using the {@link TomcatReactiveWebServerFactory} of 047 * {@link TomcatServletWebServerFactory}, but not directly. 048 * 049 * @author Brian Clozel 050 * @author Kristine Jetzke 051 * @since 2.0.0 052 */ 053public class TomcatWebServer implements WebServer { 054 055 private static final Log logger = LogFactory.getLog(TomcatWebServer.class); 056 057 private static final AtomicInteger containerCounter = new AtomicInteger(-1); 058 059 private final Object monitor = new Object(); 060 061 private final Map<Service, Connector[]> serviceConnectors = new HashMap<>(); 062 063 private final Tomcat tomcat; 064 065 private final boolean autoStart; 066 067 private volatile boolean started; 068 069 /** 070 * Create a new {@link TomcatWebServer} instance. 071 * @param tomcat the underlying Tomcat server 072 */ 073 public TomcatWebServer(Tomcat tomcat) { 074 this(tomcat, true); 075 } 076 077 /** 078 * Create a new {@link TomcatWebServer} instance. 079 * @param tomcat the underlying Tomcat server 080 * @param autoStart if the server should be started 081 */ 082 public TomcatWebServer(Tomcat tomcat, boolean autoStart) { 083 Assert.notNull(tomcat, "Tomcat Server must not be null"); 084 this.tomcat = tomcat; 085 this.autoStart = autoStart; 086 initialize(); 087 } 088 089 private void initialize() throws WebServerException { 090 logger.info("Tomcat initialized with port(s): " + getPortsDescription(false)); 091 synchronized (this.monitor) { 092 try { 093 addInstanceIdToEngineName(); 094 095 Context context = findContext(); 096 context.addLifecycleListener((event) -> { 097 if (context.equals(event.getSource()) 098 && Lifecycle.START_EVENT.equals(event.getType())) { 099 // Remove service connectors so that protocol binding doesn't 100 // happen when the service is started. 101 removeServiceConnectors(); 102 } 103 }); 104 105 // Start the server to trigger initialization listeners 106 this.tomcat.start(); 107 108 // We can re-throw failure exception directly in the main thread 109 rethrowDeferredStartupExceptions(); 110 111 try { 112 ContextBindings.bindClassLoader(context, context.getNamingToken(), 113 getClass().getClassLoader()); 114 } 115 catch (NamingException ex) { 116 // Naming is not enabled. Continue 117 } 118 119 // Unlike Jetty, all Tomcat threads are daemon threads. We create a 120 // blocking non-daemon to stop immediate shutdown 121 startDaemonAwaitThread(); 122 } 123 catch (Exception ex) { 124 stopSilently(); 125 throw new WebServerException("Unable to start embedded Tomcat", ex); 126 } 127 } 128 } 129 130 private Context findContext() { 131 for (Container child : this.tomcat.getHost().findChildren()) { 132 if (child instanceof Context) { 133 return (Context) child; 134 } 135 } 136 throw new IllegalStateException("The host does not contain a Context"); 137 } 138 139 private void addInstanceIdToEngineName() { 140 int instanceId = containerCounter.incrementAndGet(); 141 if (instanceId > 0) { 142 Engine engine = this.tomcat.getEngine(); 143 engine.setName(engine.getName() + "-" + instanceId); 144 } 145 } 146 147 private void removeServiceConnectors() { 148 for (Service service : this.tomcat.getServer().findServices()) { 149 Connector[] connectors = service.findConnectors().clone(); 150 this.serviceConnectors.put(service, connectors); 151 for (Connector connector : connectors) { 152 service.removeConnector(connector); 153 } 154 } 155 } 156 157 private void rethrowDeferredStartupExceptions() throws Exception { 158 Container[] children = this.tomcat.getHost().findChildren(); 159 for (Container container : children) { 160 if (container instanceof TomcatEmbeddedContext) { 161 TomcatStarter tomcatStarter = ((TomcatEmbeddedContext) container) 162 .getStarter(); 163 if (tomcatStarter != null) { 164 Exception exception = tomcatStarter.getStartUpException(); 165 if (exception != null) { 166 throw exception; 167 } 168 } 169 } 170 if (!LifecycleState.STARTED.equals(container.getState())) { 171 throw new IllegalStateException(container + " failed to start"); 172 } 173 } 174 } 175 176 private void startDaemonAwaitThread() { 177 Thread awaitThread = new Thread("container-" + (containerCounter.get())) { 178 179 @Override 180 public void run() { 181 TomcatWebServer.this.tomcat.getServer().await(); 182 } 183 184 }; 185 awaitThread.setContextClassLoader(getClass().getClassLoader()); 186 awaitThread.setDaemon(false); 187 awaitThread.start(); 188 } 189 190 @Override 191 public void start() throws WebServerException { 192 synchronized (this.monitor) { 193 if (this.started) { 194 return; 195 } 196 try { 197 addPreviouslyRemovedConnectors(); 198 Connector connector = this.tomcat.getConnector(); 199 if (connector != null && this.autoStart) { 200 performDeferredLoadOnStartup(); 201 } 202 checkThatConnectorsHaveStarted(); 203 this.started = true; 204 logger.info("Tomcat started on port(s): " + getPortsDescription(true) 205 + " with context path '" + getContextPath() + "'"); 206 } 207 catch (ConnectorStartFailedException ex) { 208 stopSilently(); 209 throw ex; 210 } 211 catch (Exception ex) { 212 throw new WebServerException("Unable to start embedded Tomcat server", 213 ex); 214 } 215 finally { 216 Context context = findContext(); 217 ContextBindings.unbindClassLoader(context, context.getNamingToken(), 218 getClass().getClassLoader()); 219 } 220 } 221 } 222 223 private void checkThatConnectorsHaveStarted() { 224 checkConnectorHasStarted(this.tomcat.getConnector()); 225 for (Connector connector : this.tomcat.getService().findConnectors()) { 226 checkConnectorHasStarted(connector); 227 } 228 } 229 230 private void checkConnectorHasStarted(Connector connector) { 231 if (LifecycleState.FAILED.equals(connector.getState())) { 232 throw new ConnectorStartFailedException(connector.getPort()); 233 } 234 } 235 236 private void stopSilently() { 237 try { 238 stopTomcat(); 239 } 240 catch (LifecycleException ex) { 241 // Ignore 242 } 243 } 244 245 private void stopTomcat() throws LifecycleException { 246 if (Thread.currentThread() 247 .getContextClassLoader() instanceof TomcatEmbeddedWebappClassLoader) { 248 Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); 249 } 250 this.tomcat.stop(); 251 } 252 253 private void addPreviouslyRemovedConnectors() { 254 Service[] services = this.tomcat.getServer().findServices(); 255 for (Service service : services) { 256 Connector[] connectors = this.serviceConnectors.get(service); 257 if (connectors != null) { 258 for (Connector connector : connectors) { 259 service.addConnector(connector); 260 if (!this.autoStart) { 261 stopProtocolHandler(connector); 262 } 263 } 264 this.serviceConnectors.remove(service); 265 } 266 } 267 } 268 269 private void stopProtocolHandler(Connector connector) { 270 try { 271 connector.getProtocolHandler().stop(); 272 } 273 catch (Exception ex) { 274 logger.error("Cannot pause connector: ", ex); 275 } 276 } 277 278 private void performDeferredLoadOnStartup() { 279 try { 280 for (Container child : this.tomcat.getHost().findChildren()) { 281 if (child instanceof TomcatEmbeddedContext) { 282 ((TomcatEmbeddedContext) child).deferredLoadOnStartup(); 283 } 284 } 285 } 286 catch (Exception ex) { 287 if (ex instanceof WebServerException) { 288 throw (WebServerException) ex; 289 } 290 throw new WebServerException("Unable to start embedded Tomcat connectors", 291 ex); 292 } 293 } 294 295 Map<Service, Connector[]> getServiceConnectors() { 296 return this.serviceConnectors; 297 } 298 299 @Override 300 public void stop() throws WebServerException { 301 synchronized (this.monitor) { 302 boolean wasStarted = this.started; 303 try { 304 this.started = false; 305 try { 306 stopTomcat(); 307 this.tomcat.destroy(); 308 } 309 catch (LifecycleException ex) { 310 // swallow and continue 311 } 312 } 313 catch (Exception ex) { 314 throw new WebServerException("Unable to stop embedded Tomcat", ex); 315 } 316 finally { 317 if (wasStarted) { 318 containerCounter.decrementAndGet(); 319 } 320 } 321 } 322 } 323 324 private String getPortsDescription(boolean localPort) { 325 StringBuilder ports = new StringBuilder(); 326 for (Connector connector : this.tomcat.getService().findConnectors()) { 327 if (ports.length() != 0) { 328 ports.append(' '); 329 } 330 int port = localPort ? connector.getLocalPort() : connector.getPort(); 331 ports.append(port).append(" (").append(connector.getScheme()).append(')'); 332 } 333 return ports.toString(); 334 } 335 336 @Override 337 public int getPort() { 338 Connector connector = this.tomcat.getConnector(); 339 if (connector != null) { 340 return connector.getLocalPort(); 341 } 342 return 0; 343 } 344 345 private String getContextPath() { 346 return Arrays.stream(this.tomcat.getHost().findChildren()) 347 .filter(TomcatEmbeddedContext.class::isInstance) 348 .map(TomcatEmbeddedContext.class::cast) 349 .map(TomcatEmbeddedContext::getPath).collect(Collectors.joining(" ")); 350 } 351 352 /** 353 * Returns access to the underlying Tomcat server. 354 * @return the Tomcat server 355 */ 356 public Tomcat getTomcat() { 357 return this.tomcat; 358 } 359 360}