Upgrade to Servlet 3.1, Tomcat 8 and Jetty 9

Upgrade to latest versions of Tomcat and Jetty and to the latest Servlet
API whilst will remaining compatible with Tomcat 7 and Jetty 8.

Fixes gh-1832, gh-369
This commit is contained in:
Phillip Webb
2014-11-05 16:11:55 -08:00
parent 004904af87
commit b6bacd5e8a
12 changed files with 203 additions and 108 deletions
@@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.context.request.RequestAttributes;
@@ -121,10 +122,10 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
}
}
Object message = getAttribute(requestAttributes, "javax.servlet.error.message");
if ((message != null || errorAttributes.get("message") == null)
if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
&& !(error instanceof BindingResult)) {
errorAttributes.put("message", message == null ? "No message available"
: message);
errorAttributes.put("message",
StringUtils.isEmpty(message) ? "No message available" : message);
}
}
@@ -73,10 +73,10 @@ public class BasicErrorControllerIntegrationTests {
public void testErrorForMachineClient() throws Exception {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
"http://localhost:" + this.port, Map.class);
assertThat(entity.getBody().toString(), endsWith("status=500, "
+ "error=Internal Server Error, "
+ "exception=java.lang.IllegalStateException, "
+ "message=Server Error, " + "path=/}"));
String body = entity.getBody().toString();
assertThat(body, endsWith("status=500, " + "error=Internal Server Error, "
+ "exception=java.lang.IllegalStateException, " + "message=Expected!, "
+ "path=/}"));
}
@Test
@@ -128,7 +128,8 @@ public class BasicErrorControllerMockMvcTests {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ EmbeddedServletContainerAutoConfiguration.class,
@Import({ EmbeddedServletContainerAutoConfiguration.EmbeddedTomcat.class,
EmbeddedServletContainerAutoConfiguration.class,
ServerPropertiesAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
@@ -20,7 +20,9 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
@@ -94,6 +96,7 @@ public class CliTester implements TestRule {
private <T extends OptionParsingCommand> Future<T> submitCommand(final T command,
String... args) {
clearUrlHandler();
final String[] sources = getSources(args);
return Executors.newSingleThreadExecutor().submit(new Callable<T>() {
@Override
@@ -112,6 +115,21 @@ public class CliTester implements TestRule {
});
}
/**
* The TomcatURLStreamHandlerFactory fails if the factory is already set, use
* reflection to reset it.
*/
private void clearUrlHandler() {
try {
Field field = URL.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(null, null);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected String[] getSources(String... args) {
final String[] sources = new String[args.length];
for (int i = 0; i < args.length; i++) {
+3 -3
View File
@@ -82,7 +82,7 @@
<javax-cache.version>1.0.0</javax-cache.version>
<javax-mail.version>1.5.2</javax-mail.version>
<jedis.version>2.4.2</jedis.version>
<jetty.version>8.1.15.v20140411</jetty.version>
<jetty.version>9.2.4.v20141103</jetty.version>
<jetty-jsp.version>2.2.0.v201112011158</jetty-jsp.version>
<jaxen.version>1.1.6</jaxen.version>
<jdom2.version>2.0.5</jdom2.version>
@@ -101,7 +101,7 @@
<mysql.version>5.1.32</mysql.version>
<reactor.version>1.1.5.RELEASE</reactor.version>
<reactor-spring.version>1.1.3.RELEASE</reactor-spring.version>
<servlet-api.version>3.0.1</servlet-api.version>
<servlet-api.version>3.1.0</servlet-api.version>
<slf4j.version>1.7.7</slf4j.version>
<snakeyaml.version>1.13</snakeyaml.version>
<solr.version>4.7.2</solr.version>
@@ -127,7 +127,7 @@
<thymeleaf-extras-springsecurity3.version>2.1.1.RELEASE</thymeleaf-extras-springsecurity3.version>
<thymeleaf-layout-dialect.version>1.2.5</thymeleaf-layout-dialect.version>
<thymeleaf-extras-data-attribute.version>1.3</thymeleaf-extras-data-attribute.version>
<tomcat.version>7.0.56</tomcat.version>
<tomcat.version>8.0.14</tomcat.version>
<velocity.version>1.7</velocity.version>
<velocity-tools.version>2.0</velocity-tools.version>
<wsdl4j.version>1.6.3</wsdl4j.version>
@@ -52,12 +52,11 @@ public class WarPackagingTests {
private static final Set<String> JETTY_EXPECTED_IN_WEB_INF_LIB_PROVIDED = new HashSet<String>(
Arrays.asList("spring-boot-starter-jetty-", "jetty-util-", "jetty-xml-",
"javax.servlet-", "jetty-continuation-", "jetty-io-", "jetty-http-",
"jetty-schemas-", "javax.servlet-", "jetty-io-", "jetty-http-",
"jetty-server-", "jetty-security-", "jetty-servlet-",
"jetty-webapp-", "javax.servlet.jsp-",
"org.apache.jasper.glassfish-", "javax.servlet.jsp.jstl-",
"org.apache.taglibs.standard.glassfish-", "javax.el-", "com.sun.el-",
"org.eclipse.jdt.core-", "jetty-jsp-"));
"jetty-webapp-", "javax.servlet.jsp-2", "javax.servlet.jsp-api-",
"javax.servlet.jsp.jstl-1.2.2", "javax.servlet.jsp.jstl-1.2.0",
"javax.el-", "org.eclipse.jdt.core-", "jetty-jsp-"));
private static final String BOOT_VERSION = ManagedDependencies.get()
.find("spring-boot").getVersion();
@@ -25,11 +25,14 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.ssl.SslSocketConnector;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
@@ -117,10 +120,10 @@ public class JettyEmbeddedServletContainerFactory extends
if (getSsl() != null) {
SslContextFactory sslContextFactory = new SslContextFactory();
configureSsl(sslContextFactory, getSsl());
SslSocketConnector sslConnector = new SslSocketConnector(sslContextFactory);
sslConnector.setPort(port);
server.setConnectors(new Connector[] { sslConnector });
ServerConnector connector = getSslServerConnectorFactory().getConnector(
server, sslContextFactory);
connector.setPort(port);
server.setConnectors(new Connector[] { connector });
}
for (JettyServerCustomizer customizer : getServerCustomizers()) {
@@ -130,6 +133,13 @@ public class JettyEmbeddedServletContainerFactory extends
return getJettyEmbeddedServletContainer(server);
}
private SslServerConnectorFactory getSslServerConnectorFactory() {
if (ClassUtils.isPresent("org.eclipse.jetty.server.ssl.SslSocketConnector", null)) {
return new Jetty8SslServerConnectorFactory();
}
return new Jetty9SslServerConnectorFactory();
}
/**
* Configure the SSL connection.
* @param factory the Jetty {@link SslContextFactory}.
@@ -246,7 +256,7 @@ public class JettyEmbeddedServletContainerFactory extends
handler.setBaseResource(resource);
}
else {
handler.setBaseResource(Resource.newResource(root));
handler.setBaseResource(Resource.newResource(root.getCanonicalFile()));
}
}
catch (Exception ex) {
@@ -458,4 +468,50 @@ public class JettyEmbeddedServletContainerFactory extends
}
}
/**
* Factory to create the SSL {@link ServerConnector}.
*/
private static interface SslServerConnectorFactory {
ServerConnector getConnector(Server server, SslContextFactory sslContextFactory);
}
/**
* {@link SslServerConnectorFactory} for Jetty 9.
*/
private static class Jetty9SslServerConnectorFactory implements
SslServerConnectorFactory {
@Override
public ServerConnector getConnector(Server server,
SslContextFactory sslContextFactory) {
return new ServerConnector(server, new SslConnectionFactory(
sslContextFactory, HttpVersion.HTTP_1_1.asString()),
new HttpConnectionFactory());
}
}
/**
* {@link SslServerConnectorFactory} for Jetty 8.
*/
private static class Jetty8SslServerConnectorFactory implements
SslServerConnectorFactory {
@Override
public ServerConnector getConnector(Server server,
SslContextFactory sslContextFactory) {
try {
Class<?> connectorClass = Class
.forName("org.eclipse.jetty.server.ssl.SslSocketConnector");
return (ServerConnector) connectorClass.getConstructor(
SslContextFactory.class).newInstance(sslContextFactory);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
}
@@ -16,7 +16,6 @@
package org.springframework.boot.context.embedded.tomcat;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.LinkedHashSet;
@@ -27,6 +26,7 @@ import javax.servlet.ServletContext;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.JarScannerCallback;
import org.apache.tomcat.util.scan.StandardJarScanFilter;
import org.apache.tomcat.util.scan.StandardJarScanner;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -45,8 +45,6 @@ class SkipPatternJarScanner extends StandardJarScanner {
private static final String JAR_SCAN_FILTER_CLASS = "org.apache.tomcat.JarScanFilter";
private static final String STANDARD_JAR_SCAN_FILTER_CLASS = "org.apache.tomcat.util.scan.StandardJarScanFilter";
private final JarScanner jarScanner;
private final SkipPattern pattern;
@@ -60,34 +58,24 @@ class SkipPatternJarScanner extends StandardJarScanner {
private void setPatternToTomcat8SkipFilter(SkipPattern pattern) {
if (ClassUtils.isPresent(JAR_SCAN_FILTER_CLASS, null)) {
try {
Class<?> filterClass = Class.forName(JAR_SCAN_FILTER_CLASS);
Method setJarScanner = ReflectionUtils.findMethod(
StandardJarScanner.class, "setJarScanFilter", filterClass);
setJarScanner.invoke(this, createStandardJarScanFilter(pattern));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
new Tomcat8TldSkipSetter(this).setSkipPattern(pattern);
}
}
private Object createStandardJarScanFilter(SkipPattern pattern)
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, InvocationTargetException {
Class<?> filterClass = Class.forName(STANDARD_JAR_SCAN_FILTER_CLASS);
Method setTldSkipMethod = ReflectionUtils.findMethod(filterClass, "setTldSkip",
String.class);
Object scanner = filterClass.newInstance();
setTldSkipMethod.invoke(scanner, pattern.asCommaDelimitedString());
return scanner;
}
@Override
// For Tomcat 7 compatibility
public void scan(ServletContext context, ClassLoader classloader,
JarScannerCallback callback, Set<String> jarsToSkip) {
this.jarScanner.scan(context, classloader, callback,
(jarsToSkip == null ? this.pattern.asSet() : jarsToSkip));
Method scanMethod = ReflectionUtils.findMethod(this.jarScanner.getClass(),
"scan", ServletContext.class, ClassLoader.class,
JarScannerCallback.class, Set.class);
Assert.notNull(scanMethod, "Unable to find scan method");
try {
scanMethod.invoke(this.jarScanner, context, classloader, callback,
(jarsToSkip == null ? this.pattern.asSet() : jarsToSkip));
}
catch (Exception ex) {
throw new IllegalStateException("Tomcat 7 reflection failed", ex);
}
}
/**
@@ -101,6 +89,28 @@ class SkipPatternJarScanner extends StandardJarScanner {
context.setJarScanner(scanner);
}
/**
* Tomcat 8 specific logic to setup the scanner.
*/
private static class Tomcat8TldSkipSetter {
private final StandardJarScanner jarScanner;
public Tomcat8TldSkipSetter(StandardJarScanner jarScanner) {
this.jarScanner = jarScanner;
}
public void setSkipPattern(SkipPattern pattern) {
StandardJarScanFilter filter = new StandardJarScanFilter();
filter.setTldSkip(pattern.asCommaDelimitedString());
this.jarScanner.setJarScanFilter(filter);
}
}
/**
* Skip patterns used by Spring Boot
*/
private static class SkipPattern {
private Set<String> patterns = new LinkedHashSet<String>();
@@ -16,8 +16,6 @@
package org.springframework.boot.context.embedded.tomcat;
import java.lang.reflect.Method;
import org.apache.catalina.Container;
import org.apache.catalina.core.StandardContext;
import org.springframework.util.ClassUtils;
@@ -33,9 +31,19 @@ class TomcatEmbeddedContext extends StandardContext {
private ServletContextInitializerLifecycleListener starter;
private final boolean overrideLoadOnStart;
public TomcatEmbeddedContext() {
this.overrideLoadOnStart = ReflectionUtils.findMethod(StandardContext.class,
"loadOnStartup", Container[].class).getReturnType() == boolean.class;
}
@Override
public boolean loadOnStartup(Container[] children) {
return true;
if (this.overrideLoadOnStart) {
return true;
}
return super.loadOnStartup(children);
}
public void deferredLoadOnStartup() {
@@ -49,12 +57,13 @@ class TomcatEmbeddedContext extends StandardContext {
if (classLoader != null) {
existingLoader = ClassUtils.overrideThreadContextClassLoader(classLoader);
}
if (ClassUtils.isPresent("org.apache.catalina.deploy.ErrorPage", null)) {
if (this.overrideLoadOnStart) {
// Earlier versions of Tomcat used a version that returned void. If that
// version is used our overridden loadOnStart method won't have been called
// and the original will have already run.
super.loadOnStartup(findChildren());
}
else {
callSuper(this, "loadOnStartup", findChildren(), Container[].class);
}
if (existingLoader != null) {
ClassUtils.overrideThreadContextClassLoader(existingLoader);
}
@@ -68,10 +77,4 @@ class TomcatEmbeddedContext extends StandardContext {
return this.starter;
}
private void callSuper(Object target, String name, Object value, Class<?> type) {
Method method = ReflectionUtils.findMethod(target.getClass().getSuperclass(),
name, type);
ReflectionUtils.invokeMethod(method, target, value);
}
}
@@ -604,15 +604,14 @@ public class TomcatEmbeddedServletContainerFactory extends
private Object createNativePage(ErrorPage errorPage) {
Object nativePage = null;
try {
if (ClassUtils.isPresent("org.apache.catalina.deploy.ErrorPage", null)) {
nativePage = new org.apache.catalina.deploy.ErrorPage();
if (ClassUtils.isPresent(
"org.apache.tomcat.util.descriptor.web.ErrorPage", null)) {
nativePage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
}
else {
if (ClassUtils.isPresent(
"org.apache.tomcat.util.descriptor.web.ErrorPage", null)) {
nativePage = BeanUtils.instantiate(ClassUtils.forName(
"org.apache.tomcat.util.descriptor.web.ErrorPage", null));
}
else if (ClassUtils.isPresent("org.apache.catalina.deploy.ErrorPage",
null)) {
nativePage = BeanUtils.instantiate(ClassUtils.forName(
"org.apache.catalina.deploy.ErrorPage", null));
}
}
catch (ClassNotFoundException ex) {
@@ -627,8 +626,9 @@ public class TomcatEmbeddedServletContainerFactory extends
public void addToContext(Context context) {
Assert.state(this.nativePage != null,
"Neither Tomcat 7 nor 8 detected so no native error page exists");
if (ClassUtils.isPresent("org.apache.catalina.deploy.ErrorPage", null)) {
org.apache.catalina.deploy.ErrorPage errorPage = (org.apache.catalina.deploy.ErrorPage) this.nativePage;
if (ClassUtils.isPresent("org.apache.tomcat.util.descriptor.web.ErrorPage",
null)) {
org.apache.tomcat.util.descriptor.web.ErrorPage errorPage = (org.apache.tomcat.util.descriptor.web.ErrorPage) this.nativePage;
errorPage.setLocation(this.location);
errorPage.setErrorCode(this.errorCode);
errorPage.setExceptionType(this.exceptionType);
@@ -22,11 +22,12 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import javax.naming.directory.DirContext;
import javax.servlet.ServletContext;
import org.apache.catalina.Context;
import org.apache.catalina.WebResourceRoot.ResourceSetType;
import org.apache.catalina.core.StandardContext;
import org.apache.naming.resources.FileDirContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
@@ -104,29 +105,57 @@ abstract class TomcatResources {
*/
private static class Tomcat7Resources extends TomcatResources {
private final Method addResourceJarUrlMethod;
public Tomcat7Resources(Context context) {
super(context);
this.addResourceJarUrlMethod = ReflectionUtils.findMethod(context.getClass(),
"addResourceJarUrl", URL.class);
}
@Override
protected void addJar(String jar) {
URL url = getJarUlr(jar);
if (url != null) {
try {
this.addResourceJarUrlMethod.invoke(getContext(), url);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
private URL getJarUlr(String jar) {
try {
getContext().addResourceJarUrl(new URL(jar));
return new URL(jar);
}
catch (MalformedURLException ex) {
// Ignore?
// Ignore
return null;
}
}
@Override
protected void addDir(String dir, URL url) {
if (getContext() instanceof ServletContext) {
FileDirContext files = new FileDirContext();
files.setDocBase(dir);
((StandardContext) getContext()).addResourcesDirContext(files);
try {
Class<?> fileDirContextClass = Class
.forName("org.apache.naming.resources.FileDirContext");
Method setDocBaseMethod = ReflectionUtils.findMethod(
fileDirContextClass, "setDocBase", String.class);
Object fileDirContext = fileDirContextClass.newInstance();
setDocBaseMethod.invoke(fileDirContext, dir);
Method addResourcesDirContextMethod = ReflectionUtils.findMethod(
StandardContext.class, "addResourcesDirContext",
DirContext.class);
addResourcesDirContextMethod.invoke(getContext(), fileDirContext);
}
catch (Exception ex) {
throw new IllegalStateException("Tomcat 7 reflection failed", ex);
}
}
}
}
/**
@@ -134,28 +163,8 @@ abstract class TomcatResources {
*/
static class Tomcat8Resources extends TomcatResources {
private Object resources;
private Method createWebResourceSetMethod;
private Enum<?> resourceJarEnum;
@SuppressWarnings({ "rawtypes", "unchecked" })
public Tomcat8Resources(Context context) {
super(context);
try {
this.resources = ReflectionUtils.findMethod(context.getClass(),
"getResources").invoke(context);
Class resourceSetType = ClassUtils.resolveClassName(
"org.apache.catalina.WebResourceRoot.ResourceSetType", null);
this.createWebResourceSetMethod = ReflectionUtils.findMethod(
this.resources.getClass(), "createWebResourceSet",
resourceSetType, String.class, URL.class, String.class);
this.resourceJarEnum = Enum.valueOf(resourceSetType, "RESOURCE_JAR");
}
catch (Exception ex) {
throw new IllegalStateException("Tomcat 8 reflection failed", ex);
}
}
@Override
@@ -178,7 +187,8 @@ abstract class TomcatResources {
}
URL url = new URL(resource);
String path = "/META-INF/resources";
createWebResourceSet("/", url, path);
getContext().getResources().createWebResourceSet(
ResourceSetType.RESOURCE_JAR, "/", url, path);
}
catch (Exception ex) {
// Ignore (probably not a directory)
@@ -189,12 +199,6 @@ abstract class TomcatResources {
return dir.indexOf("!/") < dir.lastIndexOf("!/");
}
private void createWebResourceSet(String webAppMount, URL url, String path)
throws Exception {
this.createWebResourceSetMethod.invoke(this.resources, this.resourceJarEnum,
webAppMount, url, path);
}
}
}
@@ -21,9 +21,10 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.ssl.SslConnector;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.Test;
@@ -114,9 +115,11 @@ public class JettyEmbeddedServletContainerFactoryTests extends
this.container.start();
JettyEmbeddedServletContainer jettyContainer = (JettyEmbeddedServletContainer) this.container;
SslConnector sslConnector = (SslConnector) jettyContainer.getServer()
ServerConnector connector = (ServerConnector) jettyContainer.getServer()
.getConnectors()[0];
assertThat(sslConnector.getSslContextFactory().getIncludeCipherSuites(),
SslConnectionFactory connectionFactory = connector
.getConnectionFactory(SslConnectionFactory.class);
assertThat(connectionFactory.getSslContextFactory().getIncludeCipherSuites(),
equalTo(new String[] { "ALPHA", "BRAVO", "CHARLIE" }));
}