SpringBoot之浅析TomCat端口号设置

简介:

我们在之前的文章中说过怎么去修改TomCat的端口号(SpringBoot修改默认端口号),我们在这篇文章中简单的说一下SpringBoot是怎么实现修改TomCat端口号的。
修改TomCat的端口号大概可以分为这样的两类吧,一种是用配置项的方式,另一种是用程序实现的方式。配置项包含:设置命令行参数、系统参数、虚拟机参数、SpringBoot默认的application.properties(或者是application.yml等类似的方式)。用程序实现的方式,则需要实现EmbeddedServletContainerCustomizer接口,并将此实现类注入为Spring的Bean。我们先说配置项的方式。
通常我们用配置项的方式来修改TomCat端口号的时候,需要进行这样的配置(或类似的方式):

server.port=8081

看到这样的一个配置项再结合我们自己在使用ConfigurationProperties的时候所进行的设置,我们可以推断一下应该会存在一个这样的JavaBean,在这个JavaBean上使用了ConfigurationProperties注解,并且它的prefix的值为server。既然有了一个这样的推想,那么我们就要去证明这个推想。在SpringBoot中也确实存在了我们所推想的这样的一个JavaBean:ServerProperties。ServerProperties这个类的UML如下所示:
ServerProperties
ServerProperties实现了EnvironmentAware接口,说明它可以获取Environment中的属性值,它也实现了Ordered接口,这个这里先记着,我们在后面再说,它也实现了EmbeddedServletContainerCustomizer接口,我们在上面说的第二种修改TomCat端口号的方式就是实现EmbeddedServletContainerCustomizer接口,并注入为Spring的Bean,而ServerProperties就实现了这个接口。但是这里还有一个问题,在这个类上没有添加Component注解(或者是相同作用的注解)。但是我们在SpringBoot中还发现了这样的一个类:ServerPropertiesAutoConfiguration。从名字我们可以猜出这个类应该是为ServerProperties提供自动配置的一个类,这个类也确实是这样的一个作用,关于SpringBoot的自动配置功能比较复杂,我们这里先不展开,有这方面疑问的童鞋可以在下面留言。我们去ServerPropertiesAutoConfiguration这个类中看一下这个类的代码:

//Configuration相当于<beans>标签
//EnableConfigurationProperties使ConfigurationProperties注解生效,并将EnableConfigurationProperties这个注解中执行的类注入为Spring的Bean
//ConditionalOnWebApplication 必须是在web开发环境中
@Configuration
@EnableConfigurationProperties
@ConditionalOnWebApplication
public class ServerPropertiesAutoConfiguration {
    //如果在当前容器中 不存在ServerProperties类型的Bean,则创建ServerProperties Bean
    @Bean
    @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
    public ServerProperties serverProperties() {
        return new ServerProperties();
    }
    //这个Bean也实现了 EmbeddedServletContainerCustomizer 接口,它同时还实现了ApplicationContextAware 接口,说明在这个类中可以获取到Spring容器的应用上下文  这个类的作用是检测在Spring 容器中是否有多于一个ServerProperties类型的Bean存在 如果是则抛出异常
    @Bean
    public DuplicateServerPropertiesDetector duplicateServerPropertiesDetector() {
        return new DuplicateServerPropertiesDetector();
    }

    private static class DuplicateServerPropertiesDetector implements
            EmbeddedServletContainerCustomizer, Ordered, ApplicationContextAware {

        private ApplicationContext applicationContext;

        @Override
        public int getOrder() {
            return 0;
        }

        @Override
        public void setApplicationContext(ApplicationContext applicationContext)
                throws BeansException {
            this.applicationContext = applicationContext;
        }

        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            // ServerProperties handles customization, this just checks we only have
            // a single bean  主要作用就是检测当前容器中是否存在多于一个ServerProperties类型的Bean存在
            String[] serverPropertiesBeans = this.applicationContext
                    .getBeanNamesForType(ServerProperties.class);
            Assert.state(serverPropertiesBeans.length == 1,
                    "Multiple ServerProperties beans registered " + StringUtils
                            .arrayToCommaDelimitedString(serverPropertiesBeans));
        }
    }
}

