在开始之前,我们实现一个之前的遗留问题,这个问题是有人在GitHub Issues(https://github.com/Meowv/Blog/issues/8)上提出来的,就是当我们对Swagger进行分组,实现IDocumentFilter接口添加了文档描述信息后,切换分组时会显示不属于当前分组的Tag。

经过研究和分析发现,是可以解决的,我不知道大家有没有更好的办法,我的实现方法请看:

//SwaggerDocumentFilter.cs
...
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var tags = new List<OpenApiTag>{...} #region 实现添加自定义描述时过滤不属于同一个分组的API var groupName = context.ApiDescriptions.FirstOrDefault().GroupName; var apis = context.ApiDescriptions.GetType().GetField("_source", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context.ApiDescriptions) as IEnumerable<ApiDescription>; var controllers = apis.Where(x => x.GroupName != groupName).Select(x => ((ControllerActionDescriptor)x.ActionDescriptor).ControllerName).Distinct(); swaggerDoc.Tags = tags.Where(x => !controllers.Contains(x.Name)).OrderBy(x => x.Name).ToList(); #endregion
}
...

根据调试代码发现,我们可以从context.ApiDescriptions获取到当前显示的是哪一个分组下的API。

然后使用GetType().GetField(string name, BindingFlags bindingAttr)获取到_source,当前项目的所有API,里面同时也包含了ABP默认生成的一些接口。

再将API中不属于当前分组的API筛选掉,用Select查询出所有的Controller名称进行去重。

因为OpenApiTag中的Name名称与Controller的Name是一致的,所以最后将包含controllers名称的tag查询出来取反,即可满足需求。


上一篇文章(https://www.cnblogs.com/meowv/p/12935693.html)集成了GitHub,使用JWT的方式完成了身份认证和授权,保护了我们写的API接口。

本篇主要实现对项目中出现的异常仅需处理,当出现不可避免的错误时,或者未授权用户调用接口时,可以进行有效的监控和日志记录。

目前调用未授权接口,会直接返回一个状态码为401的错误页面,这样显得太不友好,我们还是用之前写的统一返回模型来告诉调用者,你是未授权的,调不了我的接口,上篇也有提到过,我们将用两种方式来解决。

方式一 :使用AddJwtBearer()扩展方法下面的options.Events事件机制。

//MeowvBlogHttpApiHostingModule.cs
...
//应用程序提供的对象,用于处理承载引发的事件,身份验证处理程序
options.Events = new JwtBearerEvents
{
OnChallenge = async context =>
{
// 跳过默认的处理逻辑,返回下面的模型数据
context.HandleResponse(); context.Response.ContentType = "application/json;charset=utf-8";
context.Response.StatusCode = StatusCodes.Status200OK; var result = new ServiceResult();
result.IsFailed("UnAuthorized"); await context.Response.WriteAsync(result.ToJson());
}
};
...

在项目启动时,实例化了OnChallenge,如果用户调用未授权,将请求的状态码赋值为200,并返回模型数据。

如图所示,可以看到已经成功返回了一段比较友好的JSON数据。

{
"Code": 1,
"Message": "UnAuthorized",
"Success": false,
"Timestamp": 1590226085318
}

方式二 :使用中间件的方式。

我们注释掉上面的代码,在.HttpApi.Hosting添加文件夹Middleware,新建一个中间件ExceptionHandlerMiddleware.cs

using Meowv.Blog.ToolKits.Base;
using Meowv.Blog.ToolKits.Extensions;
using Microsoft.AspNetCore.Http;
using System;
using System.Net;
using System.Threading.Tasks; namespace Meowv.Blog.HttpApi.Hosting.Middleware
{
/// <summary>
/// 异常处理中间件
/// </summary>
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate next; public ExceptionHandlerMiddleware(RequestDelegate next)
{
this.next = next;
} /// <summary>
/// Invoke
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await ExceptionHandlerAsync(context, ex.Message);
}
finally
{
var statusCode = context.Response.StatusCode;
if (statusCode != StatusCodes.Status200OK)
{
Enum.TryParse(typeof(HttpStatusCode), statusCode.ToString(), out object message);
await ExceptionHandlerAsync(context, message.ToString());
}
}
} /// <summary>
/// 异常处理,返回JSON
/// </summary>
/// <param name="context"></param>
/// <param name="message"></param>
/// <returns></returns>
private async Task ExceptionHandlerAsync(HttpContext context, string message)
{
context.Response.ContentType = "application/json;charset=utf-8"; var result = new ServiceResult();
result.IsFailed(message); await context.Response.WriteAsync(result.ToJson());
}
}
}

RequestDelegate是一种请求委托类型,用来处理HTTP请求的函数,返回的是delegate,实现异步的Invoke方法。

这里我写了一个比较通用的方法,当出现异常时直接执行ExceptionHandlerAsync()方法,当没有异常发生时,在finally中判断当前请求状态,可能是200?404?401?等等,不管它是什么,反正不是200,获取到状态码枚举的Key值用来当作错误信息返回,最后也执行ExceptionHandlerAsync()方法,返回我们自定义的模型。

