场景还原

用fidder 模拟请求同一个controller的action接口,快速的点两次

actionfilter中的私有变量发生了并发问题!!!

监控到的日志

排查分析

  1. 先在构造函数中记录日志 看看类加载情况

  1. 分析 filter构造函数加载了8次,controller加载两次

请求两次, controller构造触发两次,挺正常的,

但是filter执行了8 次, 等于说filter中的四个方法每次执行都触发这个filter构造,如果你这么想那就大错特错

多次测试请求放心,filter是事先加载好的, 后面调用controller,只会进filter的方法,不会触发filter构造函数执行

==结论:== filter是事先就加载进去的,并不是每次触发到对应action的时候才初始化,所以filter中的私有变量根本不能使用, 我推测是共享在内存中的静态变量,但是有待验证

==进一步验证 多个电脑请求,发现filter中的构造函数也没有执行==

搜到一个相关文献

过滤器属性 设计成线程安全的。框架不能保证filter属性的单个实例一次只能服务一个请求。有鉴于此,您不能在OnActionExecuting/OnActionExecuted方法中改变属性实例状态。

考虑其中一个作为备选方案:

  • 使用HttpContext.Items项目将值存储在OnActionExecuting中,然后从action方法读取它。您可以通过 筛选器上下文 传递给OnActionExecuting的参数。
  • 将属性而不是属性放在控制器上,然后让OnActionExecuting方法将控制器强制转换为SomeController,并直接从该方法中设置属性。这将起作用,因为框架在默认情况下保证控制器实例是瞬态的;单个控制器实例永远不会为多个请求提供服务。

解决方案

https://stackoverflow.com/questions/8937200/are-actionfilterattributes-reused-across-threads-how-does-that-work

https://stackoverflow.com/questions/8937200/are-actionfilterattributes-reused-across-threads-how-does-that-work

以下是上面链接的摘要,防止网页打不开

This will depend on the version of ASP.NET MVC but you should never store instance state in an action filter that will be reused between the different methods. Here's a quote for example from one of the breaking changes in ASP.NET MVC 3:

In previous versions of ASP.NET MVC, action filters are create per request except in a few cases. This behavior was never a guaranteed behavior but merely an implementation detail and the contract for filters was to consider them stateless. In ASP.NET MVC 3, filters are cached more aggressively. Therefore, any custom action filters which improperly store instance state might be broken.

This basically means that the same instance of the action filter can be reused for different actions and if you have stored instance state in it it will probably break.

And in terms of code this means that you should absolutely never write an action filter like this:

public class TestAttribute : ActionFilterAttribute
{
private string _privateValue;

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
  _privateValue = ... some calculation
}

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
  // use _privateValue here
}
}
but you should write it like this:

public class TestAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
  var privateValue = ... some calculation
  filterContext.HttpContext.Items["__private_value__"] = privateValue;
}

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
  var privateValue = filterContext.HttpContext.Items["__private_value__"];
  // use privateValue safely here
}
}

解决方案核心代码

public class TestAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var privateValue = ... some calculation
        filterContext.HttpContext.Items["__private_value__"] = privateValue;
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var privateValue = filterContext.HttpContext.Items["__private_value__"];
        // use privateValue safely here
    }
}

对解决方案进行封装

 public void SetThreadSafeData(FilterContext context, string key, string value)
        {
            context.HttpContext.Items[key] = value;
        }

        public string GetThreadSafeData(FilterContext context, string key)
        {
            try
            {
                return (string)context.HttpContext.Items[key];
            }
            catch
            {
                return string.Empty;
            }
        }

非线程安全的源代码

==以下仅起到警示作用,避免大家和我一样踩雷,解决方案就在顶上==

  1. ActionFilter类

目的:

OnActionExecuting :生成请求的唯一标识,用于保证filter和action中的请求同源,以及在四个filter方法中传递

OnActionExecuted: 未实现,如果action中抛出异常会走到这里,而不走result的两个过滤器方法

OnResultExecuting: 在这里处理响应结果返回给前端

OnResultExecuted: 这里用于收尾,比如释放整个请求链中用到的资源如redis

