惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
MyScale Blog
MyScale Blog
Y
Y Combinator Blog
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
Google DeepMind News
Google DeepMind News
V
Vulnerabilities – Threatpost
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
Recorded Future
Recorded Future
N
News and Events Feed by Topic
B
Blog RSS Feed
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 【当耐特】
N
Netflix TechBlog - Medium
博客园 - 叶小钗
B
Blog
Vercel News
Vercel News
T
Tenable Blog
T
The Exploit Database - CXSecurity.com
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
F
Fortinet All Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Microsoft Security Blog
Microsoft Security Blog
S
Securelist
Microsoft Azure Blog
Microsoft Azure Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Palo Alto Networks Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
DataBreaches.Net
Cyberwarzone
Cyberwarzone
Engineering at Meta
Engineering at Meta
Martin Fowler
Martin Fowler
G
GRAHAM CLULEY
Project Zero
Project Zero
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
C
CERT Recently Published Vulnerability Notes
L
LangChain Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Check Point Blog
A
About on SuperTechFans
W
WeLiveSecurity
The GitHub Blog
The GitHub Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
使用 Unity 框架简化应用程序异常处理及日志记录流程 - 元视角
飞鸿踏雪 · 2018-03-22 · via 元视角

最近公司安排学习项目代码,前后花了一周左右的时间,基本熟悉了项目中的各个模块,感觉项目难度上整体偏中等。这是一个具备完整前端和后端流程的项目,在学习这个项目的过程中,我逐渐发现某些非常有趣的东西,比如在 Web API 的设计中采用严谨而完善的错误码、使用 OAuth 和 JWT 对 API 资源进行访问控制,在 JavaScript 中使用修饰器特性来实现日志记录等等,这些东西我会在后续的博客逐步去整理,今天想说的是如何通过 Unity 框架来简化应用程序异常处理和日志记录流程,而之所以关注这个问题,是因为我发现项目中接近滥用的异常处理,以及我不能忍受的大量重复代码。

背景描述

由于业务场景上的需要,我们在产品中集成了大量第三方硬件厂商的 SDK,这些 SDK 主要都是由 C/C++编写的动态链接库,因此在使用这些 SDK 的过程中,通常频繁地使用返回值来判断一个方法是否成功被调用,虽然项目上制定了严格的错误码规范,可当我看到大量的 Log()方法和业务逻辑混合在一起时,我内心依然是表示拒绝的,甚至我看到在捕获异常以后记录日志然后继续 throw 异常,这都是些什么鬼操作啊,考虑到我的语言描述得可能不太准确,大家可以从下面两段代码来感受下整体画风:

public short LoginTerminal(string uid,string pwd)
{
    try
    {
	    Log.BeginLog()
	    return SDK.Login(uid,pwd)
    }
	catch(Exception ex)
	{
	    log.LogError(ErrorCode.E2301,ex)
	    throw new TerminalException(ex.Message);
	}
    finally
	{
	    Log.EndLog()
	}
}

这是一段相对完整的业务逻辑代码,当然这里都是伪代码实现,这里我比较反感的两个地方是:第一,从头出现到尾的 BeginLog()/EndLog()这对方法;第二,在 Catch 块中记录完日志然后将异常再次抛出。经过我对项目的一番了解,BeginLog()/EndLog()这对方法会在日志中记录某个方法开始执行和结束执行的位置。在方法执行前后插入代码片段,这不就是面向切面编程(AOP)的思想吗?这里记录完日志然后再抛出异常的做法,我个人是不大认同的,因为我觉得拦截异常应该有一个统一的入口,因为异常会继续向上传递,既然如此,为什么我们不能统一地去处理异常和记录日志呢?难道就一定要让 Log 这个静态类无处不在吗?同样地,我们注意到项目还会有下面这样的代码:

public void ProcessTerminal(object sender,ProcessEventArgs args)
{
    try
    {
        Log.BeginLog();
        var terminal = (Termainal)sender;
        var result = terminal.Process(args);
    }
    finally
    {
        Log.EndLog();
    }

}

