上一篇文章(http://www.javashuo.com/article/p-xtmpxysk-du.html)完成了項目的全局異常處理和日誌記錄。html
在日誌記錄中使用的靜態方法有人指出寫法不是很優雅,遂優化一下上一篇中日誌記錄的方法,具體操做以下:git
在.ToolKits
層中新建擴展方法Log4NetExtensions.cs
。github
//Log4NetExtensions.cs using log4net; using log4net.Config; using Microsoft.Extensions.Hosting; using System.IO; using System.Reflection; namespace Meowv.Blog.ToolKits.Extensions { public static class Log4NetExtensions { public static IHostBuilder UseLog4Net(this IHostBuilder hostBuilder) { var log4netRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.Configure(log4netRepository, new FileInfo("log4net.config")); return hostBuilder; } } }
配置log4net,而後咱們直接返回IHostBuilder對象,便於在Main
方法中鏈式調用。redis
//Program.cs using Meowv.Blog.ToolKits.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using System.Threading.Tasks; namespace Meowv.Blog.HttpApi.Hosting { public class Program { public static async Task Main(string[] args) { await Host.CreateDefaultBuilder(args) .UseLog4Net() .ConfigureWebHostDefaults(builder => { builder.UseIISIntegration() .UseStartup<Startup>(); }).UseAutofac().Build().RunAsync(); } } }
而後修改MeowvBlogExceptionFilter
過濾器,代碼以下:json
//MeowvBlogExceptionFilter.cs using log4net; using Microsoft.AspNetCore.Mvc.Filters; namespace Meowv.Blog.HttpApi.Hosting.Filters { public class MeowvBlogExceptionFilter : IExceptionFilter { private readonly ILog _log; public MeowvBlogExceptionFilter() { _log = LogManager.GetLogger(typeof(MeowvBlogExceptionFilter)); } /// <summary> /// 異常處理 /// </summary> /// <param name="context"></param> /// <returns></returns> public void OnException(ExceptionContext context) { // 錯誤日誌記錄 _log.Error($"{context.HttpContext.Request.Path}|{context.Exception.Message}", context.Exception); } } }
能夠刪掉以前添加的LoggerHelper.cs
類,運行一下,一樣能夠達到預期效果。緩存
本篇將集成Redis,使用Redis來緩存數據,使用方法參考的微軟官方文檔:https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributedapp
關於Redis的介紹這裏就很少說了,這裏有一篇快速入門的文章:Redis快速入門及使用,對於不瞭解的同窗能夠看看。async
直入主題,先在appsettings.json
配置Redis的鏈接字符串。ide
//appsettings.json ... "Caching": { "RedisConnectionString": "127.0.0.1:6379,password=123456,ConnectTimeout=15000,SyncTimeout=5000" } ...
對應的,在AppSettings.cs
中讀取。工具
//AppSettings.cs ... /// <summary> /// Caching /// </summary> public static class Caching { /// <summary> /// RedisConnectionString /// </summary> public static string RedisConnectionString => _config["Caching:RedisConnectionString"]; } ...
在.Application.Caching
層添加包Microsoft.Extensions.Caching.StackExchangeRedis
,而後在模塊類MeowvBlogApplicationCachingModule
中添加配置緩存實現。
//MeowvBlogApplicationCachingModule.cs using Meowv.Blog.Domain; using Meowv.Blog.Domain.Configurations; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Caching; using Volo.Abp.Modularity; namespace Meowv.Blog.Application.Caching { [DependsOn( typeof(AbpCachingModule), typeof(MeowvBlogDomainModule) )] public class MeowvBlogApplicationCachingModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddStackExchangeRedisCache(options => { options.Configuration = AppSettings.Caching.RedisConnectionString; //options.InstanceName //options.ConfigurationOptions }); } } }
options.Configuration
是 Redis 的鏈接字符串。
options.InstanceNam
是 Redis 實例名稱,這裏沒填。
options.ConfigurationOptions
是 Redis 的配置屬性,若是配置了這個字,將優先於 Configuration 中的配置,同時它支持更多的選項。我這裏也沒填。
緊接着咱們就能夠直接使用了,直接將IDistributedCache
接口依賴關係注入便可。
能夠看到默認已經實現了這麼多經常使用的接口,已經夠我這個小項目用的了,同時在Microsoft.Extensions.Caching.Distributed.DistributedCacheExtensions
中微軟還給咱們提供了不少擴展方法。
因而,咱們我就想到寫一個新的擴展方法,能夠同時處理獲取和添加緩存的操做,當緩存存在時,直接返回,不存在時,添加緩存。
新建MeowvBlogApplicationCachingExtensions.cs
擴展方法,以下:
//MeowvBlogApplicationCachingExtensions.cs using Meowv.Blog.ToolKits.Extensions; using Microsoft.Extensions.Caching.Distributed; using System; using System.Threading.Tasks; namespace Meowv.Blog.Application.Caching { public static class MeowvBlogApplicationCachingExtensions { /// <summary> /// 獲取或添加緩存 /// </summary> /// <typeparam name="TCacheItem"></typeparam> /// <param name="cache"></param> /// <param name="key"></param> /// <param name="factory"></param> /// <param name="minutes"></param> /// <returns></returns> public static async Task<TCacheItem> GetOrAddAsync<TCacheItem>(this IDistributedCache cache, string key, Func<Task<TCacheItem>> factory, int minutes) { TCacheItem cacheItem; var result = await cache.GetStringAsync(key); if (string.IsNullOrEmpty(result)) { cacheItem = await factory.Invoke(); var options = new DistributedCacheEntryOptions(); if (minutes != CacheStrategy.NEVER) { options.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(minutes); } await cache.SetStringAsync(key, cacheItem.ToJson(), options); } else { cacheItem = result.FromJson<TCacheItem>(); } return cacheItem; } } }
咱們能夠在DistributedCacheEntryOptions
中能夠配置咱們的緩存過時時間,其中有一個判斷條件,就是當minutes = -1
的時候,不指定過時時間,那麼咱們的緩存就不會過時了。
GetStringAsync()
、SetStringAsync()
是DistributedCacheExtensions
的擴展方法,最終會將緩存項cacheItem
轉換成JSON格式進行存儲。
CacheStrategy
是在.Domain.Shared
層定義的緩存過時時間策略常量。
//MeowvBlogConsts.cs ... /// <summary> /// 緩存過時時間策略 /// </summary> public static class CacheStrategy { /// <summary> /// 一天過時24小時 /// </summary> public const int ONE_DAY = 1440; /// <summary> /// 12小時過時 /// </summary> public const int HALF_DAY = 720; /// <summary> /// 8小時過時 /// </summary> public const int EIGHT_HOURS = 480; /// <summary> /// 5小時過時 /// </summary> public const int FIVE_HOURS = 300; /// <summary> /// 3小時過時 /// </summary> public const int THREE_HOURS = 180; /// <summary> /// 2小時過時 /// </summary> public const int TWO_HOURS = 120; /// <summary> /// 1小時過時 /// </summary> public const int ONE_HOURS = 60; /// <summary> /// 半小時過時 /// </summary> public const int HALF_HOURS = 30; /// <summary> /// 5分鐘過時 /// </summary> public const int FIVE_MINUTES = 5; /// <summary> /// 1分鐘過時 /// </summary> public const int ONE_MINUTE = 1; /// <summary> /// 永不過時 /// </summary> public const int NEVER = -1; } ...
接下來去建立緩存接口類和實現類,而後再咱們的引用服務層.Application
中進行調用,拿上一篇中接入GitHub的幾個接口來作新增緩存操做。
和.Application
層格式同樣,在.Application.Caching
中新建Authorize文件夾,添加緩存接口IAuthorizeCacheService
和實現類AuthorizeCacheService
。
注意命名規範,實現類確定要繼承一個公共的CachingServiceBase
基類。在.Application.Caching
層根目錄添加MeowvBlogApplicationCachingServiceBase.cs
,繼承ITransientDependency
。
//MeowvBlogApplicationCachingServiceBase.cs using Microsoft.Extensions.Caching.Distributed; using Volo.Abp.DependencyInjection; namespace Meowv.Blog.Application.Caching { public class CachingServiceBase : ITransientDependency { public IDistributedCache Cache { get; set; } } }
而後使用屬性注入的方式,注入IDistributedCache
。這樣咱們只要繼承了基類:CachingServiceBase
,就能夠愉快的使用緩存了。
添加要緩存的接口到IAuthorizeCacheService
,在這裏咱們使用Func()
方法,咱們的接口返回什麼類型由Func()
來決定,因而添加三個接口以下:
//IAuthorizeCacheService.cs using Meowv.Blog.ToolKits.Base; using System; using System.Threading.Tasks; namespace Meowv.Blog.Application.Caching.Authorize { public interface IAuthorizeCacheService { /// <summary> /// 獲取登陸地址(GitHub) /// </summary> /// <returns></returns> Task<ServiceResult<string>> GetLoginAddressAsync(Func<Task<ServiceResult<string>>> factory); /// <summary> /// 獲取AccessToken /// </summary> /// <param name="code"></param> /// <param name="factory"></param> /// <returns></returns> Task<ServiceResult<string>> GetAccessTokenAsync(string code, Func<Task<ServiceResult<string>>> factory); /// <summary> /// 登陸成功,生成Token /// </summary> /// <param name="access_token"></param> /// <param name="factory"></param> /// <returns></returns> Task<ServiceResult<string>> GenerateTokenAsync(string access_token, Func<Task<ServiceResult<string>>> factory); } }
是否是和IAuthorizeService
代碼很像,的確,我就是直接複製過來改的。
在AuthorizeCacheService
中實現接口。
//AuthorizeCacheService.cs using Meowv.Blog.ToolKits.Base; using Meowv.Blog.ToolKits.Extensions; using System; using System.Threading.Tasks; using static Meowv.Blog.Domain.Shared.MeowvBlogConsts; namespace Meowv.Blog.Application.Caching.Authorize.Impl { public class AuthorizeCacheService : CachingServiceBase, IAuthorizeCacheService { private const string KEY_GetLoginAddress = "Authorize:GetLoginAddress"; private const string KEY_GetAccessToken = "Authorize:GetAccessToken-{0}"; private const string KEY_GenerateToken = "Authorize:GenerateToken-{0}"; /// <summary> /// 獲取登陸地址(GitHub) /// </summary> /// <param name="factory"></param> /// <returns></returns> public async Task<ServiceResult<string>> GetLoginAddressAsync(Func<Task<ServiceResult<string>>> factory) { return await Cache.GetOrAddAsync(KEY_GetLoginAddress, factory, CacheStrategy.NEVER); } /// <summary> /// 獲取AccessToken /// </summary> /// <param name="code"></param> /// <param name="factory"></param> /// <returns></returns> public async Task<ServiceResult<string>> GetAccessTokenAsync(string code, Func<Task<ServiceResult<string>>> factory) { return await Cache.GetOrAddAsync(KEY_GetAccessToken.FormatWith(code), factory, CacheStrategy.FIVE_MINUTES); } /// <summary> /// 登陸成功,生成Token /// </summary> /// <param name="access_token"></param> /// <param name="factory"></param> /// <returns></returns> public async Task<ServiceResult<string>> GenerateTokenAsync(string access_token, Func<Task<ServiceResult<string>>> factory) { return await Cache.GetOrAddAsync(KEY_GenerateToken.FormatWith(access_token), factory, CacheStrategy.ONE_HOURS); } } }
代碼很簡單,每一個緩存都有固定KEY值,根據參數生成KEY,而後調用前面寫的擴展方法,再給一個過時時間便可,能夠看到KEY裏面包含了冒號 :
,這個冒號 :
能夠起到相似於文件夾的操做,在界面化管理工具中能夠很友好的查看。
這樣咱們的緩存就搞定了,而後在.Application
層對應的Service中進行調用。代碼以下:
//AuthorizeService.cs using Meowv.Blog.Application.Caching.Authorize; using Meowv.Blog.Domain.Configurations; using Meowv.Blog.ToolKits.Base; using Meowv.Blog.ToolKits.Extensions; using Meowv.Blog.ToolKits.GitHub; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Threading.Tasks; namespace Meowv.Blog.Application.Authorize.Impl { public class AuthorizeService : ServiceBase, IAuthorizeService { private readonly IAuthorizeCacheService _authorizeCacheService; private readonly IHttpClientFactory _httpClient; public AuthorizeService(IAuthorizeCacheService authorizeCacheService, IHttpClientFactory httpClient) { _authorizeCacheService = authorizeCacheService; _httpClient = httpClient; } /// <summary> /// 獲取登陸地址(GitHub) /// </summary> /// <returns></returns> public async Task<ServiceResult<string>> GetLoginAddressAsync() { return await _authorizeCacheService.GetLoginAddressAsync(async () => { var result = new ServiceResult<string>(); var request = new AuthorizeRequest(); var address = string.Concat(new string[] { GitHubConfig.API_Authorize, "?client_id=", request.Client_ID, "&scope=", request.Scope, "&state=", request.State, "&redirect_uri=", request.Redirect_Uri }); result.IsSuccess(address); return await Task.FromResult(result); }); } /// <summary> /// 獲取AccessToken /// </summary> /// <param name="code"></param> /// <returns></returns> public async Task<ServiceResult<string>> GetAccessTokenAsync(string code) { var result = new ServiceResult<string>(); if (string.IsNullOrEmpty(code)) { result.IsFailed("code爲空"); return result; } return await _authorizeCacheService.GetAccessTokenAsync(code, async () => { var request = new AccessTokenRequest(); var content = new StringContent($"code={code}&client_id={request.Client_ID}&redirect_uri={request.Redirect_Uri}&client_secret={request.Client_Secret}"); content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); using var client = _httpClient.CreateClient(); var httpResponse = await client.PostAsync(GitHubConfig.API_AccessToken, content); var response = await httpResponse.Content.ReadAsStringAsync(); if (response.StartsWith("access_token")) result.IsSuccess(response.Split("=")[1].Split("&").First()); else result.IsFailed("code不正確"); return result; }); } /// <summary> /// 登陸成功,生成Token /// </summary> /// <param name="access_token"></param> /// <returns></returns> public async Task<ServiceResult<string>> GenerateTokenAsync(string access_token) { var result = new ServiceResult<string>(); if (string.IsNullOrEmpty(access_token)) { result.IsFailed("access_token爲空"); return result; } return await _authorizeCacheService.GenerateTokenAsync(access_token, async () => { var url = $"{GitHubConfig.API_User}?access_token={access_token}"; using var client = _httpClient.CreateClient(); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.14 Safari/537.36 Edg/83.0.478.13"); var httpResponse = await client.GetAsync(url); if (httpResponse.StatusCode != HttpStatusCode.OK) { result.IsFailed("access_token不正確"); return result; } var content = await httpResponse.Content.ReadAsStringAsync(); var user = content.FromJson<UserResponse>(); if (user.IsNull()) { result.IsFailed("未獲取到用戶數據"); return result; } if (user.Id != GitHubConfig.UserId) { result.IsFailed("當前帳號未受權"); return result; } var claims = new[] { new Claim(ClaimTypes.Name, user.Name), new Claim(ClaimTypes.Email, user.Email), new Claim(JwtRegisteredClaimNames.Exp, $"{new DateTimeOffset(DateTime.Now.AddMinutes(AppSettings.JWT.Expires)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") }; var key = new SymmetricSecurityKey(AppSettings.JWT.SecurityKey.SerializeUtf8()); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var securityToken = new JwtSecurityToken( issuer: AppSettings.JWT.Domain, audience: AppSettings.JWT.Domain, claims: claims, expires: DateTime.Now.AddMinutes(AppSettings.JWT.Expires), signingCredentials: creds); var token = new JwtSecurityTokenHandler().WriteToken(securityToken); result.IsSuccess(token); return await Task.FromResult(result); }); } } }
直接return咱們的緩存接口,當查詢到Redis中存在KEY值的緩存就不會再走咱們的具體的實現方法了。
注意注意,千萬不要忘了在.Application
層的模塊類中添加依賴緩存模塊MeowvBlogApplicationCachingModule
,否則就會報錯報錯報錯(我就是忘了添加...)
//MeowvBlogApplicationCachingModule.cs using Meowv.Blog.Domain; using Meowv.Blog.Domain.Configurations; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Caching; using Volo.Abp.Modularity; namespace Meowv.Blog.Application.Caching { [DependsOn( typeof(AbpCachingModule), typeof(MeowvBlogDomainModule) )] public class MeowvBlogApplicationCachingModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddStackExchangeRedisCache(options => { options.Configuration = AppSettings.Caching.RedisConnectionString; }); } } }
此時項目的層級目錄結構。
好的,編譯運行項目,如今去調用接口看看效果,爲了真實,這裏我先將我redis緩存數據所有幹掉。
訪問接口,.../auth/url,成功返回數據,如今再去看看咱們的redis。
成功將KEY爲:Authorize:GetLoginAddress 添加進去了,這裏直接使用RedisDesktopManager進行查看。
那麼再次調用這個接口,只要沒有過時,就會直接返回數據了,調試圖以下:
能夠看到,是能夠直接取到緩存數據的,其餘接口你們本身試試吧,同樣的效果。
是否是很簡單,用最少的代碼集成Redis進行數據緩存,你學會了嗎?😁😁😁