您的位置:首页 > 其它

ABP使用及框架解析系列 - [Unit of Work part.2-框架实现]

2015-10-30 14:09 274 查看

前言


ABP

ABP是“ASP.NET Boilerplate Project”的简称。

ABP的官方网站:http://www.aspnetboilerplate.com

ABP在Github上的开源项目:https://github.com/aspnetboilerplate

ABP其他学习博客推荐及介绍:/article/5144754.html

ABP中Unit of Work概念及使用

如果这是你首次接触ABP框架或ABP的Unit of Work,推荐先看看 ABP使用及框架解析系列-[Unit of Work part.1-概念及使用]



框架实现



温馨提示
1.ABP的Unit of Work相关代码路径为:/Abp/Domain/Uow

2.框架实现中,代码不会贴全部的,但是会说明代码在项目中的位置,并且为了更加直观和缩短篇幅,对代码更细致的注释,直接在代码中,不要忘记看代码注释哈,。

3.博客中的代码,有一些方法是可以点击的!



动态代理/拦截器/AOP

上面讲到Unit of Work有两个默认实现,领域服务和仓储库的每个方法默认就是一个工作单元,这个是如何实现的呢?在方法上添加一个UnitOfWork特性也就让该方法为一个工作单元,这又是如何实现的呢?上面的标题已然暴露了答案——动态代理

在ABP中,使用了Castle的DynamicProxy进行动态代理,在组件注册是进行拦截器的注入,具体代码如下:

internal static class UnitOfWorkRegistrar
{
public static void Initialize(IIocManager iocManager)
{//该方法会在应用程序启动的时候调用,进行事件注册
iocManager.IocContainer.Kernel.ComponentRegistered += ComponentRegistered;
}

private static void ComponentRegistered(string key, IHandler handler)
{
if (UnitOfWorkHelper.IsConventionalUowClass(handler.ComponentModel.Implementation))
{//判断类型是否实现了IRepository或IApplicationService,如果是,则为该类型注册拦截器(UnitOfWorkInterceptor)
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));
}
else if (handler.ComponentModel.Implementation.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Any(UnitOfWorkHelper.HasUnitOfWorkAttribute))
{//或者类型中任何一个方法上应用了UnitOfWorkAttribute,同样为类型注册拦截器(UnitOfWorkInterceptor)
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));
}
}
}


public static bool IsConventionalUowClass(Type type)
{
return typeof(IRepository).IsAssignableFrom(type) || typeof(IApplicationService).IsAssignableFrom(type);
}


public static bool HasUnitOfWorkAttribute(MemberInfo methodInfo)
{
return methodInfo.IsDefined(typeof(UnitOfWorkAttribute), true);
}


拦截器UnitOfWorkInterceptor实现了IInterceptor接口,在调用注册了拦截器的类的方法时,会被拦截下来,而去执行IInterceptor的Intercept方法,下面是Intercept方法的代码实现:

public void Intercept(IInvocation invocation)
{
if (_unitOfWorkManager.Current != null)
{//如果当前已经在工作单元中,则直接执行被拦截类的方法
invocation.Proceed();
return;
}

//获取方法上的UnitOfWorkAttribute,如果没有返回NULL,invocation.MethodInvocationTarget为被拦截类的类型
var unitOfWorkAttr = UnitOfWorkAttribute.GetUnitOfWorkAttributeOrNull(invocation.MethodInvocationTarget);
if (unitOfWorkAttr == null || unitOfWorkAttr.IsDisabled)
{//如果当前方法上没有UnitOfWorkAttribute或者是设置为Disabled,则直接调用被拦截类的方法
invocation.Proceed();
return;
}

//走到这里就表示是需要将这个方法作为工作单元了,详情点击查看
PerformUow(invocation, unitOfWorkAttr.CreateOptions());
}


internal static UnitOfWorkAttribute GetUnitOfWorkAttributeOrNull(MemberInfo methodInfo)
{
//获取方法上标记的UnitOfWorkAttribute
var attrs = methodInfo.GetCustomAttributes(typeof(UnitOfWorkAttribute), false);
if (attrs.Length > 0)
{
return (UnitOfWorkAttribute)attrs[0];
}

if (UnitOfWorkHelper.IsConventionalUowClass(methodInfo.DeclaringType))
{//如果方法上没有标记UnitOfWorkAttribute,但是方法的所属类实现了IRepository或IApplicationService,则返回一个默认UnitOfWorkAttribute
return new UnitOfWorkAttribute();
}

return null;
}


