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

  1. 创建一个标注类 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;
    }
}
  1. 创建一个静态实现类 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值去调用方法。

在本人开发中,我会对外暴露一个方法,去记录这些调用目的,可以根据调用目的查询日志,接口启动时初始化该类,仅在接口启动时慢一点。
如果选择第一次使用时初始化,那么在客户端第一次调用接口会慢一些。
  1. 完善实现类 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();
}

至此,主要的逻辑实现已经介绍完成,下面介绍使用方式。

  1. 定义一个逻辑方法
// 该方法默认不使用事务,需要记录调用日志
[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 };
}
  1. 使用拓展
    按上面介绍的旧方式,目前是公开多个接口 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# 使用反射原理构建接口后台简单架构的主要内容,如果未能解决你的问题,请参考以下文章

C# 使用反射原理构建接口后台简单架构

C# 使用反射原理构建接口后台简单架构

c#代码片段快速构建代码

C#怎么使用反射获取事件的响应方法

如何在运行时使用反射构建这个 c#“表达式”?

使用c#反射实现接口可视化调试页面