C# 使用反射原理构建接口后台简单架构
Posted 言00FFCC
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C# 使用反射原理构建接口后台简单架构相关的知识,希望对你有一定的参考价值。
业务背景:
在日常接口开发中,一个业务逻辑方法开发完成后,就需要对该方法公布一个接口供外界其他应用调用。
这种方式在多人协作开发中,对公布的接口无法做到规范化管理,并且开发人员每次都需要定义新接口,再去写该接口的文档,会极大浪费开发人员的开发时间,无法做到开发人员只关注业务逻辑。
那么由此延伸一个问题,是否有一种设计,仅公布一个接口,再根据一个请求的目的性,去自动调用指定的方法呢?
对开发人员来说,就仅需要做一些标注,然后写文档只需要累加不同的请求目的的值、不同的参数即可,也大大简化了接口文档。
基于以上问题,本文将介绍如何使用反射的方式,来实现自动调用指定的方法。
以往的方式
在讲反射方式之前,首先还是需要说一下以往的方式,方便下面反射方法的理解。
在以往的开发中,会根据一个字符串值 intent
,来调用同个接口中的不同的方法。接口会以模块化来命名。比如说: ProductionSerivce
接口、WarehouseService
接口。
[HttpPost]
public string ProductionSerivce(string intent, string u_guid, string pms)
{
object obj = null;
switch (intent)
{
case "获取生产指令单":
obj = 业务逻辑方法;
break;
...
default:
obj = new { res = false,msg = "调用目的不明确。"};
break;
}
return obj.ConvertJson();
}
[HttpPost]
public string WarehouseService(string intent, string u_guid, string pms)
{
object obj = null;
switch (intent)
{
case "获取原料库存":
obj = 业务逻辑方法;
break;
...
default:
obj = new { res = false,msg = "调用目的不明确。"};
break;
}
return obj.ConvertJson();
}
上面的参数intent
就是调用目的,根据该值来调用同个接口的不同逻辑。
u_guid
是业务逻辑中的人员标识。pms
是调用方法所需的参数集,该参数集为Json字符串。
例子中,如果需要新增接口,仅需要在该接口模块中新增 case
分组,比如需要新增一个查询辅料库存
的接口,那么只需要在case
分组下
case "查询辅料库存":
obj = 查询辅料库存的业务逻辑;
break;
这种方式也有弊端,当每次新增业务逻辑,将会一直叠加 case 分组。
非常不利于后期维护,时刻破坏了类的开闭原则。
那么,接下来将介绍如何使用反射来自动化这个过程。
反射的方式
让开发人员标注方法,说到标注,就离不开
Attribute
。
- 创建一个标注类
MethodDispatcherAttribute
该类用来标记该业务逻辑方法是供接口调用的。
public class MethodDispatcherAttribute : Attribute
{
public string Intent { get; set; }
public bool IsTransaction { get; set; }
public bool IsWriteLog { get; set; }
/// <summary>
/// 标记接口调用
/// </summary>
/// <param name="intent">供后台代码自动调用的调用目的</param>
/// <param name="transaction">是否开启方法事务</param>
/// <param name="writeLog">写入操作日志</param>
public MethodDispatcherAttribute(string intent, bool transaction = false, bool writeLog = true)
{
this.Intent = intent;
this.IsTransaction = transaction;
this.IsWriteLog = writeLog;
}
}
- 创建一个静态实现类
MethodDispatcherExtends
该类是反射的主要逻辑类,用来调配根据调用目的intent
的值调用指定方法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.IO;
using System.Diagnostics;
public static class MethodDispatcherExtends
{
/// <summary>
/// 目的调度缓存 根据调用目的找到对应的方法
/// </summary>
static Dictionary<string, InvokeMethodInfo> IntentDispatcher = new Dictionary<string, InvokeMethodInfo>();
/// <summary>
/// 初始化反射加载所有方法
/// </summary>
static MethodDispatcherExtends()
{
bool isDebug = bool.Parse(ConfigurationHandle.GetAppSettings("isDebug"));
// 获取当前执行程序的路径
string path = isDebug ? AppDomain.CurrentDomain.RelativeSearchPath : Environment.CurrentDirectory;
// 获取文件夹下所有的dll
string[] files = Directory.GetFiles(path, "*.dll");
foreach (var file in files)
{
// 加载指定的dll文件
Assembly assembly = Assembly.LoadFile(file);
// 加载所有的公开类
Type[] types = assembly.GetExportedTypes();
foreach (var type in types)
{
// 加载类中的方法
MethodInfo[] meths = type.GetMethods();
foreach (var methodInfo in meths)
{
// 获取方法上是否标记了 MethodDispatcher
var callMethon = (MethodDispatcherAttribute)methodInfo.GetCustomAttribute(typeof(MethodDispatcherAttribute));
if (callMethon == null) continue;
if (string.IsNullOrEmpty(callMethon.Intent)) throw new Exception("程序内部错误,定义了空的调用目的。");
// 注意,使用该方式,需要保证所有的dll中 intent的值不能重复
if (IntentDispatcher.ContainsKey(callMethon.Intent)) throw new Exception("程序内部错误,定义了相同的调用目的。");
// InvokeMethodInfo 是描述记录该方法一些标注特性的类
IntentDispatcher.Add(callMethon.Intent, new InvokeMethodInfo { ExportedType = type, IsTransaction = callMethon.IsTransaction, IsWriteLog = callMethon.IsWriteLog, CustomMethodInfo = methodInfo });
}
}
}
}
}
ConfigurationHandle 是自定义读取配置文件值的方法类,根据key获取值。如果是Debug状态,则读取的路径是当前开发路径。
internal class InvokeMethodInfo
{
/// <summary>
/// 是否开启事务操作
/// </summary>
public bool IsTransaction { get; set; }
/// <summary>
/// 是否写入操作日志
/// </summary>
public bool IsWriteLog { get; set; }
/// <summary>
/// 类
/// </summary>
public Type ExportedType { get; set; }
/// <summary>
/// 执行的方法
/// </summary>
public MethodInfo CustomMethodInfo { get; set; }
}
MethodDispatcherExtends 需要初始化,可以是第一次使用时,也可以对外暴露一个初始化方法,在接口启动时初始化该类,初始化会调用静态构造函数,将所有的调用目的、方法,缓存起来,下次使用自动根据
intent
值去调用方法。
在本人开发中,我会对外暴露一个方法,去记录这些调用目的,可以根据调用目的查询日志,接口启动时初始化该类,仅在接口启动时慢一点。
如果选择第一次使用时初始化,那么在客户端第一次调用接口会慢一些。
- 完善实现类
MethodDispatcherExtends
MethodDispatcherExtends
的初始化已完成,接下来就实现最主要的逻辑,根据调用目的调用不同的类方法。
/// <summary>
/// 根据调用目的调用对应方法
/// </summary>
/// <param name="intent">调用目的</param>
/// <param name="pms">调用参数集 第一个参数为string的json、第二个参数为Guid的U_GUID、第三个参数为string的address</param>
/// <returns></returns>
public static string Invoker(this string intent, params object[] pms)
{
// 先判断调用目的是否存在,如果不存在则通知客户端
if (!IntentDispatcher.ContainsKey(intent)) return new { res = false, msg = "调用目的不明确" }.ConvertJson();
// 根据调用目的获取描述 InvokeMethodInfo
var invokeMethod = IntentDispatcher[intent];
// 创建该类
object obj = Activator.CreateInstance(invokeMethod.ExportedType);
// 然后需要获取该类的参数,如果不进行下面的判断,会报调用方法参数不匹配。
var parameters = invokeMethod.CustomMethodInfo.GetParameters();
object[] callParameters = new object[parameters.Length];
// pms[1] 是接口方法过来的参数 参数我定义了 规则顺序
// 下标0:代表了调用的参数Json,下标1:代表了用户的GUID,下标2:代表了调用客户端的地址。
// 相对应的,逻辑方法也定义了参数顺序规则, 按顺序为 json接口参数字符串,用户人员GUID
pms[1] = string.IsNullOrEmpty(pms[1].ToString()) ? Guid.Empty : Guid.Parse(pms[1].ToString());
switch (parameters.Length)
{
case 1:
// 如果第一个参数是GUID,那么就用下标是 1的用户GUID
callParameters[0] = parameters[0].ParameterType == typeof(Guid) ? pms[1] : pms[0];
break;
case 2:
// 如果是两个参数,仅需要拿参数的前两位。
callParameters = pms.Take(2).ToArray();
break;
}
object result;
// 创建了一个计时器,主要用来记录日志时,记录该方法调用所用时长,根据这个时长可以做一些接口内部逻辑优化。
Stopwatch watch = new Stopwatch();
bool res = true;
try
{
watch.Restart();
// 判断是否启用了事务
if (invokeMethod.IsTransaction)
{
// TransactionContainer 是自己封装的一个事务类。这里不做过多解释,可以直接使用原生的 TransactionScope
TransactionContainer tran = new TransactionContainer();
result = tran.TransactionExecute(() =>
{
return invokeMethod.CustomMethodInfo.Invoke(obj, callParameters);
});
}
else
result = invokeMethod.CustomMethodInfo.Invoke(obj, callParameters);
watch.Stop();
}
catch (Exception ex)
{
res = false;
// 记录错误日志,可以使用其他方式记录。
LocalWriteLog.WriteErrorLog(ex, pms);
result = new { res = false, msg = ex.InnerException.Message };
}
// 判断方法是否需要记录调用日志,默认是开启的。
if (invokeMethod.IsWriteLog)
{
// 这里的记录日志,供读者自定义,这里就不提供方式了。
LocalWriteLog.WriteUseLog(intent, pms[2].ToString(), (Guid)pms[1], res, pms[0].ToString(), result.ConvertJson(), watch.Elapsed);
}
// ConvertJson 是自定义将object转换为json的拓展方法,使用的是Newtonsoft.Json.JsonConvert
return result.ConvertJson();
}
至此,主要的逻辑实现已经介绍完成,下面介绍使用方式。
- 定义一个逻辑方法
// 该方法默认不使用事务,需要记录调用日志
[MethodDispatcher("查询原药库存")]
public object SearthDrugsList(string pms)
{
var obj = pms.JsonToObject();
int pageIndex = int.Parse(obj["PageIndex"].ToString());
int pageSize = int.Parse(obj["PageSize"].ToString());
string search = obj["Search"].ToString().Trim().ToUpper();
...
// 这里是查询的逻辑
...
return new { code = 0, count, res = true, msg = "数据获取成功", data = list };
}
// 该方法使用事务,默认记录调用日志
[MethodDispatcher("明细出库确认", transaction:true)]
public object ShippingEntry(string pms, Guid U_GUID)
{
var obj = pms.JsonToObject();
Guid DetGuid = Guid.Parse(obj["DET_GUID"].ToString());
...
// 出库的逻辑
...
return new { res = true, msg = "明细出库成功" };
}
// 该方法不使用事务,不记录调用日志
[MethodDispatcher("获取未完工生产计划单", false, false)]
public object SearchNowProductionPlan()
{
// 查询的业务逻辑
return new { res = true, code = 0, msg = "获取当前正在生产的生产计划单成功。", data = list };
}
- 使用拓展
按上面介绍的旧方式,目前是公开多个接口ProductionSerivce
接口、WarehouseService
接口 等等,那么就需要将其整合成一个接口对外暴露。
这里以 WebAPI 为例
public class MainEntranceController : ApiController
{
[HttpPost]
public HttpResponseMessage CallMethod([FromBody] CallParameters callPms)
{
// 直接根据调用目的拓展调用指定的方法
string result = callPms.Intent.Invoker(callPms.Parameters, callPms.UGUID, callPms.ClientAddress);
return result.ResultFormat();
}
}
public class CallParameters
{
// 调用目的
public string Intent { get; set; }
// 人员的GUID
public string UGUID { get; set; }
// 调用方法的参数json集
public string Parameters { get; set; }
// NetAddress是自定义获取客户端地址的拓展类
// 调用接口的客户端地址
public string ClientAddress { get; set; } = NetAddress.GetAddress();
}
/// <summary>
/// 拓展方法,将结果以 json 方式返回,WebAPI默认的返回格式为 xml
/// <summary>
public static class ResponseMessageExtend
{
public static HttpResponseMessage ResultFormat(this string result)
{
return new HttpResponseMessage { Content = new StringContent(result, System.Text.Encoding.UTF8, "application/json") };
}
}
那么,需要调用仅需要调用 MainEntranceController
控制器下的 CallMethod
方法即可。
总结
使用反射的方式去实现接口统一化管理,有利有弊。
优点方面:极大简化了前后端开发人员的对接,对于前端开发人员来说,他只需要从后端开发人员那里知道他所定义的 intent
调用目的、pms
集,即可调用接口。对于后端开发人员而言,他只需要开发指定的逻辑方法,也仅需要标注 [MethodDispatcher]
,配置好调用目的,即可,就无需重复去定义接口,或者重复去开闭接口方法中的case
。
缺点方面:对于接口而言,也就有且仅有一个接口,也就不存在什么管理之说,对于模块化管理的接口,这种方式肯定不是首选,且这种架构在使用途中,最大的问题,在于定位逻辑方法的难度,需要用到全文搜索去搜索该调用目的。
优缺点方面,选择最适合自己的才是硬道理,对于小型开发公司来说,人员的成本投入不会很大,那么这种方式还是有必要的,对于开发人员来说,也不用整天重复劳作。
最后,我会尽可能详细的去介绍、去解释如何实现,如果在本文中有错漏之处,还请各位大佬们多多指教。如果我的文章能帮到你,也请各位不吝点个赞点个收藏点个关注,如果对文中代码有疑问,也请下方评论,我会一一回复。谢谢各位看官。
以上是关于C# 使用反射原理构建接口后台简单架构的主要内容,如果未能解决你的问题,请参考以下文章