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.jetty; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.InetSocketAddress; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.nio.channels.ReadableByteChannel; 026import java.time.Duration; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.List; 031 032import org.eclipse.jetty.http.MimeTypes; 033import org.eclipse.jetty.server.AbstractConnector; 034import org.eclipse.jetty.server.ConnectionFactory; 035import org.eclipse.jetty.server.Connector; 036import org.eclipse.jetty.server.Handler; 037import org.eclipse.jetty.server.HttpConfiguration; 038import org.eclipse.jetty.server.Server; 039import org.eclipse.jetty.server.ServerConnector; 040import org.eclipse.jetty.server.handler.ErrorHandler; 041import org.eclipse.jetty.server.handler.HandlerWrapper; 042import org.eclipse.jetty.server.session.DefaultSessionCache; 043import org.eclipse.jetty.server.session.FileSessionDataStore; 044import org.eclipse.jetty.server.session.SessionHandler; 045import org.eclipse.jetty.servlet.ErrorPageErrorHandler; 046import org.eclipse.jetty.servlet.ServletHolder; 047import org.eclipse.jetty.servlet.ServletMapping; 048import org.eclipse.jetty.util.resource.JarResource; 049import org.eclipse.jetty.util.resource.Resource; 050import org.eclipse.jetty.util.resource.ResourceCollection; 051import org.eclipse.jetty.util.thread.ThreadPool; 052import org.eclipse.jetty.webapp.AbstractConfiguration; 053import org.eclipse.jetty.webapp.Configuration; 054import org.eclipse.jetty.webapp.WebAppContext; 055 056import org.springframework.boot.web.server.ErrorPage; 057import org.springframework.boot.web.server.MimeMappings; 058import org.springframework.boot.web.server.WebServer; 059import org.springframework.boot.web.servlet.ServletContextInitializer; 060import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; 061import org.springframework.boot.web.servlet.server.ServletWebServerFactory; 062import org.springframework.context.ResourceLoaderAware; 063import org.springframework.core.io.ResourceLoader; 064import org.springframework.util.Assert; 065import org.springframework.util.StringUtils; 066 067/** 068 * {@link ServletWebServerFactory} that can be used to create a {@link JettyWebServer}. 069 * Can be initialized using Spring's {@link ServletContextInitializer}s or Jetty 070 * {@link Configuration}s. 071 * <p> 072 * Unless explicitly configured otherwise this factory will create servers that listen for 073 * HTTP requests on port 8080. 074 * 075 * @author Phillip Webb 076 * @author Dave Syer 077 * @author Andrey Hihlovskiy 078 * @author Andy Wilkinson 079 * @author EddĂș MelĂ©ndez 080 * @author Venil Noronha 081 * @author Henri Kerola 082 * @since 2.0.0 083 * @see #setPort(int) 084 * @see #setConfigurations(Collection) 085 * @see JettyWebServer 086 */ 087public class JettyServletWebServerFactory extends AbstractServletWebServerFactory 088 implements ConfigurableJettyWebServerFactory, ResourceLoaderAware { 089 090 private List<Configuration> configurations = new ArrayList<>(); 091 092 private boolean useForwardHeaders; 093 094 /** 095 * The number of acceptor threads to use. 096 */ 097 private int acceptors = -1; 098 099 /** 100 * The number of selector threads to use. 101 */ 102 private int selectors = -1; 103 104 private List<JettyServerCustomizer> jettyServerCustomizers = new ArrayList<>(); 105 106 private ResourceLoader resourceLoader; 107 108 private ThreadPool threadPool; 109 110 /** 111 * Create a new {@link JettyServletWebServerFactory} instance. 112 */ 113 public JettyServletWebServerFactory() { 114 } 115 116 /** 117 * Create a new {@link JettyServletWebServerFactory} that listens for requests using 118 * the specified port. 119 * @param port the port to listen on 120 */ 121 public JettyServletWebServerFactory(int port) { 122 super(port); 123 } 124 125 /** 126 * Create a new {@link JettyServletWebServerFactory} with the specified context path 127 * and port. 128 * @param contextPath the root context path 129 * @param port the port to listen on 130 */ 131 public JettyServletWebServerFactory(String contextPath, int port) { 132 super(contextPath, port); 133 } 134 135 @Override 136 public WebServer getWebServer(ServletContextInitializer... initializers) { 137 JettyEmbeddedWebAppContext context = new JettyEmbeddedWebAppContext(); 138 int port = (getPort() >= 0) ? getPort() : 0; 139 InetSocketAddress address = new InetSocketAddress(getAddress(), port); 140 Server server = createServer(address); 141 configureWebAppContext(context, initializers); 142 server.setHandler(addHandlerWrappers(context)); 143 this.logger.info("Server initialized with port: " + port); 144 if (getSsl() != null && getSsl().isEnabled()) { 145 customizeSsl(server, address); 146 } 147 for (JettyServerCustomizer customizer : getServerCustomizers()) { 148 customizer.customize(server); 149 } 150 if (this.useForwardHeaders) { 151 new ForwardHeadersCustomizer().customize(server); 152 } 153 return getJettyWebServer(server); 154 } 155 156 private Server createServer(InetSocketAddress address) { 157 Server server = new Server(getThreadPool()); 158 server.setConnectors(new Connector[] { createConnector(address, server) }); 159 return server; 160 } 161 162 private AbstractConnector createConnector(InetSocketAddress address, Server server) { 163 ServerConnector connector = new ServerConnector(server, this.acceptors, 164 this.selectors); 165 connector.setHost(address.getHostString()); 166 connector.setPort(address.getPort()); 167 for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) { 168 if (connectionFactory instanceof HttpConfiguration.ConnectionFactory) { 169 ((HttpConfiguration.ConnectionFactory) connectionFactory) 170 .getHttpConfiguration().setSendServerVersion(false); 171 } 172 } 173 return connector; 174 } 175 176 private Handler addHandlerWrappers(Handler handler) { 177 if (getCompression() != null && getCompression().getEnabled()) { 178 handler = applyWrapper(handler, 179 JettyHandlerWrappers.createGzipHandlerWrapper(getCompression())); 180 } 181 if (StringUtils.hasText(getServerHeader())) { 182 handler = applyWrapper(handler, JettyHandlerWrappers 183 .createServerHeaderHandlerWrapper(getServerHeader())); 184 } 185 return handler; 186 } 187 188 private Handler applyWrapper(Handler handler, HandlerWrapper wrapper) { 189 wrapper.setHandler(handler); 190 return wrapper; 191 } 192 193 private void customizeSsl(Server server, InetSocketAddress address) { 194 new SslServerCustomizer(address, getSsl(), getSslStoreProvider(), getHttp2()) 195 .customize(server); 196 } 197 198 /** 199 * Configure the given Jetty {@link WebAppContext} for use. 200 * @param context the context to configure 201 * @param initializers the set of initializers to apply 202 */ 203 protected final void configureWebAppContext(WebAppContext context, 204 ServletContextInitializer... initializers) { 205 Assert.notNull(context, "Context must not be null"); 206 context.setTempDirectory(getTempDirectory()); 207 if (this.resourceLoader != null) { 208 context.setClassLoader(this.resourceLoader.getClassLoader()); 209 } 210 String contextPath = getContextPath(); 211 context.setContextPath(StringUtils.hasLength(contextPath) ? contextPath : "/"); 212 context.setDisplayName(getDisplayName()); 213 configureDocumentRoot(context); 214 if (isRegisterDefaultServlet()) { 215 addDefaultServlet(context); 216 } 217 if (shouldRegisterJspServlet()) { 218 addJspServlet(context); 219 context.addBean(new JasperInitializer(context), true); 220 } 221 addLocaleMappings(context); 222 ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); 223 Configuration[] configurations = getWebAppContextConfigurations(context, 224 initializersToUse); 225 context.setConfigurations(configurations); 226 context.setThrowUnavailableOnStartupException(true); 227 configureSession(context); 228 postProcessWebAppContext(context); 229 } 230 231 private void configureSession(WebAppContext context) { 232 SessionHandler handler = context.getSessionHandler(); 233 Duration sessionTimeout = getSession().getTimeout(); 234 handler.setMaxInactiveInterval( 235 isNegative(sessionTimeout) ? -1 : (int) sessionTimeout.getSeconds()); 236 if (getSession().isPersistent()) { 237 DefaultSessionCache cache = new DefaultSessionCache(handler); 238 FileSessionDataStore store = new FileSessionDataStore(); 239 store.setStoreDir(getValidSessionStoreDir()); 240 cache.setSessionDataStore(store); 241 handler.setSessionCache(cache); 242 } 243 } 244 245 private boolean isNegative(Duration sessionTimeout) { 246 return sessionTimeout == null || sessionTimeout.isNegative(); 247 } 248 249 private void addLocaleMappings(WebAppContext context) { 250 getLocaleCharsetMappings().forEach((locale, charset) -> context 251 .addLocaleEncoding(locale.toString(), charset.toString())); 252 } 253 254 private File getTempDirectory() { 255 String temp = System.getProperty("java.io.tmpdir"); 256 return (temp != null) ? new File(temp) : null; 257 } 258 259 private void configureDocumentRoot(WebAppContext handler) { 260 File root = getValidDocumentRoot(); 261 File docBase = (root != null) ? root : createTempDir("jetty-docbase"); 262 try { 263 List<Resource> resources = new ArrayList<>(); 264 Resource rootResource = (docBase.isDirectory() 265 ? Resource.newResource(docBase.getCanonicalFile()) 266 : JarResource.newJarResource(Resource.newResource(docBase))); 267 resources.add((root != null) ? new LoaderHidingResource(rootResource) 268 : rootResource); 269 for (URL resourceJarUrl : this.getUrlsOfJarsWithMetaInfResources()) { 270 Resource resource = createResource(resourceJarUrl); 271 if (resource.exists() && resource.isDirectory()) { 272 resources.add(resource); 273 } 274 } 275 handler.setBaseResource( 276 new ResourceCollection(resources.toArray(new Resource[0]))); 277 } 278 catch (Exception ex) { 279 throw new IllegalStateException(ex); 280 } 281 } 282 283 private Resource createResource(URL url) throws Exception { 284 if ("file".equals(url.getProtocol())) { 285 File file = new File(url.toURI()); 286 if (file.isFile()) { 287 return Resource.newResource("jar:" + url + "!/META-INF/resources"); 288 } 289 } 290 return Resource.newResource(url + "META-INF/resources"); 291 } 292 293 /** 294 * Add Jetty's {@code DefaultServlet} to the given {@link WebAppContext}. 295 * @param context the jetty {@link WebAppContext} 296 */ 297 protected final void addDefaultServlet(WebAppContext context) { 298 Assert.notNull(context, "Context must not be null"); 299 ServletHolder holder = new ServletHolder(); 300 holder.setName("default"); 301 holder.setClassName("org.eclipse.jetty.servlet.DefaultServlet"); 302 holder.setInitParameter("dirAllowed", "false"); 303 holder.setInitOrder(1); 304 context.getServletHandler().addServletWithMapping(holder, "/"); 305 context.getServletHandler().getServletMapping("/").setDefault(true); 306 } 307 308 /** 309 * Add Jetty's {@code JspServlet} to the given {@link WebAppContext}. 310 * @param context the jetty {@link WebAppContext} 311 */ 312 protected final void addJspServlet(WebAppContext context) { 313 Assert.notNull(context, "Context must not be null"); 314 ServletHolder holder = new ServletHolder(); 315 holder.setName("jsp"); 316 holder.setClassName(getJsp().getClassName()); 317 holder.setInitParameter("fork", "false"); 318 holder.setInitParameters(getJsp().getInitParameters()); 319 holder.setInitOrder(3); 320 context.getServletHandler().addServlet(holder); 321 ServletMapping mapping = new ServletMapping(); 322 mapping.setServletName("jsp"); 323 mapping.setPathSpecs(new String[] { "*.jsp", "*.jspx" }); 324 context.getServletHandler().addServletMapping(mapping); 325 } 326 327 /** 328 * Return the Jetty {@link Configuration}s that should be applied to the server. 329 * @param webAppContext the Jetty {@link WebAppContext} 330 * @param initializers the {@link ServletContextInitializer}s to apply 331 * @return configurations to apply 332 */ 333 protected Configuration[] getWebAppContextConfigurations(WebAppContext webAppContext, 334 ServletContextInitializer... initializers) { 335 List<Configuration> configurations = new ArrayList<>(); 336 configurations.add( 337 getServletContextInitializerConfiguration(webAppContext, initializers)); 338 configurations.addAll(getConfigurations()); 339 configurations.add(getErrorPageConfiguration()); 340 configurations.add(getMimeTypeConfiguration()); 341 return configurations.toArray(new Configuration[0]); 342 } 343 344 /** 345 * Create a configuration object that adds error handlers. 346 * @return a configuration object for adding error pages 347 */ 348 private Configuration getErrorPageConfiguration() { 349 return new AbstractConfiguration() { 350 351 @Override 352 public void configure(WebAppContext context) throws Exception { 353 ErrorHandler errorHandler = context.getErrorHandler(); 354 context.setErrorHandler(new JettyEmbeddedErrorHandler(errorHandler)); 355 addJettyErrorPages(errorHandler, getErrorPages()); 356 } 357 358 }; 359 } 360 361 /** 362 * Create a configuration object that adds mime type mappings. 363 * @return a configuration object for adding mime type mappings 364 */ 365 private Configuration getMimeTypeConfiguration() { 366 return new AbstractConfiguration() { 367 368 @Override 369 public void configure(WebAppContext context) throws Exception { 370 MimeTypes mimeTypes = context.getMimeTypes(); 371 for (MimeMappings.Mapping mapping : getMimeMappings()) { 372 mimeTypes.addMimeMapping(mapping.getExtension(), 373 mapping.getMimeType()); 374 } 375 } 376 377 }; 378 } 379 380 /** 381 * Return a Jetty {@link Configuration} that will invoke the specified 382 * {@link ServletContextInitializer}s. By default this method will return a 383 * {@link ServletContextInitializerConfiguration}. 384 * @param webAppContext the Jetty {@link WebAppContext} 385 * @param initializers the {@link ServletContextInitializer}s to apply 386 * @return the {@link Configuration} instance 387 */ 388 protected Configuration getServletContextInitializerConfiguration( 389 WebAppContext webAppContext, ServletContextInitializer... initializers) { 390 return new ServletContextInitializerConfiguration(initializers); 391 } 392 393 /** 394 * Post process the Jetty {@link WebAppContext} before it's used with the Jetty 395 * Server. Subclasses can override this method to apply additional processing to the 396 * {@link WebAppContext}. 397 * @param webAppContext the Jetty {@link WebAppContext} 398 */ 399 protected void postProcessWebAppContext(WebAppContext webAppContext) { 400 } 401 402 /** 403 * Factory method called to create the {@link JettyWebServer}. Subclasses can override 404 * this method to return a different {@link JettyWebServer} or apply additional 405 * processing to the Jetty server. 406 * @param server the Jetty server. 407 * @return a new {@link JettyWebServer} instance 408 */ 409 protected JettyWebServer getJettyWebServer(Server server) { 410 return new JettyWebServer(server, getPort() >= 0); 411 } 412 413 @Override 414 public void setResourceLoader(ResourceLoader resourceLoader) { 415 this.resourceLoader = resourceLoader; 416 } 417 418 @Override 419 public void setUseForwardHeaders(boolean useForwardHeaders) { 420 this.useForwardHeaders = useForwardHeaders; 421 } 422 423 @Override 424 public void setAcceptors(int acceptors) { 425 this.acceptors = acceptors; 426 } 427 428 @Override 429 public void setSelectors(int selectors) { 430 this.selectors = selectors; 431 } 432 433 /** 434 * Sets {@link JettyServerCustomizer}s that will be applied to the {@link Server} 435 * before it is started. Calling this method will replace any existing customizers. 436 * @param customizers the Jetty customizers to apply 437 */ 438 public void setServerCustomizers( 439 Collection<? extends JettyServerCustomizer> customizers) { 440 Assert.notNull(customizers, "Customizers must not be null"); 441 this.jettyServerCustomizers = new ArrayList<>(customizers); 442 } 443 444 /** 445 * Returns a mutable collection of Jetty {@link JettyServerCustomizer}s that will be 446 * applied to the {@link Server} before the it is created. 447 * @return the {@link JettyServerCustomizer}s 448 */ 449 public Collection<JettyServerCustomizer> getServerCustomizers() { 450 return this.jettyServerCustomizers; 451 } 452 453 @Override 454 public void addServerCustomizers(JettyServerCustomizer... customizers) { 455 Assert.notNull(customizers, "Customizers must not be null"); 456 this.jettyServerCustomizers.addAll(Arrays.asList(customizers)); 457 } 458 459 /** 460 * Sets Jetty {@link Configuration}s that will be applied to the {@link WebAppContext} 461 * before the server is created. Calling this method will replace any existing 462 * configurations. 463 * @param configurations the Jetty configurations to apply 464 */ 465 public void setConfigurations(Collection<? extends Configuration> configurations) { 466 Assert.notNull(configurations, "Configurations must not be null"); 467 this.configurations = new ArrayList<>(configurations); 468 } 469 470 /** 471 * Returns a mutable collection of Jetty {@link Configuration}s that will be applied 472 * to the {@link WebAppContext} before the server is created. 473 * @return the Jetty {@link Configuration}s 474 */ 475 public Collection<Configuration> getConfigurations() { 476 return this.configurations; 477 } 478 479 /** 480 * Add {@link Configuration}s that will be applied to the {@link WebAppContext} before 481 * the server is started. 482 * @param configurations the configurations to add 483 */ 484 public void addConfigurations(Configuration... configurations) { 485 Assert.notNull(configurations, "Configurations must not be null"); 486 this.configurations.addAll(Arrays.asList(configurations)); 487 } 488 489 /** 490 * Returns a Jetty {@link ThreadPool} that should be used by the {@link Server}. 491 * @return a Jetty {@link ThreadPool} or {@code null} 492 */ 493 public ThreadPool getThreadPool() { 494 return this.threadPool; 495 } 496 497 /** 498 * Set a Jetty {@link ThreadPool} that should be used by the {@link Server}. If set to 499 * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly. 500 * @param threadPool a Jetty ThreadPool to be used 501 */ 502 public void setThreadPool(ThreadPool threadPool) { 503 this.threadPool = threadPool; 504 } 505 506 private void addJettyErrorPages(ErrorHandler errorHandler, 507 Collection<ErrorPage> errorPages) { 508 if (errorHandler instanceof ErrorPageErrorHandler) { 509 ErrorPageErrorHandler handler = (ErrorPageErrorHandler) errorHandler; 510 for (ErrorPage errorPage : errorPages) { 511 if (errorPage.isGlobal()) { 512 handler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, 513 errorPage.getPath()); 514 } 515 else { 516 if (errorPage.getExceptionName() != null) { 517 handler.addErrorPage(errorPage.getExceptionName(), 518 errorPage.getPath()); 519 } 520 else { 521 handler.addErrorPage(errorPage.getStatusCode(), 522 errorPage.getPath()); 523 } 524 } 525 } 526 } 527 } 528 529 private static final class LoaderHidingResource extends Resource { 530 531 private final Resource delegate; 532 533 private LoaderHidingResource(Resource delegate) { 534 this.delegate = delegate; 535 } 536 537 @Override 538 public Resource addPath(String path) throws IOException, MalformedURLException { 539 if (path.startsWith("/org/springframework/boot")) { 540 return null; 541 } 542 return this.delegate.addPath(path); 543 } 544 545 @Override 546 public boolean isContainedIn(Resource resource) throws MalformedURLException { 547 return this.delegate.isContainedIn(resource); 548 } 549 550 @Override 551 public void close() { 552 this.delegate.close(); 553 } 554 555 @Override 556 public boolean exists() { 557 return this.delegate.exists(); 558 } 559 560 @Override 561 public boolean isDirectory() { 562 return this.delegate.isDirectory(); 563 } 564 565 @Override 566 public long lastModified() { 567 return this.delegate.lastModified(); 568 } 569 570 @Override 571 public long length() { 572 return this.delegate.length(); 573 } 574 575 @Override 576 @Deprecated 577 public URL getURL() { 578 return this.delegate.getURL(); 579 } 580 581 @Override 582 public File getFile() throws IOException { 583 return this.delegate.getFile(); 584 } 585 586 @Override 587 public String getName() { 588 return this.delegate.getName(); 589 } 590 591 @Override 592 public InputStream getInputStream() throws IOException { 593 return this.delegate.getInputStream(); 594 } 595 596 @Override 597 public ReadableByteChannel getReadableByteChannel() throws IOException { 598 return this.delegate.getReadableByteChannel(); 599 } 600 601 @Override 602 public boolean delete() throws SecurityException { 603 return this.delegate.delete(); 604 } 605 606 @Override 607 public boolean renameTo(Resource dest) throws SecurityException { 608 return this.delegate.renameTo(dest); 609 } 610 611 @Override 612 public String[] list() { 613 return this.delegate.list(); 614 } 615 616 } 617 618}