diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml
index 30c9b1b682..7fdf23e3d2 100644
--- a/spring-boot-dependencies/pom.xml
+++ b/spring-boot-dependencies/pom.xml
@@ -1546,6 +1546,27 @@
jetty-io
${jetty.version}
+
+ org.eclipse.jetty
+ jetty-jsp
+ ${jetty.version}
+
+
+ org.eclipse.jetty.orbit
+ javax.servlet
+
+
+
+
+ org.eclipse.jetty
+ apache-jstl
+ ${jetty.version}
+
+
+ org.eclipse.jetty
+ apache-jsp
+ ${jetty.version}
+
org.eclipse.jetty
jetty-jmx
diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc
index b668fb722c..ff8377f17c 100644
--- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc
+++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc
@@ -2128,7 +2128,8 @@ packaged as an executable archive), there are some limitations in the JSP suppor
and will also be deployable to a standard container (not limited to, but including
Tomcat). An executable jar will not work because of a hard coded file pattern in Tomcat.
-* Jetty does not currently work as an embedded container with JSPs.
+* With Jetty it should work if you use war packaging, i.e. an executable war will work,
+ and will also be deployable to any standard container.
* Undertow does not support JSPs.
diff --git a/spring-boot-samples/README.adoc b/spring-boot-samples/README.adoc
index 1ebef9e19d..79cb240275 100644
--- a/spring-boot-samples/README.adoc
+++ b/spring-boot-samples/README.adoc
@@ -111,6 +111,9 @@ The following sample applications are provided:
| link:spring-boot-sample-jetty-ssl[spring-boot-sample-jetty-ssl]
| Embedded Jetty configured to use SSL
+| link:spring-boot-sample-jetty-jsp[spring-boot-sample-jetty-jsp]
+| Web application that uses JSP templates with Jetty
+
| link:spring-boot-sample-jetty8[spring-boot-sample-jetty8]
| Embedded Jetty 8
diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml
index 5ce36b613f..fbe02fd085 100644
--- a/spring-boot-samples/pom.xml
+++ b/spring-boot-samples/pom.xml
@@ -55,6 +55,7 @@
spring-boot-sample-jersey
spring-boot-sample-jersey1
spring-boot-sample-jetty
+ spring-boot-sample-jetty-jsp
spring-boot-sample-jetty-ssl
spring-boot-sample-jetty8
spring-boot-sample-jetty8-ssl
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/pom.xml b/spring-boot-samples/spring-boot-sample-jetty-jsp/pom.xml
new file mode 100644
index 0000000000..e261bc7b2c
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-samples
+ 1.4.0.BUILD-SNAPSHOT
+
+ spring-boot-sample-jetty-jsp
+ war
+ Spring Boot Jetty JSP Sample
+ Spring Boot Jetty JSP Sample
+ http://projects.spring.io/spring-boot/
+
+ Pivotal Software, Inc.
+ http://www.spring.io
+
+
+ ${basedir}/../..
+ /
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-el
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jetty
+ provided
+
+
+ javax.servlet
+ jstl
+
+
+ org.eclipse.jetty
+ apache-jsp
+ provided
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+
+
+
+
+
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyException.java b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyException.java
new file mode 100644
index 0000000000..5599f18558
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample.jetty.jsp;
+
+public class MyException extends RuntimeException {
+
+ public MyException(String message) {
+ super(message);
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyRestResponse.java b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyRestResponse.java
new file mode 100644
index 0000000000..232ba8fd48
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyRestResponse.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample.jetty.jsp;
+
+public class MyRestResponse {
+
+ private String message;
+
+ public MyRestResponse(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return this.message;
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/SampleJettyJspApplication.java b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/SampleJettyJspApplication.java
new file mode 100644
index 0000000000..880531aabd
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/SampleJettyJspApplication.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample.jetty.jsp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.context.web.SpringBootServletInitializer;
+
+@SpringBootApplication
+public class SampleJettyJspApplication extends SpringBootServletInitializer {
+
+ @Override
+ protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+ return application.sources(SampleJettyJspApplication.class);
+ }
+
+ public static void main(String[] args) throws Exception {
+ SpringApplication.run(SampleJettyJspApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/WelcomeController.java b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/WelcomeController.java
new file mode 100644
index 0000000000..66283d792e
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/WelcomeController.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample.jetty.jsp;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@Controller
+public class WelcomeController {
+
+ @Value("${application.message:Hello World}")
+ private String message = "Hello World";
+
+ @RequestMapping("/")
+ public String welcome(Map model) {
+ model.put("time", new Date());
+ model.put("message", this.message);
+ return "welcome";
+ }
+
+ @RequestMapping("/fail")
+ public String fail() {
+ throw new MyException("Oh dear!");
+ }
+
+ @RequestMapping("/fail2")
+ public String fail2() {
+ throw new IllegalStateException();
+ }
+
+ @ExceptionHandler(MyException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public @ResponseBody MyRestResponse handleMyRuntimeException(MyException exception) {
+ return new MyRestResponse("Some data I want to send back to the client.");
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/resources/application.properties
new file mode 100644
index 0000000000..f18efd1664
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+spring.mvc.view.prefix: /WEB-INF/jsp/
+spring.mvc.view.suffix: .jsp
+application.message: Hello Spring Boot
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/webapp/WEB-INF/jsp/welcome.jsp b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/webapp/WEB-INF/jsp/welcome.jsp
new file mode 100644
index 0000000000..3196dac625
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/webapp/WEB-INF/jsp/welcome.jsp
@@ -0,0 +1,18 @@
+
+
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+
+
+
+
+
+
+ Spring URL: ${springUrl} at ${time}
+
+ JSTL URL: ${url}
+
+ Message: ${message}
+
+
+
diff --git a/spring-boot-samples/spring-boot-sample-jetty-jsp/src/test/java/sample/jetty/jsp/SampleWebJspApplicationTests.java b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/test/java/sample/jetty/jsp/SampleWebJspApplicationTests.java
new file mode 100644
index 0000000000..013231731a
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jetty-jsp/src/test/java/sample/jetty/jsp/SampleWebJspApplicationTests.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample.jetty.jsp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.boot.context.embedded.LocalServerPort;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Basic integration tests for JSP application.
+ *
+ * @author Phillip Webb
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@DirtiesContext
+public class SampleWebJspApplicationTests {
+
+ @LocalServerPort
+ private int port;
+
+ @Test
+ public void testJspWithEl() throws Exception {
+ ResponseEntity entity = new TestRestTemplate()
+ .getForEntity("http://localhost:" + this.port, String.class);
+ assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(entity.getBody()).contains("/resources/text.txt");
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JasperInitializer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JasperInitializer.java
new file mode 100644
index 0000000000..813d15620e
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JasperInitializer.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.context.embedded.jetty;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+
+import javax.servlet.ServletContainerInitializer;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.webapp.WebAppContext;
+
+import org.springframework.util.ClassUtils;
+
+/**
+ * Jetty {@link AbstractLifeCycle} to initialize jasper.
+ *
+ * @author Vladimir Tsanev
+ */
+public class JasperInitializer extends AbstractLifeCycle {
+
+ private final WebAppContext context;
+ private final ServletContainerInitializer initializer;
+
+ JasperInitializer(WebAppContext context) {
+ this.context = context;
+ this.initializer = newInitializer();
+ }
+
+ private static ServletContainerInitializer newInitializer() {
+ try {
+ try {
+ return (ServletContainerInitializer) ClassUtils
+ .forName("org.eclipse.jetty.apache.jsp.JettyJasperInitializer",
+ null)
+ .newInstance();
+ }
+ catch (Exception ex) {
+ // try the original initializer
+ return (ServletContainerInitializer) ClassUtils
+ .forName("org.apache.jasper.servlet.JasperInitializer", null)
+ .newInstance();
+ }
+ }
+ catch (Exception ex) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ if (this.initializer == null) {
+ return;
+ }
+ try {
+ URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
+ @Override
+ public URLStreamHandler createURLStreamHandler(String protocol) {
+ if ("war".equals(protocol)) {
+ return new WarUrlStreamHandler();
+ }
+ return null;
+ }
+ });
+ }
+ catch (Error ex) {
+ // Ignore
+ }
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(this.context.getClassLoader());
+ try {
+ try {
+ setExtendedListenerTypes(true);
+ this.initializer.onStartup(null, this.context.getServletContext());
+ }
+ finally {
+ setExtendedListenerTypes(false);
+ }
+ }
+ finally {
+ Thread.currentThread().setContextClassLoader(classLoader);
+ }
+ }
+
+ private void setExtendedListenerTypes(boolean extended) {
+ try {
+ this.context.getServletContext().setExtendedListenerTypes(extended);
+ }
+ catch (NoSuchMethodError ex) {
+ // Not available on Jetty 8
+ }
+ }
+
+ /**
+ * {@link URLStreamHandler} for {@literal war} protocol compatible with jasper's
+ * {@link URL urls} produced by
+ * {@link org.apache.tomcat.util.scan.JarFactory#getJarEntryURL(URL, String)}.
+ */
+ static class WarUrlStreamHandler extends URLStreamHandler {
+
+ @Override
+ protected void parseURL(URL u, String spec, int start, int limit) {
+ String path = "jar:" + spec.substring("war:".length());
+
+ int separator = path.indexOf("*/");
+ if (separator >= 0) {
+ path = path.substring(0, separator) + "!/"
+ + path.substring(separator + 2);
+ }
+
+ setURL(u, u.getProtocol(), "", -1, null, null, path, null, null);
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException {
+ return new WarURLConnection(u);
+ }
+ }
+
+ /**
+ * {@link URLConnection} to support {@literal war} protocol.
+ */
+ static class WarURLConnection extends URLConnection {
+
+ private final URLConnection connection;
+
+ protected WarURLConnection(URL url) throws IOException {
+ super(url);
+ this.connection = new URL(url.getFile()).openConnection();
+ }
+
+ @Override
+ public void connect() throws IOException {
+ if (!this.connected) {
+ this.connection.connect();
+ this.connected = true;
+ }
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ connect();
+ return this.connection.getInputStream();
+ }
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java
index ff0cef47b3..7a670dc220 100644
--- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java
+++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java
@@ -352,6 +352,7 @@ public class JettyEmbeddedServletContainerFactory
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
+ context.addBean(new JasperInitializer(context), true);
}
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
Configuration[] configurations = getWebAppContextConfigurations(context,