private void PerformUow(IInvocation invocation, UnitOfWorkOptions options)
{
if (AsyncHelper.IsAsyncMethod(invocation.Method))
{//被拦截的方法为异步方法
PerformAsyncUow(invocation, options);
}
else
{//被拦截的方法为同步方法
PerformSyncUow(invocation, options);
}
}


private void PerformSyncUow(IInvocation invocation, UnitOfWorkOptions options)
{
//手动创建一个工作单元,将被拦截的方法直接放在工作单元中
using (var uow = _unitOfWorkManager.Begin(options))
{
invocation.Proceed();
uow.Complete();
}
}


private void PerformAsyncUow(IInvocation invocation, UnitOfWorkOptions options)
{
//异步方法的处理相对麻烦,需要将工作单元的Complete和Dispose放到异步任务中
var uow = _unitOfWorkManager.Begin(options);

invocation.Proceed();

if (invocation.Method.ReturnType == typeof(Task))
{//如果是无返回值的异步任务
invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
(Task)invocation.ReturnValue,
async () => await uow.CompleteAsync(),
exception => uow.Dispose()
);
}
else //Task<TResult>
{//如果是有返回值的异步任务
invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
invocation.Method.ReturnType.GenericTypeArguments[0],
invocation.ReturnValue,
async () => await uow.CompleteAsync(),
(exception) => uow.Dispose()
);
}
}


/// <summary>
/// 修改异步返回结果,并且使用try...catch...finally
/// </summary>
/// <param name="actualReturnValue">方法原始返回结果</param>
/// <param name="postAction">期望返回结果</param>
/// <param name="finalAction">无论是否异常都将执行的代码</param>
/// <returns>新的异步结果</returns>
public static async Task AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func<Task> postAction, Action<Exception> finalAction)
{
Exception exception = null;
//在方法被拦截方法执行前调用工作单元的Begin,修改异步结果为调用工作单元的CompleteAsync方法,并保证工作单元会被Dispose掉
try
{
await actualReturnValue;
await postAction();
}
catch (Exception ex)
{
exception = ex;
throw;
}
finally
{
finalAction(exception);
}
}


public static object CallAwaitTaskWithPostActionAndFinallyAndGetResult(Type taskReturnType, object actualReturnValue, Func<Task> action, Action<Exception> finalAction)
{
//有返回值的异步任务重写更为复杂,需要先通过反射来为泛型传值,然后才可调用泛型方法来重写异步返回值
return typeof (InternalAsyncHelper)
.GetMethod("AwaitTaskWithPostActionAndFinallyAndGetResult", BindingFlags.Public | BindingFlags.Static)
.MakeGenericMethod(taskReturnType)
.Invoke(null, new object[] { actualReturnValue, action, finalAction });
}


public static async Task<T> AwaitTaskWithPostActionAndFinallyAndGetResult<T>(Task<T> actualReturnValue, Func<Task> postAction, Action<Exception> finalAction)
{
Exception exception = null;
//该方法与之前无返回值的异步任务调用的方法相同,只是多了个泛型
try
{
var result = await actualReturnValue;
await postAction();
return result;
}
catch (Exception ex)
{
exception = ex;
throw;
}
finally
{
finalAction(exception);
}
}


总结来说,就是通过拦截器在执行方法的时候,先判断是否需要进行工作单元操作。如果需要,则在执行方法前开启工作单元,在执行方法后关闭工作单元。

在上面的代码中,我们可以看到,工作单元都是通过_unitOfWorkManager(IUnitOfWorkManager)这样一个对象进行的,下面我们就来解析这个类到底是如何进行单元控制的。

IUnitOfWorkManager、IUnitOfWorkCompleteHandle

public interface IUnitOfWorkManager
{
IActiveUnitOfWork Current { get; }
IUnitOfWorkCompleteHandle Begin();
IUnitOfWorkCompleteHandle Begin(TransactionScopeOption scope);
IUnitOfWorkCompleteHandle Begin(UnitOfWorkOptions options);
}


