• 使用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组件,基本格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    <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(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

    然后在ClassLibrary1中定义肉夹馍拦截器,其它项目在引用ClassLibrary1时,就无需再引用肉夹馍的包了,仅仅引用ClassLibrary1即可。

    aop/肉夹馍不仅仅用在这个场景,我们这里只是简单描述。它详细文档请参考官方文档:inversionhourglass/Rougamo: An AOP component that takes effect at compile time, similar to PostSharp. (github.com)

    实现

    基于肉夹馍的统一异常处理拦截器定义

    复制代码
    /// 
    /// 基于abp和bootstrapblazor的全局异常处理拦截器
    /// 
    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();
            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获取默认值的扩展方法
    /// 
    /// 获取类型默认值
    /// 
    /// 
    /// 
    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来简化这个操作。

  • 相关阅读:
    Java项目:jsp在线考试系统
    谷歌自研 Tensor 芯片,8核CPU,20核GPU……
    短链系统设计-服务设计
    2022-10-31-基于用户的协同过滤推荐算法实现+MAE+RMSE的求解+项目代码+运行结果图
    kickstarter数据采集
    阿里云配置ssl(Apache)
    代码随想录算法训练营第二十五天| LeetCode216. 组合总和 III、LeetCode17. 电话号码的字母组合
    力扣 -- 44. 通配符匹配
    纠正一下网上文章所说“利用RPC绕过CFG”的错误说法
    2024/6/30 英语每日一段
  • 原文地址:https://www.cnblogs.com/jionsoft/p/17783675.html