From b303b3fe352bba72d16c179851615826128d617a Mon Sep 17 00:00:00 2001 From: Vladimir Tsanev Date: Mon, 29 Feb 2016 19:17:31 +0200 Subject: [PATCH] Support JSPs in Embedded Jetty JSPs are now supported in executable WARs with embedded Jetty. Fixes gh-367 Closes gh-5290 --- spring-boot-dependencies/pom.xml | 21 +++ .../main/asciidoc/spring-boot-features.adoc | 3 +- spring-boot-samples/README.adoc | 3 + spring-boot-samples/pom.xml | 1 + .../spring-boot-sample-jetty-jsp/pom.xml | 83 +++++++++ .../java/sample/jetty/jsp/MyException.java | 25 +++ .../java/sample/jetty/jsp/MyRestResponse.java | 31 ++++ .../jetty/jsp/SampleJettyJspApplication.java | 36 ++++ .../sample/jetty/jsp/WelcomeController.java | 59 +++++++ .../src/main/resources/application.properties | 3 + .../src/main/webapp/WEB-INF/jsp/welcome.jsp | 18 ++ .../jsp/SampleWebJspApplicationTests.java | 54 ++++++ .../embedded/jetty/JasperInitializer.java | 165 ++++++++++++++++++ .../JettyEmbeddedServletContainerFactory.java | 1 + 14 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/pom.xml create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyException.java create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/MyRestResponse.java create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/SampleJettyJspApplication.java create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/java/sample/jetty/jsp/WelcomeController.java create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/resources/application.properties create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/src/main/webapp/WEB-INF/jsp/welcome.jsp create mode 100644 spring-boot-samples/spring-boot-sample-jetty-jsp/src/test/java/sample/jetty/jsp/SampleWebJspApplicationTests.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JasperInitializer.java 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,