1.Quartz监听器
Quartz的监听器用于当前任务调度中你所关注事件发生时,能够及时获取这一事件的通知,类似于String AOP在事件执行前后运行相应的指定程序
Quartz监听器的三大类型:
- JobListener:任务监听器
- TriggerListener:触发器监听器
- SchedulerListener:调度器监听器
三者的使用方法类似
监听器的分类:
- 全局监听器:能够接收到所有的Job/Trigger的事件通知
- 非全局监听器:只能接收到在其上注册的Job/Trigger事件,不在其注册不好监听
1.1 JobListener
任务调度过程中,与任务Job相关的事件包括:Job开始要执行的提示,Job执行完成的提示
JobListener接口源码:
public interface JobListener { String getName(); void jobToBeExecuted(JobExecutionContext var1); void jobExecutionVetoed(JobExecutionContext var1); void jobWasExecuted(JobExecutionContext var1, JobExecutionException var2); }
- getName():用于获取该JobListener的名称
- jobToBeExecuted:Scheduler在JobDetail将要被执行时调用这个方法
- jobExecutionVetoed:Scheduler在JobDetail即将被执行,但又被TriggerListener否决时会调用该方法
- jobWasExecuted:Scheduler在JobDetail被执行之后调用该方法
例:
① Job类
public class MyJobListerJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { Date date = new Date(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateformat = simpleDateFormat.format(date); System.out.println(dateformat+"--数据库备份!!"); } }
② Job监听器
public class MyJobListener implements JobListener { @Override public String getName() { String ListenerName = this.getClass().getSimpleName(); return ListenerName; } @Override public void jobToBeExecuted(JobExecutionContext jobExecutionContext) { System.out.println("Job即将被执行时调用!!"); } @Override public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) { System.out.println("Job即将被执行,但又被TriggerListener否决时调用!!"); } @Override public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) { System.out.println("Job被执行之后调用!!"); } }
③ Scheduler
Scheduler defaultScheduler = StdSchedulerFactory.getDefaultScheduler(); JobDetail jobDetail = JobBuilder.newJob(MyJobListerJob.class) .withIdentity("job1", "group1") .build(); SimpleTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5).withRepeatCount(2))//5秒执行一次,总共3次(从0开始) .build(); defaultScheduler.scheduleJob(jobDetail,trigger); //创建注册一个全局的Job监听器 defaultScheduler.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs()); //创建注册一个局部的Job监听器,指定任务Job //defaultScheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(JobKey.jobKey("job1","group1"))); defaultScheduler.start();
输出:
Job即将被执行时调用!! 2022-10-15 12:30:22--数据库备份!! Job被执行之后调用!! Job即将被执行时调用!! 2022-10-15 12:30:27--数据库备份!! Job被执行之后调用!!
1.2 TriggerListener
任务调度过程中,与触发器Trigger相关的事件包括:触发器触发,触发器未正常触发,触发器完成等
TriggerLIstener接口源码:
public interface TriggerListener { String getName(); void triggerFired(Trigger var1, JobExecutionContext var2); boolean vetoJobExecution(Trigger var1, JobExecutionContext var2); void triggerMisfired(Trigger var1); void triggerComplete(Trigger var1, JobExecutionContext var2, CompletedExecutionInstruction var3); }
- getName:获取用于触发器的名称
- triggerFired:当与监听器相关联的Trigger被触发,Job上的execute()方法将被执行时,Scheduler就调用执行该方法
- vetoJobExecution:在Trigger触发后,Job将要被执行时由Scheduler调用这个方法。TriggerLIstener给了一个选择去否决Job的执行,假如这个方法返回true,这个job将不会为此Trigger触发而得到执行
- triggerMisfired:Scheduler调用这个方法是在Trigger错过触发时,你应该关注此方法中持续时间长的逻辑;在出现许多错过触发的Trigger时,长逻辑会导致骨牌效验
- triggerComplete:Trigger被触发并且完成了Job的执行时,Scheduler调用这个方法
例:
① TriggerListen
public class MyTriggenListener implements TriggerListener { @Override public String getName() { return this.getClass().getSimpleName(); } @Override public void triggerFired(Trigger trigger, JobExecutionContext jobExecutionContext) { System.out.println("Trigger被触发!!"); } @Override public boolean vetoJobExecution(Trigger trigger, JobExecutionContext jobExecutionContext) { System.out.println("Job没有被触发!!"); return true;//返回true JOB不会被执行 } @Override public void triggerMisfired(Trigger trigger) { System.out.println("Trigger错过触发!!"); } @Override public void triggerComplete(Trigger trigger, JobExecutionContext jobExecutionContext, Trigger.CompletedExecutionInstruction completedExecutionInstruction) { System.out.println("Trigger完成之后被触发!!"); } }
② Scheduler
Scheduler defaultScheduler = StdSchedulerFactory.getDefaultScheduler(); JobDetail jobDetail = JobBuilder.newJob(MyTriggerListenerJob.class) .withIdentity("job1", "group1") .build(); SimpleTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5).withRepeatCount(2))//5秒执行一次,总共3次(从0开始) .build(); defaultScheduler.scheduleJob(jobDetail,trigger); //创建注册一个全局的trigger监听器 defaultScheduler.getListenerManager().addTriggerListener(new MyTriggenListener(), EverythingMatcher.allTriggers()); //创建注册一个局部的trigger监听器,指定trigger的名称和组 //defaultScheduler.getListenerManager().addTriggerListener(new MyTriggenListener(), KeyMatcher.keyEquals(TriggerKey.triggerKey("trigger1","group1"))); defaultScheduler.start();
输出:
Trigger被触发!! job没有被触发! Trigger被触发!! job没有被触发!
1.3 SchedulerListener
SchedulerListener与TriggerListener、JobListener类似,但它仅接收来自Scheduler自身的消息,而不一定是某个具体的trigger或job的消息。
Scheduler相关的消息包括:job/trigger的增加、job/trigger的删除、scheduler内部发生的严重错误以及scheduler关闭的消息等
SchedulerListener接口源码:
public interface SchedulerListener { public void jobScheduled(Trigger trigger); public void jobUnscheduled(String triggerName, String triggerGroup); public void triggerFinalized(Trigger trigger); public void triggersPaused(String triggerName, String triggerGroup); public void triggersResumed(String triggerName, String triggerGroup); public void jobsPaused(String jobName, String jobGroup); public void jobsResumed(String jobName, String jobGroup); public void schedulerError(String msg, SchedulerException cause); public void schedulerStarted(); public void schedulerInStandbyMode(); public void schedulerShutdown(); public void schedulingDataCleared(); }
- jobScheduled() 和 jobUnscheduled():Scheduler 在有新的 JobDetail 部署或卸载时调用这两个中的相应方法。
- triggerFinalized() :当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。
- triggersPaused():Scheduler 调用这个方法是发生在一个 Trigger 或 Trigger 组被暂停时。假如是 Trigger 组的话,triggerName 参数将为 null。
- triggersResumed():Scheduler 调用这个方法是发生成一个 Trigger 或 Trigger 组从暂停中恢复时。假如是 Trigger 组的话,triggerName 参数将为 null。
- jobsPaused():当一个或一组 JobDetail 暂停时调用这个方法。
- jobsResumed():当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。
- schedulerError():在 Scheduler 的正常运行期间产生一个严重错误时调用这个方法。
- schedulerStarted():当scheduler开启时,调用该方法
- schedulerShutdown():当Schenedler停止时调用
- schedulingDataCleared():当Scheduler数据被清除时,调用该方法
例:
① MySchedulerListener
public class MySchedulerListener implements SchedulerListener { @Override public void jobScheduled(Trigger trigger) { System.out.println("MySchedulerListener jobScheduled trigger"); } @Override public void jobUnscheduled(TriggerKey triggerKey) { System.out.println("MySchedulerListener jobScheduled triggerKey"); } @Override public void triggerFinalized(Trigger trigger) { System.out.println("MySchedulerListener triggerFinalized"); } @Override public void triggerPaused(TriggerKey triggerKey) { System.out.println("MySchedulerListener triggerPaused"); } @Override public void triggersPaused(String triggerGroup) { System.out.println("MySchedulerListener triggersPaused"); } @Override public void triggerResumed(TriggerKey triggerKey) { System.out.println("MySchedulerListener triggerResumed triggerKey"); } @Override public void triggersResumed(String triggerGroup) { System.out.println("MySchedulerListener triggerResumed triggerGroup"); } @Override public void jobAdded(JobDetail jobDetail) { System.out.println("MySchedulerListener jobAdded"); } @Override public void jobDeleted(JobKey jobKey) { System.out.println("MySchedulerListener jobDeleted"); } @Override public void jobPaused(JobKey jobKey) { System.out.println("MySchedulerListener jobPaused jobKey"); } @Override public void jobsPaused(String jobGroup) { System.out.println("MySchedulerListener jobsPaused jobGroup"); } @Override public void jobResumed(JobKey jobKey) { System.out.println("MySchedulerListener jobResumed jobKey"); } @Override public void jobsResumed(String jobGroup) { System.out.println("MySchedulerListener jobsResumed jobGroup"); } @Override public void schedulerError(String msg, SchedulerException cause) { System.out.println("MySchedulerListener schedulerError"); } @Override public void schedulerInStandbyMode() { System.out.println("MySchedulerListener schedulerInStandbyMode"); } @Override public void schedulerStarted() { System.out.println("MySchedulerListener schedulerStarted"); } @Override public void schedulerStarting() { System.out.println("MySchedulerListener schedulerStarting"); } @Override public void schedulerShutdown() { System.out.println("MySchedulerListener schedulerShutdown"); } @Override public void schedulerShuttingdown() { System.out.println("MySchedulerListener schedulerShuttingdown"); } @Override public void schedulingDataCleared() { System.out.println("MySchedulerListener schedulingDataCleared"); } }
② 调度
public class SchedulerListenerDemo { public static void main(String[] args) throws Exception { System.out.println("------- 初始化 ----------------------"); // Scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // 添加监听器 SchedulerListener schedulerListener = new MySchedulerListener(); scheduler.getListenerManager().addSchedulerListener(schedulerListener); // Job JobDetail job = newJob(SimpleJob1.class).withIdentity("job1", "group1") .build(); // Tirgger Trigger trigger = newTrigger().withIdentity("trigger1", "group1") .startNow().build(); // 将job任务加入到调度器 scheduler.scheduleJob(job, trigger); // 开始任务 System.out.println("------- 开始执行调度器 Scheduler ----------------"); scheduler.start(); try { System.out.println("------- 等待 30 秒... --------------"); Thread.sleep(30L * 1000L); } catch (Exception e) { e.printStackTrace(); } scheduler.shutdown(true); System.out.println("------- 关闭调度器 -----------------"); SchedulerMetaData metaData = scheduler.getMetaData(); System.out.println("~~~~~~~~~~ 执行了 " + metaData.getNumberOfJobsExecuted() + " 个 jobs."); } }
输出:
MySchedulerListener jobAdded MySchedulerListener jobScheduled trigger ------- 开始执行调度器 Scheduler ---------------- MySchedulerListener schedulerStarting INFO QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started. MySchedulerListener schedulerStarted ------- 等待 30 秒... -------------- Job1 - 任务key group1.job1执行时间:2017-11-16 22:14:33 MySchedulerListener triggerFinalized MySchedulerListener jobDeleted INFO QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutting down. INFO QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED paused. MySchedulerListener schedulerInStandbyMode MySchedulerListener schedulerShuttingdown MySchedulerListener schedulerShutdown INFO QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutdown complete. ------- 关闭调度器 ----------------- ~~~~~~~~~~ 执行了 1 个 jobs.
2. Quartz任务持久化
2.1 Quartz Store概述
Quartz提供了两种不同类型的任务存储方式:
- 内存存储(默认)
- 数据库存储
这两种方式都是基于org.quartz.spi.JobStore接口来实现的
JobStore的实现类结构:
- org.quartz.spi.JobStore 是任务存储的顶层接口类
- org.quartz.simpl.RAMJobStore 是内存存储机制实现类
- org.quartz.impl.jdbcjobstore.JobStoreSupport 是基于JDBC数据库存储的抽象类
- org.quartz.impl.jdbcjobstore.JobStoreCMT 是受应用容器管理事务的数据库存储实现类
- org.quartz.impl.jdbcjobstore.JobStoreTX 是不受应用容器事务管理的数据库存储实现类
在项目开发中,我们无需调用JobStore实现类中的方法,只需配置即可,但是了解还是很有必要的,因为可以让我们在项目应用中选择更加适合的存储类型(内存/数据库/文件)
2.2 JobStrore接口实现类
2.2.1 RAMJobstore内存存储任务
Quartz默认的存储机制就是使用内存进行存储的
Quartz的jar包中的默认配置文件quartz.properties:
org.quartz.scheduler.instanceName = DefaultQuartzScheduler org.quartz.scheduler.rmi.export = false org.quartz.scheduler.rmi.proxy = false org.quartz.scheduler.wrapJobExecutionInUserTransaction = false org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount = 10 org.quartz.threadPool.threadPriority = 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true org.quartz.jobStore.misfireThreshold = 60000 org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
主要看org.quartz.jobStore.class这个属性,属性值org.quartz.simpl.RAMJobStore就是内存存储机制的实现类。如果需要使用别的存储机制,那就将此值替换为别的实现类即可
使用内存存储的优点是任务的存储和读取的速度极快,和数据库持久化相比差别还是非常大的,而且框架搭建简单,开箱即用。它的缺点是当Quartz程序或应用程序停止了,伴随在内存中的数据也会被回收,任务等数据就永久丢失了
使用内存存储时,注意配置文件中只需要保留基本的线程池配置和jobStore的实现类等几个简单的属性就行。如果使用了实现类中没有的属性,启动的时候会报错
2.2.2 数据库存储数据
JobStoreTX和JobStoreCMT都是JobStoreSupport抽象类的实现类,JobStoreSupport是基于JDBC实现了一些基本的功能的抽象类。如果想要自己实现一套关于JDBC存储方式,那么可以继承此抽象类
Quartz支持的数据库:
- Oracle
- MySQL
- Microsoft SQL Server
- HSQLDB
- PostgreSQL
- DB2
- Cloudscape/Derby
- Pointbase
- Informix
- Firebird
- ……
quartz官网:http://www.quartz-scheduler.org/downloads/
Quartz的数据库表结构: 在quartz官网下载quartz文件,然后解压文件,找到其中的sql文件。quartz2.4.0版本的sql路径为:\quartz-2.4.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore
quartz持久化各个表的含义:
表名 | 含义 |
QRTZ_CALENDARS | 以 Blob 类型存储 Quartz 的 Calendar 信息 |
QRTZ_CRON_TRIGGERS | 存储CronTrigger触发器信息,包括Cron表达式和时区等信息 |
QRTZ_FIRED_TRIGGERS | 存储已触发的Trigger状态信息和关联的Job执行信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组信息 |
QRTZ_SCHEDULER_STATE | 存储有关Scheduler的状态信息 |
QRTZ_LOCKS | 存储程序锁信息 |
QRTZ_JOB_DETAILS | 存储Job的详细信息 |
QRTZ_JOB_LISTENERS | 存储Job配置的JobListener信息 |
QRTZ_SIMPLE_TRIGGERS | 存储SimpleTrigger触发器信息,包括重复次数,间隔等信息 |
QRTZ_BLOG_TRIGGERS | 存储Blob类型的Trigger,一般用于自定义触发器 |
QRTZ_TRIGGER_LISTENERS | 存储已配置的TriggerListener信息 |
QRTZ_TRIGGERS | 存储已配置的Trigger的信息 |
表的前缀可以在quartz.properties中进行自定义配置,假如在项目中需要两套调度器实例,要分别持久化两套job信息,此时就可以自定义前缀进行区分:
org.quartz.jobStore.tablePrefix=QRTZ1_ org.quartz.jobStore.tablePrefix=QRTZ2_
2.2.3 JobStoreTX
TX就是事务的意思,此存储机制用于Quartz独立于应用容器的事务管理,如果是Tomcat容器管理的数据源,那我们定义的事务也不会传播给Quartz框架内部。通俗的讲就是不管我们的Service服务本身业务代码是否执行成功,只要代码中调用了Quartz API的数据库操作,那任务状态就永久持久化了,就算业务代码抛出运行时异常任务状态也不会回滚到之前的状态
JobStoreTX的配置:
1)配置org.quartz.jobStroe.class属性
org.quartz.jobStore.class= org.quartz.impl.jdbcjobstore.JobStoreTX
2)配置驱动代理,以MySQL为例
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
3)配置数据源
org.quartz.jobStore.dataSource=qzDS org.quartz.dataSource.qzDS.driver= com.mysql.jdbc.Driver org.quartz.dataSource.qzDS.URL= jdbc:mysql://localhost:3306/testDB org.quartz.dataSource.qzDS.user= root org.quartz.dataSource.qzDS.password= admin org.quartz.dataSource.qzDS.maxConnection= 20
列出一个可用的数据库代理类表格,方便大家使用,如果表格中没有列出你想要的代理类,那就使用标准的 JDBC 代理:org.quartz.impl.jdbcjobstore.StdDriverDelegate
数据库平台 | Quartz 代理类 |
Cloudscape/Derby | org.quartz.impl.jdbcjobstore.CloudscapeDelegate |
DB2 (version 6.x) | org.quartz.impl.jdbcjobstore.DB2v6Delegate |
DB2 (version 7.x) | org.quartz.impl.jdbcjobstore.DB2v7Delegate |
DB2 (version 8.x) | org.quartz.impl.jdbcjobstore.DB2v8Delegate |
HSQLDB | org.quartz.impl.jdbcjobstore.PostgreSQLDelegate |
Oracle | org.quartz.impl.jdbcjobstore.oracle.OracleDelegate |
MS SQL Server | org.quartz.impl.jdbcjobstore.MSSQLDelegate |
Pointbase | org.quartz.impl.jdbcjobstore.PointbaseDelegate |
PostgreSQL | org.quartz.impl.jdbcjobstore.PostgreSQLDelegate |
(WebLogic JDBC Driver) | org.quartz.impl.jdbcjobstore.WebLogicDelegate |
(WebLogic 8.1 with Oracle) | org.quartz.impl.jdbcjobstore.oracle.weblogic.WebLogicOracleDelegate |
注:
- org.quartz.dataSource.qzDS.URL属性名末尾的URL字符串必须是大写,如果写成org.quartz.dataSource.qzDS.url ,那初始化调度实例时就会报错
- org.quartz.jobStore.dataSource属性,这个属性的意思是给数据源起一个名字。这里属性值配置的是“qzDS”,你也可以配置成别的任意字符串,比如:“abc”,如果真这么做,那就需要将org.quartz.dataSource.qzDS.driver和其他配置的“qzDS”更换为“abc”,配置:
org.quartz.jobStore.dataSource=abc org.quartz.dataSource.abc.driver= com.mysql.jdbc.Driver org.quartz.dataSource.abc.URL= jdbc:mysql://localhost:3306/testDB org.quartz.dataSource.abc.user= root org.quartz.dataSource.abc.password= admin org.quartz.dataSource.abc.maxConnection= 20
那么Quartz为什么设计要org.quartz.jobStore.dataSource属性呢?
这个属性主要的目的就是在同一个数据库中需要使用多套Quartz,一般大家只需要一套数据源就可以完成业务工作,除非有一些特别的需求。比如SaaS模式下可以对不同公司的任务调度进行管理等。
提供一个完整job持久化的配置文件,该一下数据源、用户名和密码即可:
org.quartz.scheduler.instanceName=DefaultQuartzScheduler org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount=20 org.quartz.threadPool.threadPriority=5 org.quartz.jobStore.misfireThreshold=60000 org.quartz.jobStore.class= org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.tablePrefix=qrtz_ org.quartz.jobStore.dataSource=qzDS org.quartz.dataSource.qzDS.driver= com.mysql.jdbc.Driver org.quartz.dataSource.qzDS.URL= jdbc:mysql://localhost:3306/testDB org.quartz.dataSource.qzDS.user= root org.quartz.dataSource.qzDS.password= admin org.quartz.dataSource.qzDS.maxConnection= 20
2.2.4 JobStoreCMT
CMT的全称是Container Managed Transactions,表示容器管理事务,也就是让应用容器托管事务。这里假设应用容器是Tomcat,并且项目和Quartz都是使用Tomcat配置的数据源,那么项目和Quartz的代码中就可以共用同一个事务,不管是业务代码还是Quartz内部抛出异常,Service服务内的所有数据操作都会回滚到原始状态
JobStoreCMT和JobStoreTX最大的区别 是JobStoreCMT需要配置两个数据源,一个是受应用容器管理的数据源,还有一个是不受应用容器管理的数据源
JobStoreCMT的配置:
1)配置org.quartz.jobStroe.class属性
2)配置驱动代理,以Mysql为例,其它代理类参考上面表格
3)配置两个数据源
① 配置不受应用容器管理的数据源
org.quartz.jobStore.nonManagedTXDataSource = qzDS org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/testDB org.quartz.dataSource.qzDS.user = root org.quartz.dataSource.qzDS.password = admin org.quartz.dataSource.qzDS.maxConnections = 10
nonManagedTXDataSource就是非管理事务数据源的意思
② 配置受应用容器管理的数据源
org.quartz.dataSource.dataSource=myDS org.quartz.dataSource.jndiURL = jdbc/mysql org.quartz.dataSource.myDS.jndiAlwaysLookup = DB_JNDI_ALWAYS_LOOKUP org.quartz.dataSource.myDS.java.naming.factory.initial = org.apache.naming.java.javaURLContextFactory org.quartz.dataSource.myDS.java.naming.provider.url = http://localhost:8080 org.quartz.dataSource.myDS.java.naming.security.principal = root org.quartz.dataSource.myDS.java.naming.security.credentials = admin
受应用容器管理的数据源配置属性的含义:
- org.quartz.dataSource.NAME.jndiURL:受应用服务器管理的DataSource的JNDI URL
- org.quartz.dataSource.NAME.java.naming.factory.initial:JNDI InitialContextFactory的类名称
- org.quartz.dataSource.NAME.java.naming.provider.url:连接到JNDI的URL
- org.quartz.dataSource.NAME.java.naming.security.principal:连接到 JNDI 的用户名
- org.quartz.dataSource.NAME.java.naming.security.credential:连接到 JNDI 的用户凭证密码
选择:
- JobStoreCMT在实际开发的过程中我还没有在项目中使用过此种方式,一般情况下都是使用的JobStoreTX,如果大家的项目中有着严格的事务管理,那么建议使用JobStoreCMT存储方式
Comments | NOTHING
Warning: Undefined variable $return_smiles in /www/wwwroot/wql_luoqin_ltd/wp-content/themes/Sakura/functions.php on line 1109