ABP中,默认将UnitOfWorkManager作为IUnitOfWorkManager作为实现类,其实现中,Current直接取得ICurrentUnitOfWorkProvider对象的Current属性,后续解析ICurrentUnitOfWorkProvider。而IUnitOfWorkManager的三个Begin只是重载,最后都将调用第三个Begin的重载方法。下面是它的代码实现:

public IUnitOfWorkCompleteHandle Begin(UnitOfWorkOptions options)
{
//为未赋值的参数设置默认值
options.FillDefaultsForNonProvidedOptions(_defaultOptions);

if (options.Scope == TransactionScopeOption.Required && _currentUnitOfWorkProvider.Current != null)
{//如果当前Scope的设置为Required(而非RequiredNew),并且当前已存在工作单元,那么久返回下面这样的一个对象
return new InnerUnitOfWorkCompleteHandle();
}
//走到这里,表示需要一个新的工作单元,通过IoC创建IUnitOfWork实现对象,然后开始工作单元,并设置此工作单元为当前工作单元
var uow = _iocResolver.Resolve<IUnitOfWork>();

uow.Completed += (sender, args) =>
{
_currentUnitOfWorkProvider.Current = null;
};
uow.Failed += (sender, args) =>
{
_currentUnitOfWorkProvider.Current = null;
};
uow.Disposed += (sender, args) =>
{
_iocResolver.Release(uow);
};

uow.Begin(options);
_currentUnitOfWorkProvider.Current = uow;

return uow;
}


public IActiveUnitOfWork Current
{
get { return _currentUnitOfWorkProvider.Current; }
}


internal void FillDefaultsForNonProvidedOptions(IUnitOfWorkDefaultOptions defaultOptions)
{
if (!IsTransactional.HasValue)
{
IsTransactional = defaultOptions.IsTransactional;
}

if (!Scope.HasValue)
{
Scope = defaultOptions.Scope;
}

if (!Timeout.HasValue && defaultOptions.Timeout.HasValue)
{
Timeout = defaultOptions.Timeout.Value;
}

if (!IsolationLevel.HasValue && defaultOptions.IsolationLevel.HasValue)
{
IsolationLevel = defaultOptions.IsolationLevel.Value;
}
}


Begin方法最后返回的对象继承自IUnitOfWorkCompleteHandle,让我们看看IUnitOfWorkCompleteHandle的接口声明又是什么样的:

public interface IUnitOfWorkCompleteHandle : IDisposable
{
void Complete();

Task CompleteAsync();
}


总共也就两个方法,而且意思相同,都是用来完成当前工作单元的,一个同步一个异步。同时实现了IDisposable接口,结合IUnitOfWorkManager使用Begin的方式便可理解其含义(使用using)。

在之前的Begin实现中,我们看到,其返回路线有两个,一个返回了InnerUnitOfWorkCompleteHandle对象,另一个返回了IUnitOfWork实现对象。IUnitOfWork稍后详细解析,让我们先来解析InnerUnitOfWorkCompleteHandle :

internal class InnerUnitOfWorkCompleteHandle : IUnitOfWorkCompleteHandle
{
public const string DidNotCallCompleteMethodExceptionMessage = "Did not call Complete method of a unit of work.";

private volatile bool _isCompleteCalled;
private volatile bool _isDisposed;

public void Complete()
{
_isCompleteCalled = true;
}

public async Task CompleteAsync()
{
_isCompleteCalled = true;
}

public void Dispose()
{
if (_isDisposed)
{
return;
}

_isDisposed = true;

if (!_isCompleteCalled)
{
if (HasException())
{
return;
}

throw new AbpException(DidNotCallCompleteMethodExceptionMessage);
}
}

private static bool HasException()
{
try
{
return Marshal.GetExceptionCode() != 0;
}
catch (Exception)
{
return false;
}
}
}


我们可以看到,Complete和CompleteAsync里面,都只是简单的将_isCompleteCalled设置为true,在Dispose方法中,也仅仅只是判断是否已经dispose过、是否完成、是否有异常,没有更多的动作。这样大家会不会有一个疑问“这个工作单元的完成和释放工作里没有具体的完成操作,怎么就算完成工作单元了?”,这时,让我们结合使用到InnerUnitOfWorkCompleteHandle的地方来看:

