使用特性+反射来做超轻量级的定时调度服务
1、定义特性,特性中包含执行间隔的属性,在每个需要定时调度执行的方法上加上该特性,表示该方法参与定时调度。
2、编写方法,默认在调度服务启动时,会先访问该方法来获取全部需要定时执行的方法和间隔;
3、每个方法执行时,可以向服务返回日志,同时也可以返回下次执行的时间,服务将在指定的时间来调用该服务,默认按调度规则执行。若需要额外指定下次执行的时间可以在返回值中返回;
4、其实可以考虑增加本次调度的间隔参数,以方便被调用服务判断当前执行的间隔是否是常规间隔还是自定义间隔。
-------------------------------------------------------------------------------
代码在公司,后面再贴上来。该工作主要将原有的Windows服务,做得太重,需要导入100多个DLL,现在重新改造,把服务执行和服务调度完全剥离,服务调度(Windows Service)就是一个一百行代码左右的超轻量服务,后续新增或维护服务,均基于接口,不需要对服务、服务配置做任何改动。
下面是调用端的主要代码,可以编写成控制台、Windows服务等,其中的HttpProc就是常规的HTTP调用的类,可以用WebClient代替,定时执行:
-------------------------------------------------------------------------------
/// <summary> /// https://soleds.cnblogs.com
/// 2019-10-10 By 王晓龙
/// 简化系统定时执行服务 /// </summary>
public partial class AutoExecutes : ServiceBase { public AutoExecutes() { InitializeComponent(); } /// <summary> /// 定时器 /// </summary> private System.Timers.Timer tm = new System.Timers.Timer(1000); /// <summary> /// 任务列表锁对象,锁住任务的读写,可能发生的并发处在初始化&定时器可能发生并发操作。 /// </summary> private static Object MyLock = new object(); /// <summary> /// 远程已配置的任务执行时间计划列表(每30分钟会被自动刷新一次,以保证服务更新后依然可以得到及时修正) /// </summary> private List<AutoExecuteAttribute> RemoteConfigs = new List<AutoExecuteAttribute>(); /// <summary> /// 需要用于远程调用的服务列表,键名为要执行的日期时间,键值为在该时间需要调用的方法列表 /// </summary> private Dictionary<DateTime, List<string>> RemoteServices = new Dictionary<DateTime, List<string>>(); /// <summary> /// 配置文件,目前只需要配置一个URL /// </summary> private ServiceConfig config = new ServiceConfig(); protected override void OnStart(string[] args) { Logs.Info("准备启动服务..."); this.AutoLog = true; //重置任务列表 this.RemoteServices.Clear(); //读取任务配置文件 var taskConfig = AppDomain.CurrentDomain.BaseDirectory + "config.json"; if (!System.IO.File.Exists(taskConfig)) { //编写示例任务 var sc = new ServiceConfig() { url = "https://api.baidu.com/RemoteWebServices.aspx" }; File.WriteText(AppDomain.CurrentDomain.BaseDirectory + "config-示例.json",Newtonsoft.Json.JsonConvert.SerializeObject(sc), false); Logs.Info("服务启动失败,没有找到配置文件 config.json"); return; } //读取配置文件 string strJson = File.ReadText(taskConfig); this.config = Newtonsoft.Json.JsonConvert.DeserializeObject<ServiceConfig>(strJson); //启动定时器 tm.Interval = 100; tm.Elapsed += Tm_Elapsed; tm.Start(); Logs.Info("准备从远程抓取服务配置信息!"); //初次启动,获取服务配置 var res = OnRemoteConfigsUpdate(); if (res) { Logs.Info("服务已成功启动!当前任务总数:" + this.RemoteServices.Sum(t => t.Value.Count).ToString()); } else { Logs.Error("服务启动失败,从远程获取服务不成功!" + config.url + "?Action=GetActionConfig 请检查远程服务是否能正常调用,并重新启动当前服务!"); } } /// <summary> /// 秒数计时器,用于每30分钟强制刷新一次远程服务配置 /// </summary> private DateTime LastLoadConfigTime = DateTime.Now; /// <summary> /// 刷新远程服务配置 /// </summary> private bool OnRemoteConfigsUpdate() { string InitUrl = config.url + "?Action=GetActionConfig"; var urlSign = "";//加入到HTTP头中的请求签名,可以使用任意算法产生。 //通过HTTP构建并获取任务列表 HttpProc http = new HttpProc(InitUrl); http.encoding = System.Text.Encoding.UTF8; http.Headers.Add("webSign", urlSign); string res = http.Proc(); if (res.StartsWith("[") && res.EndsWith("]")) { var ncs = new List<AutoExecuteAttribute>(); ncs = Newtonsoft.Json.JsonConvert.DeserializeObject<List<AutoExecuteAttribute>>(res); //判断是否有移除掉的服务,如果有的话,先移除 foreach (var item in new List<AutoExecuteAttribute>(this.RemoteConfigs)) { if (ncs.FirstOrDefault(t => t.ActionName.Equals(item.ActionName)) == null) { this.RemoteConfigs.RemoveAll(t => t.ActionName.Equals(item.ActionName)); } } //判断是否有新增的服务 foreach (var nc in ncs) { var curr = this.RemoteConfigs.Where(t => t.ActionName.Equals(nc.ActionName)).FirstOrDefault(); if (curr == null) { //这是新的服务 this.RemoteConfigs.Add(nc); //加入待处理服务列表 lock (MyLock) { DateTime nextTime = DateTime.Now.AddSeconds(nc.ExecuteSeconds); if (!this.RemoteServices.ContainsKey(nextTime)) { this.RemoteServices.Add(nextTime, new List<string>()); } this.RemoteServices[nextTime].Add(nc.ActionName); } } else { //判断是否更改了轮询间隔 if (curr.ExecuteSeconds != nc.ExecuteSeconds) { curr.ExecuteSeconds = nc.ExecuteSeconds; } } } return true; } else { return false; } } /// <summary> /// 执行URL调用 /// </summary> /// <param name="url"></param> /// <returns></returns> private CallbackResult Execute(string url,string action) { //生成请求头签名 var urlSign = "";//加入到HTTP头中的请求签名,可以使用任意算法产生。 //写日志 Logs.Custom(action, "即将准备执行任务调用,调度签名(" + urlSign + "),任务URL:" + url); //执行URL调用 Stopwatch sw = new Stopwatch(); sw.Start(); HttpProc http = new HttpProc(url); http.TimeOut = 1000 * 60 * 3;//超时时间默认为30秒,更改为3分钟。 http.encoding = System.Text.Encoding.UTF8; http.Headers.Add("webSign", urlSign); string result = http.Proc(); sw.Stop(); if (result.StartsWith("{") && result.EndsWith("}")) { //转换为对象返回 CallbackResult res = new CallbackResult(); res = Newtonsoft.Json.JsonConvert.DeserializeObject<CallbackResult>(result); Logs.Custom(action, " URL调度执行完毕,消耗时长:" + sw.ElapsedMilliseconds.ToString() + "ms,任务URL:" + url + ",远程执行时长:" + res.ExecutedTimes.ToString() + "ms,返回结果:" + result); return res; } else { //写日志 Logs.Custom(action, " URL调用返回值不符合要求:" + result); return new CallbackResult(); } } /// <summary> /// 处理定时事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Tm_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { DateTime curr = DateTime.Now; //服务配置自动刷新计时 if (Convert.ToInt32(DateTime.Now.Subtract(LastLoadConfigTime).TotalMinutes) == 30) { LastLoadConfigTime = DateTime.Now; //启动配置刷新服务 Task.Run(new Action(() => { OnRemoteConfigsUpdate(); })); } //遍历任务 lock (MyLock) { //获取全部记录,早于或等于当前时间的任务,都应该被执行 var list = this.RemoteServices.Where(t => t.Key <= curr); foreach (var item in list) { //使用并行,有多少个任务就有多少个并行查询。 item.Value.AsParallel().ForAll(new Action<string>(action => { //执行当前任务,并获得下次任务执行时间,如果没有指定时间,则从抓取的任务配置上读取当前Action对象的执行时间 string url = config.url + "?Action=" + action; //执行远程调用,并获取返回值,返回后,计算下次执行时间,加入到任务列表 var ExecuteCallback = Execute(url, action); if (ExecuteCallback != null) { //取当前Action的间隔配置 var ActionTime = this.RemoteConfigs.Where(t => t.ActionName.Equals(action)).Select(t => t.ExecuteSeconds).FirstOrDefault(); if (ActionTime == 0) { //配置的时间无效 return; } //判断下次调用日期时间 DateTime nextTime = DateTime.Now.AddSeconds(ActionTime); if (ExecuteCallback.NextTime > 0) { nextTime = DateTime.Now.AddSeconds(ExecuteCallback.NextTime); } //将时间加入列表 if (!this.RemoteServices.ContainsKey(nextTime)) { this.RemoteServices.Add(nextTime, new List<string>()); } this.RemoteServices[nextTime].Add(action); } })); //移除当前时间的记录 this.RemoteServices.Remove(item.Key); } } } protected override void OnStop() { tm.Stop(); Logs.Info("服务已停止!"); } }
-------------------------------------------------------------------------------
下面是页面文件,某个URL上的aspx,用于做服务的定时调度,Windows URL调度服务,根据该URL中获取配置信息,及所有需要调用的服务(每个带有指定特性的方法,都是一个服务项)清单,定时调用执行。
/// <summary> /// https://soleds.cnblogs.com
/// 2019-10-10 By 王晓龙
/// 简化系统定时执行服务 /// </summary> public partial class Interfaces_RemoteWebServices : Pagebase { //不要修改,用于返回执行计时器,以及可选决定下一次执行的时间返回给服务控制调度间隔。 private CallbackResult callback = new CallbackResult(); protected void Page_Load(object sender, EventArgs e) { string action = MyRequest("action", 0); if (string.IsNullOrEmpty(action)) { Response.Write("出错啦,action不能为空!"); Response.End(); } //判断是否有请求头,请求头来自域名的加密 var webSign = Request.Headers["webSign"]; if (!string.IsNullOrEmpty(webSign)) { //检查 //验证请求签名的代码,涉及到安全,省略; } else { Response.Write("签名验证失败,自动执行已取消!"); Response.End(); } //做IP同网段验证,先获取客户端IP string ClientIPAddress = ""; //处理IP地址初始化,默认支持负载均衡自动获取IP if (Request.Headers.AllKeys.Contains("X-Forwarded-For")) { ClientIPAddress = Request.Headers["X-Forwarded-For"].Split(",".ToCharArray())[0];//客户端的IP地址 } else if (Request.Headers.AllKeys.Contains("X-Real-IP")) { ClientIPAddress = Request.Headers["X-Real-IP"].Split(",".ToCharArray())[0];//客户端的IP地址 } else { ClientIPAddress = Request.UserHostAddress;//客户端的IP地址 } try { //获取本机IP string localhsotIP = ""; var Ips = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList; foreach (var ip in Ips) { string pattrn = @"(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])"; if (System.Text.RegularExpressions.Regex.IsMatch(ip.ToString(), pattrn)) { localhsotIP = ip.ToString(); break; } } //验证客户端IP if (!ClientIPAddress.Equals("128.76.21.179") && localhsotIP.StartsWith("172.16.")) { //其它IP访问现网服务器,拒绝访问; Logs.Custom("Service_Request", $"from {ClientIPAddress} 试图访问现网服务器 {localhsotIP},自动拒绝!"); Response.Write("异常请求!外部主机试图访问现网服务!"); Response.End(); } } catch (Exception ex) { Logs.Custom("Service_Request", $"from {ClientIPAddress} Has Exception:" + ex.ToString()); } //检查是否有和Action对应的方法,使用反射,如果有的话,直接调用提取结果;注意方法必须为公有 System.Reflection.MethodInfo info = this.GetType().GetMethod(action, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase, null, new Type[] { }, null); if (info != null) { try { var result = info.Invoke(this, null); if (result != null) { Logs.Custom("自动执行日志", $"执行:{action} 响应消息:" + result.ToString()); Response.Write(result.ToString()); } else { CallbackResult res = new CallbackResult() { Status = false, StatusMessage = "服务执行成功!" }; Response.Write(res.ToString()); } } catch (Exception ex) { CallbackResult res = new CallbackResult() { Status = false, StatusMessage = "服务调度执行失败,错误消息:" + ex.Message + " 错误跟踪:" + ex.StackTrace }; Response.Write(res.ToString()); } Response.End(); } else { Response.Write("所请求的方法有误,请核实!"); Response.End(); } } /// <summary> /// 获取全部服务配置,在服务启动,或者每30分钟会自动抓取更新服务调度列表,不要修改或删除该方法或更改方法名称,全部服务的调度依赖该函数 /// </summary> public string GetActionConfig() { List<AutoExecuteAttribute> result = new List<AutoExecuteAttribute>(); //反射,获取全部拥有该特性的方法 var methods = this.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase); foreach (var mi in methods) { //获取当前方法的特性 var customAttribute = mi.GetCustomAttribute(typeof(AutoExecuteAttribute)); if (customAttribute != null) { AutoExecuteAttribute attr = (AutoExecuteAttribute)customAttribute; attr.ActionName = mi.Name; //判断是否需要返回 if (!attr.IsReleaseEnvironment && !attr.IsPreReleaseEnvironment) { //当前非生产环境,且指定在非生产环境下不允许执行。 continue; } result.Add(attr); } } return Newtonsoft.Json.JsonConvert.SerializeObject(result); } /// <summary> /// 管理端推送(示例,每个方法就是一个服务,需要添加特性AutoExecute,默认指定执行间隔,即两次调度中间的间隔秒数,callback是执行结果,如果有日志需要输出集中到服务调用端,可以追加到 callback.StatusMessage 中,调用端在完成后会判断结果,并将StatusMessage写入日志文件。) /// </summary> /// <returns></returns> [AutoExecute(15)] public CallbackResult ShopMessage() { callback.NextTime = 1.0; callback.StatusMessage += $"开始处理商家端推送:{DateTime.Now}!"; ServiceNos.AsParallel().ForAll(sn => { try { //执行代码省略........ } catch (Exception ex) { callback.SetError(ex); } }); return callback; } } /// <summary> /// 响应类,可以在代码里返回该对象,返回该对象时,该对象将表示下一次执行的日期时间 /// </summary> public class CallbackResult { private Stopwatch sw = new Stopwatch(); /// <summary> /// 建议进入函数时就声明该对象,声明时即自动启动计时器 /// </summary> public CallbackResult() { sw.Start(); } public void SetError(Exception ex) { this.StatusMessage += "错误消息:" + ex.Message + ",错误跟踪:" + ex.StackTrace; } /// <summary> /// 返回下一次执行的秒数差,0表示按服务配置时间,其它值表示按该时间。 /// </summary> public double NextTime { get; set; } = 0.00; /// <summary> /// 执行结果状态,true表示执行成功,false表示执行失败 /// </summary> public bool Status { get; set; } = true; /// <summary> /// 执行结果的描述,数据写入并追加到这里时,会自动做为日志写入自动执行日志。 /// </summary> public string StatusMessage { get; set; } = "执行成功"; /// <summary> /// 获取执行计时 /// </summary> public int ExecutedTimes { get { return Convert.ToInt32(sw.ElapsedMilliseconds); } set { } } /// <summary> /// 获取响应JSON /// </summary> /// <returns></returns> public override string ToString() { return Newtonsoft.Json.JsonConvert.SerializeObject(this); } } /// <summary> /// 服务配置专用特性类,用于控制服务的调度间隔、调度时间 /// </summary> [AttributeUsage(AttributeTargets.Method)] public class AutoExecuteAttribute : Attribute { /// <summary> /// 调度执行的Action参数,该参数由系统自动根据不同方法赋值,不需要填写 /// </summary> public string ActionName { get; set; } /// <summary> /// 间隔秒数,同一个服务调用,由同一个线程调度,只有HTTP响应后才会计算下一次的执行时间,可以使用小数,例如0.5表示500ms /// </summary> public double ExecuteSeconds { get; set; } /// <summary> /// 当前是否生产环境,通过web.Config读取配置。 /// </summary> public bool IsReleaseEnvironment { get { return Convert.ToBoolean(System.Configuration.ConfigurationManager.AppSettings.Get("IsReleaseEnvironment")); } } /// <summary> /// 在非生产环境下是否允许执行。默认执行。 /// </summary> public bool IsPreReleaseEnvironment { get; set; } = true; /// <summary> /// 构造函数 /// </summary> /// <param name="scounds">当前服务执行间隔</param> public AutoExecuteAttribute(double Seconds) { this.ExecuteSeconds = Seconds; } /// <summary> /// 构造函数 /// </summary> /// <param name="scounds">当前服务执行间隔</param> public AutoExecuteAttribute(double Seconds, bool isPreReleaseEnvironment) { this.ExecuteSeconds = Seconds; this.IsPreReleaseEnvironment = isPreReleaseEnvironment; } }
callback.StatusMessage