【问题标题】:Error when a Quartz job runs with "Object Reference not set to an instance of an object"当 Quartz 作业运行时出现“对象引用未设置为对象的实例”时出错
【发布时间】:2018-09-17 01:44:26
【问题描述】:

我有一个在Application_Start 期间在Global.asax.cs 中设置的 Quartz 作业

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    Logger.log("About to Setup Retry Job");
    JobScheduler.Start();
}

这会调用 Start 方法,然后安排作业。

作业每 20 秒运行一次并引发异常。这是我的工作。

public class RetryTempJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        try
        {
            Logger.log("Executing Job");
            new ProcessOrder().retryFailedOrders();
            //Logger.log("Done Executing Syspro Job");
            await Console.Error.WriteLineAsync("Done Executing Syspro Job");
        }
        catch (Exception se)
        {
            await Console.Error.WriteLineAsync("" + se.InnerException);
        }
    }
}

Logger.log("Executing Job"); 这一行抛出异常。这是一个打开日志文件并写入的“静态方法”。此方法适用于我网站的其他任何地方。

这是异常消息: {"对象引用未设置为对象的实例。"}

InnerException 为 NULL。这是堆栈:

DarwinsShoppingCart.dll!DarwinsShoppingCart.SharedClasses.JobScheduler.RetrySyspro.Execute(Quartz.IJobExecutionContext context) Line 69 C#
    Quartz.dll!Quartz.Core.JobRunShell.Run(System.Threading.CancellationToken cancellationToken)    Unknown
    mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Quartz.Core.JobRunShell.<Run>d__9>(ref Quartz.Core.JobRunShell.<Run>d__9 stateMachine)    Unknown
    Quartz.dll!Quartz.Core.JobRunShell.Run(System.Threading.CancellationToken cancellationToken)    Unknown
    Quartz.dll!Quartz.Core.QuartzSchedulerThread.Run.AnonymousMethod__0()   Unknown
    mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.Task>.InnerInvoke() Unknown
    mscorlib.dll!System.Threading.Tasks.Task.Execute()  Unknown
    mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj)   Unknown
    mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot)    Unknown
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Unknown
    mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Unknown
    mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()    Unknown
    mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Unknown

这是我的 Logger 类代码

public static void log(string strLog)
{
    StreamWriter log;
    FileStream fileStream = null;
    DirectoryInfo logDirInfo = null;
    FileInfo logFileInfo;
    string username = Environment.UserName;
    string logFilePath = HttpContext.Current.Server.MapPath("~/log/Log.txt");
    logFileInfo = new FileInfo(logFilePath);
    logDirInfo = new DirectoryInfo(logFileInfo.DirectoryName);
    double fileSize = ConvertBytesToMegabytes(logFileInfo.Length);
    if (fileSize > 30)
    {
        string FileDate = DateTime.Now.ToString().Replace("/", "-").Replace(":", "-");
        string oldfilepath = HttpContext.Current.Server.MapPath("~/log/log-" + FileDate + ".txt");
        File.Move(logFileInfo.FullName, oldfilepath);
    }
    if (!logFileInfo.Exists)
    {
        fileStream = logFileInfo.Create();
    }
    else
    {
        fileStream = new FileStream(logFilePath, FileMode.Append);
    }
    log = new StreamWriter(fileStream);

    log.WriteLine(DateTime.Now.ToString("MM-dd HH:mm:ss") + " " + username + " " + strLog);
    log.Close();
}

【问题讨论】:

  • 发布您的Logger 课程代码。如果您在石英下运行时尝试访问HttpContext,那么您的日子不好过——我曾经有过same issue
  • @BagusTesa 我添加了记录器类代码
  • 好吧,我仍然支持我的猜测,您在请求中调用HttpContext.Current 是一个问题。Quartz 在执行其任务时,它不绑定到任何请求和HttpContext.Current可以返回空值。您必须找到另一种方法来找到真正的路径,而不依赖HttpContext,可能使用HttpRuntime.AppDomainPath or some other。虽然我不确定他们在Quartz 的线程中的表现如何.. 我还没有尝试过自己..
  • 有没有办法创建一个网络服务并从石英作业发布到该网络服务?
  • 实际上,有一个 hack,您可以将大部分代码逻辑放入某种端点,然后让 Quartz 作业使用 WebClient 或从 Quartz 作业中访问该端点HttpClient 或其他任何东西。 hacky,但可以完成工作。

标签: c# asp.net quartz-scheduler quartz.net


【解决方案1】:

鉴于记录器可以在请求中调用,也可以在请求之外调用。如果一个作业开始并且没有请求,HttpContext 将是 null

考虑将记录器与HttpContext 等实现问题解耦,并抽象出映射路径的过程并利用依赖注入

public interface IPathProvider {
    string MapPath(string path);
}

您还应该避免静态记录器代码异味,因为它使代码难以单独维护和测试。

public interface ILogger {
    void log(string message);
    //...
}

public class Logger : ILogger {
    private readonly IPathProvider pathProvider;

    public Logger(IPathProvider pathProvider) {
        this.pathProvider = pathProvider;
    }

