前言
spring boot工程需要同时支持http请求和https请求,spring boot 2.x的官方文档解释的很清晰,这里是地址:Configure SSL,上面也有github的示例,也可以参考配置多个连接器这个示例,以硬编码的一种方式来配置https连接器,地址:Enable Multiple Connectors with Tomcat。
不过我用的是spring boot 1.x的版本,这里的代码方案是不支持的。因为有些类,我在spring boot 1.x的版本是没有找到的,官方文档我也没找到1.x版本的,所以只能通过去看下源码找下有没有突破口。
源码分析
首先是查找spring boot的servlet容器,在它的自动配置包(autoconfigure)下,org.springframework.boot.autoconfigure.web在这个路径下,找到了一个类:EmbeddedServletContainerAutoConfiguration, 下面是几行关键的源码:
public class EmbeddedServletContainerAutoConfiguration {
/**
* Nested configuration if Tomcat is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory();
}
}
/**
* Nested configuration if Jetty is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
WebAppContext.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
@Bean
public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
return new JettyEmbeddedServletContainerFactory();
}
}
/**
* Nested configuration if Undertow is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedUndertow {
@Bean
public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
return new UndertowEmbeddedServletContainerFactory();
}
}
//...
}
根据源码,可以看到是配置哪个servlet容器的工厂bean,默认是tomcat的,因为最下面2个条件不满足,通过IDEA很清楚就看到了,如下:
这几个类在类路径下是不存在的,除非显式引入相关依赖。
所以,我们只看第一个tomcat的servlet容器工厂bean。
代码中,配置bean的时候,只是很简单new 了一个TomcatEmbeddedServletContainerFacotry的实例,并没有其它额外配置。然后看下TomcatEmbeddedServletContainerFacotry的源码,类上的注释说明这个工厂用来创建tomcat的内嵌的servlet容器。
看下这个类的方法:
方法比较多,分析了下之后,定位到了上面红色标记的方法`addAdditionalTomcatConnectors`方法,看下源码及注释:
/**
* 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));
}
可以用来增加一些额外的连接器,比如SSL 或者AJP。
所以我们只要自定义TomcatEmbeddedServletContainerFacotry这个bean在构造的设置增加一个额外的https连接器即可。这里不用担心会与spring boot这个冲突,上面的代码有这个条件:
所以,只要我们自定义的这个优先级高,最先被解析加载,spring boot在解析它这个自动配置的bean定义的时候,发现已经有这个bean了,就不会再配置它的默认的TomcatEmbeddedServletContainerFacotry这个bean了。
实现
因为application.properties不支持同时配置Http和https两种协议,只能配置一个,另一个必须通过编码的方式,所以我在实现的时候,想到通过编码方式配置https的复杂性(2.x版本虽然配置方式不支持1.x版本,但是给了一个建议,就是通过application.properties配置https,编码的方式配置http),所以我就采用了这个建议,在application.properties中配置https,如下:
server.port=8443
server.ssl.key-store=classpath:sample.jks
server.ssl.key-store-password=secret
server.ssl.key-password=password
这个配置文件的sample.jks这个签名证书是我直接从2.x版本的github的示例代码中下载的,也可以通过keytool自己生成,我这里图个省事。
sample.jks的工程类路径(resources目录)下,如果因为工程环境原因,这样写解析不到这个smaple.jks文件,也可以把路径写成绝对文件路径。
然后在启动类里配置TomcatEmbeddedServletContainerFacotry类的bean,如下:
@SpringBootApplication
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
@Bean
public TomcatEmbeddedServletContainerFactory embeddedServletContainerFactory() {
TomcatEmbeddedServletContainerFactory containerFactory = new TomcatEmbeddedServletContainerFactory();
Connector connector = new Connector(TomcatEmbeddedServletContainerFactory.DEFAULT_PROTOCOL);
connector.setPort(8080);
containerFactory.addAdditionalTomcatConnectors(connector);
return containerFactory;
}
}
启动之后,成功日志如下(有两个端口,一个http,一个https) :
这样一个基本的配置就完了。
其它想法
在最初我是想通过另一处方式配置,不是自定义这个bean,而是在spring 初始化的过程,从spring 容器拿到TomcatEmbeddedServletContainerFacotry这个bean,然后调用addAdditionalTomcatConnectors方法完成。后来分析了它的初始化顺序,发现还是不要采用这种方式了:
原因是这样,如果我想在初始化的时候,拿到这个bean进行一些操作,可以实现spring 提供的一些回调接口来实现,一般是:BeanFactoryPostProcessor、BeanPostProcessor、XXXAware、InitializingBean、SmartInitializingSingleton...等这些个,但是servlet容器的初始化是在onRefresh这个方法内,只有BeanFactoryPostProcessor的执行时机早于它。但是实在是不建议在BeanFactoryPostProcessor类的postProcessBeanFactory方法内做这个获取bean的操作,很容易导致某些bean的提前初始化,稍为复杂点的工程,这都是很容易出一些无法预料的问题的。
因此,思来想去,还是用上面那种方案比较好。