美文网首页征服Spring
Spring 定时任务

Spring 定时任务

作者: Java及SpringBoot | 来源:发表于2018-06-07 09:59 被阅读27次

    一、实现方法

    spring中实现的定时任务,大致有四种方法:

    • web接口,使用crontab调用
    • 使用@Scheduled注解
    • 使用Quartz
    • java类继承TimerTask

    这四类的比较如下:

    方法 配置项 集群模式 使用场景
    web接口 较少 集群模式下需要申请域名,通过域名调用 web项目
    @Scheduled注解 不支持集群模式,集群模式下每个节点都会调用注解标示的任务 单节点项目
    TimerTask 不支持集群模式,集群模式下每个节点都会调用注解标示的任务 单节点项目
    Quartz 需要额外数据库来支持集群模式 分布式项目

    下面详细介绍下基于注解和quartz的使用方法。

    二、@Scheduled注解

    2.1 依赖包

    Scheduled依赖包

    <dependency> 
            <groupId>org.springframework</groupId> 
            <artifactId>spring-context</artifactId> 
            <version>${spring.version}</version> 
    </dependency>
    

    2.2 配置文件

    Schedule配置文件

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns:task="http://www.springframework.org/schema/task" 
        xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context" 
        xsi:schemaLocation=" 
    http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-3.0.xsd 
    http://www.springframework.org/schema/task 
    http://www.springframework.org/schema/task/spring-task-3.1.xsd"> 
       
        <!-- 开启定时任务 --> 
        <task:annotation-driven />
        <!-- 配置线程池,若不配置所有的定时任务会串行执行 --> 
            <task:annotation-driven scheduler="myScheduler"/>  
        <task:scheduler id="myScheduler" pool-size="5"/> 
       
        <!-- 开启注解 --> 
        <context:annotation-config /> 
        <!-- 指定相关的包路径 --> 
        <context:component-scan base-package="com.spring.task"/>   <!-- MyTask中使用注解 -->
      
        <!-- 当然你也可以不在java类中使用注解,此时需要如下配置 -->        
            <bean id="myTask" class="com.spring.task.MyTask2"></bean>  <!-- MyTask2中不使用注解,在配置文件配置执行信息 --> 
        <task:scheduled-tasks>
          <!-- 这里表示的是每隔五秒执行一次 -->
          <task:scheduled ref="myTask2" method="show" cron="*/5 * * * * ?" />
          <task:scheduled ref="myTask2" method="print" cron="*/10 * * * * ?"/>
        </task:scheduled-tasks>
    </beans>
    

    2.3 Job类实现

    Scheduled任务类

    package com.spring.task;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
      
    /**
     * 基于注解的定时器任务
     */
    @Component
    public class MyTask {
      
      /**
       * 定时计算。每天凌晨 01:00 执行一次
       */
      @Scheduled(cron = "0 0 1 * * *")
      public void show() {
        System.out.println("show method 2");
      }
      
      /**
       * 启动时执行一次,之后每隔2秒执行一次
       */
      @Scheduled(fixedRate = 1000*2) 
      public void print() {
        System.out.println("print method 2");
      }
    }
    非注解任务  展开原码
    package com.spring.task;
      
    /**
     * 不使用注解,自定义任务,此时就是普通的java类
     */
    public class MyTask2 {
      
      public void show() {
        System.out.println("show method 1");
      }
      
      public void print() {
        System.out.println("print method 1");
      }
    }
    

    三、Quartz

    3.1 Quartz任务调度的基本实现原理

    Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于Java实现。作为一个优秀的开源调度框架,Quartz具有以下特点:

    (1)强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;

    (2)灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;

    (3)分布式和集群能力,Terracotta收购后在原来功能基础上作了进一步提升。本文将对该部分相加阐述。

    3.1.1 Quartz 核心元素

    Quartz任务调度的核心元素为:Scheduler——任务调度器、Trigger——触发器、Job——任务。其中trigger和job是任务调度的元数据,scheduler是实际执行调度的控制器。

    Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。这四种trigger可以满足企业应用中的绝大部分需求。

    Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:requestsRecovery和durability,其中requestsRecovery表示任务是否在发生故障的时候在其他节点执行,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。

    Scheduler由scheduler工厂创建:DirectSchedulerFactory或者StdSchedulerFactory。第二种工厂StdSchedulerFactory使用较多,因为DirectSchedulerFactory使用起来不够方便,需要作许多详细的手工编码设置。Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。

    Quartz核心元素之间的关系如图所示:

    01.png

    ​ 核心元素关系图

    3.1.2 Quartz 线程视图

    在Quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。

    02.png

    ​ Quartz线程视图

    Scheduler调度线程主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。

    3.1.3 Quartz Job数据存储

    Quartz中的trigger和job需要存储下来才能被使用。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。

    03.png

    3.2 Quartz集群原理

    3.2.1 Quartz 集群架构

    一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的,如图所示。

    04.png

    ​ Quartz集群架构

    3.2.2 Quartz集群相关数据库表

    因为Quartz集群依赖于数据库,所以必须首先创建Quartz数据库表,Quartz发布包中包括了所有被支持的数据库平台的SQL脚本。这些SQL脚本存放于<quartz_home>/docs/dbTables 目录下。这些表的简要介绍如和创建语句如下。

    数据表说明

    # 数据库所需表
    # QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 信息
    # QRTZ_CRON_TRIGGERS 存储 Cron Trigger,包括Cron表达式和时区信息
    # QRTZ_FIRED_TRIGGERS 存储与已触发的 Trigger 相关的状态信息,以及相联 Job的执行信息QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的 Trigger组的信息
    # QRTZ_SCHEDULER_STATE 存储少量的有关 Scheduler 的状态信息,和别的Scheduler实例(假如是用于一个集群中)
    # QRTZ_LOCKS 存储程序的悲观锁的信息(假如使用了悲观锁)
    # QRTZ_JOB_DETAILS 存储每一个已配置的 Job 的详细信息
    # QRTZ_JOB_LISTENERS 存储有关已配置的 JobListener的信息
    # QRTZ_SIMPLE_TRIGGERS存储简单的Trigger,包括重复次数,间隔,以及已触的次数
    # QRTZ_BLOG_TRIGGERS Trigger 作为 Blob 类型存储(用于 Quartz 用户用JDBC创建他们自己定制的 Trigger 类型,JobStore并不知道如何存储实例的时候)
    # QRTZ_TRIGGER_LISTENERS 存储已配置的 TriggerListener的信息
    # QRTZ_TRIGGERS 存储已配置的 Trigger 的信息
    

    重要表说明:

    调度器状态表(QRTZ_SCHEDULER_STATE)

    说明:集群中节点实例信息,Quartz定时读取该表的信息判断集群中每个实例的当前状态。

    instance_name:配置文件中org.quartz.scheduler.instanceId配置的名字,如果设置为AUTO,quartz会根据物理机名和当前时间产生一个名字。

    last_checkin_time:上次检入时间

    checkin_interval:检入间隔时间

    触发器与任务关联表(qrtz_fired_triggers)

    存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息。

    触发器信息表(qrtz_triggers)

    trigger_name:trigger的名字,该名字用户自己可以随意定制,无强行要求

    trigger_group:trigger所属组的名字,该名字用户自己随意定制,无强行要求

    job_name:qrtz_job_details表job_name的外键

    job_group:qrtz_job_details表job_group的外键

    trigger_state:当前trigger状态设置为ACQUIRED,如果设为WAITING,则job不会触发

    trigger_cron:触发器类型,使用cron表达式

    任务详细信息表(qrtz_job_details)

    说明:保存job详细信息,该表需要用户根据实际情况初始化

    job_name:集群中job的名字,该名字用户自己可以随意定制,无强行要求。

    job_group:集群中job的所属组的名字,该名字用户自己随意定制,无强行要求。

    job_class_name:集群中job实现类的完全包名,quartz就是根据这个路径到classpath找到该job类的。

    is_durable:是否持久化,把该属性设置为1,quartz会把job持久化到数据库中

    job_data:一个blob字段,存放持久化job对象。

    权限信息表(qrtz_locks)

    3.2.3 Quartz Scheduler在集群中的启动流程

    Quartz Scheduler自身是察觉不到被集群的,只有配置给Scheduler的JDBC JobStore才知道。当Quartz Scheduler启动时,它调用JobStore的schedulerStarted()方法,它告诉JobStore Scheduler已经启动了。schedulerStarted() 方法是在JobStoreSupport类中实现的。JobStoreSupport类会根据quartz.properties文件中的设置来确定Scheduler实例是否参与到集群中。假如配置了集群,一个新的ClusterManager类的实例就被创建、初始化并启动。ClusterManager是在JobStoreSupport类中的一个内嵌类,继承了java.lang.Thread,它会定期运行,并对Scheduler实例执行检入的功能。Scheduler也要查看是否有任何一个别的集群节点失败了。检入操作执行周期在quartz.properties中配置。

    3.2.4 侦测失败的Scheduler节点

    当一个Scheduler实例执行检入时,它会查看是否有其他的Scheduler实例在到达他们所预期的时间还未检入。这是通过检查SCHEDULER_STATE表中Scheduler记录在LAST_CHEDK_TIME列的值是否早于org.quartz.jobStore.clusterCheckinInterval来确定的。如果一个或多个节点到了预定时间还没有检入,那么运行中的Scheduler就假定它(们) 失败了。

    3.2.5 从故障实例中恢复Job

    当一个Sheduler实例在执行某个Job时失败了,有可能由另一正常工作的Scheduler实例接过这个Job重新运行。要实现这种行为,配置给JobDetail对象的Job可恢复属性必须设置为true(job.setRequestsRecovery(true))。如果可恢复属性被设置为false(默认为false),当某个Scheduler在运行该job失败时,它将不会重新运行;而是由另一个Scheduler实例在下一次触发时间触发。Scheduler实例出现故障后多快能被侦测到取决于每个Scheduler的检入间隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。

    3.3 Quartz集群实例(Quartz+Spring)

    3.3.1 依赖包

    Quartz依赖

    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>${quartz.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>
    

    3.3.2 配置文件

    有三类配置文件:数据源配置、quartz基本配置、quartz任务配置

    3.3.2.1 数据源配置

    1.jdbc配置

    jdbc配置

    quartz.url=jdbc:mysql://10.111.17.78:3306/kec_scheduler?useUnicode=true&characterEncoding=utf8
    quartz.username=root
    quartz.password=root
    quartz.driverClassName=com.mysql.jdbc.Driver
    

    2.数据源定义

    数据源配置

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      
        <!-- 属性文件读入 -->
        <bean id="propertyConfigurer"
              class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="locations">
                <list>
                    <value>classpath:database.properties</value>
                </list>
            </property>
        </bean>
      
        <!-- 数据源定义,使用c3p0 连接池 -->
        <bean id="quartzDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
              destroy-method="close">
            <property name="driverClass" value="${quartz.driverClassName}" />
            <property name="jdbcUrl" value="${quartz.url}" />
            <property name="user" value="${quartz.username}" />
            <property name="password" value="${quartz.password}" />
            <property name="initialPoolSize" value="2" />
            <property name="minPoolSize" value="10" />
            <property name="maxPoolSize" value="20" />
            <property name="acquireIncrement" value="2" />
            <property name="maxIdleTime" value="1800" />
        </bean>
      
        <!-- 使用jdbc访问数据库 -->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="quartzDataSource" />
        </bean>
    </beans>
    

    3.3.2.2 quartz基本配置

    quartz基本配置

    #==============================================================   
    #Configure Main Scheduler Properties   
    #============================================================== 
    org.quartz.scheduler.instanceName = DimServerQuartz     # 可为任何值,用在 JDBC JobStore 中来唯一标识实例,但是所有集群节点中必须相同
    org.quartz.scheduler.instanceId = AUTO                  # 基于主机名和时间戳来产生实例ID
      
    #==============================================================   
    #Configure ThreadPool   
    #============================================================== 
    org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
    org.quartz.threadPool.threadCount = 10
    org.quartz.threadPool.threadPriority = 5
    org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
      
      
    #==============================================================   
    #Configure JobStore   
    #============================================================== 
    org.quartz.jobStore.misfireThreshold = 60000
    # JobStoreTX,将任务持久化到数据中。因为集群中节点依赖于数据库来传播 Scheduler 实例的状态,你只能在使用 JDBC JobStore 时应用 Quartz 集群。
    # 这意味着你必须使用 JobStoreTX 或是 JobStoreCMT 作为 Job 存储;你不能在集群中使用 RAMJobStore。
    org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
    # 根据选择的数据库类型不同而不同,我这里的是mysql,所以是org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    org.quartz.jobStore.tablePrefix = QRTZ_               # 表前缀
    org.quartz.jobStore.maxMisfiresToHandleAtATime=10
    org.quartz.jobStore.isClustered = true                # 属性为 true,你就告诉了 Scheduler 实例要它参与到一个集群当中
    org.quartz.jobStore.clusterCheckinInterval = 3600000  # 调度实例失效的检查时间间隔,检查间隔3600s
      
    #============================================================== 
    #Configure DataSource  我通过配置文件引入数据源信息,如2.1中的配置
    #==============================================================
    # org.quartz.jobStore.dataSource = myDS     # 别名
    # org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
    # org.quartz.dataSource.myDS.URL = jdbc:mysql://192.168.31.18:3306/test?useUnicode=true&amp;characterEncoding=UTF-8
    # org.quartz.dataSource.myDS.user = root
    # org.quartz.dataSource.myDS.password = 123456
    # org.quartz.dataSource.myDS.maxConnections = 30
      
    org.quartz.scheduler.skipUpdateCheck = true                                                 # 不检查版本更新
    # org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin # 打印步骤信息,可在调试时使用
    org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin     # 停止时的清理插件
    org.quartz.plugin.shutdownhook.cleanShutdown = true
    

    3.3.2.3 quartz任务配置

    quartz任务配置

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      
        <!-- 数据库配置文件 -->
        <import resource="classpath:datasource.xml"></import>
             
            <!-- 为了支持使用注解所写的类 -->
        <bean id="jobFactory" lazy-init="false" autowire="no" class="com.test.scheduler.api.AutoWiredJobFactory"/>  
      
        <bean name="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
            <property name="jobFactory" ref="jobFactory"/>         <!-- 正常情况下QuartzJobBean是无法使用注解的,若想使用需引入配置,该类的实现下面会展示 -->
            <property name="overwriteExistingJobs" value="true"/>  <!-- 是否覆盖已有job,防止启动时重复执行停止时的任务 -->
            <property name="dataSource">                           <!-- 数据源配置 -->
                <ref bean="quartzDataSource" />
            </property>
            <property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
            <property name="configLocation" value="classpath:quartz/quartz.properties" />   <!-- quartz基本配置 -->
      
            <property name="triggers">                             <!-- 在此添加自定义的trigger -->
                <list>
                    <ref bean="SyncTrigger" />
                </list>
            </property>
        </bean>
                    
            <!-- 自定义的trigger -->
        <bean id="SyncTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
            <property name="jobDetail" ref="SyncJob" />
            <property name="cronExpression" value="0/10 * * * * ?" />   <!-- 每隔10s执行一次 -->
        </bean>
      
        <!-- 自定义的job -->
        <bean id="SyncJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
            <property name="durability" value="true"></property>       <!-- 是否持久化,集群模式时需为true -->
            <property name="requestsRecovery" value="true"></property> <!-- 失败或者重启时是否在其他节点恢复 -->
            <property name="jobClass"                                  <!-- 继承QuartzJobBean的自定义任务实现类 -->
                      value="com.scheduler.api.SyncScheduler.SyncJob">
            </property>
        </bean>
     
    </beans>
    

    3.3.3 Job类实现

    自定义的任务(job),需要继承QuartzJobBean并实现其中的executeInternal函数,另外为了支持使用注解需要一个额外的AutoWiredJobFactory类。

    3.3.3.1 Job类

    自定义Job

    package com.scheduler.api.SyncScheduler;
      
    import org.quartz.DisallowConcurrentExecution;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.scheduling.quartz.QuartzJobBean;
    import org.springframework.stereotype.Service;
      
    @Service
    @DisallowConcurrentExecution  // 不允许并发执行
    public class SyncJob extends QuartzJobBean {
      
        @Autowired
        private JdbcTemplate jdbcTemplate;
      
        private Logger logger = LoggerFactory.getLogger(SyncJob.class);
      
        @Override
        public void executeInternal(JobExecutionContext jobexecutioncontext) throws JobExecutionException {
      
            //这里执行定时调度业务
            logger.info("testMethod.......1");
            System.out.println("2--testMethod......."+System.currentTimeMillis()/1000);
        }
    }
    

    3.3.3.2 AutoWiredJobFactory类

    AutoWiredJobFactory类

    package com.scheduler.api;
      
    import org.quartz.spi.TriggerFiredBundle;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
    import org.springframework.scheduling.quartz.AdaptableJobFactory;
      
    /**
     * 供 quartz scheduler 使用,使其支持注解
     */
    public class AutoWiredJobFactory extends AdaptableJobFactory {
      
        @Autowired
        private AutowireCapableBeanFactory capableBeanFactory;
      
        @Override
        protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
            //调用父类方法
            Object jobInstance = super.createJobInstance(bundle);
            //进行注入
            capableBeanFactory.autowireBean(jobInstance);
            return jobInstance;
        }
    }
    

    3.4 注意事项

    3.4.1 时间同步问题

    Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。

    节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。

    3.4.2 节点争抢Job问题

    因为Quartz使用了一个随机的负载均衡算法, Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。

    3.4.3 从集群获取Job列表问题

    当前,如果不直接进到数据库查询的话,还没有一个简单的方式来得到集群中所有正在执行的Job列表。请求一个Scheduler实例,将只能得到在那个实例上正运行Job的列表。Quartz官网建议可以通过写一些访问数据库JDBC代码来从相应的表中获取全部的Job信息。

    3.5 参考文档

    http://www.cnblogs.com/zhenyuyaodidiao/p/4755649.html (Quartz集群原理及配置应用)

    http://veiking.iteye.com/blog/2372284 (Quartz在集群、分布式系统中的应用)

    http://www.jianshu.com/p/14f86c6efe22 (分布式定时任务(二))

    http://www.jianshu.com/p/a518dd3229de (分布式定时任务(三))

    相关文章

      网友评论

        本文标题:Spring 定时任务

        本文链接:https://www.haomeiwen.com/subject/wavwsftx.html