前言

问题的起因是在帮同事解决遇到的一个问题,他的本意是在EF Core中为了解决避免多个线程使用同一个DbContext实例的问题。但是由于对Microsoft.Extensions.DependencyInjection体系的深度不是很了解,结果遇到了新的问题,当时整得我也有点蒙了,所以当时也没解决,而且当时快下班了,就想着第二天再解决。在地铁上,经过我一系列的思维跳跃,终于想到了问题的原因,第二天也顺利的解决了这个问题。虽然我前面说了EFCore,但是本质和EFCore没有关系,只是凑巧。解决了之后觉得这个问题是个易错题,觉得挺有意思的,便趁机记录一下。

问题演示

接下来我们还原一下当时的场景,以下代码只是作为演示,无任何具体含义,只是为了让操作显得更清晰一下,接下来就贴一下当时的场景代码

[Route("api/[controller]/[action]")]
[ApiController]
public class InformationController : ControllerBase
{
private readonly LibraryContext _libraryContext;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<InformationController> _logger; public InformationController(LibraryContext libraryContext,
IServiceProvider serviceProvider,
ILogger<InformationController> logger)
{
_libraryContext = libraryContext;
_serviceProvider = serviceProvider;
_logger = logger;
} [HttpGet]
public string GetFirst()
{
var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault();
//这里直接使用了Task方式
Task.Run(() => {
try
{
//Task里创建了新的IServiceScope
using var scope = _serviceProvider.CreateScope();
//通过IServiceScope创建具体实例
LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>();
var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
}
});
return caseInfo.Title;
}
}

再次强调一下,上述代码纯粹是为了让演示更清晰,无任何业务含义,不喜勿喷。咱们首先看一下这段代码表现出来的意思,就是在ASP.NET Core的项目里,在Task.Run里使用IServiceProvider去创建Scope的场景。如果对ASP.NET Core Controller生命周期和IServiceProvider不够了解的话,会很容易遇到这个问题,且不知道是什么原因。上述这段代码会偶现一个错误

Cannot access a disposed object.
Object name: 'IServiceProvider'.

这里为什么说是偶现呢?因为会不会出现异常完全取决于Task.Run里的代码是在当前请求输出之前执行完成还是之后完成。说到这里相信有一部分同学已经猜到了代码报错的原因了。问题的本质很简单,是因为IServiceProvider被释放掉了。我们知道默认情况下ASP.NET Core为每次请求处理会创建单独的IServiceScope,这会关乎到声明周期为Scope对象的声明周期。所以如果Task.Run里的逻辑在请求输出之前执行完成,那么代码运行没任何问题。如果是在请求完成之后完成再执行CreateScope操作,那必然会报错。因为Task.Run里的逻辑何时被执行,这个是由系统CPU调度本身决定的,特别是CPU比较繁忙的时候,这种异常会变得更加频繁。

这个问题不仅仅是在Task.Run这种场景里,类似的本质就是在一个IServiceScope里创建一个新的子Scope作用域的时候,这个时候需要注意的是父级的IServiceProvider释放问题,如果父级的IServiceProvider已经被释放了,那么基于这个Provider再去创建Scope则会出现异常。但是这个问题在结合Task或者多线程的时候,更容易出现问题。

解决问题

既然我们知道了它为何会出现异常,那么解决起来也就顺理成章了。那就是保证当前请求执行完成之前,最好保证Task.Run里的逻辑也要执行完成,所以我们上述的代码会变成这样

[HttpGet]
public async Task<string> GetFirst()
{
var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault();
//这里使用了await Task方式
await Task.Run(() => {
try
{
//Task里创建了新的IServiceScope
using var scope = _serviceProvider.CreateScope();
//通过IServiceScope创建具体实例
LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>();
var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
}
});
return caseInfo.Title;
}

试一下,发现确实能解决问题,因为等待Task完成能保证Task里的逻辑能在请求执行完成之前完成。但是,很多时候我们并不需要等待Task执行完成,因为我们就是希望它在后台线程去执行这些操作,而不需要阻塞执行。

上面我们提到了本质是解决在IServiceScope创建子Scope时遇到的问题,因为这里注入进来的IServiceProvider本身是Scope的,只在当前请求内有效,所以基于IServiceProvider去创建IServiceScope要考虑到当前IServiceProvider是否释放。那么我们就得打破这个枷锁,我们要想办法在根容器中去创建新的IServiceScope。这一点我大微软自然是考虑到了,在Microsoft.Extensions.DependencyInjection体系中提供了IServiceScopeFactory这个根容器的作用域,基于根容器创建的IServiceScope可以得到平行与当前请求作用域的独立的作用域,而不受当前请求的影响。改造上面的代码用以下形式

[Route("api/[controller]/[action]")]
[ApiController]
public class InformationController : ControllerBase
{
private readonly LibraryContext _libraryContext;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<InformationController> _logger; public InformationController(LibraryContext libraryContext,
IServiceScopeFactory scopeFactory,
ILogger<InformationController> logger)
{
_libraryContext = libraryContext;
_scopeFactory = scopeFactory;
_logger = logger;
} [HttpGet]
public string GetFirst()
{
var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault();
//这里直接使用了Task方式
Task.Run(() => {
try
{
//Task里创建了新的IServiceScope
using var scope = _scopeFactory.CreateScope();
//通过IServiceScope创建具体实例
LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>();
var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
}
});
return caseInfo.Title;
}
}

如果你是调试起来的话你可以看到IServiceScopeFactory的具体实例是Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope类型的,它里面包含了一个IsRootScope属性,通过这个属性我们可以知道当前容器作用域是否是根容器作用域。当使用IServiceProvider实例的时候IsRootScopefalse,当使用IServiceScopeFactory实例的时候IsRootScopetrue。使用CreateScope创建IServiceScope实例的时候,注意用完了需要释放,否则可能会导致TransientScope类型的实例得不到释放。在之前的文章咱们曾提到过TransientScope类型的实例都是在当前容器作用域释放的时候释放的,这个需要注意一下。

问题探究

上面我们了解到了在每次请求的时候使用IServiceProvider和使用IServiceScopeFactory的时候他们作用域的实例来源是不一样的。IServiceScopeFactory来自根容器,IServiceProvider则是来自当前请求的Scope。顺着这个思路我们可以看一下他们两个究竟是如何的不相同。这个问题还得从构建Controller实例的时候,注入到Controller中的实例作用域的问题。

请求中的IServiceProvider

在之前的文章<ASP.NET Core Controller与IOC的羁绊>我们知道,Controller是每次请求都会创建新的实例,我们再次拿出来这段核心的代码来看一下,在DefaultControllerActivator类的Create方法中[点击查看源码]

internal class DefaultControllerActivator : IControllerActivator
{
private readonly ITypeActivatorCache _typeActivatorCache;
public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache)
{
_typeActivatorCache = typeActivatorCache;
} public object Create(ControllerContext controllerContext)
{
//省略一系列判断代码 var serviceProvider = controllerContext.HttpContext.RequestServices;
//这里传递的IServiceProvider本质就是来自HttpContext.RequestServices
return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
}
}

通过这个方法我们可以看到创建Controller实例时,如果存在构造依赖,本质则是通过HttpContext.RequestServices实例创建出来的,而它本身就是IServiceProvider的实例,ITypeActivatorCache实例中则存在真正创建Controller实例的逻辑,具体可以查看TypeActivatorCache类的实现[点击查看源码]

internal class TypeActivatorCache : ITypeActivatorCache
{
private readonly Func<Type, ObjectFactory> _createFactory =
(type) => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes);
private readonly ConcurrentDictionary<Type, ObjectFactory> _typeActivatorCache =
new ConcurrentDictionary<Type, ObjectFactory>(); public TInstance CreateInstance<TInstance>(
IServiceProvider serviceProvider,
Type implementationType)
{
//省略一系列判断代码 var createFactory = _typeActivatorCache.GetOrAdd(implementationType, _createFactory);
//创建Controller的时候,需要的依赖实例都是来自IServiceProvider
return (TInstance)createFactory(serviceProvider, arguments: null);
}
}

其实在这里我们就可以得到一个结论,我们在当前请求默认通过构造注入的IServiceProvider的实例其实就是HttpContext.RequestServices,也就是针对当前请求的作用域有效,同样的是来自当前作用域的Scope周期的对象实例也是在当前请求结束就会释放。验证这个很简单可以写个demo来演示一下

[Route("api/[controller]/[action]")]
[ApiController]
public class InformationController : ControllerBase
{
private readonly IServiceProvider _serviceProvider; public InformationController(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
} [HttpGet]
public bool[] JudgeScope([FromServices]IServiceProvider scopeProvider)
{
//比较构造注入的和在HttpContext获取的
bool isEqualOne = _serviceProvider == HttpContext.RequestServices;
//比较通过Action绑定的和在HttpContext获取的
bool isEqualTwo = scopeProvider == HttpContext.RequestServices;
return new[] { isEqualOne, isEqualTwo };
}
}

毫无疑问,默认情况下isEqualOne和isEqualTwo的结构都是true,这也验证了我们上面的结论。因此在当前请求默认注入IServiceProvider实例的时候,都是来自HttpContext.RequestServices的实例。

请求中的IServiceProvider和IServiceScopeFactory

上面我们看到了在当前请求中获取IServiceProvider实例本身就是Scope的,而且在当前请求中通过各种注入方式获取到的实例都是相同的。那么接下来我们就可以继续跟踪,本质的HttpContext.RequestServices的IServiceProvider到底来自什么地方呢?我们找到HttpContext默认的实现类DefaultHttpContext中关于RequestServices属性的定义[点击查看源码]

//接受
public IServiceScopeFactory ServiceScopeFactory { get; set; } = default!;
//数据来自RequestServicesFeature
private static readonly Func<DefaultHttpContext, IServiceProvidersFeature> _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory);
//缓存来自_newServiceProvidersFeature
private IServiceProvidersFeature ServiceProvidersFeature =>
_features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!;
//数据来自ServiceProvidersFeature的RequestServices
public override IServiceProvider RequestServices
{
get { return ServiceProvidersFeature.RequestServices; }
set { ServiceProvidersFeature.RequestServices = value; }
}

通过上面的源码我们可以看到HttpContext.RequestServices的数据最终来自RequestServicesFeature类的RequestServices属性,我们可以直接找到RequestServicesFeature类的定义[点击查看源码]

public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable
{
private readonly IServiceScopeFactory? _scopeFactory;
private IServiceProvider? _requestServices;
private IServiceScope? _scope;
private bool _requestServicesSet;
private readonly HttpContext _context; public RequestServicesFeature(HttpContext context, IServiceScopeFactory? scopeFactory)
{
_context = context;
_scopeFactory = scopeFactory;
} public IServiceProvider RequestServices
{
get
{
if (!_requestServicesSet && _scopeFactory != null)
{
//释放掉之前没释放掉的RequestServicesFeature实例
_context.Response.RegisterForDisposeAsync(this);
//通过IServiceScopeFactory创建Scope
_scope = _scopeFactory.CreateScope();
//RequestServices来自IServiceScopeFactory的CreateScope实例
_requestServices = _scope.ServiceProvider;
//填充已经设置了RequestServices的标识
_requestServicesSet = true;
}
return _requestServices!;
}
set
{
_requestServices = value;
_requestServicesSet = true;
}
} //释放的真实逻辑
public ValueTask DisposeAsync()
{
switch (_scope)
{
case IAsyncDisposable asyncDisposable:
var vt = asyncDisposable.DisposeAsync();
if (!vt.IsCompletedSuccessfully)
{
return Awaited(this, vt);
} vt.GetAwaiter().GetResult();
break;
case IDisposable disposable:
disposable.Dispose();
break;
}
//释放时重置相关属性
_scope = null;
_requestServices = null; return default; static async ValueTask Awaited(RequestServicesFeature servicesFeature, ValueTask vt)
{
await vt;
servicesFeature._scope = null;
servicesFeature._requestServices = null;
}
}
//IDisposable的Dispose的方法,通过using可隐式调用
public void Dispose()
{
DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}

通过上面的两段源码,我们得到了许多关于IServiceProvider和IServiceScopeFactory的相关信息。

  • DefaultHttpContext的RequestServices值来自于RequestServicesFeature实例的RequestServices属性
  • RequestServicesFeature的RequestServices属性的值通过IServiceScopeFactory通过CreateScope创建的
  • 构建RequestServicesFeature的IServiceScopeFactory值来自于DefaultHttpContext的ServiceScopeFactory属性

那么接下来我们直接可以找到DefaultHttpContext的ServiceScopeFactory属性是谁给它赋的值,我们找到创建HttpContext的地方,在DefaultHttpContextFactory的Create方法里[点击查看源码]

public class DefaultHttpContextFactory : IHttpContextFactory
{
private readonly IHttpContextAccessor? _httpContextAccessor;
private readonly FormOptions _formOptions;
private readonly IServiceScopeFactory _serviceScopeFactory; public DefaultHttpContextFactory(IServiceProvider serviceProvider)
{
_httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
_formOptions = serviceProvider.GetRequiredService<IOptions<FormOptions>>().Value;
//通过IServiceProvider的GetRequiredService直接获取IServiceScopeFactory
_serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
} //创建HttpContext实例的方法
public HttpContext Create(IFeatureCollection featureCollection)
{
if (featureCollection is null)
{
throw new ArgumentNullException(nameof(featureCollection));
} var httpContext = new DefaultHttpContext(featureCollection);
Initialize(httpContext);
return httpContext;
} private DefaultHttpContext Initialize(DefaultHttpContext httpContext)
{
//IHttpContextAccessor也是在这里赋的值
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = httpContext;
} httpContext.FormOptions = _formOptions;
//DefaultHttpContext的ServiceScopeFactory属性值来自注入的IServiceProvider
httpContext.ServiceScopeFactory = _serviceScopeFactory; return httpContext;
}
}

这里我们可以看到IServiceScopeFactory的实例来自于通过DefaultHttpContextFactory注入的IServiceProvider实例,这里获取IServiceScopeFactory的地方并没有CreateScope,所以这里的IServiceScopeFactoryIServiceProvider中的实例都是来自根容器。这个我们还可以通过注册DefaultHttpContextFactory地方看到

[点击查看源码]

services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();

通过这里可以看到DefaultHttpContextFactory注册的是单例模式,注册它的地方则是在IHostBuilderConfigureServices方法里。关于每次请求的创建流程,不是本文的重点,但是为了让大家对本文讲解的IServiceScopeFactoryIServiceProvider来源更清楚,咱们可以大致的描述一下

  • GenericWebHostService类实现自IHostedService,在StartAsync方法中启动了IServer实例,默认则是启动的Kestrel。
  • IServer启动的方法StartAsync中会传递HostingApplication实例,构建HostingApplication实例的时候则会依赖IHttpContextFactory实例,而IHttpContextFactory实例则是在构建GenericWebHostService服务的时候注入进来的。
  • 当每次请求ASP.NET Core服务的时候会调用HostingApplicationCreateContext方法,该方法中则会创建HttpContext实例,每次请求结束后则调用该类的DisposeContext释放HttpContext实例。