写好了中间件,然后在OnApplicationInitialization(...)中使用它。

        public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
...
// 异常处理中间件
app.UseMiddleware<ExceptionHandlerMiddleware>();
...
}

同样可以达到效果,相比之下他还支持状态非401的错误返回,比如我们访问一个不存在的页面:https://localhost:44388/aaa ,也可以友好的进行处理。

当然这两种方式可以共存,互不影响。

还有一种处理异常的方式,就是我们的过滤器Filter,abp已经默认为我们实现了全局的异常模块,详情可以看其文档:https://docs.abp.io/zh-Hans/abp/latest/Exception-Handling ,在这里,我准备移除abp提供的异常处理模块,自己实现一个。

先看一下目前的异常显示情况,我们在HelloWorldController中写一个异常接口。

//HelloWorldController.cs
...
[HttpGet]
[Route("Exception")]
public string Exception()
{
throw new NotImplementedException("这是一个未实现的异常接口");
}
...

按理说,他应该会执行到我们写的ExceptionHandlerMiddleware中间件中去,但是被我们的Filter进行拦截了,现在我们移除默认的拦截器AbpExceptionFilter

还是在模块类MeowvBlogHttpApiHostingModuleConfigureServices()方法中。

Configure<MvcOptions>(options =>
{
var filterMetadata = options.Filters.FirstOrDefault(x => x is ServiceFilterAttribute attribute && attribute.ServiceType.Equals(typeof(AbpExceptionFilter))); // 移除 AbpExceptionFilter
options.Filters.Remove(filterMetadata);
});

options.Filters中找到AbpExceptionFilter,然后Remove掉,此时再看一下有异常的接口。

当我们注释掉我们的中间件时,他就会显示如下图这样。

这个页面有没有很熟悉的感觉?相信做过.net core开发的都遇到过吧。

ok,现在为止已经完美显示了。但到这里还远远不够,说好的自己实现Filter呢?我们现在实现Filter又有什么用呢?我们可以在Filter中可以做一些日志记录。

.HttpApi.Hosting层添加文件夹Filters,新建一个MeowvBlogExceptionFilter.cs的Filter,他需要实现我们的IExceptionFilter接口的OnExceptionAsync()方法即可。

//MeowvBlogExceptionFilter.cs
using Meowv.Blog.ToolKits.Helper;
using Microsoft.AspNetCore.Mvc.Filters; namespace Meowv.Blog.HttpApi.Hosting.Filters
{
public class MeowvBlogExceptionFilter : IExceptionFilter
{
/// <summary>
/// 异常处理
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public void OnException(ExceptionContext context)
{
// 日志记录
LoggerHelper.WriteToFile($"{context.HttpContext.Request.Path}|{context.Exception.Message}", context.Exception);
}
}
}

OnException(...)方法很简单,这里只做了记录日志的操作,剩下的交给我们中间件去处理吧。

注意,一定要在移除默认AbpExceptionFilter后,将我们自己实现的MeowvBlogExceptionFilter在模块类ConfigureServices()方法中注入到系统。

...
Configure<MvcOptions>(options =>
{
...
// 添加自己实现的 MeowvBlogExceptionFilter
options.Filters.Add(typeof(MeowvBlogExceptionFilter));
});
...

说到日志,就有很多种处理方式,请选择你熟悉的方式,我这里将使用log4net进行处理,仅供参考。

.ToolKits层添加log4net包,使用命令安装:Install-Package log4net,然后添加文件夹Helper,新建一个LoggerHelper.cs

//LoggerHelper.cs
using log4net;
using log4net.Config;
using log4net.Repository;
using System;
using System.IO; namespace Meowv.Blog.ToolKits.Helper
{
public static class LoggerHelper
{
private static readonly ILoggerRepository Repository = LogManager.CreateRepository("NETCoreRepository");
private static readonly ILog Log = LogManager.GetLogger(Repository.Name, "NETCorelog4net"); static LoggerHelper()
{
XmlConfigurator.Configure(Repository, new FileInfo("log4net.config"));
} /// <summary>
/// 写日志
/// </summary>
/// <param name="message"></param>
/// <param name="ex"></param>
public static void WriteToFile(string message)
{
Log.Info(message);
} /// <summary>
/// 写日志
/// </summary>
/// <param name="message"></param>
/// <param name="ex"></param>
public static void WriteToFile(string message, Exception ex)
{
if (string.IsNullOrEmpty(message))
message = ex.Message; Log.Error(message, ex);
}
}
}

.HttpApi.Hosting中添加log4net配置文件,log4net.config配置文件如下:

//log4net.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>
<log4net debug="false"> <appender name="info" type="log4net.Appender.RollingFileAppender,log4net">
<param name="File" value="log4net/info/" />
<param name="AppendToFile" value="true" />
<param name="MaxSizeRollBackups" value="-1"/>
<param name="MaximumFileSize" value="5MB"/>
<param name="RollingStyle" value="Composite" />
<param name="DatePattern" value="yyyyMMdd\\HH&quot;.log&quot;" />
<param name="StaticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout,log4net">
<param name="ConversionPattern" value="%n
{
&quot;system&quot;: &quot;Meowv.Blog&quot;,
&quot;datetime&quot;: &quot;%d&quot;,
&quot;description&quot;: &quot;%m&quot;,
&quot;level&quot;: &quot;%p&quot;,
&quot;info&quot;: &quot;%exception&quot;
}" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="INFO" />
<levelMax value="INFO" />
</filter>
</appender> <appender name="error" type="log4net.Appender.RollingFileAppender,log4net">
<param name="File" value="log4net/error/" />
<param name="AppendToFile" value="true" />
<param name="MaxSizeRollBackups" value="-1"/>
<param name="MaximumFileSize" value="5MB"/>
<param name="RollingStyle" value="Composite" />
<param name="DatePattern" value="yyyyMMdd\\HH&quot;.log&quot;" />
<param name="StaticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout,log4net">
<param name="ConversionPattern" value="%n
{
&quot;system&quot;: &quot;Meowv.Blog&quot;,
&quot;datetime&quot;: &quot;%d&quot;,
&quot;description&quot;: &quot;%m&quot;,
&quot;level&quot;: &quot;%p&quot;,
&quot;info&quot;: &quot;%exception&quot;
}" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="ERROR" />
<levelMax value="ERROR" />
</filter>
</appender> <root>
<level value="ALL"></level>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</root> </log4net> </configuration>

此时再去调用 .../HelloWorld/Exception,将会得到日志文件,内容是以JSON格式进行存储的。

关于Filter的更多用法可以参考微软官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters

到这里,系统的异常处理和日志记录便完成了,你学会了吗?

基于 abp vNext 和 .NET Core 开发博客项目 - 异常处理和日志记录的更多相关文章

  1. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(一)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  2. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(二)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  3. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(三)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  4. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(四)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  5. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(五)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  6. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(一)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  7. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(二)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  8. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(三)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  9. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(四)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

随机推荐

  1. Mybatis自动生成插件对数据库类型为text的处理

    2019独角兽企业重金招聘Python工程师标准>>> 如果数据库中的字段为text或者blob这种大文本类型,在使用MybatisGenerator工具自动生成代码的时候会将其进行 ...

  2. Spring Developer Tools 源码分析:三、重启自动配置'

    接上文 Spring Developer Tools 源码分析:二.类路径监控,接下来看看前面提到的这些类是如何配置,如何启动的. spring-boot-devtools 使用了 Spring Bo ...

  3. 2019年2月5日训练日记关于int字节数,long int 字节数的讨论

    今天做到了个非常有意思的题目,是关于int最大最小值.用sizeof(int)查寻,返回四个字节,4个字节计算应该是4*8=32位,其中一位为符号位,且最高为不能为2所以应该减一,2^31-1=214 ...

  4. 图论--2-SAT--Tarjan连通分量+拓扑排序O(N+M)模板

    #include <cstdio> #include <cstring> #include <queue> #include <vector> #inc ...

  5. C. Ilya And The Tree 树形dp 暴力

    C. Ilya And The Tree 写法还是比较容易想到,但是这么暴力的写法不是那么的敢写. 就直接枚举了每一个点上面的点的所有的情况,对于这个点不放进去特判一下,然后排序去重提高效率. 注意d ...

  6. zabbix 告警信息与恢复信息

    名称: Action-Email 默认接收人: 故障{TRIGGER.STATUS},服务器:{HOSTNAME1}发生: {TRIGGER.NAME}故障! 默认信息: 告警主机:{HOSTNAME ...

  7. JavaWeb实战:报价计算系统(layui+tomcat+cookie实现)

    JavaWeb实战:报价计算系统(layui+tomcat+cookie实现) 系统概述: 该系统是文物物流公司的一个小功能模块,用于帮助用户计算运费.点击查看实际效果 系统文档: 添加展品: 在表单 ...

  8. hdu2336 (匈牙利最大匹配+二分)

    Describe 这是一个简单的游戏,在一个n*n的矩阵中,找n个数使得这n个数都在不同的行和列里并且要求这n个数中的最大值和最小值的差值最小. Input 输入一个整数T表示T组数据. 对于每组数据 ...

  9. Taro UI开发小程序实现左滑喜欢右滑不喜欢效果

    前言:年后入职了一家新公司,与前同事交接完之后,发现公司有一个四端的项目(iOS,Android,H5,小程序),iOS和安卓都实现了左滑右滑的效果,而h5和小程序端没实现,询问得知前同事因网上没找到 ...

  10. 缓冲 buffer 和缓存 cache 的区别

    缓存(cache)是在读取硬盘中的数据时,把最常用的数据保存在内存的缓存区中,再次读取该数据时,就不去硬盘中读取了,而在缓存中读取. 缓冲(buffer)是在向硬盘写入数据时,先把数据放入缓冲区,然后 ...