使用aop(肉夹馍)为BlazorServer实现统一异常处理
背景
用户做一个操作往往对应一个方法的执行,而方法内部会调用别的方法,内部可能又会调用别的方法,从而形成一个调用链。我们一般是在最顶层的方法去加try,而不是调用链的每一层都去加try。
在web开发中,用户的一个操作通常对应一个http请求,常见的mvc中一个controller的action会来执行这个处理。由于asp.net core是基于中间件管道的,很常见的方式就是定义一个“异常处理中间件”,它用try包住后续中间件的执行,在catch中捕获异常,记录日志,并返回一个统一的异常json结构返回给调用方。
一般我们会认为“全局异常处理中间件”是一个兜底的方式,我们仍然应该在action中去try,因为那是具体业务逻辑处理的起点,不过我觉得木有必要,完全可以定义一个UserFriendlyException代表业务逻辑异常,而系统默认的Exception认为是系统级异常,在action的调用链中,不需要try,只是发现不满足业务规则时: throw new UserFriendlyException 即可,而其它系统异常我们完全可以不考虑,最后在“全局异常处理中间件”中判断异常类型,若是UserFriendlyException则直接返回ex.Message给前端,不用做日志记录;系统级异常则应该记录异常堆栈日志,并返回一个友好的异常消息给前端。
Blazor server中的默认异常处理及其问题
在blazor server中,首次请求后服务端和浏览器建立了长连接websocket,后续的浏览器和服务端的交互没有类似http这种请求响应了,那在哪里做全局异常拦截呢?
blazor中提供了ErrorBoundary组件,基本格式如下:
<ErrorBoundary @ref="errorBoundary">
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="errorUI"> A rotten gremlin got us. Sorry!</p>
<button @onclieck="fanhui">返回</button>
</ErrorContent>
</ErrorBoundary>
@code {
private ErrorBoundary? errorBoundary;
protected void fanhui() {
errorBoundary?.Recover();
}
}
当ChildContent中的未发生异常时就显示ChildContent中的组件,若ChildContent中的组件发生了异常,则ChildContent隐藏ErrorContent会被显示出来。可以点击返回重写渲染ChildContent。具体的参考它的文档:处理 ASP.NET Core Blazor 应用中的错误
它的问题是当errorBoundary?.Recover(); 后,ChildContent中的组件是重新渲染的,之前的临时状态会丢失,比如我在填写一个表单,提交时异常了,返回后又得重新填写。
BootstrapBlazor中的异常处理及其问题
BootstrapBlazor是一套bootstrap风格的blazor ui组件库,它提供了全局异常处理方式,参考文档
它的方式是在根组件上截获异常,这样应用中的任何组件发生异常时它都能处理,与blazor自带的ErrorBoundary的最大区别是,bootstarpblazor会记录日志,并使用消息提示窗口提示用户发生了异常,而原本界面不会被替换掉。
尽管如此,它的设计思路仍然是兜底方式,也就是说异常发生时,它不会尝试恢复控件原本的临时状态,比如:一个按钮 点击后 显示正在加载...且按钮被禁用,然后加载数据时发生异常,全局异常会执行日志记录,并弹出提示用户,但按钮会一直显示“正在加载”,且按钮时禁用的,此时用户只能关闭此组件,重新打开。
解决思路
综上所述,无论是blazor自带的还是ui库提供的全局异常处理方案,在异常发生时用户要么需要刷新页面,要么需要重新打开组件。关键问题在于blazor server不像mvc那种基于http请求响应的方式,无法像asp.net core本身的那种定义全局异常处理中间件,所以我们还是需要手动处理blazor server中的每一个顶层方法,这样整个应用层序中会出现大量的try...,,顶多我们可以定义一个执行委托的方法,它类似这样的代码:
1 public T Execute<T>(Action act){
2 try{
3 act();
4 }
5 catch(UserFriendlyException ex)
6 {
7 //提示用户
8 }
9 catch(Exception ex)
10 {
11 //记录日志
12 //提示用户
13 }
14 }
原本要执行的方法使用此方法来执行,从而实现统一异常处理,但这种方式仍然繁琐。
razor组件中的顶层方法一般有两种,一种是生命周期事件函数,另一种是我们为控件定义的事件处理程序,我们可以使用aop的方式拦截这两种方法,这样大部分代码都不需要手动处理异常了。
修改IL的AOP之肉夹馍
Fody是一个开源库,它简化了IL植入代码的方式,肉夹馍是基于dofy实现的aop框架,我们也不需要关系Fody,直接按肉夹馍方式做拦截器即可。
假设你有个类库项目叫:ClassLibrary1,那么它通过如下方式引用肉夹馍的nuget
<PackageReference Include="Rougamo.Fody" Version="2.0.0" IncludeAssets="all" PrivateAssets="contentfiles;analyzers" />
然后在ClassLibrary1中定义肉夹馍拦截器,其它项目在引用ClassLibrary1时,就无需再引用肉夹馍的包了,仅仅引用ClassLibrary1即可。
aop/肉夹馍不仅仅用在这个场景,我们这里只是简单描述。它详细文档请参考官方文档:inversionhourglass/Rougamo: An AOP component that takes effect at compile time, similar to PostSharp. (github.com)
实现
基于肉夹馍的统一异常处理拦截器定义
/// <summary>
/// 基于abp和bootstrapblazor的全局异常处理拦截器
/// </summary>
public class AbpBBExceptionAttribute : MoAttribute
{
//public override AccessFlags Flags => AccessFlags.Method; /*
* 省略访问修饰符标识拦截所有方法
* 返回类型* 就是忽略
* 继承于Microsoft.AspNetCore.Components.ComponentBase的所有子类
* 的所有方法
*/
public override string? Pattern => "method(protected * BXJG.Utils.Components.AbpBaseComponent+.*(..))"; //public override Feature Features => Feature.Observe;//加了这个就不灵了,不晓得为啥
// ComponentBase const string scopedServicesKey = nameof(scopedServicesKey);// "scopedServices";
const string loggerKey = nameof(loggerKey);
const string snackbarKey = nameof(snackbarKey);
//const string isIntercepKey = nameof(isIntercepKey);//是否拦截 public override void OnEntry(MethodContext context)
{
var services = IocManager.Instance.CreateScope();
context.Datas.Add(scopedServicesKey, services); var loggerFactory = services.Resolve<ILoggerFactory>();
var logger = loggerFactory.Create(context.TargetType.FullName); context.Datas.Add(loggerKey, logger); var temp = context.Target.GetType().GetProperty("MessageService", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public).GetValue(context.Target);
var snackbar = temp as MessageService;
context.Datas.Add(snackbarKey, snackbar);
} public override void OnException(MethodContext context)
{
var snackbar = context.Datas[snackbarKey] as MessageService;
//Task t; if (context.Exception is UserFriendlyException)
{
snackbar.Show(new MessageOption
{
Content = $"错误!{context.Exception.Message}",
Color = Color.Danger,
ShowBorder = true,
ShowShadow = true
});
}
else
{
var logger = context.Datas[loggerKey] as ILogger;
logger.Error(@"{context.TargetType.FullName }.{context.Method.Name}" + context.Exception.StackTrace); // ( context.Target as ComponentBase).tryinv snackbar.Show(new MessageOption
{
Content = $"服务端发生未处理异常!请稍后重试,若多次失败,请联系系统管理员11。",
Color = Color.Danger,
ShowBorder = true,
ShowShadow = true
});
//snackbar.Add($"服务端发生未处理异常!请稍后重试,若多次失败,请联系系统管理员。", Severity.Error);
} //if (context.ReturnValue is Task xx)
//{
// xx.ContinueWith(async cccc => await t);
//}
//else if (context.ReturnValue is ValueTask xx1)
//{
// xx1.AsTask().ContinueWith(async cccc => await t);
//} // 处理异常并将返回值设置为newReturnValue,如果方法无返回值(void),直接传入null即可
context.HandledException(this, context.RealReturnType.GetDefaultValue());
} public override void OnExit(MethodContext context)
{
(context.Datas[scopedServicesKey] as IScopedIocResolver)!.Dispose();
}
} //根据类型Type获取默认值的扩展方法
/// <summary>
/// 获取类型默认值
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static object? GetDefaultValue(this Type type)
{
if (type.Name.ToLower() == "void")
return null; if (type.IsValueType)
return RuntimeHelpers.GetUninitializedObject(type); return null;
}
public override string? Pattern => "method(protected * BXJG.Utils.Components.AbpBaseComponent+.*(..))";这个代码指明此拦截器可以用于:方法、且访问修饰符必须是protected的、返回类型是任意的、且必须是 BXJG.Utils.Components.AbpBaseComponent的子类(因为我的项目定义了统一的这个抽象类,你自己应该根据自己项目去定)、方法名是任意的、参数是任意的。
上面我们仅仅是定义了拦截器,且限定了能用于哪些方法,在需要使用此拦截器的项目中,随便找个类,在namespance上面一行加入:
using System.Text;
using System.Threading.Tasks; [assembly: AbpBBException] namespace BXJG.AbpBootstrapBlazor
{
[DependsOn(typeof(BXJG.Utils.BXJGUtilsRCLModule))]
public class AbpBootstrapBlazorModule: AbpModule
{
这表示AbpBootstrapBlazorModule这个类所在的程序集中的类(具体哪些类拦截器中的Pattern规定了)都会应用这个拦截器。若组件中某些方法不是顶层方法,应该定义为private,或加[IgnoreMo],则这个方法就不会被拦截了。丢几个图,看看效果:
总结
总的来说blazor中,自带的异常处理方式仅仅是个兜底的方式,在实际项目中并不好用,无法减轻我们做异常处理的工作量。我们还是需要手动为每个顶层方法(一般是组件生命周期方法和其它事件处理程序)实施try做异常处理,不过我们可以使用aop来简化这个操作。
使用aop(肉夹馍)为BlazorServer实现统一异常处理的更多相关文章
- 编程小白入门分享三:Spring AOP统一异常处理
Spring AOP统一异常处理 简介 在Controller层,Service层,可能会有很多的try catch代码块.这将会严重影响代码的可读性."美观性".怎样才可以把更多 ...
- springboot aop + logback + 统一异常处理 打印日志
1.src/resources路径下新建logback.xml 控制台彩色日志打印 info日志和异常日志分不同文件存储 每天自动生成日志 结合myibatis方便日志打印(debug模式) < ...
- Spring Boot统一异常处理实践
摘要: SpringBoot异常处理. 原文:Spring MVC/Boot 统一异常处理最佳实践 作者:赵俊 前言 在 Web 开发中, 我们经常会需要处理各种异常, 这是一件棘手的事情, 对于很多 ...
- 【异常处理】Springboot对Controller层方法进行统一异常处理
Controller层方法,进行统一异常处理 提供两种不同的方案,如下: 方案1:使用 @@ControllerAdvice (或@RestControllerAdvice), @ExceptionH ...
- 统一异常处理@RestContrllerAdvice,@ExceptionHandler(转载)
思想很重要 统一异常处理实现方式:使用注解@RestContrllerAdvice,@ExceptionHandler 先想明白一个问题:定义统一异常处理类之后,是不是在Contrller中就不用捕获 ...
- Spring中的统一异常处理方式
源自:https://segmentfault.com/a/1190000016236188 在具体的SSM项目开发中,由于Controller层为处于请求处理的最顶层,再往上就是框架代码的. 因此, ...
- SpringBoot小技巧:统一异常处理
SpringBoot小技巧:统一异常处理 情景描述 对于接口的定义,我们通常会有一个固定的格式,比如: 但是调用方在请求我们的API时把接口地址写错了,就会得到一个404错误,且不同于我们定义的数据格 ...
- SpringBoot统一异常处理后TX-LCN分布式事务无法捕获异常进行回滚
通常我们使用SpringBoot都会进行统一异常处理,例如写一个BaseController,在BaseController里进行统一异常处理,然后其他的Controller都继承BaseContro ...
- Springboot对Controller层方法进行统一异常处理
Controller层方法,进行统一异常处理 提供两种不同的方案,如下: 方案1:使用 @@ControllerAdvice (或@RestControllerAdvice), @ExceptionH ...
- 利用过滤器Filter和特性Attribute实现对Web API返回结果的封装和统一异常处理
在我们开发Web API应用的时候,我们可以借鉴ABP框架的过滤器Filter和特性Attribute的应用,实现对Web API返回结果的封装和统一异常处理,本篇随笔介绍利用AuthorizeAtt ...
随机推荐
- Servlet案例:发生错误org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expecte
20-Jun-2020 20:48:31.466 信息 [http-nio-8080-exec-7] com.alibaba.druid.pool.DruidDataSource.info {data ...
- 一文了解 io.Copy 函数
1. 引言 io.Copy 函数是一个非常好用的函数,能够非常方便得将数据进行拷贝.本文我们将从io.Copy 函数的基本定义出发,讲述其基本使用和实现原理,以及一些注意事项,基于此完成对io.Cop ...
- 基于thumbnailator封装图片处理工具类,实现图片的裁剪、压缩、图片水印、文字水印、多行文字水印等功能
目录 一.前言 二.工具类的依赖和简单介绍 1.添加依赖 2.简单的使用 3.加载需要处理的图片 4.添加图片处理规则 4.1 Builder的方式 4.2 使用规则工厂的方式 5.输出处理后的图片 ...
- Anchored Neighborhood Regression【阅读笔记】GR全局回归
论文信息 [Anchored Neighborhood Regression for Fast Example-Based uper Resolution]-TIMOFTER, 2013, IEEE ...
- 2023-07-19:布尔表达式 是计算结果不是 true 就是 false 的表达式 有效的表达式需遵循以下约定: ‘t‘,运算结果为 true ‘f‘,运算结果为 false ‘!(subExpr
2023-07-19:布尔表达式 是计算结果不是 true 就是 false 的表达式 有效的表达式需遵循以下约定: 't',运算结果为 true 'f',运算结果为 false '!(subExpr ...
- TodoList - 开源研究系列文章
接着上次的代码,整理出一个待办列表的程序,比较简单易用,读者可自行修改成自己需要的程序. 1. 项目目录: 目录见下图,对代码进行了划分,比较简单.主处理类在Helper目录里. 2. ...
- zabbix 使用监控项原型(自动发现规则)
以kafka为例,需要先对 topic-parttion 做发现,脚本如下 cat topic_parttion_discovery.py #!/usr/bin/env python import j ...
- 探索Java通信面试的奥秘:揭秘IO模型、选择器和网络协议,了解面试中的必备知识点!
了解常见的TCP/UDP TCP(Transmission Control Protocol)是一种面向连接的可靠的传输协议.类似于打电话,它通过建立一个连接和保证数据的可靠传输来提高通信的可靠性.然 ...
- 关于zbar
python3不支持zbar,可以使用pyzbar python2支持zbar
- 王道oj/problem16
网址:http://oj.lgwenda.com/problem/16 思路:都在注释里,注意增删查的参数以及停止条件 代码: #define _CRT_SECURE_NO_WARNINGS#incl ...