Spring Schedule 为什么执行了两次?

1,007 阅读3分钟

背景

最近使用spring 的 @Scheduled 注解来执行定时任务,但是根据日志发现了该方法执行了两次,所以来探明原因。

image.png

注解配置如下:

// 类注解
@Lazy(false)
@Component


// 方法注解
@Scheduled(cron = "0 0 */3 * * ?")
public void method1() {
    // xxx
}

使用环境

  • jdk 1.8.0_321-b07
  • system CentOS 7.9
  • spring 4.3.5.RELEASAE
  • tomcat Apache Tomcat Version 7.0.103

分析

首先修改执行周期为每30秒一次,方便测试: @Scheduled(cron = "*/30 * * * * ?")

重新部署查看日志:

image.png

确实30秒一次,但是还是两次,执行了两次。

阶段结论

执行了两次,可以的出的结论是, 任务被提交了两次,或者有两个地方执行任务。

我们一个个排查:

  1. 任务是否被提交了两次?

我们看定时任务配置, 确实是标准方法,只有 @Scheduled(cron = "*/30 * * * * ?")注解,全局搜索是否存在scheduled 配置,发现是有的:

<task:executor id="executor" pool-size="10" />
<task:scheduler id="scheduler" pool-size="10" />
<task:annotation-driven scheduler="scheduler" executor="executor" proxy-target-class="true" />

在spring-contenxt.xml里发现定义了schedule 的初始池大小,但是也没有异常。

如果任务被提交两次,那么肯定有个先后顺序,而不是每次都基本同时执行,那么很大可能存在两个池子来执行任务了。

  1. 有两个地方执行任务 如果是两个地方执行任务,那么就有可能三 schedule 线程池被初始化了两次。

去tomcat启动日志查看, 确实发现了异常:

[INFO][2023-09-15 15:29:03.122][main][o.s.web.context.ContextLoader]- Root WebApplicationContext: initialization started

...

[INFO][2023-09-15 15:29:10.450][main][o.s.web.context.ContextLoader]- Root WebApplicationContext: initialization started

Root WebApplicationContext 被初始化了两次, 那么问题应该就是这个造成的。

解决

context 初始化两次,一般是配置文件配置不正确造成的, 根据很多网上答案,说是 spring-mvc.xml和 spring-context.xml 在 web.xml配置出现了问题,

检查配置

1.检查 spring-context.xml:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-context.xml</param-value>
</context-param>

正常

2.检查是否有多个DispatchServlet

<servlet>
    <servlet-name>springServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

只有一个,配置正常。

既然都正常,那么问题出在那里? 此时只能怀疑是不是tomcat 问题了。

检查tomcat

官网下载tomcat,解压后把工程包丢进去,启动tomcat 查看日志, 果然发现问题没有出现,那么问题就是出在tomcat配置了。

检查旧的tomcat配置, 只改了两处, 一处是 catalina.sh 启动脚本里加入了 JAVA_OPS ,设置JVM参数。 这里基本不会出问题,就算出问题也不会影响 spring加载。

另一处是, server.xml 里,Host标签 加了一行:

<Context path="" docBase="myapp" debug="0" reloadable="true" crossContext="true"/>

主要目的是从 / 访问工程,而不是从 /myapp 访问。 这么配置有问题吗?

去tomcat 官网查看这里配置, server.xml 到底是如何配置context的.

Host

关于 Host 的配置介绍在这里: tomcat.apache.org/tomcat-9.0-…

这个是原始的Host 配置文件:

<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
属性解释
appBase应用基本目录。 用来包含部署应用到这个虚拟主机的路径目录。可以绝对路经,或者相对 $CATALINA_BASE 的路径。 默认是 webapps
name通常是这个虚拟主机的网络名, 通常注册再你的 DNS 服务器上。 不能是通配符
unpackWARstrue 如果你想要把appBase路径下的web应用 war包解压到相应目录,就设置为true, false就是不解压,直接运行war 文件。默认是true
autoDeploytomcat周期检查是否有新的或更新的应用, 如果true, 周期性检查 appBase 和 xmlBase 目录,自动部署应用.

接下来看内嵌组建,也就是我们之前出错的 Context:

image.png

可以内嵌一个或多个 Context 元素在 Host元素里面, 每个都代表一个不同的web应用。 我们根据提升到 Context页面:

Context

Context 元素代表一个web应用。每个 web应用都基于 war包 或者一个包含解压文件的特定目录。 你可以配置多个 Context ,每个虚拟主机都必须有不重复的context 名字, context path 不需要不重复。更多的,一个 Context 必须具有0长度字符的上下文路径。

当autoDeploy 或者 deployOnStartup 由主机执行, name 和 context path 都由应用程序文件名派生。

没有指定 context ,那么 context name 和 context path 就是相同的。 如果 context path 是空字符串, base name 将会是 ROOT 或者 base name 将会是 '/'.

具体path的介绍:

The context path of this web application, which is matched against the beginning of each request URI to select the appropriate web application for processing. All of the context paths within a particular Host must be unique. If you specify a context path of an empty string (""), you are defining the default web application for this Host, which will process all requests not assigned to other Contexts.

This attribute must only be used when statically defining a Context in server.xml. In all other circumstances, the path will be inferred from the filenames used for either the .xml context file or the docBase.

Even when statically defining a Context in server.xml, this attribute must not be set unless either the docBase is not located under the Host's appBase or both deployOnStartup and autoDeploy are false. If this rule is not followed, double deployment is likely to result.

最后一句, 如果这个规则没有被遵守, 那么就会有两次部署! 这个就是我们被两次部署的原因了。那么规则是什么:

If you want to deploy a WAR file or a directory using a context path that is not related to the base file name then one of the following options must be used to prevent double-deployment:

  • Disable autoDeploy and deployOnStartup and define all Contexts in server.xml
  • Locate the WAR and/or directory outside of the Host's appBase and use a context.xml file with a docBase attribute to define it.

如果想部署的应用不依赖文件名, 然后你必须如下二选一:

1.禁用 autoDeploy 和 deployOnStartup , 定义server.xml的 Context

2.把应用放在 appBase 路径的外面,并且使用一个context.xml 用 docBase属性指定应用目录

测试这个规则1

翻出来我们之前的配置:

<Context path="" docBase="myapp" debug="0" reloadable="true" crossContext="true"/>

那么,如果要成功,就需要把上层的Host元素的属性 autoDeploy=false ,deployOnStartup=false

配置如下:

<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="false" deployOnStartup="false">

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

        <Context path="" docBase="myapp" debug="0" reloadable="true" crossContext="true"/>

      </Host>

启动后,成功部署.

image.png

测试这个规则2

这里我们可以直接默认 autoDeploy 和 deployOnStartup 是 true.

在 path 属性介绍里,说要制指定 Context的 docBase ,且docBase 必须在 appBase外面,也就是自己的app 不能放在 webapps 之下。

那么我们可以直接把myapp挪到外面, 然后在 docBase指定:

<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true" deployOnStartup="true">

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

        <Context path="" docBase="/opt/tomcat_services/myapp" debug="0" reloadable="true" crossContext="true"/>

      </Host>

重启后测试正常。

但是,官方不推荐直接改server.xml,不建议把 context定义到 server.xml 里

image.png

理由是,改变了server.xml 就需要重启tomcat, 另外,这是一种侵入式修改, 也容易被自定义的context覆盖掉。 那么推荐哪种写法?

  • In an individual file at /META-INF/context.xml inside the application files. Optionally (based on the Host's copyXML attribute) this may be copied to $CATALINA_BASE/conf/[enginename]/[hostname]/ and renamed to application's base file name plus a ".xml" extension.

要么,在自己应用的 META-INF/定义一个 context.xml, 要么在tomcat/conf/[enginename]/[hostname]/ 目录下定义 myapp.xml ,设置context

我们直接看 第二种。 这里 enginename 和 hostname 都是 server.xml里 Engine元素定义过的,默认是:

image.png

也就是,我们 直接在 tomcat conf/Catalina/localhost/ 下建自己的myapp.xml ,里面配置context :

<Context path="" docBase="/opt/tomcat_services/myapp" debug="0" reloadable="true" crossContext="true"/>

重启tomcat , 正常加载。 但是, 这里如果不用 /myapp 是访问不到服务的

理由还是path里的介绍:

This attribute must only be used when statically defining a Context in server.xml. In all other circumstances, the path will be inferred from the filenames used for either the .xml context file or the docBase.

这个path属性 ONLY 使用在静态定义server.xml 的Context , 所有的其他情况, path 会被文件名代替。

所以,要想使用 / ,还是要改 server.xml ,除非把文件改名为 ROOT.war ,代替webapps 下的ROOT.

结论

  • spring scheduled 执行了两次,是因为tomcat的 context 配置不正确,导致两次加载实例。
  • tomcat 配置context 有两种方式, 修改server.xml 或 建立自己的 context xml文件
  • 官方推荐建立自己的context 这种方式,但是指定docBase,程序文件不能放在 webapps 之下
  • 要想使用 / , 要么添加 server.xml context path , 要么替换掉 webapps的 ROOT