if (options.Scope == TransactionScopeOption.Required && _currentUnitOfWorkProvider.Current != null)
{
return new InnerUnitOfWorkCompleteHandle();
}


这个判断,代表了当前已经存在工作单元,所以这个就是用于工作单元嵌套。内部的工作单元在提交和释放时,不需要做实际的提交和释放,只需要保证没有异常抛出,然后最外层工作单元再进行实际的提交和释放。这也就说明,在Begin方法中的另一条路线,返回IUnitOfWork实现对象才是最外层的事务对象。

IUnitOfWork

public interface IUnitOfWork : IActiveUnitOfWork, IUnitOfWorkCompleteHandle
{
//唯一的标识ID
string Id { get; }

//外层工作单元
IUnitOfWork Outer { get; set; }

//开始工作单元
void Begin(UnitOfWorkOptions options);
}


IUnitOfWork除了继承了IUnitOfWorkCompleteHandle接口,拥有了Complete方法外,还继承了IActiveUnitOfWork接口:

public interface IActiveUnitOfWork
{
//三个事件
event EventHandler Completed;
event EventHandler<UnitOfWorkFailedEventArgs> Failed;
event EventHandler Disposed;

//工作单元设置
UnitOfWorkOptions Options { get; }

//数据过滤配置
IReadOnlyList<DataFilterConfiguration> Filters { get; }

bool IsDisposed { get; }

void SaveChanges();

Task SaveChangesAsync();

IDisposable DisableFilter(params string[] filterNames);
IDisposable EnableFilter(params string[] filterNames);
bool IsFilterEnabled(string filterName);
IDisposable SetFilterParameter(string filterName, string parameterName, object value);
}


这个接口中包含了很多Filter相关的属性与方法,都是些数据过滤相关的,这里不对其进行介绍,所以这个接口里,主要包含了三个事件(Completed、Failed、Disposed),工作单元设置(Options),IsDisposed以及同步异步的SaveChanges。

除了IUnitOfWork接口,ABP还提供了一个实现IUnitOfWork接口的抽象基类UnitOfWorkBase,UnitOfWorkBase的主要目的,是为开发人员处理一些前置、后置工作和异常处理,所以UnitOfWorkBase实现了部分方法,并提供了一些抽象方法,在实现的部分中做好前后置工作,然后调用抽象方法,将主要实现交给子类,并对其进行异常处理,下面以Begin实现与Complete实现为例:

public void Begin(UnitOfWorkOptions options)
{
if (options == null)
{
throw new ArgumentNullException("options");
}

//防止Begin被多次调用
PreventMultipleBegin();
Options = options;

//过滤配置
SetFilters(options.FilterOverrides);

//抽象方法,子类实现
BeginUow();
}

public void Complete()
{
//防止Complete被多次调用
PreventMultipleComplete();
try
{
//抽象方法,子类实现
CompleteUow();
_succeed = true;
OnCompleted();
}
catch (Exception ex)
{
_exception = ex;
throw;
}
}


private void PreventMultipleBegin()
{
if (_isBeginCalledBefore)
{//如果已经调用过Begin方法,再次调用则抛出异常
throw new AbpException("This unit of work has started before. Can not call Start method more than once.");
}

_isBeginCalledBefore = true;
}


private void PreventMultipleComplete()
{
if (_isCompleteCalledBefore)
{//如果已经掉用过Complete方法,再次调用则抛出异常
throw new AbpException("Complete is called before!");
}

_isCompleteCalledBefore = true;
}


UnitOfWorkBase的其他实现与上面的类似(非Unit of Work相关内容略去),便不全贴出。但是上面的代码,大家有发现一个问题吗? 就是那些PreventMultiple…方法的实现,用来防止多次Begin、Complete等。这些方法的实现,在单线程下是没有问题的,但是里面并没有加锁,所以不适用于多线程。这是作者的BUG吗? 然而,这却并不是BUG,而是设计如此,为何呢?因为在设计上,一个线程共用一个工作单元,也就不存在多线程了。关于这部分内容,后续将会介绍。

IUnitOfWorkManager目的是提供一个简洁的IUnitOfWork管理对象,而IUnitOfWork则提供了整个工作单元需要的所有控制(Begin、SaveChanges、Complete、Dispose)。而具体应该如何保证一个线程共用一个工作单元,如何获取当前的工作单元,则由ICurrentUnitOfWorkProvider进行管控,正如在解析UnitOfWorkManager时,说明了它的Current实际上就是调用ICurrentUnitOfWorkProvider实现对象的Current属性。

ICurrentUnitOfWorkProvider

public interface ICurrentUnitOfWorkProvider
{
IUnitOfWork Current { get; set; }
}


ICurrentUnitOfWorkProvider仅仅只声明了一个Current属性,那么重点让我们来看看Current在实现类(CallContextCurrentUnitOfWorkProvider)中是如何写的吧:

[DoNotWire]
public IUnitOfWork Current
{
get { return GetCurrentUow(Logger); }
set { SetCurrentUow(value, Logger); }
}


上面标注的DoNotWire是为了不让IoC进行属性注入,Current内部分别调用了GetCurrentUow和SetCurrentUow,要取值,先要设值,让我来先看看set吧:

private static void SetCurrentUow(IUnitOfWork value, ILogger logger)
{
if (value == null)
{//如果在set的时候设置为null,便表示要退出当前工作单元
ExitFromCurrentUowScope(logger);
return;
}

//获取当前工作单元的key
var unitOfWorkKey = CallContext.LogicalGetData(ContextKey) as string;
if (unitOfWorkKey != null)
{
IUnitOfWork outer;
if (UnitOfWorkDictionary.TryGetValue(unitOfWorkKey, out outer))
{
if (outer == value)
{
logger.Warn("Setting the same UOW to the CallContext, no need to set again!");
return;
}
//到这里也就表示当前存在工作单元,那么再次设置工作单元,不是替换掉当前的工作单元而是将当前工作单元作为本次设置的工作单元的外层工作单元
value.Outer = outer;
}
}

unitOfWorkKey = value.Id;
if (!UnitOfWorkDictionary.TryAdd(unitOfWorkKey, value))
{//如果向工作单元中添加工作单元失败,便抛出异常
throw new AbpException("Can not set unit of work! UnitOfWorkDictionary.TryAdd returns false!");
}
//设置当前线程的工作单元key
CallContext.LogicalSetData(ContextKey, unitOfWorkKey);
}


private static void ExitFromCurrentUowScope(ILogger logger)
{
//ContextKey为一个常量字符串
//CallContext可以理解为每个线程的独有key,value集合类,每个线程都会有自己的存储区,
//   也就是在线程A中设置一个key为xx的value,在线程B中通过xx取不到,并可以存入相同键的value,但是不会相互覆盖、影响
//根据ContextKey从线程集合中取出当前工作单元key
var unitOfWorkKey = CallContext.LogicalGetData(ContextKey) as string;
if (unitOfWorkKey == null)
{//没有取到值,表示当前无工作单元
logger.Warn("There is no current UOW to exit!");
return;
}

IUnitOfWork unitOfWork;
//UnitOfWorkDictionary类型为ConcurrentDictionary,线程安全字典,用于存储所有工作单元(单线程上最多只能有一个工作单元,但是多线程可能会有多个)
if (!UnitOfWorkDictionary.TryGetValue(unitOfWorkKey, out unitOfWork))
{//根据key没有取到value,从线程集合(CallContext)中释放该key
CallContext.FreeNamedDataSlot(ContextKey);
return;
}

//从工作单元集合中移除当前工作单元
UnitOfWorkDictionary.TryRemove(unitOfWorkKey, out unitOfWork);
if (unitOfWork.Outer == null)
{//如果当前工作单元没有外层工作单元,则从线程集合(CallContext)中释放该key
CallContext.FreeNamedDataSlot(ContextKey);
return;
}

var outerUnitOfWorkKey = unitOfWork.Outer.Id;   //这里也就表明了key实际上就是UnitOfWork的Id
if (!UnitOfWorkDictionary.TryGetValue(outerUnitOfWorkKey, out unitOfWork))
{//如果当前工作单元有外层工作单元,但是从工作单元集合中没有取到了外层工作单元,那么同样从线程集合(CallContext)中释放该key
CallContext.FreeNamedDataSlot(ContextKey);
return;
}
//能到这里,就表示当前工作单元有外层工作单元,并且从工作单元集合中获取到了外层工作单元,那么就设外层工作单元为当前工作单元
CallContext.LogicalSetData(ContextKey, outerUnitOfWorkKey);
}


看完set,再让我们来看看get吧:

private static IUnitOfWork GetCurrentUow(ILogger logger)
{
//获取当前工作单元key
var unitOfWorkKey = CallContext.LogicalGetData(ContextKey) as string;
if (unitOfWorkKey == null)
{
return null;
}

IUnitOfWork unitOfWork;
if (!UnitOfWorkDictionary.TryGetValue(unitOfWorkKey, out unitOfWork))
{//如果根据key获取不到当前工作单元,那么就从当前线程集合(CallContext)中释放key
CallContext.FreeNamedDataSlot(ContextKey);
return null;
}

if (unitOfWork.IsDisposed)
{//如果当前工作单元已经dispose,那么就从工作单元集合中移除,并将key从当前线程集合(CallContext)中释放
logger.Warn("There is a unitOfWorkKey in CallContext but the UOW was disposed!");
UnitOfWorkDictionary.TryRemove(unitOfWorkKey, out unitOfWork);
CallContext.FreeNamedDataSlot(ContextKey);
return null;
}

return unitOfWork;
}


总的说来,所有的工作单元存储在线程安全的字典对象中(ConcurrentDictionary),每个主线程共用一个工作单元的实现,通过线程集合(CallContext)实现。

UnitOfWork实现

从上面的分析可以看出,ABP/Domain/Uow路径下,主要只是提供了一套抽象接口,并没有提供实际的实现,IUnitOfWork最多也只是提供了一个UnitOfWorkBase抽象类,这样的自由性非常大,我非常喜欢这种方式。

当然ABP也另起了几个项目来提供一些常用的ORM的Unit of Work封装:

1.Ef: Abp.EntityFramework/EntityFramework/Uow

2.NH: Abp.NHibernate/NHibernate/Uow

3.Mongo: Abp.MongoDB/MongoDb/Uow

4.Memory: Abp.MemoryDb/MemoryDb/Uow

其中Mongo和Memory都没有进行实质性的单元操作,Ef中使用TransactionScope进行单元操作,NH中使用ITransaction来进行单元操作。

ABP/Domain/Uow结构说明

UnitOfWorkRegistrar····································注册拦截器,实现两种默认的UnitOfWork,详见最上面的默认行为

UnitOfWorkInterceptor··································Unit of Work拦截器,实现以AOP的方式进行注入单元控制

IUnitOfWorkManager····································简洁的UnitOfWork管理对象

 UnitOfWorkManager··································IUnitOfWorkManager默认实现

ICurrentUnitOfWorkProvider···························当前UnitOfWork管理对象

 CallContextCurrentUnitOfWorkProvider············ICurrentUnitOfWorkProvider默认实现

IUnitOfWork···············································工作单元对象(Begin、SaveChanges、Complete、Dispose)

 UnitOfWorkBase·······································IUnitOfWork抽象实现类,封装实际操作的前后置操作及异常处理

 NullUnitOfWork········································IUnitOfWork空实现

IActiveUnitOfWork·······································IUnitOfWork操作对象,不包含Begin与Complete操作

IUnitOfWorkCompleteHandle··························工作单元完成对象,用于实现继承工作单元功能

 InnerUnitOfWorkCompleteHandle··················IUnitOfWorkCompleteHandle实现之一,用于继承外部工作单元

IUnitOfWorkDefaultOptions····························UnitOfWork默认设置

 UnitOfWorkDefaultOptions··························IUnitOfWorkDefaultOptions默认实现

UnitOfWorkOptions·····································UnitOfWork配置对象

UnitOfWorkAttribute····································标记工作单元的特性

UnitOfWorkFailedEventArgs··························UnitOfWork的Failed事件参数

UnitOfWorkHelper·····································工具类

AbpDataFilters·········································数据过滤相关

DataFilterConfiguration·······························数据过滤相关
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: