為什么選擇Windows Service作為承載主體?
Quartz.Net只是一個作業調度框架,其承載的主體沒有限制,可以是ConsoleApp, WebForm, WebApp, MVC etc. 但是只有Windows Service是最穩定的,而且支持自啟動
如何選擇合適的Windows Service?
直接使用Quartz.Net自動的Server端,即Windows Service + Topself,優點:無需重復開發、跨平台
安裝自帶的Windows Service
1. 新建目錄
目錄:D:\Developer\QuartzNET\Server,並拷貝如下文件(之所以新建拷貝,是不想影響原有的代碼)
2. 命令安裝
應管理員身份打開CMD,依次輸入如下命令
cd D:\Developer\QuartzNET\Server
D:
Quartz.Server.exe install (對應卸載命令Quartz.Server.exe uninstall)
Win熱鍵+R,輸入services.msc調出service列表,可以看到服務已安裝
輸入以下命令啟動服務(或者手動啟動也可以,對應停止命令sc stop QuartzServer)
創建調度作業
1. 獲得Remote Schedule
var properties = new NameValueCollection(); properties["quartz.scheduler.instanceName"] = "ServerScheduler"; // set remoting expoter properties["quartz.scheduler.proxy"] = "true"; properties["quartz.scheduler.proxy.address"] = string.Format("tcp://{0}:{1}/{2}", "localhost", "555", "QuartzScheduler"); // Get a reference to the scheduler var sf = new StdSchedulerFactory(properties); return sf.GetScheduler();
為什么是上面三個屬性,因為Server服務端公布的屬性如下(詳見quartz.config文件)
# You can configure your scheduler in either <quartz> configuration section # or in quartz properties file # Configuration section has precedence quartz.scheduler.instanceName = ServerScheduler # configure thread pool info quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz quartz.threadPool.threadCount = 10 quartz.threadPool.threadPriority = Normal # job initialization plugin handles our xml reading, without it defaults are used quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz quartz.plugin.xml.fileNames = ~/quartz_jobs.xml # export this server to remoting context quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz quartz.scheduler.exporter.port = 555 quartz.scheduler.exporter.bindName = QuartzScheduler quartz.scheduler.exporter.channelType = tcp quartz.scheduler.exporter.channelName = httpQuartz
ScheduleJob如下
public void Run() { // Get an instance of the Quartz.Net scheduler var schd = GetScheduler(); // Start the scheduler if its in standby if (!schd.IsStarted) schd.Start(); // Define the Job to be scheduled var job = JobBuilder.Create<HelloWorldJob>() .WithIdentity("WriteHelloToLog", "IT") .RequestRecovery() .Build(); // Associate a trigger with the Job var trigger = (ICronTrigger)TriggerBuilder.Create() .WithIdentity("WriteHelloToLog", "IT") .WithCronSchedule("0 0/1 * 1/1 * ? *") // visit http://www.cronmaker.com/ Queues the job every minute .StartAt(DateTime.UtcNow) .WithPriority(1) .Build(); //schd.DeleteJob(new JobKey("WriteHelloToLog", "IT")); // Validate that the job doesn't already exists if (!schd.CheckExists(job.Key)) { var schedule = schd.ScheduleJob(job, trigger); Console.WriteLine("Job '{0}' scheduled for '{1}'", "WriteHelloToLog", schedule.ToString("r")); } }
具體的HelloWorldJob如下

class HelloWorldJob : IJob { private static readonly ILog Log = LogManager.GetLogger(typeof(HelloWorldJob)); /// <summary> /// Empty constructor for job initilization /// <para> /// Quartz requires a public empty constructor so that the /// scheduler can instantiate the class whenever it needs. /// </para> /// </summary> public HelloWorldJob() { } public void Execute(IJobExecutionContext context) { try { Log.InfoFormat("{0}****{0}Job {1} fired @ {2} next scheduled for {3}{0}***{0}", Environment.NewLine, context.JobDetail.Key, context.FireTimeUtc.Value.ToString("r"), context.NextFireTimeUtc.Value.ToString("r")); Log.InfoFormat("{0}***{0}Hello World!{0}***{0}", Environment.NewLine); } catch (Exception ex) { Log.InfoFormat("{0}***{0}Failed: {1}{0}***{0}", Environment.NewLine, ex.Message); } } }
控制台

class Program { static void Main(string[] args) { try { // Infinite loop, so that the console doesn't close on you while (true) { var sj = new ScheduledJob(); sj.Run(); Console.WriteLine(@"{0}Check Quartz.net\Trace\application.log.txt for Job updates{0}", Environment.NewLine); Console.WriteLine("{0}Press Ctrl^C to close the window. The job will continue " + "to run via Quartz.Net windows service, " + "see job activity in the Quartz.Net Trace file...{0}", Environment.NewLine); Thread.Sleep(10000 * 100000); } } catch (Exception ex) { Console.WriteLine("Failed: {0}", ex.Message); Console.ReadKey(); } } }
編譯無誤后,把編譯后的exe文件拷貝到D:\Developer\QuartzNET\Server目錄下,然后停止並重啟QuartzServer服務。
最后F5本作業,然后按Crtl+C推出,你會看到服務已成功運行,作業也成功執行
釋疑
1. 為什么要重啟服務?
編譯拷貝文件后一定要重啟服務,因為Windows Service如果不重啟的話無法識別新的exe文件或者DLL文件
2. 創建基於此Windows Service的調度作業的關鍵是什么?
調度作業和公布的Remote Schedule要保持一致,即tcp://localhost:555/QuartzScheduler,具體看quartz.config配置
3. 可以保存調度作業到數據庫嗎?
可以,在server端的quartz.config中附加如下配置
# job store quartz.jobStore.misfireThreshold =60000 quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.StdAdoDelegate, Quartz quartz.jobStore.useProperties = true quartz.jobStore.dataSource = default quartz.jobStore.tablePrefix = QRTZ_ quartz.dataSource.default.connectionString = Server=10.7.11.114;Database=CCLOG_QuartzNET;User Id=CCLOG_LMS_PRO;Password=Testlms20!! quartz.dataSource.default.provider = SqlServer-20
4. 使用log4net記錄日志
更改Server的Quartz.Server.exe.config配置文件如下

<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="quartz" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /> <sectionGroup name="common"> <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" /> </sectionGroup> </configSections> <common> <logging> <factoryAdapter type="Common.Logging.Log4Net.Log4NetLoggerFactoryAdapter, Common.Logging.Log4net1211"> <arg key="configType" value="INLINE" /> <!--<arg key="configFile" value="Trace/ApplicationLog.txt" />--> <!--<arg key="level" value="INFO" />--> </factoryAdapter> </logging> </common> <appSettings> <add key="log4net.Internal.Debug" value="false"/> </appSettings> <log4net> <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender"> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%d [%t] %-5p %l - %m%n" /> </layout> </appender> <appender name="EventLogAppender" type="log4net.Appender.EventLogAppender"> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%d [%t] %-5p %l - %m%n" /> </layout> </appender> <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <param name="File" value="Trace/ApplicationLog.txt" /> <appendToFile value="true" /> <!--Make the rolling file name with the date and size--> <rollingStyle value="Composite" /> <datePattern value="yyyyMM" /> <maxSizeRollBackups value="100" /> <maximumFileSize value="2MB" /> <!--Make the rolling file name like this MyQuartzLog201303.txt, or the deault will be MyQuartzLog.txt201303--> <PreserveLogFileNameExtension value="true" /> <staticLogFileName value="false" /> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%-5p%d{yyyy-MM-dd hh:mm:ss} – %m%n" /> </layout> </appender> <root> <level value="INFO" /> <appender-ref ref="ConsoleAppender" /> <appender-ref ref="EventLogAppender" /> <appender-ref ref="RollingFileAppender"/> </root> </log4net> </configuration>