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.nio.charset.Charset; 021import java.nio.charset.StandardCharsets; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.List; 027 028import org.apache.catalina.Context; 029import org.apache.catalina.Engine; 030import org.apache.catalina.Host; 031import org.apache.catalina.LifecycleListener; 032import org.apache.catalina.Valve; 033import org.apache.catalina.connector.Connector; 034import org.apache.catalina.core.AprLifecycleListener; 035import org.apache.catalina.loader.WebappLoader; 036import org.apache.catalina.startup.Tomcat; 037import org.apache.coyote.AbstractProtocol; 038import org.apache.coyote.http2.Http2Protocol; 039import org.apache.tomcat.util.scan.StandardJarScanFilter; 040 041import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; 042import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; 043import org.springframework.boot.web.server.WebServer; 044import org.springframework.http.server.reactive.HttpHandler; 045import org.springframework.http.server.reactive.TomcatHttpHandlerAdapter; 046import org.springframework.util.Assert; 047import org.springframework.util.ClassUtils; 048import org.springframework.util.StringUtils; 049 050/** 051 * {@link ReactiveWebServerFactory} that can be used to create a {@link TomcatWebServer}. 052 * 053 * @author Brian Clozel 054 * @since 2.0.0 055 */ 056public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFactory 057 implements ConfigurableTomcatWebServerFactory { 058 059 private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 060 061 /** 062 * The class name of default protocol used. 063 */ 064 public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol"; 065 066 private File baseDirectory; 067 068 private List<Valve> engineValves = new ArrayList<>(); 069 070 private List<LifecycleListener> contextLifecycleListeners = new ArrayList<>( 071 Collections.singleton(new AprLifecycleListener())); 072 073 private List<TomcatContextCustomizer> tomcatContextCustomizers = new ArrayList<>(); 074 075 private List<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new ArrayList<>(); 076 077 private String protocol = DEFAULT_PROTOCOL; 078 079 private Charset uriEncoding = DEFAULT_CHARSET; 080 081 private int backgroundProcessorDelay; 082 083 /** 084 * Create a new {@link TomcatServletWebServerFactory} instance. 085 */ 086 public TomcatReactiveWebServerFactory() { 087 } 088 089 /** 090 * Create a new {@link TomcatServletWebServerFactory} that listens for requests using 091 * the specified port. 092 * @param port the port to listen on 093 */ 094 public TomcatReactiveWebServerFactory(int port) { 095 super(port); 096 } 097 098 @Override 099 public WebServer getWebServer(HttpHandler httpHandler) { 100 Tomcat tomcat = new Tomcat(); 101 File baseDir = (this.baseDirectory != null) ? this.baseDirectory 102 : createTempDir("tomcat"); 103 tomcat.setBaseDir(baseDir.getAbsolutePath()); 104 Connector connector = new Connector(this.protocol); 105 tomcat.getService().addConnector(connector); 106 customizeConnector(connector); 107 tomcat.setConnector(connector); 108 tomcat.getHost().setAutoDeploy(false); 109 configureEngine(tomcat.getEngine()); 110 TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); 111 prepareContext(tomcat.getHost(), servlet); 112 return new TomcatWebServer(tomcat, getPort() >= 0); 113 } 114 115 private void configureEngine(Engine engine) { 116 engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay); 117 for (Valve valve : this.engineValves) { 118 engine.getPipeline().addValve(valve); 119 } 120 } 121 122 protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet) { 123 File docBase = createTempDir("tomcat-docbase"); 124 TomcatEmbeddedContext context = new TomcatEmbeddedContext(); 125 context.setPath(""); 126 context.setDocBase(docBase.getAbsolutePath()); 127 context.addLifecycleListener(new Tomcat.FixContextListener()); 128 context.setParentClassLoader(ClassUtils.getDefaultClassLoader()); 129 skipAllTldScanning(context); 130 WebappLoader loader = new WebappLoader(context.getParentClassLoader()); 131 loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName()); 132 loader.setDelegate(true); 133 context.setLoader(loader); 134 Tomcat.addServlet(context, "httpHandlerServlet", servlet).setAsyncSupported(true); 135 context.addServletMappingDecoded("/", "httpHandlerServlet"); 136 host.addChild(context); 137 configureContext(context); 138 } 139 140 private void skipAllTldScanning(TomcatEmbeddedContext context) { 141 StandardJarScanFilter filter = new StandardJarScanFilter(); 142 filter.setTldSkip("*.jar"); 143 context.getJarScanner().setJarScanFilter(filter); 144 } 145 146 /** 147 * Configure the Tomcat {@link Context}. 148 * @param context the Tomcat context 149 */ 150 protected void configureContext(Context context) { 151 this.contextLifecycleListeners.forEach(context::addLifecycleListener); 152 this.tomcatContextCustomizers 153 .forEach((customizer) -> customizer.customize(context)); 154 } 155 156 protected void customizeConnector(Connector connector) { 157 int port = (getPort() >= 0) ? getPort() : 0; 158 connector.setPort(port); 159 if (StringUtils.hasText(this.getServerHeader())) { 160 connector.setAttribute("server", this.getServerHeader()); 161 } 162 if (connector.getProtocolHandler() instanceof AbstractProtocol) { 163 customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler()); 164 } 165 if (getUriEncoding() != null) { 166 connector.setURIEncoding(getUriEncoding().name()); 167 } 168 // Don't bind to the socket prematurely if ApplicationContext is slow to start 169 connector.setProperty("bindOnInit", "false"); 170 if (getSsl() != null && getSsl().isEnabled()) { 171 customizeSsl(connector); 172 } 173 TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer( 174 getCompression()); 175 compression.customize(connector); 176 for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) { 177 customizer.customize(connector); 178 } 179 } 180 181 private void customizeProtocol(AbstractProtocol<?> protocol) { 182 if (getAddress() != null) { 183 protocol.setAddress(getAddress()); 184 } 185 } 186 187 private void customizeSsl(Connector connector) { 188 new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector); 189 if (getHttp2() != null && getHttp2().isEnabled()) { 190 connector.addUpgradeProtocol(new Http2Protocol()); 191 } 192 } 193 194 @Override 195 public void setBaseDirectory(File baseDirectory) { 196 this.baseDirectory = baseDirectory; 197 } 198 199 @Override 200 public void setBackgroundProcessorDelay(int delay) { 201 this.backgroundProcessorDelay = delay; 202 } 203 204 /** 205 * Set {@link TomcatContextCustomizer}s that should be applied to the Tomcat 206 * {@link Context}. Calling this method will replace any existing customizers. 207 * @param tomcatContextCustomizers the customizers to set 208 */ 209 public void setTomcatContextCustomizers( 210 Collection<? extends TomcatContextCustomizer> tomcatContextCustomizers) { 211 Assert.notNull(tomcatContextCustomizers, 212 "TomcatContextCustomizers must not be null"); 213 this.tomcatContextCustomizers = new ArrayList<>(tomcatContextCustomizers); 214 } 215 216 /** 217 * Returns a mutable collection of the {@link TomcatContextCustomizer}s that will be 218 * applied to the Tomcat {@link Context}. 219 * @return the listeners that will be applied 220 */ 221 public Collection<TomcatContextCustomizer> getTomcatContextCustomizers() { 222 return this.tomcatContextCustomizers; 223 } 224 225 /** 226 * Add {@link TomcatContextCustomizer}s that should be added to the Tomcat 227 * {@link Context}. 228 * @param tomcatContextCustomizers the customizers to add 229 */ 230 @Override 231 public void addContextCustomizers( 232 TomcatContextCustomizer... tomcatContextCustomizers) { 233 Assert.notNull(tomcatContextCustomizers, 234 "TomcatContextCustomizers must not be null"); 235 this.tomcatContextCustomizers.addAll(Arrays.asList(tomcatContextCustomizers)); 236 } 237 238 /** 239 * Set {@link TomcatConnectorCustomizer}s that should be applied to the Tomcat 240 * {@link Connector}. Calling this method will replace any existing customizers. 241 * @param tomcatConnectorCustomizers the customizers to set 242 */ 243 public void setTomcatConnectorCustomizers( 244 Collection<? extends TomcatConnectorCustomizer> tomcatConnectorCustomizers) { 245 Assert.notNull(tomcatConnectorCustomizers, 246 "TomcatConnectorCustomizers must not be null"); 247 this.tomcatConnectorCustomizers = new ArrayList<>(tomcatConnectorCustomizers); 248 } 249 250 /** 251 * Add {@link TomcatConnectorCustomizer}s that should be added to the Tomcat 252 * {@link Connector}. 253 * @param tomcatConnectorCustomizers the customizers to add 254 */ 255 @Override 256 public void addConnectorCustomizers( 257 TomcatConnectorCustomizer... tomcatConnectorCustomizers) { 258 Assert.notNull(tomcatConnectorCustomizers, 259 "TomcatConnectorCustomizers must not be null"); 260 this.tomcatConnectorCustomizers.addAll(Arrays.asList(tomcatConnectorCustomizers)); 261 } 262 263 /** 264 * Returns a mutable collection of the {@link TomcatConnectorCustomizer}s that will be 265 * applied to the Tomcat {@link Connector}. 266 * @return the customizers that will be applied 267 */ 268 public Collection<TomcatConnectorCustomizer> getTomcatConnectorCustomizers() { 269 return this.tomcatConnectorCustomizers; 270 } 271 272 @Override 273 public void addEngineValves(Valve... engineValves) { 274 Assert.notNull(engineValves, "Valves must not be null"); 275 this.engineValves.addAll(Arrays.asList(engineValves)); 276 } 277 278 /** 279 * Returns a mutable collection of the {@link Valve}s that will be applied to the 280 * Tomcat {@link Engine}. 281 * @return the engine valves that will be applied 282 */ 283 public List<Valve> getEngineValves() { 284 return this.engineValves; 285 } 286 287 /** 288 * Set the character encoding to use for URL decoding. If not specified 'UTF-8' will 289 * be used. 290 * @param uriEncoding the uri encoding to set 291 */ 292 @Override 293 public void setUriEncoding(Charset uriEncoding) { 294 this.uriEncoding = uriEncoding; 295 } 296 297 /** 298 * Returns the character encoding to use for URL decoding. 299 * @return the URI encoding 300 */ 301 public Charset getUriEncoding() { 302 return this.uriEncoding; 303 } 304 305 /** 306 * Set {@link LifecycleListener}s that should be applied to the Tomcat 307 * {@link Context}. Calling this method will replace any existing listeners. 308 * @param contextLifecycleListeners the listeners to set 309 */ 310 public void setContextLifecycleListeners( 311 Collection<? extends LifecycleListener> contextLifecycleListeners) { 312 Assert.notNull(contextLifecycleListeners, 313 "ContextLifecycleListeners must not be null"); 314 this.contextLifecycleListeners = new ArrayList<>(contextLifecycleListeners); 315 } 316 317 /** 318 * Returns a mutable collection of the {@link LifecycleListener}s that will be applied 319 * to the Tomcat {@link Context}. 320 * @return the context lifecycle listeners that will be applied 321 */ 322 public Collection<LifecycleListener> getContextLifecycleListeners() { 323 return this.contextLifecycleListeners; 324 } 325 326 /** 327 * Add {@link LifecycleListener}s that should be added to the Tomcat {@link Context}. 328 * @param contextLifecycleListeners the listeners to add 329 */ 330 public void addContextLifecycleListeners( 331 LifecycleListener... contextLifecycleListeners) { 332 Assert.notNull(contextLifecycleListeners, 333 "ContextLifecycleListeners must not be null"); 334 this.contextLifecycleListeners.addAll(Arrays.asList(contextLifecycleListeners)); 335 } 336 337 /** 338 * Factory method called to create the {@link TomcatWebServer}. Subclasses can 339 * override this method to return a different {@link TomcatWebServer} or apply 340 * additional processing to the Tomcat server. 341 * @param tomcat the Tomcat server. 342 * @return a new {@link TomcatWebServer} instance 343 */ 344 protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { 345 return new TomcatWebServer(tomcat, getPort() >= 0); 346 } 347 348 /** 349 * The Tomcat protocol to use when create the {@link Connector}. 350 * @param protocol the protocol 351 * @see Connector#Connector(String) 352 */ 353 public void setProtocol(String protocol) { 354 Assert.hasLength(protocol, "Protocol must not be empty"); 355 this.protocol = protocol; 356 } 357 358}