    public void log(string strLog) {
        FileStream fileStream = null;
        string username = Environment.UserName;
        string logFilePath = pathProvider.MapPath("~/log/Log.txt");
        var logFileInfo = new FileInfo(logFilePath);
        var logDirInfo = new DirectoryInfo(logFileInfo.DirectoryName);
        double fileSize = ConvertBytesToMegabytes(logFileInfo.Length);
        if (fileSize > 30) {
            string FileDate = DateTime.Now.ToString().Replace("/", "-").Replace(":", "-");
            string oldfilepath = pathProvider.MapPath("~/log/log-" + FileDate + ".txt");
            File.Move(logFileInfo.FullName, oldfilepath);
        }
        if (!logFileInfo.Exists) {
            fileStream = logFileInfo.Create();
        } else {
            fileStream = new FileStream(logFilePath, FileMode.Append);
        }
        using(fileStream) {
            using (var log = new StreamWriter(fileStream)) {
                log.WriteLine(DateTime.Now.ToString("MM-dd HH:mm:ss") + " " + username + " " + strLog);
                log.Close();
            }
        }
    }
}

并让工作明确依赖于抽象。

public class RetryTempJob : IJob {
    private readonly ILogger logger;

    public RetryTempJob(ILogger logger) {
        this.logger = logger;
    }

    public async Task Execute(IJobExecutionContext context) {
        try {
            logger.log("Executing Job");
            new ProcessOrder().retryFailedOrders();
            //logger.log("Done Executing Syspro Job");
            await Console.Error.WriteLineAsync("Done Executing Syspro Job");
        } catch (Exception se) {
            await Console.Error.WriteLineAsync("" + se.InnerException);
        }
    }
}

这里可以抽象出更多内容,但这有点超出示例的范围。

现在设计问题已经解决,我们可以查看路径提供程序实现来处理HttpContext 的情况。

Server.MapPath() 需要 HttpContextHostingEnvironment.MapPath 不需要。

参考What is the difference between Server.MapPath and HostingEnvironment.MapPath?

实现可以尝试检查上下文是否为空,但Server.MapPath() 最终会调用HostingEnvironment.MapPath(),所以最好只使用HostingEnvironment.MapPath()

public class PathProvider : IPathProvider {
    public string MapPath(string path) {
        return HostingEnvironment.MapPath(path);
    }
}

现在剩下的就是配置调度程序以允许依赖注入,并让您决定要使用哪个 DI 框架。

创建一个继承自默认 Quartz.NET 作业工厂SimpleJobFactory 的作业工厂。这个新的作业工厂将在其构造函数中采用IServiceProvider,并覆盖SimpleJobFactory 提供的默认NewJob() 方法。

class MyDefaultJobFactory : SimpleJobFactory {
    private readonly IServiceProvider container;

    public MyDefaultJobFactory(IServiceProvider container) {
        this.container = container;
    }

    public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) {
        IJobDetail jobDetail = bundle.JobDetail;
        Type jobType = jobDetail.JobType;
        try {
            // this will inject any dependencies that the job requires
            return (IJob) this.container.GetService(jobType); 
        } catch (Exception e) {
            var errorMessage = string.Format("Problem instantiating job '{0}'.", jobType.FullName);
            throw new SchedulerException(errorMessage, e);
        }
    }
}

有许多 DI 框架可供选择,但在本示例中,我使用了 .Net Core Dependency Injection Extension,由于 .Net Core 的模块化特性,它可以轻松地放入您的项目中。

最后,在应用启动时配置服务集合

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    var services = new ServiceCollection();
    var serviceProvider = ConfigureService(services);
    var logger = serviceProvider.GetService<ILogger>();
    logger.log("About to Setup Retry Job");
    var jobScheduler = serviceProvider.GetRequiredService<IScheduler>();

    //...add jobs as needed
    jobScheduler.ScheduleJob(.....);

    //and start scheduler
    jobScheduler.Start();
}

private IServiceProvider ConfigureService(IServiceCollection services) {
    //Add job dependencies
    services.AddSingleton<ILogger, Logger>();
    services.AddSingleton<IPathProvider, PathProvider>();
    //add scheduler
    services.AddSingleton<IScheduler>(sp => {
        var scheduler = new StdSchedulerFactory().GetScheduler();
        scheduler.JobFactory = new MyDefaultJobFactory(sp);
        return scheduler;
    });

    //...add any other dependencies

    //build and return service provider
    return services.BuildServiceProvider();
}

正如我之前所说,您可以使用您想要的任何其他 DI/IoC 容器。重构后的代码现在足够灵活,您只需将其包装在 IServiceProvider 派生类中并将其提供给作业工厂即可。

通过清除代码中产生异味的问题,您可以更轻松地管理它。

【讨论】:

    【解决方案2】:

    使用 Quarz Job 时没有HttpContext。它运行我一个单独的线程。所以在一个有Quarz Job的网站上我使用HostingEnvironment

    那么

    HttpContext.Current.Server.MapPath("~/log/Log.txt")
    

    使用

    using System.Web.Hosting;
    
    HostingEnvironment.MapPath("~/log/Log.txt");
    

    【讨论】:

      猜你喜欢
      • 2012-02-01
      • 2014-06-04
      • 1970-01-01
      • 1970-01-01
      • 2019-07-03
      • 1970-01-01
      • 1970-01-01
      • 2019-10-30
      • 1970-01-01
      相关资源
      最近更新 更多