通过上面的分析,我们看到了在哪里将ServerProperties包装为Spring容器的Bean的。下面我们来简单的说一下ServerProperties这个类中都为我们提供了什么东西:

    /**
     * 启动端口号
     * Server HTTP port.
     */
    private Integer port;
    /**
     *  ServletConetxt上下文路径
     * Context path of the application.
     */
    private String contextPath;
    /**
     * DispatcherServlet 主要的 servlet Mapping    
     * Path of the main dispatcher servlet.
     */
    private String servletPath = "/";
    /** 
     * ServletContext参数
     * ServletContext parameters.
     */
    private final Map<String, String> contextParameters = new HashMap<String, String>();

以及它的内部类,分别用来做和TomCat设置相关的内容、Jetty设置相关的内容、Undertow设置相关的内容以及Session设置相关的内容。关于ServerProperties中的属性值的设置请参考之前的文章,这里就不再多说了。
ServerProperties
在ServerProperties中最重要的一个方法是customize方法,这个方法是用来设置容器相关的内容的。

    //这里的ConfigurableEmbeddedServletContainer 请看这个类中的内容EmbeddedServletContainerAutoConfiguration,看完你应该就会明白它是什么了
    public void customize(ConfigurableEmbeddedServletContainer container) {
        //端口号
        if (getPort() != null) {
            container.setPort(getPort());
        }
        //IP地址
        if (getAddress() != null) {
            container.setAddress(getAddress());
        }
        //ContextPath
        if (getContextPath() != null) {
            container.setContextPath(getContextPath());
        }
        if (getDisplayName() != null) {
            container.setDisplayName(getDisplayName());
        }
        //Session超时时间
        if (getSession().getTimeout() != null) {
            container.setSessionTimeout(getSession().getTimeout());
        }
        container.setPersistSession(getSession().isPersistent());
        container.setSessionStoreDir(getSession().getStoreDir());
        //SSL
        if (getSsl() != null) {
            container.setSsl(getSsl());
        }
        //JspServlet
        if (getJspServlet() != null) {
            container.setJspServlet(getJspServlet());
        }
        if (getCompression() != null) {
            container.setCompression(getCompression());
        }
        container.setServerHeader(getServerHeader());
        //如果是TomCat服务器
        if (container instanceof TomcatEmbeddedServletContainerFactory) {
            getTomcat().customizeTomcat(this,
                    (TomcatEmbeddedServletContainerFactory) container);
        }
        //如果是Jetty服务器
        if (container instanceof JettyEmbeddedServletContainerFactory) {
            getJetty().customizeJetty(this,
                    (JettyEmbeddedServletContainerFactory) container);
        }
        //如果是Undertow服务器
        if (container instanceof UndertowEmbeddedServletContainerFactory) {
            getUndertow().customizeUndertow(this,
                    (UndertowEmbeddedServletContainerFactory) container);
        }
        container.addInitializers(new SessionConfiguringInitializer(this.session));
        //ServletContext 参数
        container.addInitializers(new InitParameterConfiguringServletContextInitializer(
                getContextParameters()));
    }

现在的关键问题是customize这个方法是在什么时候被调用的呢?通过翻开它的调用链,我们在SpringBoot中发现了这样的一个类:EmbeddedServletContainerCustomizerBeanPostProcessor在这个类中有这样的一个方法,

    //这个方法 如果你对Spring中的生命周期熟悉的话,那么你看到这个方法的时候一定不会陌生,同时这个类应该是实现了BeanPostProcessor 那么现在还存在的一个问题是,只有这个类是一个Spring中的Bean的时候,它才会被调用到,那么这个类是什么时候被注入到Spring的容器中的呢 
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        //如果是ConfigurableEmbeddedServletContainer类型 才会继续下面的动作 
        //所以这里是对我们在应用程序中所使用的应用服务器进行设置
        if (bean instanceof ConfigurableEmbeddedServletContainer) {
            postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
        }
        return bean;
    }
    private void postProcessBeforeInitialization(
            ConfigurableEmbeddedServletContainer bean) {
        //getCustomizers()获取容器中的EmbeddedServletContainerCustomizer的实现类,ServerProperties当然算是一个,我们上面提到的DuplicateServerPropertiesDetector 也是一个
        for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
            //调用EmbeddedServletContainerCustomizer的实现类中的customize方法
            customizer.customize(bean);
        }
    }