public class BPMFilterAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// 数据请求版本标识
        /// </summary>
        private string DataVersionGuid = null;
        private string requestData = null;
        private bool isSubmit = false;

        private BPMFilterAttribute()
        {
        }

        public BPMFilterAttribute(bool isSubmit)
        {
            this.isSubmit = isSubmit;
        }

        /// <summary>
        /// 重写
        /// </summary>
        /// <param name="context">上下文</param>
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var exceptionStr = string.Empty;
            try
            {
                var request = context.ActionArguments.Values.FirstOrDefault() as BaseRequest;
                if (request == null)
                {
                    return;
                }
                requestData = context.ActionArguments.Values.FirstOrDefault()?.PackJson() ?? string.Empty;
                request.DataVersionGuid = Guid.NewGuid().ToString().Replace("-", "");
                DataVersionGuid = request.DataVersionGuid;
            }
            catch (Exception ex)
            {
                exceptionStr = ex.ToString();
            }
            finally
            {
                if (isSubmit)
                {
                    var logData = new SystemLogEntity()
                    {
                        ExceptionData = exceptionStr,
                        LogData = $"OnActionExecuting执行了,DataVersionGuid={DataVersionGuid}",
                        CreateTime = DateTime.Now.ToLocalTime(),
                        BusinessId = DataVersionGuid,
                        RequestRaw = requestData,
                        Remark = "OnActionExecuting",
                    };
                    MongoDBHelper.Instance().InsertOne(logData, "SystemLogEntity");
                }
            }
        }

        public override void OnActionExecuted(ActionExecutedContext context)
        {
            // 如果action中抛出异常,这里会收到context.Exception,且不会走OnResultExecuting和OnResultExecuted,直接api结束。
            // 否则context.Exception为null,会进入走OnResultExecuting和OnResultExecuted
            var exceptionStr = string.Empty;
            try
            {
                bool isError = context.Exception != null;
                if (!isError)
                {
                    // 写入数据版本到数据库需要业务在save单据信息后,且push到bpm前自行调用setversion方法完成。 因为系统设计原因,adddto实体无法传递到bpm服务中,也就没法在bpm服务中取到adddto的DataVersionGuid.
                    // 废除,不可取----TODO:写入数据版本到数据库中,便于bpmmq回调是通过此值判断数据是否一致===>实现不了 aop拿不到业务单据主键,无法绑定版本号和主键id对应关系,只能插入业务代码里setversion
                }
            }
            catch (Exception ex)
            {
            }
        }

        /// <summary>
        /// 重写
        /// </summary>
        /// <param name="context">上下文</param>
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var exceptionStr = string.Empty;
            StringBuilder log = new StringBuilder();
            var formId = string.Empty;
            try
            {
                if (!BPMConfigHelper.IsDirectApproving())
                {
                    string url = BPMDataVersionService.GetStartUrl(DataVersionGuid);
                    log.Append($"1.url={url}");
                    var versionModel = BPMDataVersionService.GetBPMVersionModel(DataVersionGuid);
                    log.Append($"||2.versionModel={versionModel?.PackJson() ?? string.Empty}");
                    formId = versionModel?.BFormID ?? string.Empty;
                    if (versionModel != null && !versionModel.StartUrl.IsNullOrWhiteSpace())
                    {

                        var instances = ServiceHelperProvider.Instance.DbContext.Boost.Queryable<Instance>()
                            .Where(t => t.DomainId.ToString() == versionModel.BFormID)
                            .First();
                        log.Append($"||3.查询instances:{instances?.PackJson() ?? string.Empty}");
                        if (instances?.IsGoNewBPM == true)
                        {
                            // 这里处理bpm中间页情况需要提交后仍为草稿的逻辑
                            var budgetStatus = CommonEnums.ApprovalStatus.Return.GetHashCode();
                            log.Append($"||4.执行updateStatus");
                            ServiceHelperProvider.Instance.BPMCallBack.UpdateStatus(versionModel.BFormID, versionModel.BFKCode, (int)CommonApproveStatusEnum.撤回, budgetStatus);
                            log.Append($"||5.执行updateStatus---完成");
                        }
                    }

                    PathString path = context.HttpContext.Request.Path;
                    if (!path.HasValue || context.Result is FileResult)
                    {
                        return;
                    }

                    if (context.Result is EmptyResult) //&& !url.IsNullOrWhiteSpace() 
                    {
                        context.Result = new ObjectResult(new { StartUrl = url, DataVersionGuid = DataVersionGuid });
                    }
                    else if (context.Result is ObjectResult && !url.IsNullOrWhiteSpace())
                    {
                        ObjectResult result = context.Result as ObjectResult;
                        if (result.DeclaredType != null & result.DeclaredType.Name.ToLower() == "object")
                        {
                            var obj = result.Value.PackJson().PackToJObject(); obj.Add("startUrl", url); // 驼峰写法
                            result.Value = obj;
                            context.Result = result;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                exceptionStr = ex.ToString();
            }
            finally
            {
                if (isSubmit)
                {
                    var logData = new SystemLogEntity()
                    {
                        ExceptionData = exceptionStr,
                        LogData = log.ToString(),
                        CreateTime = DateTime.Now.ToLocalTime(),
                        BusinessId = DataVersionGuid,
                        Remark = formId,
                    };
                    MongoDBHelper.Instance().InsertOne(logData, "SystemLogEntity");
                }
            }
        }

        /// <summary>
        /// OnResultExecuted
        /// </summary>
        /// <param name="context">context</param>
        public override void OnResultExecuted(ResultExecutedContext context)
        {
            // 释放redis
            BPMDataVersionService.ReleaseBPMRedis(DataVersionGuid);
        }

    }
  1. Controller类

    /// <summary>
    /// 报销控制器
    /// </summary>
    [Route("v1/hello")]
    public class HelloController : ControllerBase
    {
     [BPMFilter(true)]
     [HttpPost("submit-data")]
     public object SubmitData([FromBody]HelloRequest request)
     {
         return _helloService.SubmitData(request);
     }
    }