diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc
index 1ee00538be..b6b11cd9e2 100644
--- a/spring-boot-docs/src/main/asciidoc/howto.adoc
+++ b/spring-boot-docs/src/main/asciidoc/howto.adoc
@@ -453,6 +453,44 @@ that sets up the connector to be secure:
}
----
+[[howto-enable-multiple-connectors-in-tomcat]]
+=== Enable Multiple Connectors Tomcat
+Add a `org.apache.catalina.connector.Connector` to the
+`TomcatEmbeddedServletContainerFactory` which can allow multiple connectors eg a HTTP and
+HTTPS connector:
+
+[source,java,indent=0,subs="verbatim,quotes,attributes"]
+----
+ @Bean
+ public EmbeddedServletContainerFactory servletContainer() {
+ TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
+ tomcat.addAdditionalTomcatConnectors(createSslConnector());
+ return tomcat;
+ }
+
+ private Connector createSslConnector() {
+ Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
+ Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
+ try {
+ File keystore = new ClassPathResource("keystore").getFile();
+ File truststore = new ClassPathResource("keystore").getFile();
+ connector.setScheme("https");
+ connector.setSecure(true);
+ connector.setPort(8443);
+ protocol.setSSLEnabled(true);
+ protocol.setKeystoreFile(keystore.getAbsolutePath());
+ protocol.setKeystorePass("changeit");
+ protocol.setTruststoreFile(truststore.getAbsolutePath());
+ protocol.setTruststorePass("changeit");
+ protocol.setKeyAlias("apitester");
+ return connector;
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException("can't access keystore: [" + "keystore"
+ + "] or truststore: [" + "keystore" + "]", ex);
+ }
+ }
+----
[[howto-use-jetty-instead-of-tomcat]]
diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml
index cbcfef522b..1aa4beb05e 100644
--- a/spring-boot-samples/pom.xml
+++ b/spring-boot-samples/pom.xml
@@ -31,6 +31,7 @@
spring-boot-sample-servlet
spring-boot-sample-simple
spring-boot-sample-tomcat
+ spring-boot-sample-tomcat-multi-connectors
spring-boot-sample-traditional
spring-boot-sample-web-method-security
spring-boot-sample-web-secure
diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/pom.xml b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/pom.xml
new file mode 100644
index 0000000000..9341cf5bd7
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-samples
+ 1.0.0.BUILD-SNAPSHOT
+
+ spring-boot-sample-tomcat-multi-connectors
+ jar
+
+ ${basedir}/../..
+
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework
+ spring-webmvc
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/SampleTomcatTwoConnectorsApplication.java b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/SampleTomcatTwoConnectorsApplication.java
new file mode 100644
index 0000000000..66e9dfc5ca
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/SampleTomcatTwoConnectorsApplication.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2012-2014 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.tomcat;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import org.apache.catalina.connector.Connector;
+import org.apache.coyote.http11.Http11NioProtocol;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
+import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.util.FileCopyUtils;
+
+/**
+ * Sample Application to show Tomcat running 2 connectors
+ *
+ * @author Brock Mills
+ */
+@Configuration
+@EnableAutoConfiguration
+@ComponentScan
+public class SampleTomcatTwoConnectorsApplication {
+
+ @Bean
+ public EmbeddedServletContainerFactory servletContainer() {
+ TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
+ tomcat.addAdditionalTomcatConnectors(createSslConnector());
+ return tomcat;
+ }
+
+ private Connector createSslConnector() {
+ Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
+ Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
+ try {
+ File keystore = getKeyStoreFile();
+ File truststore = keystore;
+ connector.setScheme("https");
+ connector.setSecure(true);
+ connector.setPort(8443);
+ protocol.setSSLEnabled(true);
+ protocol.setKeystoreFile(keystore.getAbsolutePath());
+ protocol.setKeystorePass("changeit");
+ protocol.setTruststoreFile(truststore.getAbsolutePath());
+ protocol.setTruststorePass("changeit");
+ protocol.setKeyAlias("apitester");
+ return connector;
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException("cant access keystore: [" + "keystore"
+ + "] or truststore: [" + "keystore" + "]", ex);
+ }
+ }
+
+ private File getKeyStoreFile() throws IOException {
+ ClassPathResource resource = new ClassPathResource("keystore");
+ try {
+ return resource.getFile();
+ }
+ catch (Exception ex) {
+ File temp = File.createTempFile("keystore", ".tmp");
+ FileCopyUtils.copy(resource.getInputStream(), new FileOutputStream(temp));
+ return temp;
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ SpringApplication.run(SampleTomcatTwoConnectorsApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/web/SampleController.java b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/web/SampleController.java
new file mode 100644
index 0000000000..1af3656bb2
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/web/SampleController.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2012-2014 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.tomcat.web;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class SampleController {
+
+ @RequestMapping("/hello")
+ public String helloWorld() {
+ return "hello";
+ }
+}
diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/resources/keystore b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/resources/keystore
new file mode 100644
index 0000000000..6547e5b519
Binary files /dev/null and b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/resources/keystore differ
diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/test/java/sample/tomcat/SampleTomcatTwoConnectorsApplicationTests.java b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/test/java/sample/tomcat/SampleTomcatTwoConnectorsApplicationTests.java
new file mode 100644
index 0000000000..b72b20835d
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/test/java/sample/tomcat/SampleTomcatTwoConnectorsApplicationTests.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2012-2014 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.tomcat;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.web.client.RestTemplate;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Basic integration tests for 2 connector demo application.
+ *
+ * @author Brock Mills
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = SampleTomcatTwoConnectorsApplication.class)
+@WebAppConfiguration
+@IntegrationTest
+@DirtiesContext
+public class SampleTomcatTwoConnectorsApplicationTests {
+
+ @BeforeClass
+ public static void setUp() {
+
+ try {
+ // setup ssl context to ignore certificate errors
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ X509TrustManager tm = new X509TrustManager() {
+
+ @Override
+ public void checkClientTrusted(
+ java.security.cert.X509Certificate[] chain, String authType)
+ throws java.security.cert.CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(
+ java.security.cert.X509Certificate[] chain, String authType)
+ throws java.security.cert.CertificateException {
+ }
+
+ @Override
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ };
+ ctx.init(null, new TrustManager[] { tm }, null);
+ SSLContext.setDefault(ctx);
+ }
+ catch (Exception ex) {
+ ex.printStackTrace();
+ }
+
+ }
+
+ @Test
+ public void testHello() throws Exception {
+ RestTemplate template = new RestTemplate();
+ final MySimpleClientHttpRequestFactory factory = new MySimpleClientHttpRequestFactory(
+ new HostnameVerifier() {
+
+ @Override
+ public boolean verify(final String hostname, final SSLSession session) {
+ return true; // these guys are alright by me...
+ }
+ });
+ template.setRequestFactory(factory);
+
+ ResponseEntity entity = template.getForEntity(
+ "http://localhost:8080/hello", String.class);
+ assertEquals(HttpStatus.OK, entity.getStatusCode());
+ assertEquals("hello", entity.getBody());
+
+ ResponseEntity httpsEntity = template.getForEntity(
+ "https://localhost:8443/hello", String.class);
+ assertEquals(HttpStatus.OK, httpsEntity.getStatusCode());
+ assertEquals("hello", httpsEntity.getBody());
+
+ }
+
+ /**
+ * Http Request Factory for ignoring SSL hostname errors. Not for production use!
+ */
+ class MySimpleClientHttpRequestFactory extends SimpleClientHttpRequestFactory {
+
+ private final HostnameVerifier verifier;
+
+ public MySimpleClientHttpRequestFactory(final HostnameVerifier verifier) {
+ this.verifier = verifier;
+ }
+
+ @Override
+ protected void prepareConnection(final HttpURLConnection connection,
+ final String httpMethod) throws IOException {
+ if (connection instanceof HttpsURLConnection) {
+ ((HttpsURLConnection) connection).setHostnameVerifier(this.verifier);
+ }
+ super.prepareConnection(connection, httpMethod);
+ }
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java
index 6d2c73f902..f12ea68b76 100644
--- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java
+++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java
@@ -119,7 +119,7 @@ public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer
}
}
connector.getProtocolHandler().start();
- this.logger.info("Tomcat started on port: " + connector.getLocalPort());
+ logPorts();
}
catch (Exception ex) {
this.logger.error("Cannot start connector: ", ex);
@@ -129,6 +129,16 @@ public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer
}
}
+ private void logPorts() {
+ StringBuilder ports = new StringBuilder();
+ for (Connector additionalConnector : this.tomcat.getService().findConnectors()) {
+ ports.append(ports.length() == 0 ? "" : " ");
+ ports.append(additionalConnector.getLocalPort() + "/"
+ + additionalConnector.getScheme());
+ }
+ this.logger.info("Tomcat started on port(s): " + ports.toString());
+ }
+
@Override
public synchronized void stop() throws EmbeddedServletContainerException {
try {
diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java
index e9385f20a4..e4d28442f1 100644
--- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java
+++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java
@@ -65,6 +65,7 @@ import org.springframework.util.StreamUtils;
*
* @author Phillip Webb
* @author Dave Syer
+ * @author Brock Mills
* @see #setPort(int)
* @see #setContextLifecycleListeners(Collection)
* @see TomcatEmbeddedServletContainer
@@ -84,6 +85,8 @@ public class TomcatEmbeddedServletContainerFactory extends
private List tomcatConnectorCustomizers = new ArrayList();
+ private List additionalTomcatConnectors = new ArrayList();
+
private ResourceLoader resourceLoader;
private String protocol = DEFAULT_PROTOCOL;
@@ -130,6 +133,10 @@ public class TomcatEmbeddedServletContainerFactory extends
tomcat.getHost().setAutoDeploy(false);
tomcat.getEngine().setBackgroundProcessorDelay(-1);
+ for (Connector additionalConnector : this.additionalTomcatConnectors) {
+ tomcat.getService().addConnector(additionalConnector);
+ }
+
prepareContext(tomcat.getHost(), initializers);
this.logger.info("Server initialized with port: " + getPort());
return getTomcatEmbeddedServletContainer(tomcat);
@@ -430,6 +437,24 @@ public class TomcatEmbeddedServletContainerFactory extends
return this.tomcatConnectorCustomizers;
}
+ /**
+ * Add {@link Connector}s in addition to the default connector, e.g. for SSL or AJP
+ * @param connectors the connectors to add
+ */
+ public void addAdditionalTomcatConnectors(Connector... connectors) {
+ Assert.notNull(connectors, "Connectors must not be null");
+ this.additionalTomcatConnectors.addAll(Arrays.asList(connectors));
+ }
+
+ /**
+ * Returns a mutable collection of the {@link Connector}s that will be added to the
+ * Tomcat
+ * @return the additionalTomcatConnectors
+ */
+ public List getAdditionalTomcatConnectors() {
+ return this.additionalTomcatConnectors;
+ }
+
private static class TomcatErrorPage {
private final String location;
diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java
index 221badea97..bb1ab962d2 100644
--- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java
+++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java
@@ -115,6 +115,26 @@ public class TomcatEmbeddedServletContainerFactoryTests extends
}
}
+ @Test
+ public void tomcatAdditionalConnectors() throws Exception {
+ TomcatEmbeddedServletContainerFactory factory = getFactory();
+ Connector[] listeners = new Connector[4];
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i] = mock(Connector.class);
+ }
+ factory.addAdditionalTomcatConnectors(listeners);
+ this.container = factory.getEmbeddedServletContainer();
+ assertEquals(listeners.length, factory.getAdditionalTomcatConnectors().size());
+ }
+
+ @Test
+ public void addNullAdditionalConnectorThrows() {
+ TomcatEmbeddedServletContainerFactory factory = getFactory();
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Connectors must not be null");
+ factory.addAdditionalTomcatConnectors((Connector[]) null);
+ }
+
@Test
public void sessionTimeout() throws Exception {
TomcatEmbeddedServletContainerFactory factory = getFactory();