如果你对Spring中的生命周期熟悉的话,那么你看到postProcessBeforeInitialization这个方法的时候一定不会陌生,首先应该想到它应该是实现了BeanPostProcessor这个接口。那么现在还存在的一个问题是,只有这个类是一个Spring中的Bean的时候,它才会被调用到,那么这个类是什么时候被注入到Spring的容器中的呢 ?答案就在EmbeddedServletContainerAutoConfiguration这个类中。这个类的作用是自动配置嵌入式的Servlet容器。在这个类上用了这样的一个注解:

@Import(BeanPostProcessorsRegistrar.class)

Import这个注解在实现SpringBoot的自动配置功能的时候起到了非常重要的作用!Import这个注解中的value所指定的Class可以分为这样的三类:一类是实现了ImportSelector接口,一类是实现了ImportBeanDefinitionRegistrar接口,不属于前面说的这两种的就是第三种,具体的可以看一下这个方法org.springframework.context.annotation.ConfigurationClassParser#processImports。而上面所提到的BeanPostProcessorsRegistrar这个类就是实现了ImportBeanDefinitionRegistrar这个接口的。我们看一下这个类中的内容(EmbeddedServletContainerAutoConfiguration的内部类):

    //这个类实现了ImportBeanDefinitionRegistrar接口,同时也实现了BeanFactoryAware 接口
    public static class BeanPostProcessorsRegistrar
            implements ImportBeanDefinitionRegistrar, BeanFactoryAware {

        private ConfigurableListableBeanFactory beanFactory;

        @Override
        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            if (beanFactory instanceof ConfigurableListableBeanFactory) {
                this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
            }
        }
        //注入Bean定义  注意这里的Bean 都是用注解的方法注入的bean  如标注Component注解的Bean
        //这个方法的调用链先不介绍的
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                BeanDefinitionRegistry registry) {
            if (this.beanFactory == null) {
                return;
            }
            //注入EmbeddedServletContainerCustomizerBeanPostProcessor
            registerSyntheticBeanIfMissing(registry,
                    "embeddedServletContainerCustomizerBeanPostProcessor",
                    EmbeddedServletContainerCustomizerBeanPostProcessor.class);
            //注入ErrorPageRegistrarBeanPostProcessor
            registerSyntheticBeanIfMissing(registry,
                    "errorPageRegistrarBeanPostProcessor",
                    ErrorPageRegistrarBeanPostProcessor.class);
        }

        private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry,
                String name, Class<?> beanClass) {
            //如果容器中不存在指定类型的Bean定义
            if (ObjectUtils.isEmpty(
                    this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
                //创一个RootBean定义
                RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
                //合成的Bean 不是应用自己创建的基础建设角色的Bean
                beanDefinition.setSynthetic(true);
                //注入Spring容器中
                registry.registerBeanDefinition(name, beanDefinition);
            }
        }
    }