说了这么多其实就是为了方便让大家得到一个关系,即在每次请求中获取的IServiceProvider实例来自HttpContext.RequestServices实例,HttpContext.RequestServices实例来自IServiceScopeFactory来自CreateScope方法创建的实例,而IServiceScopeFactory实例则是来自根容器,且DefaultHttpContextFactory的生命周期则和当前ASP.NET Core保持一致。

后续插曲

就在解决这个问题后不久,有一次不经意间翻阅微软的官方文档,发现官方文档有提到相关的问题,而且也是结合efcore来讲的。标题是《Do not capture services injected into the controllers on background threads》翻译成中文大概就是不要在后台线程上捕获注入控制器的服务,说的正是这个问题,微软给我们的建议是

  • 注入一个IServiceScopeFactory以便在后台工作项中创建一个范围。
  • IServiceScopeFactory是一个单例对象。
  • 在后台线程中创建一个新的依赖注入范围。
  • 不引用控制器中的任何东西。
  • 不从传入请求中捕获DbContext。

得到的结论和我们在本文描述的基本上是差不多的,而且微软也很贴心的给我们提供了相关示例

[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory serviceScopeFactory)
{
_ = Task.Run(async () =>
{
await Task.Delay(1000); using (var scope = serviceScopeFactory.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();
context.Contoso.Add(new Contoso());
await context.SaveChangesAsync();
}
}); return Accepted();
}

原来还是自己的坑自己最了解,也不得不说微软现在的文档确实挺详细的,同时也提醒我们有空还是得多翻一翻文档避免踩坑。

总结

本文主要是通过帮助同事解决问题而得到的灵感,觉得挺有意思的,希望能帮助更多的人了解这个问题,且能避免这个问题。我们应该深刻理解ASP.NET Core处理每次请求则都会创建一个Scope,这会影响当前请求获取的IServiceProvider实例,和通过IServiceProvider创建的生命周期为Scope的实例。如果把握不住,则可以理解为当前请求直接注入的服务,和当前服务直接注入的IServiceProvider实例。如果想获取根容器的实例则可以通过获取IServiceScopeFactory实例获取,最后请注意IServiceScope的释放问题。

    曾几何时,特别喜欢去解决遇到的问题,特别喜欢那种解决问题沉浸其中的过程。解决了问题,了解到为什么会让自己感觉很通透,也更深刻,不经意间的也扩展了自己的认知边界。这个过程得到的经验是一种通识,是一种意识。而思维和意识则是我们适应这个不断在变化时代的底层逻辑。

欢迎扫码关注我的公众号

解决ASP.NET Core在Task中使用IServiceProvider的问题的更多相关文章

  1. 如何解决 ASP.NET Core 中的依赖问题

    依赖性注入是一种技术,它允许我们注入一个特定类的依赖对象,而不是直接创建这些实例. 使用依赖注入的好处显而易见,它通过放松模块间的耦合,来增强系统的可维护性和可测试性. 依赖注入允许我们修改具体实现, ...

  2. ASP.NET Core 2.2中的Endpoint路由

    Endpoint路由 在ASP.NET Core 2.2中,新增了一种路由,叫做Endpoint(终结点)路由.本文将以往的路由系统称为传统路由. 本文通过源码的方式介绍传统路由和Endpoint路由 ...

  3. 如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?

    原文:如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容? 文章名称: 如何在ASP.NET Core自定义中间件读取Request.Body和 ...

  4. ASP.NET Core 3.0中使用动态控制器路由

    原文:Dynamic controller routing in ASP.NET Core 3.0 作者:Filip W 译文:https://www.cnblogs.com/lwqlun/p/114 ...

  5. 翻译一篇英文文章,主要是给自己看的——在ASP.NET Core Web Api中如何刷新token

    原文地址 :https://www.blinkingcaret.com/2018/05/30/refresh-tokens-in-asp-net-core-web-api/ 先申明,本人英语太菜,每次 ...

  6. 将终结点图添加到你的ASP.NET Core应用程序中

    在本文中,我将展示如何使用DfaGraphWriter服务在ASP.NET Core 3.0应用程序中可视化你的终结点路由.上面文章我向您演示了如何生成一个有向图(如我上篇文章中所示),可以使用Gra ...

  7. ASP.NET Core HTTP 管道中的那些事儿

    前言 马上2016年就要过去了,时间可是真快啊. 上次写完 Identity 系列之后,反响还不错,所以本来打算写一个 ASP.NET Core 中间件系列的,但是中间遇到了很多事情.首先是 NPOI ...

  8. ASP.NET Core 1.0 中使用 Swagger 生成文档

    github:https://github.com/domaindrivendev/Ahoy 之前文章有介绍在ASP.NET WebAPI 中使用Swagger生成文档,ASP.NET Core 1. ...

  9. 用ASP.NET Core 1.0中实现邮件发送功能

    准备将一些项目迁移到 asp.net core 先从封装类库入手,在遇到邮件发送类时发现在 asp.net core 1.0中并示提供SMTP相关类库,于是网上一搜发现了MailKit 好东西一定要试 ...

随机推荐

  1. 04 Springboot 格式化LocalDateTime

    Springboot 格式化LocalDateTime 我们知道在springboot中有默认的json解析器,Spring Boot 中默认使用的 Json 解析技术框架是 jackson.我们点开 ...

  2. 【工程应用七】接着折腾模板匹配算法 (Optimization选项 + no_pregeneration模拟 + 3D亚像素插值)

    在折腾中成长,在折腾中永生. 接着玩模板匹配,最近主要研究了3个课题. 1.创建模型的Optimization选项模拟(2022.5.16日) 这两天又遇到一个做模板匹配隐藏的高手,切磋起来后面就还是 ...

  3. java接口多实现注入方法总结

    1. 单实现接口注入方法 1.1 构造注入(推荐) @RequiredArgsConstructor public class TestController { // 其只有一个具体的实现类 priv ...

  4. 记一次Tomcat卡死在 Deploying web application 步骤的问题

    公司有一个历史的遗留项目是传统的MVC架构的前后不分离的项目,一开始使用JDK1.7写的,后来前一阵老板说想在这个远古项目上加点功能,顺带换换皮,于是乎一帮程序员们就用JDK1.8重新翻新了一遍项目顺 ...

  5. linux挂载新硬盘并进行分区格式化

    最近要给小伙伴们写几篇文章,关于<linux下误删除文件之后该如何恢复>.对于没有进程占用的文件想要进行数据恢复,不同的文件系统格式需要使用不同的工具,比如:ext4.xfs等.我找遍了我 ...

  6. 16岁男生信息竞赛成瘾心理出现问题 妈妈:他竟说要AK我

    16岁男生信息竞赛成瘾心理出现问题 -- 妈妈:他竟说要AK我 "我儿子最近快走火入魔了,医生,你救救他吧."40出头的林女士拉着儿子走进江苏省人民医院临床心理科.近几年,信息竞赛 ...

  7. docker 快速上手

    Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口 安装 docker 设置仓库 $ sudo yum install -y yum-utils $ sudo yum-conf ...

  8. Scalable Multi-Party Private Set-Intersection-解读

    本文记录阅读该paper的笔记. 摘要 本文给出两种MPSI协议,采用的是星型拓扑结构,即有一个leader,需要和其他参与者交互.优点是并非所有各方都必须同时在线: (1)能抗半诚实攻击 通信复杂度 ...

  9. 『忘了再学』Shell流程控制 — 38、while循环和until循环介绍

    目录 1.while循环 2.until循环 1.while循环 对while循环来讲,只要条件判断式成立,循环就会一直继续,直到条件判断式不成立,循环才会停止.和for循环的第二种格式for((初始 ...

  10. SAP 日期计算

    1. CONVERSION_EXIT_IDATE_OUTPUT     INPUT:      20200601     OUTPUT:   03FEB2008 2. CONVERT_DATE_TO_ ...