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.servlet.context;
018
019import java.util.Collection;
020import java.util.Collections;
021import java.util.EventListener;
022import java.util.HashMap;
023import java.util.LinkedHashSet;
024import java.util.Map;
025import java.util.Set;
026
027import javax.servlet.Filter;
028import javax.servlet.Servlet;
029import javax.servlet.ServletConfig;
030import javax.servlet.ServletContext;
031import javax.servlet.ServletException;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035
036import org.springframework.beans.BeansException;
037import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
038import org.springframework.beans.factory.config.Scope;
039import org.springframework.beans.factory.support.DefaultListableBeanFactory;
040import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
041import org.springframework.boot.web.server.WebServer;
042import org.springframework.boot.web.servlet.FilterRegistrationBean;
043import org.springframework.boot.web.servlet.ServletContextInitializer;
044import org.springframework.boot.web.servlet.ServletContextInitializerBeans;
045import org.springframework.boot.web.servlet.ServletRegistrationBean;
046import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
047import org.springframework.context.ApplicationContext;
048import org.springframework.context.ApplicationContextException;
049import org.springframework.core.io.Resource;
050import org.springframework.util.StringUtils;
051import org.springframework.web.context.ContextLoader;
052import org.springframework.web.context.ContextLoaderListener;
053import org.springframework.web.context.ServletContextAware;
054import org.springframework.web.context.WebApplicationContext;
055import org.springframework.web.context.support.GenericWebApplicationContext;
056import org.springframework.web.context.support.ServletContextAwareProcessor;
057import org.springframework.web.context.support.ServletContextResource;
058import org.springframework.web.context.support.ServletContextScope;
059import org.springframework.web.context.support.WebApplicationContextUtils;
060
061/**
062 * A {@link WebApplicationContext} that can be used to bootstrap itself from a contained
063 * {@link ServletWebServerFactory} bean.
064 * <p>
065 * This context will create, initialize and run an {@link WebServer} by searching for a
066 * single {@link ServletWebServerFactory} bean within the {@link ApplicationContext}
067 * itself. The {@link ServletWebServerFactory} is free to use standard Spring concepts
068 * (such as dependency injection, lifecycle callbacks and property placeholder variables).
069 * <p>
070 * In addition, any {@link Servlet} or {@link Filter} beans defined in the context will be
071 * automatically registered with the web server. In the case of a single Servlet bean, the
072 * '/' mapping will be used. If multiple Servlet beans are found then the lowercase bean
073 * name will be used as a mapping prefix. Any Servlet named 'dispatcherServlet' will
074 * always be mapped to '/'. Filter beans will be mapped to all URLs ('/*').
075 * <p>
076 * For more advanced configuration, the context can instead define beans that implement
077 * the {@link ServletContextInitializer} interface (most often
078 * {@link ServletRegistrationBean}s and/or {@link FilterRegistrationBean}s). To prevent
079 * double registration, the use of {@link ServletContextInitializer} beans will disable
080 * automatic Servlet and Filter bean registration.
081 * <p>
082 * Although this context can be used directly, most developers should consider using the
083 * {@link AnnotationConfigServletWebServerApplicationContext} or
084 * {@link XmlServletWebServerApplicationContext} variants.
085 *
086 * @author Phillip Webb
087 * @author Dave Syer
088 * @see AnnotationConfigServletWebServerApplicationContext
089 * @see XmlServletWebServerApplicationContext
090 * @see ServletWebServerFactory
091 */
092public class ServletWebServerApplicationContext extends GenericWebApplicationContext
093                implements ConfigurableWebServerApplicationContext {
094
095        private static final Log logger = LogFactory
096                        .getLog(ServletWebServerApplicationContext.class);
097
098        /**
099         * Constant value for the DispatcherServlet bean name. A Servlet bean with this name
100         * is deemed to be the "main" servlet and is automatically given a mapping of "/" by
101         * default. To change the default behavior you can use a
102         * {@link ServletRegistrationBean} or a different bean name.
103         */
104        public static final String DISPATCHER_SERVLET_NAME = "dispatcherServlet";
105
106        private volatile WebServer webServer;
107
108        private ServletConfig servletConfig;
109
110        private String serverNamespace;
111
112        /**
113         * Create a new {@link ServletWebServerApplicationContext}.
114         */
115        public ServletWebServerApplicationContext() {
116        }
117
118        /**
119         * Create a new {@link ServletWebServerApplicationContext} with the given
120         * {@code DefaultListableBeanFactory}.
121         * @param beanFactory the DefaultListableBeanFactory instance to use for this context
122         */
123        public ServletWebServerApplicationContext(DefaultListableBeanFactory beanFactory) {
124                super(beanFactory);
125        }
126
127        /**
128         * Register ServletContextAwareProcessor.
129         * @see ServletContextAwareProcessor
130         */
131        @Override
132        protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
133                beanFactory.addBeanPostProcessor(
134                                new WebApplicationContextServletContextAwareProcessor(this));
135                beanFactory.ignoreDependencyInterface(ServletContextAware.class);
136                registerWebApplicationScopes();
137        }
138
139        @Override
140        public final void refresh() throws BeansException, IllegalStateException {
141                try {
142                        super.refresh();
143                }
144                catch (RuntimeException ex) {
145                        stopAndReleaseWebServer();
146                        throw ex;
147                }
148        }
149
150        @Override
151        protected void onRefresh() {
152                super.onRefresh();
153                try {
154                        createWebServer();
155                }
156                catch (Throwable ex) {
157                        throw new ApplicationContextException("Unable to start web server", ex);
158                }
159        }
160
161        @Override
162        protected void finishRefresh() {
163                super.finishRefresh();
164                WebServer webServer = startWebServer();
165                if (webServer != null) {
166                        publishEvent(new ServletWebServerInitializedEvent(webServer, this));
167                }
168        }
169
170        @Override
171        protected void onClose() {
172                super.onClose();
173                stopAndReleaseWebServer();
174        }
175
176        private void createWebServer() {
177                WebServer webServer = this.webServer;
178                ServletContext servletContext = getServletContext();
179                if (webServer == null && servletContext == null) {
180                        ServletWebServerFactory factory = getWebServerFactory();
181                        this.webServer = factory.getWebServer(getSelfInitializer());
182                }
183                else if (servletContext != null) {
184                        try {
185                                getSelfInitializer().onStartup(servletContext);
186                        }
187                        catch (ServletException ex) {
188                                throw new ApplicationContextException("Cannot initialize servlet context",
189                                                ex);
190                        }
191                }
192                initPropertySources();
193        }
194
195        /**
196         * Returns the {@link ServletWebServerFactory} that should be used to create the
197         * embedded {@link WebServer}. By default this method searches for a suitable bean in
198         * the context itself.
199         * @return a {@link ServletWebServerFactory} (never {@code null})
200         */
201        protected ServletWebServerFactory getWebServerFactory() {
202                // Use bean names so that we don't consider the hierarchy
203                String[] beanNames = getBeanFactory()
204                                .getBeanNamesForType(ServletWebServerFactory.class);
205                if (beanNames.length == 0) {
206                        throw new ApplicationContextException(
207                                        "Unable to start ServletWebServerApplicationContext due to missing "
208                                                        + "ServletWebServerFactory bean.");
209                }
210                if (beanNames.length > 1) {
211                        throw new ApplicationContextException(
212                                        "Unable to start ServletWebServerApplicationContext due to multiple "
213                                                        + "ServletWebServerFactory beans : "
214                                                        + StringUtils.arrayToCommaDelimitedString(beanNames));
215                }
216                return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
217        }
218
219        /**
220         * Returns the {@link ServletContextInitializer} that will be used to complete the
221         * setup of this {@link WebApplicationContext}.
222         * @return the self initializer
223         * @see #prepareWebApplicationContext(ServletContext)
224         */
225        private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
226                return this::selfInitialize;
227        }
228
229        private void selfInitialize(ServletContext servletContext) throws ServletException {
230                prepareWebApplicationContext(servletContext);
231                registerApplicationScope(servletContext);
232                WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(),
233                                servletContext);
234                for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
235                        beans.onStartup(servletContext);
236                }
237        }
238
239        private void registerApplicationScope(ServletContext servletContext) {
240                ServletContextScope appScope = new ServletContextScope(servletContext);
241                getBeanFactory().registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
242                // Register as ServletContext attribute, for ContextCleanupListener to detect it.
243                servletContext.setAttribute(ServletContextScope.class.getName(), appScope);
244        }
245
246        private void registerWebApplicationScopes() {
247                ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(
248                                getBeanFactory());
249                WebApplicationContextUtils.registerWebApplicationScopes(getBeanFactory());
250                existingScopes.restore();
251        }
252
253        /**
254         * Returns {@link ServletContextInitializer}s that should be used with the embedded
255         * web server. By default this method will first attempt to find
256         * {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain
257         * {@link EventListener} beans.
258         * @return the servlet initializer beans
259         */
260        protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
261                return new ServletContextInitializerBeans(getBeanFactory());
262        }
263
264        /**
265         * Prepare the {@link WebApplicationContext} with the given fully loaded
266         * {@link ServletContext}. This method is usually called from
267         * {@link ServletContextInitializer#onStartup(ServletContext)} and is similar to the
268         * functionality usually provided by a {@link ContextLoaderListener}.
269         * @param servletContext the operational servlet context
270         */
271        protected void prepareWebApplicationContext(ServletContext servletContext) {
272                Object rootContext = servletContext.getAttribute(
273                                WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
274                if (rootContext != null) {
275                        if (rootContext == this) {
276                                throw new IllegalStateException(
277                                                "Cannot initialize context because there is already a root application context present - "
278                                                                + "check whether you have multiple ServletContextInitializers!");
279                        }
280                        return;
281                }
282                Log logger = LogFactory.getLog(ContextLoader.class);
283                servletContext.log("Initializing Spring embedded WebApplicationContext");
284                try {
285                        servletContext.setAttribute(
286                                        WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);
287                        if (logger.isDebugEnabled()) {
288                                logger.debug(
289                                                "Published root WebApplicationContext as ServletContext attribute with name ["
290                                                                + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
291                                                                + "]");
292                        }
293                        setServletContext(servletContext);
294                        if (logger.isInfoEnabled()) {
295                                long elapsedTime = System.currentTimeMillis() - getStartupDate();
296                                logger.info("Root WebApplicationContext: initialization completed in "
297                                                + elapsedTime + " ms");
298                        }
299                }
300                catch (RuntimeException | Error ex) {
301                        logger.error("Context initialization failed", ex);
302                        servletContext.setAttribute(
303                                        WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
304                        throw ex;
305                }
306        }
307
308        private WebServer startWebServer() {
309                WebServer webServer = this.webServer;
310                if (webServer != null) {
311                        webServer.start();
312                }
313                return webServer;
314        }
315
316        private void stopAndReleaseWebServer() {
317                WebServer webServer = this.webServer;
318                if (webServer != null) {
319                        try {
320                                webServer.stop();
321                                this.webServer = null;
322                        }
323                        catch (Exception ex) {
324                                throw new IllegalStateException(ex);
325                        }
326                }
327        }
328
329        @Override
330        protected Resource getResourceByPath(String path) {
331                if (getServletContext() == null) {
332                        return new ClassPathContextResource(path, getClassLoader());
333                }
334                return new ServletContextResource(getServletContext(), path);
335        }
336
337        @Override
338        public String getServerNamespace() {
339                return this.serverNamespace;
340        }
341
342        @Override
343        public void setServerNamespace(String serverNamespace) {
344                this.serverNamespace = serverNamespace;
345        }
346
347        @Override
348        public void setServletConfig(ServletConfig servletConfig) {
349                this.servletConfig = servletConfig;
350        }
351
352        @Override
353        public ServletConfig getServletConfig() {
354                return this.servletConfig;
355        }
356
357        /**
358         * Returns the {@link WebServer} that was created by the context or {@code null} if
359         * the server has not yet been created.
360         * @return the embedded web server
361         */
362        @Override
363        public WebServer getWebServer() {
364                return this.webServer;
365        }
366
367        /**
368         * Utility class to store and restore any user defined scopes. This allow scopes to be
369         * registered in an ApplicationContextInitializer in the same way as they would in a
370         * classic non-embedded web application context.
371         */
372        public static class ExistingWebApplicationScopes {
373
374                private static final Set<String> SCOPES;
375
376                static {
377                        Set<String> scopes = new LinkedHashSet<>();
378                        scopes.add(WebApplicationContext.SCOPE_REQUEST);
379                        scopes.add(WebApplicationContext.SCOPE_SESSION);
380                        SCOPES = Collections.unmodifiableSet(scopes);
381                }
382
383                private final ConfigurableListableBeanFactory beanFactory;
384
385                private final Map<String, Scope> scopes = new HashMap<>();
386
387                public ExistingWebApplicationScopes(ConfigurableListableBeanFactory beanFactory) {
388                        this.beanFactory = beanFactory;
389                        for (String scopeName : SCOPES) {
390                                Scope scope = beanFactory.getRegisteredScope(scopeName);
391                                if (scope != null) {
392                                        this.scopes.put(scopeName, scope);
393                                }
394                        }
395                }
396
397                public void restore() {
398                        this.scopes.forEach((key, value) -> {
399                                if (logger.isInfoEnabled()) {
400                                        logger.info("Restoring user defined scope " + key);
401                                }
402                                this.beanFactory.registerScope(key, value);
403                        });
404                }
405
406        }
407
408}