到现在为止,关于SpringBoot设置TomCat启动端口号的简单分析就算是结束了。但是这里还有一个问题,如果我们既用配置项的形式设置了TomCat的端口号,同时又自定义了一个实现了EmbeddedServletContainerCustomizer接口的Bean,并且没有Order相关的设置,那么最终生效的会是哪个配置呢?答案是实现了EmbeddedServletContainerCustomizer接口的Spring Bean。在org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor#postProcessBeforeInitialization(org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer)中的方法的内容如下:

    private void postProcessBeforeInitialization(
            ConfigurableEmbeddedServletContainer bean) {
        //getCustomizers() 从当前的Spirng容器中获取所有EmbeddedServletContainerCustomizer 类型的Bean
        //getCustomizers() 中的Bean是进行过排序之后的  所以这里Order值大的会覆盖Order值小的设置
        for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
            customizer.customize(bean);
        }
    }

    private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
        if (this.customizers == null) {
            // Look up does not include the parent context
            //从当前的Spring容器中获取EmbeddedServletContainerCustomizer 类型的Bean
            this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
                    this.beanFactory
                            .getBeansOfType(EmbeddedServletContainerCustomizer.class,
                                    false, false)
                            .values());
            //将获取到的Bean进行排序 排序是根据Order的值进行排序的  如果你的Bean没有进行过任何关于Order值的设置的话,那么你的Bean将位于最后的位置了
            Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
            this.customizers = Collections.unmodifiableList(this.customizers);
        }
        return this.customizers;
    }
相关文章
|
1月前
|
JSON Java 数据格式
springboot中表字段映射中设置JSON格式字段映射
springboot中表字段映射中设置JSON格式字段映射
93 1
|
24天前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
48 1
|
16天前
|
安全 网络安全 网络架构
什么是端口转发?什么是端口映射?如何设置端口映射
端口映射与端口转发是网络配置中两个常被混淆的概念。端口映射是指将外部网络请求通过路由器转发至内部网络特定主机的过程,增强了内网安全性。而端口转发则是指路由器依据端口将外部请求定向至具体设备,实现内外网通信。两者虽相似,但应用场景和原理有所不同。通过工具如花生壳,可轻松设置端口映射,实现外网访问内网服务。
52 1
|
15天前
|
安全 Java 应用服务中间件
如何将Spring Boot应用程序运行到自定义端口
如何将Spring Boot应用程序运行到自定义端口
26 0
|
2月前
|
Java Spring
【SpringBoot】技能一之修改端口与banner样式
【SpringBoot】技能一之修改端口与banner样式
29 5
|
2月前
|
存储 Java API
简单两步,Spring Boot 写死的定时任务也能动态设置:技术干货分享
【10月更文挑战第4天】在Spring Boot开发中,定时任务通常通过@Scheduled注解来实现,这种方式简单直接,但存在一个显著的限制:任务的执行时间或频率在编译时就已经确定,无法在运行时动态调整。然而,在实际工作中,我们往往需要根据业务需求或外部条件的变化来动态调整定时任务的执行计划。本文将分享一个简单两步的解决方案,让你的Spring Boot应用中的定时任务也能动态设置,从而满足更灵活的业务需求。
132 4
|
5月前
|
负载均衡 网络协议 Linux
|
4月前
|
Java Spring 开发者
解锁 Spring Boot 自动化配置的黑科技:带你走进一键配置的高效开发新时代,再也不怕繁琐设置!
【8月更文挑战第31天】Spring Boot 的自动化配置机制极大简化了开发流程,使开发者能专注业务逻辑。通过 `@SpringBootApplication` 注解组合,特别是 `@EnableAutoConfiguration`,Spring Boot 可自动激活所需配置。例如,添加 JPA 依赖后,只需在 `application.properties` 配置数据库信息,即可自动完成 JPA 和数据源设置。这一机制基于多种条件注解(如 `@ConditionalOnClass`)实现智能配置。深入理解该机制有助于提升开发效率并更好地解决问题。
78 0
|
4月前
|
网络协议
【qt】TCP的监听 (设置服务器IP地址和端口号)
【qt】TCP的监听 (设置服务器IP地址和端口号)
243 0
|
5月前
|
前端开发 应用服务中间件 nginx
网页设计,若依项目修改(It must be done)01----若依打包位置,nginx代理前端静态资源和后端接口,就是怎样设置转载,访问固定端口,让他访问其他资料的配置文件,访问/,给你那些
网页设计,若依项目修改(It must be done)01----若依打包位置,nginx代理前端静态资源和后端接口,就是怎样设置转载,访问固定端口,让他访问其他资料的配置文件,访问/,给你那些