#############################################################################################################################################

 1、 建立数据库11张表

#############################################################################################################################################

先在数据库中建立quartz需要的11张表(我这里用的是Oracle数据库),根据不同的数据库quartz分别提供了不同的初始化sql文件,sql文件路径在 quartz-2.3.0-SNAPSHOT-0724\src\org\quartz\impl\jdbcjobstore下:

 tables_cloudscape.sql
tables_cubrid.sql
tables_db2.sql
tables_db2_v8.sql
tables_db2_v72.sql
tables_db2_v95.sql
tables_derby.sql
tables_derby_previous.sql
tables_firebird.sql
tables_h2.sql
tables_hsqldb.sql
tables_hsqldb_old.sql
tables_informix.sql
tables_mysql.sql
tables_mysql_innodb.sql
tables_oracle.sql
tables_pointbase.sql
tables_postgres.sql
tables_sapdb.sql
tables_solid.sql
tables_sqlServer.sql
tables_sybase.sql

#############################################################################################################################################

 2、 配置定时器数据库等相关配置:quartz.properties

#############################################################################################################################################

#============================================================================
# Configure Main Scheduler Properties
#============================================================================
#调度器实例名称
org.quartz.scheduler.instanceName: SchedulerJoyce0725
org.quartz.scheduler.instanceId: InstanceJoyce0725 #============================================================================
# Configure ThreadPool
#============================================================================
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 5
org.quartz.threadPool.threadPriority: 1 #============================================================================
# 配置Oracle数据库,命名dataSource为myDS
#============================================================================
### 支持PostgreSQL数据库
###org.quartz.dataSource.myDS.driver=org.postgresql.Driver
###org.quartz.dataSource.myDS.URL=jdbc:postgresql://localhost:5432/quartz
org.quartz.dataSource.myDS.driver=oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.myDS.URL=jdbc:oracle:thin:@localhost:1521:orcl
org.quartz.dataSource.myDS.user=zhuwen
org.quartz.dataSource.myDS.password=ZHUwen12
org.quartz.dataSource.myDS.maxConnections=5
org.quartz.dataSource.myDS.validationQuery=select 0 FROM DUAL #============================================================================
# 配置job任务存储策略,指定一个叫myDS的dataSource
#============================================================================
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
### 支持PostgreSQL数据库
###org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
### 支持Oracle数据库
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.dataSource=myDS
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true

################################################################################################################

3、 11张表不同定时方式分别存储了不同数据到不同的表

################################################################################################################

ScheduleBuilder是trigger触发器的触发规则定制类,旗下有4种触发器实现类:  CalendarIntervalScheduleBuilder、CronScheduleBuilder、DailyTimeIntervalScheduleBuilder、SimpleScheduleBuilder。

这里演示了CronScheduleBuilder和SimpleScheduleBuilder两种定时方式,分别执行后面的StoreSimpleTrigger2OracleExample.java 和 StoreCronTrigger2OracleExample.java 就能看到数据库如下的差别:

这4中实现类在数据库的11张表中存储一个任务时,分别会产生不一样的数据,用颜色标注insert语句如下:

select * from qrtz_blob_triggers;   --没有insert语句就表示此表一直没有数据

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_calendars;
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_cron_triggers;

Insert into QRTZ_CRON_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,CRON_EXPRESSION,TIME_ZONE_ID)
values ('SchedulerJoyce0725','cronTrigger','cronGroup1','0/5 * * * * ?','Asia/Shanghai');

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_fired_triggers;
Insert into QRTZ_FIRED_TRIGGERS (SCHED_NAME,ENTRY_ID,TRIGGER_NAME,TRIGGER_GROUP,INSTANCE_NAME,FIRED_TIME,SCHED_TIME,PRIORITY,STATE,JOB_NAME,JOB_GROUP,IS_NONCONCURRENT,REQUESTS_RECOVERY)
values ('SchedulerJoyce0725','InstanceJoyce07251564061848994','cronTrigger','cronGroup1','InstanceJoyce0725',1564061855002,1564061855000,5,'EXECUTING','simpleRecoveryJob','cronGroup1','0','0');  --每一次远程启动时ENTRY_ID总是会变一变

Insert into QRTZ_FIRED_TRIGGERS (SCHED_NAME,ENTRY_ID,TRIGGER_NAME,TRIGGER_GROUP,INSTANCE_NAME,FIRED_TIME,SCHED_TIME,PRIORITY,STATE,JOB_NAME,JOB_GROUP,IS_NONCONCURRENT,REQUESTS_RECOVERY) values ('SchedulerJoyce0725','InstanceJoyce07251564066439267','simpleTriger1','simpleGroup','InstanceJoyce0725',1564066446293,1564066451270,5,'ACQUIRED',null,null,'0','0');

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_job_details;
Insert into QRTZ_JOB_DETAILS (SCHED_NAME,JOB_NAME,JOB_GROUP,DESCRIPTION,JOB_CLASS_NAME,IS_DURABLE,IS_NONCONCURRENT,IS_UPDATE_DATA,REQUESTS_RECOVERY,JOB_DATA)
values ('SchedulerJoyce0725','simpleRecoveryJob','cronGroup1',null,'org.quartz.examples.example15.SimpleRecoveryJob','0','0','0','0',
TO_BLOB(HEXTORAW('...'))|| TO_BLOB(HEXTORAW('...')));

Insert into QRTZ_JOB_DETAILS (SCHED_NAME,JOB_NAME,JOB_GROUP,DESCRIPTION,JOB_CLASS_NAME,IS_DURABLE,IS_NONCONCURRENT,IS_UPDATE_DATA,REQUESTS_RECOVERY,JOB_DATA)
values ('SchedulerJoyce0725','simpleJob1','simpleGroup',null,'org.quartz.examples.example15.SimpleRecoveryJob','0','0','0','1',
TO_BLOB(HEXTORAW('...'))|| TO_BLOB(HEXTORAW('...')));

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_locks;
Insert into qrtz_locks (SCHED_NAME,LOCK_NAME) values ('SchedulerJoyce0725','STATE_ACCESS');
Insert into qrtz_locks (SCHED_NAME,LOCK_NAME) values ('SchedulerJoyce0725','TRIGGER_ACCESS');

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_paused_trigger_grps;

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_scheduler_state;
Insert into qrtz_scheduler_state (SCHED_NAME,INSTANCE_NAME,LAST_CHECKIN_TIME,CHECKIN_INTERVAL)
values ('SchedulerJoyce0725','InstanceJoyce0725',1564061857522,7500);

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_simple_triggers;

Insert into QRTZ_SIMPLE_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,REPEAT_COUNT,REPEAT_INTERVAL,TIMES_TRIGGERED)

values ('SchedulerJoyce0725','simpleTriger1','simpleGroup',20,5000,2);

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_simprop_triggers;

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

select * from qrtz_triggers;
Insert into qrtz_triggers (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,JOB_NAME,JOB_GROUP,DESCRIPTION,NEXT_FIRE_TIME,PREV_FIRE_TIME,PRIORITY,TRIGGER_STATE,TRIGGER_TYPE,START_TIME,END_TIME,CALENDAR_NAME,MISFIRE_INSTR,JOB_DATA)
values ('SchedulerJoyce0725','cronTrigger','cronGroup1','simpleRecoveryJob','cronGroup1',null,1564061865000,1564061860000,5,'ACQUIRED','CRON',1564061849000,0,null,0, EMPTY_BLOB());

Insert into QRTZ_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,JOB_NAME,JOB_GROUP,DESCRIPTION,NEXT_FIRE_TIME,PREV_FIRE_TIME,PRIORITY,TRIGGER_STATE,TRIGGER_TYPE,START_TIME,END_TIME,CALENDAR_NAME,MISFIRE_INSTR,JOB_DATA) values ('SchedulerJoyce0725','simpleTriger1','simpleGroup','simpleJob1','simpleGroup',null,1564066451270,1564066446270,5,'ACQUIRED','SIMPLE',1564066441270,0,null,0, EMPTY_BLOB());

#############################################################################################################################################

4、  job任务类,SimpleRecoveryJob.java

#############################################################################################################################################

package org.quartz.examples.example15;

import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.Date; /**
* 一个job作业。
*/
public class SimpleRecoveryJob implements Job { private static Logger LOG = LoggerFactory.getLogger(SimpleRecoveryJob.class); private static final String COUNT = "count"; //必须要有public修饰的无参构造函数
public SimpleRecoveryJob() {
} //任务执行方法
public void execute(JobExecutionContext context) throws JobExecutionException { JobKey jobKey = context.getJobDetail().getKey(); // 如果由于“恢复”情况而重新执行作业,此方法将返回true。
if (context.isRecovering()) {
LOG.info("恢复作业:SimpleRecoveryJob: " + jobKey + " RECOVERING at " + new Date());
} else {
LOG.info("不恢复作业:SimpleRecoveryJob: " + jobKey + " starting at " + new Date());
} JobDataMap data = context.getJobDetail().getJobDataMap();
int count;
if (data.containsKey(COUNT)) {
count = data.getInt(COUNT);
} else {
count = 0;
}
count++;
data.put(COUNT, count); LOG.info("SimpleRecoveryJob: " + jobKey + " done at " + new Date() + "\n Execution #" + count); } }

#############################################################################################################################################

5、  简单定时任务存储到数据库表,StoreSimpleTrigger2OracleExample.java

#############################################################################################################################################

package org.quartz.examples.example15;

import static org.quartz.DateBuilder.futureDate;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger; import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 简单定时任务存储到数据库表。
* 存储job任务到数据库,这里打印的job任务都是: 不恢复作业……
*/
public class StoreSimpleTrigger2OracleExample { private static Logger LOG = LoggerFactory.getLogger(StoreSimpleTrigger2OracleExample.class); public void run(boolean inClearJobs, boolean inScheduleJobs) throws Exception { // 初始化调度器
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler(); if (inClearJobs) {
sched.clear();
LOG.warn("***** Deleted existing jobs/triggers *****");
} LOG.info("------- Initialization Complete -----------"); if (inScheduleJobs) { LOG.info("------- Scheduling Jobs ------------------"); String schedId = sched.getSchedulerInstanceId(); // ========================================================
// ============ job1
// ========================================================
int count = 1;
JobDetail job = newJob(SimpleRecoveryJob.class).withIdentity("simpleJob" + count, "simpleGroup").requestRecovery() // 如果job执行过程中宕机,则job重新执行
.build();
SimpleTrigger trigger = newTrigger().withIdentity("simpleTriger" + count, "simpleGroup")
.startAt(futureDate(1, IntervalUnit.SECOND))
.withSchedule(simpleSchedule().withRepeatCount(20).withIntervalInSeconds(5)).build();
LOG.info(job.getKey() + " will run at: " + trigger.getNextFireTime() + " and repeat: "
+ trigger.getRepeatCount() + " times, every " + trigger.getRepeatInterval() / 1000 + " seconds");
sched.scheduleJob(job, trigger);
//
// // ========================================================
// // ============ job2
// // ========================================================
// count++;
// job = newJob(SimpleRecoveryJob.class).withIdentity("job0724_" + count, schedId).requestRecovery() // 如果job执行过程中宕机,则job重新执行
// .build();
// trigger = newTrigger().withIdentity("triger0724_" + count, schedId).startAt(futureDate(2, IntervalUnit.SECOND))
// .withSchedule(simpleSchedule().withRepeatCount(20).withIntervalInSeconds(5)).build();
// LOG.info(job.getKey() + " will run at: " + trigger.getNextFireTime() + " and repeat: "
// + trigger.getRepeatCount() + " times, every " + trigger.getRepeatInterval() / 1000 + " seconds");
// sched.scheduleJob(job, trigger); } LOG.info("------- Starting Scheduler ---------------");
sched.start();
try {
Thread.sleep(3600L * 1000L);
} catch (Exception e) {
//
}
sched.shutdown();
LOG.info("------- Shutdown Complete ----------------");
} public static void main(String[] args) throws Exception {
boolean clearJobs = true; // 是否清空job任务
boolean scheduleJobs = true; // 是否调度任务 for (String arg : args) {
if (arg.equalsIgnoreCase("clearJobs")) {
clearJobs = true;
} else if (arg.equalsIgnoreCase("dontScheduleJobs")) {
scheduleJobs = false;
}
} StoreSimpleTrigger2OracleExample example = new StoreSimpleTrigger2OracleExample();
example.run(clearJobs, scheduleJobs);
}
}

#############################################################################################################################################

         6、  cron定义定时任务存储到数据库表,StoreCronTrigger2OracleExample.java

#############################################################################################################################################

package org.quartz.examples.example15;

import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger; import java.util.Date; import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* cron定义定时任务存储到数据库表。
* 存储job任务到数据库,这里打印的job任务都是: 不恢复作业……
*/
public class StoreCronTrigger2OracleExample { private static Logger LOG = LoggerFactory.getLogger(StoreCronTrigger2OracleExample.class); public void run(boolean inClearJobs) throws Exception { // 初始化调度器
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler(); if (inClearJobs) {
sched.clear();
LOG.warn("***** Deleted existing jobs/triggers *****");
} // ========================================================
// ============ job1 每20秒执行一次,无限期重复
// ========================================================
JobDetail job = newJob(SimpleRecoveryJob.class).withIdentity("simpleRecoveryJob_Recovery", "cronGroup2")
.requestRecovery() //如果job任务所在服务宕机了或由于其它原因job任务被中断,请标记JobExecutionContext.isRecovering()=true。
//让我可以拿这个标记知道怎么去适配业务场景。
//但是只有恢复任务后首次执行任务时,拿到Recovering标记值为true,此后该任务Recovering值又标记为了false。
.build();
//每5秒执行一次
CronTrigger trigger = newTrigger().withIdentity("cronTrigger_Recovery", "cronGroup2").withSchedule(cronSchedule("0/5 * * * * ?")).build();
Date ft = sched.scheduleJob(job, trigger);
LOG.info(job.getKey() + " has been scheduled to run at: " + ft + " and repeat based on expression: "
+ trigger.getCronExpression()); LOG.info("------- Starting Scheduler ---------------");
sched.start();
try {
Thread.sleep(3600L * 1000L);
} catch (Exception e) {
//
} // 暂停执行任务
sched.pauseJob(job.getKey());
LOG.info("调度器暂停执行定时器,主线程睡眠11秒!!!!会错过执行job1的N次定时任务。模拟当定时器的执行线程由于抢不到CPU时间或其他事件错过执行的情况。");
Thread.sleep(11L * 1000L);
// 继续执行任务
sched.resumeJob(job.getKey()); //当定时器得到继续执行的命令时,被错过执行的任务次数,就会按照misfire的定义去执行 sched.shutdown();
LOG.info("------- Shutdown Complete ----------------");
} public static void main(String[] args) throws Exception {
boolean clearJobs = false; // 是否清空job任务 StoreCronTrigger2OracleExample example = new StoreCronTrigger2OracleExample();
example.run(clearJobs);
}
}

#############################################################################################################################################

7、  执行数据库中的定时任务,RunOracleExistingJobExample.java

#############################################################################################################################################

package org.quartz.examples.example15;

import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 先运行ClusterExample.java,确定Oracle数据库中已存在job任务.
* 这里打印的job任务都是从数据库表里恢复回来的,所以这些任务的第一次执行会打印: 恢复作业……
* 此后job任务就只打印:不恢复作业……
*/
public class RunOracleExistingJobExample { private static Logger LOG = LoggerFactory.getLogger(RunOracleExistingJobExample.class); public void run(boolean inClearJobs) throws Exception { // 初始化调度器
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler(); if (inClearJobs) {
sched.clear();
LOG.warn("***** Deleted existing jobs/triggers *****");
} LOG.info("------- Starting Scheduler ---------------");
sched.start();
try {
Thread.sleep(3600L * 1000L);
} catch (Exception e) {
//
} sched.shutdown();
LOG.info("------- Shutdown Complete ----------------");
} public static void main(String[] args) throws Exception {
boolean clearJobs = false; // 是否清空job任务,这里为不清空 RunOracleExistingJobExample example = new RunOracleExistingJobExample();
example.run(clearJobs);
}
}

quartz2.3.0(十五)执行、暂停、继续执行、清除,花式操作数据库中持久化的job任务的更多相关文章

  1. 孤荷凌寒自学python第五十二天初次尝试使用python读取Firebase数据库中记录

    孤荷凌寒自学python第五十二天初次尝试使用python读取Firebase数据库中记录 (完整学习过程屏幕记录视频地址在文末) 今天继续研究Firebase数据库,利用google免费提供的这个数 ...

  2. 【hibernate spring data jpa】执行了save()方法 sql语句也执行了,但是数据并未插入数据库中

    执行了save()方法  sql语句也执行了,但是数据并未插入数据库中 解决方法: 是因为执行了save()方法,也执行了sql语句,但是因为使用的是 @Transactional 注解,不是手动去提 ...

  3. Java-JUC(十五):synchronized执行流程分析

    一.锁对象及 synchronized 的使用 synchronized 通过互斥锁(Mutex Lock)来实现,同一时刻,只有获得锁的线程才可以执行锁内的代码. 锁对象分为两种: 实例对象(一个类 ...

  4. 从零开始学ios开发(十五):Navigation Controllers and Table Views(中)

    这篇内容我们继续上一篇的例子接着做下去,为其再添加3个table view的例子,有了之前的基础,学习下面的例子会变得很简单,很多东西都是举一反三,稍稍有些不同的内容,好了,闲话少说,开始这次的学习. ...

  5. ExpandoObject与DynamicObject的使用 RabbitMQ与.net core(一)安装 RabbitMQ与.net core(二)Producer与Exchange ASP.NET Core 2.1 : 十五.图解路由(2.1 or earler) .NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

    ExpandoObject与DynamicObject的使用   using ImpromptuInterface; using System; using System.Dynamic; names ...

  6. python语言(五)匿名函数、读写excel、操作数据库、加密、redis操作

    一.匿名函数 递归:就是调用自己 def func(): num = int(input('num:')) if num % 2 ==0: print('是偶数') return else: func ...

  7. quartz2.3.0(五)制定错过执行任务的misfire策略,用pause,resume模拟job暂停执行和继续执行

    感谢兄台: <quartz-misfire 错失.补偿执行> misfire定义 misfire:被错过的执行任务策略 misfire重现——CronTrigger job任务类: pac ...

  8. 第十五篇:使用 FP-growth 算法高效挖掘海量数据中的频繁项集

    前言 对于如何发现一个数据集中的频繁项集,前文讲解的经典 Apriori 算法能够做到. 然而,对于每个潜在的频繁项,它都要检索一遍数据集,这是比较低效的.在实际的大数据应用中,这么做就更不好了. 本 ...

  9. 十五、CI框架之自动加载数据库

    一.在config的autoload.php文件中,如果写入以下代码,那么在控制器中无需再次加载数据库了,相当于全局自动加载数据库了 不忘初心,如果您认为这篇文章有价值,认同作者的付出,可以微信二维码 ...

随机推荐

  1. Codevs 3122 奶牛代理商 VIII(状压DP)

    3122 奶牛代理商 VIII 时间限制: 3 s 空间限制: 256000 KB 题目等级 : 大师 Master 题目描述 Description 小徐是USACO中国区的奶牛代理商,专门出售质优 ...

  2. CF888G 【Xor-MST】

    妙妙题-- 看到\(MST\),想到\(Kruskal\),看到异或,想到\(Trie\) 首先我们模拟一下\(Kruskal\)的流程:找到最小边,如果联通就忽略,未联通就加边 我们把所有点权值加入 ...

  3. Linux修改服务器Oracle字符集

    Linux安装Oracle时太仓促,没设置好,导入dmp字符集(ZHS16GBK)与服务器字符集(WE8MSWIN1252)对不上,导致导入数据失败: [oracle@ORACLE ~]$ sqlpl ...

  4. 网络营销CPA、CPS、CPM、CPT、CPC 是什么

    网络营销之所以越来越受到重视一个主要的原因就是因为“精准”.相比较传统媒体的陈旧广告形式,网络营销能为广告主带来更为确切的效果与回报,更有传统媒体所没有的即时互动性.很多企业借助于精准的网络营销成为人 ...

  5. XmlIgnore的解释和使用

    XmlIgnore是一个自定义属性,用来指明在序列化时是否序列化一个属性.如下面的例子: public class Group { public string GroupName; [XmlIgnor ...

  6. Java实现批量将word文档转换成PDF

    先导入words的jar包 需要jar包的私聊我发你 代码如下:import com.aspose.words.Document;import java.io.File; public class W ...

  7. redis 锁的案例

    1: redis 锁 作为一种术装饰器使用 基本逻辑: 1:声明一个redislock类  定义生成锁和释放锁两个方法 2:生成锁使用了一个默认值 setnx ; 如果当前时间大于 第一次锁的生成时间 ...

  8. App installation failed (A valid provisioning profile for this executable was not found)

    真机调试build success ,App installation failed (A valid provisioning profile for this executable was not ...

  9. [LeetCode] 366. Find Leaves of Binary Tree 找二叉树的叶节点

    Given a binary tree, find all leaves and then remove those leaves. Then repeat the previous steps un ...

  10. UE4 window打包ios备忘

    1.生成SHH key 2.安装证书 *.cer,*.p12 以下转自:http://wangjie.rocks/2017/11/30/ue4-ios-build-on-windows/ 问题一 12 ...