这种代码看起来不再关注异常,可和第一段一样,从头出现到尾的 BeginLog()/EndLog()简直不能忍,而且这里的 try…finally 结构难免让人想起 using 的语法糖,那么这样是不是可以考虑让这个 Log 拥有类似的结构,换言之,我们总不能一直都在每一个方法里,重复写 BeginLog()/EndLog()这两个方法吧,既然 EndLog()方法总是在 finally 块里被执行,那为什么不考虑把它放到 Dispose()方法里(前提是有一个结构实现 IDispose 接口)。你问我是不是有代码洁癖啊?我真的没有,我就是懒,不喜欢重复做一件事情。所谓"管中窥豹,可见一斑",大家可以想象整个项目会是什么样子。

好了,为了避免让自己写这种糟糕的代码,我决心使用 Unity 框架来简化下这里的异常处理和日志记录流程,一个有追求的程序,如果可以交给自动化工具去做的事情,为什么要一次又一次地重复去写呢?我们可以吐槽一段代码写得有多糟糕,可我们所做的任何努力,都是为了让自己不变成这个样子。Unity 框架提供的 AOP,即面向切面编程,不就可以做这样的事情吗?所以,能动手的就直接动手,君子有所为有所不为,不要重复自己,

Unity 框架与 AOP

好啦,交待完故事背景,今天的主角终于可以登场啦!经常关注我博客的朋友,一定知道我个人比较喜欢 IoC/AOP 这类所谓的"奇技淫巧",就在今天我还在和一位同事在讨论 Ioc,这位同事认为 Ioc 增加了代码的复杂性,不认为 Ioc 会为项目带来明显的便利性。其实我相信大道至简,任何框架对我们而言都是高度抽象的,可正是因为有了这些抽象的层次,我们渐渐学会了关注核心的东西。这里提到了 Ioc,即控制反转,或者我们可以称之为依赖注入,那么 Unity 框架就是.NET 下众多依赖注入框架之一,这里称之为 Unity 框架,主要是避免和跨平台游戏引擎 Unity 产生混淆,以下全部称之为 Unity 框架。Unity 框架中提供了核心的依赖注入相关的接口,而微软的企业最佳实践库中为 Unity 扩展出了 AOP 相关的功能。除此以外,Spring.NET、Aspect.Core、AspectF 等都是.NET 下的 AOP 方案。那么在今天的故事中,我们遇到了的一个场景是在指定方法执行前、后插入代码片段,这是面向切面编程(AOP)的基本思想,为此,我们考虑使用 Unity 框架来简化应用程序中异常处理及日志记录流程。

Unity 中的三种拦截器

Unity 中提供了三种典型的拦截器,为了选择一种合适的拦截器来实现我们的功能,我们首先来了解下这三种不同的拦截器各自的应用场景:

  • TransparentProxyInterceptor:即透明代理拦截器,基于.NET Remoting 技术实现代理,它可以拦截对象的所有函数,缺点是被拦截对象必须继承自 MarshalByRefObject。
  • InterfaceInterceptor:顾名思义,即接口拦截器,仅拦截指定接口,显然只要目标类型实现了指定接口就可以拦截。C#不支持多继承,选择这种方式对代码的影响最小。
  • VirtualMethodInterceptor:顾名思义,即虚方法拦截器,仅拦截虚方法,这个对目标类型的要求就非常高啦,一般我们不会考虑这种方式。

对 Unity 框架而言,不管我们使用哪一种拦截器,我们都需要通过 UnityContainer 这个容器来为目标类型注入拦截器,这样 Unity 框架会帮助我们生成代理对象,我们只要在使用代理对象的时候,这些拦截器才会真正工作。博主曾经以为定义好下面这些 Handler 就可以了,简直是图样图森破。好了,一个基本的代码流程如下,请不要问我配置文件怎么配,我真的不喜欢配置文件,搞得跟某配置狂魔语言似的,反正这些配置文件这次记住了下次还是会忘的,可下面这几行代码是不会轻易忘记的啊:

var container = new UnityContainer().AddNewExtension<Interception>().RegisterType<IBussiness, Bussiness>();
container.Configure<Interception>().SetInterceptorFor<IBussiness>(new InterfaceInterceptor());
var bussiness = container.Resolve<IBussiness>();

注意,这里不要直接从 Github 或者 Nuget 上下载 Unity 框架,因为最新版的 Unity 我实在是不会用啊!:joy: 我喜欢开箱即用的产品,我愿意钻研啊,可 DeadLine 永远会有终点! 我们需要从微软企业最佳实践库中下载以下动态链接库:

  • CommonServiceLocator.dll
  • Microsoft.Practices.Unity.Configuration.dll
  • Microsoft.Practices.Unity.dll
  • Microsoft.Practices.Unity.Interception.Configuration.dll
  • Microsoft.Practices.Unity.Interception.dll 考虑到我们这里需要实现两种功能,针对异常的异常处理流程,以及正常的日志记录流程,为此我们将实现 ExceptionHandler 和 LogHandler 两个组件。下面我们来一起了解这两个组件的实现过程,这里博主选择了最简单的 ICallHandler 接口,而非更一般的 IInterceptionBehavior 接口,主要希望让这个过程更简单些,同时实现在方法粒度上的可控,即我们可以选择性的去拦截某一个方法,而非全部的方法,因为在实际业务中并非所有的方法都需要拦截。

LogHandler 的实现

LogHandler 主要用于记录日志,所以我们需要记录方法的名字,方法的参数以及方法执行的结果,甚至是是否引发异常,这些功能在 AOP 中是相对基础的功能,Unity 框架为我们提供了这些基础设施,我们只要就可以获取到这些信息,然后将其记录到日志中即可。这里的代码如下:

public class LogHandler : ICallHandler
{
    int ICallHandler.Order { get; set; }

    IMethodReturn ICallHandler.Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        var methodInfo = input.MethodBase;
        var methodName = methodInfo.Name;
        Logger.Log(string.Format("----------开始调用{0}----------", methodName));
        var parameters = methodInfo.GetParameters();
        var arguments = input.Arguments;
        var logInfo = parameters.Select(e => string.Format("{0}:{1}", e.Name, arguments[e.Position]));
        Logger.Log("传入的参数为:" + string.Join(",", logInfo.ToArray()));
        var result = getNext()(input, getNext);
        if (result.Exception != null)
            Logger.Log(string.Format("调用异常:{0}-{1}", result.Exception.Message, result.Exception.StackTrace));
        Logger.Log(string.Format("调用{0}的结果为:{1}", methodName, result.ReturnValue));
        Logger.Log(string.Format("----------结束调用{0}----------", methodName));
        return result;
    }
}

为了让这个 Handler 更好用一些,我们希望它可以以 Attribute 的方式出现在方法上面,这样被标记过的方法就会被 Unity 框架拦截,所以我们需要一个继承自 Attribute 类的东西,知道我为什么不喜欢配置文件吗?因为我有 Attribute 啊!幸运的是 Unity 框架为我们提供了这样一个基类:HandlerAttribute,由此下面的代码可以这样写:

[AttributeUsage(AttributeTargets.Method,AllowMultiple = true)]
class LogHandlerAttribute : HandlerAttribute
{
    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        return new LogHandler();
    }
}

ExceptionHandler 的实现

对于 ExceptionHandler 来说,它相比 LogHandler 增加的功能在于,它需要处理异常,按照目前项目的异常处理习惯,这种和硬件相关的方法都会被定义为一个 ErrorCode,为此我们的 ExceptionHandler 类中需要增加一个 ErrorCode 类型的成员,这是一个枚举类型。这里的代码实现如下:

public class ExceptionHandler : ICallHandler
{
    int ICallHandler.Order { get; set; }

    public string ErrorCode { get; set; }

    IMethodReturn ICallHandler.Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        var methodInfo = input.MethodBase;
        var methodName = methodInfo.Name;
        Logger.Log(string.Format("--------------方法{0}执行开始--------------", methodName));
        var parameters = methodInfo.GetParameters();
        var arguments = input.Arguments;
        var logInfo = parameters.Select(e => string.Format("{0}:{1}", e.Name, arguments[e.Position]));
        Logger.Log("传入的参数为:" + string.Join(",", logInfo.ToArray()));
        var result = getNext()(input, getNext);
        if (result.Exception != null)
        {
            Logger.Log(string.Format("Error Code is {0}", ErrorCode));
            result.Exception = null;
            Logger.Log(string.Format("--------------方法{0}执行结束--------------", methodName));
            throw new Exception(ErrorCode);
        }
        Logger.Log(string.Format("--------------方法{0}执行结束--------------", methodName));
        return result;
    }
}

可以注意到 ExceptionHandler 相比 LogHandler 的变化,主要发生在异常处理这部分,如你所愿,我在拦截到异常以后抛出了一个对应 ErrorCode 的异常,虽然我不赞同这种做法,但为了尊重现有项目的编程风格,我只能写有这样一行看起来非常拙劣的代码,我真的没有代码洁癖,我仅仅是觉得它还不够好,就像我觉得自己还不够好一样,同样,它需要定义一个对应的 Attribute 类,这样我们可以更加自由地使用这些特性:

[AttributeUsage(AttributeTargets.Method,AllowMultiple = true)]
class LogHandlerAttribute : HandlerAttribute
{
    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        return new LogHandler();
    }
}

本文小结

好了,现在我们可以来看,如何使用这篇文章中定义的两个组件:

var container = new UnityContainer().AddNewExtension<Interception>().RegisterType<IBussiness, Bussiness>();
container.Configure<Interception>().SetInterceptorFor<IBussiness>(new InterfaceInterceptor());
var bussiness = container.Resolve<IBussiness>();
var sum = bussiness.Add(12,23);
Console.WriteLine(sum);
var div = bussiness.Divide(1,0)
Console.WriteLine(div)

IBussiness 接口及其实现类 Bussiness 定义如下:

public interface IBussiness
{

    int Add(int a, int b);
    int Divide(int a, int b);
}

public class Bussiness : MarshalByRefObject, IBussiness
{
    [LogHandler]
    public int Add(int a, int b)
    {
        return a + b;
    }

    [ExceptionHandler(ErrorCode = "E2303")]
    public int Divide(int a, int b)
    {
        return a / b;
    }
}

好了,现在我们来看一下结果: 使用AOP简化后的异常处理和日志记录流程 使用AOP简化后的异常处理和日志记录流程

我们为此付出的代价是什么?第一,要有一个接口,写接口难道还有疑问吗?第二,要添加 Attribute 到指定方法上面,我保证这点时间足够你写好几遍重复代码了。第三,需要依赖注入机制,这个可能是到目前为止最大的影响,因为有了依赖注入以后,对象的实例化都交给了 Unity 框架,看起来我们好像被束缚了手脚,不能再任性地 new 一个对象实例出来,可这不正是依赖注入的精髓所在吗?我们就是需要 Unity 框架,来帮助我们管理这些模块间的依赖关系及其生命周期,如果你觉得这点代码不能接受,抱歉,任何依赖注入框架拯救不了你!

今天这篇文章,我们从一个实际项目的背景出发,引出使用 Unity 框架来简化异常处理和日志记录流程这一想法,在正式实践这一想法前,我们首先了解了 Unity 框架中提供的三种拦截器及其各自优劣,在此基础上我们实现了 LogHandler 和 ExceptionHandler 两个组件,并展示了如何使用这两个组件,探讨使用整个 AOP 机制对现有项目的影响有多大,以及为什么我们需要 Unity 框架等问题,框架固然重要,了解为什么使用框架则更重要!好啦,这就是今天这篇文章的内容啦,再次谢谢大家关注我的博客,各位晚安!:smile: