Quartz.Net 学习随手记之04 构建Windows Service承载服务
为什么选择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>