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}