[Abp 源碼分析]十五、自動審計記錄

来源:https://www.cnblogs.com/myzony/archive/2018/09/29/9723531.html
-Advertisement-
Play Games

0.簡介 Abp 框架為我們自帶了審計日誌功能,審計日誌可以方便地查看每次請求介面所耗的時間,能夠幫助我們快速定位到某些性能有問題的介面。除此之外,審計日誌信息還包含有每次調用介面時客戶端請求的參數信息,客戶端的 IP 與客戶端使用的瀏覽器。有了這些數據之後,我們就可以很方便地復現介面產生 BUG ...


0.簡介

Abp 框架為我們自帶了審計日誌功能,審計日誌可以方便地查看每次請求介面所耗的時間,能夠幫助我們快速定位到某些性能有問題的介面。除此之外,審計日誌信息還包含有每次調用介面時客戶端請求的參數信息,客戶端的 IP 與客戶端使用的瀏覽器。有了這些數據之後,我們就可以很方便地復現介面產生 BUG 時的一些環境信息。

當然如果你腦洞更大的話,可以根據這些數據來開發一個可視化的圖形界面,方便開發與測試人員來快速定位問題。

PS:

如果使用了 Abp.Zero 模塊則自帶的審計記錄實現是存儲到資料庫當中的,但是在使用 EF Core + MySQL(EF Provider 為 Pomelo.EntityFrameworkCore.MySql) 在高併發的情況下會有資料庫連接超時的問題,這塊推薦是重寫實現,自己採用 Redis 或者其他存儲方式。

如果需要禁用審計日誌功能,則需要在任意模塊的預載入方法(PreInitialize()) 當中增加如下代碼關閉審計日誌功能。

public class XXXStartupModule
{
    public override PreInitialize()
    {
        // 禁用審計日誌
        Configuration.Auditing.IsEnabled = false;
    }
}

1.啟動流程

審計組件與參數校驗組件一樣,都是通過 MVC 過濾器與 Castle 攔截器來實現記錄的。也就是說,在每次調用介面/方法時都會進入 過濾器/攔截器 並將其寫入到資料庫表 AbpAuditLogs 當中。

其核心思想十分簡單,就是在執行具體介面方法的時候,先使用 StopWatch 對象來記錄執行完一個方法所需要的時間,並且還能夠通過 HttpContext 來獲取到一些客戶端的關鍵信息。

2.1 過濾器註入

同上一篇文章所講的一樣,過濾器是在 AddAbp() 方法內部的 ConfigureAspNetCore() 方法註入的。

private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{
    // ... 其他代碼
    
    //Configure MVC
    services.Configure<MvcOptions>(mvcOptions =>
    {
        mvcOptions.AddAbp(services);
    });
    
    // ... 其他代碼
}

而下麵就是過濾器的註入方法:

internal static class AbpMvcOptionsExtensions
{
    public static void AddAbp(this MvcOptions options, IServiceCollection services)
    {
        // ... 其他代碼
        AddFilters(options);
        // ... 其他代碼
    }
    
    // ... 其他代碼

    private static void AddFilters(MvcOptions options)
    {
        // ... 其他過濾器註入
        
        // 註入審計日誌過濾器
        options.Filters.AddService(typeof(AbpAuditActionFilter));
        
        // ... 其他過濾器註入
    }
    
    // ... 其他代碼
}

2.2 攔截器註入

註入攔截器的地方與 DTO 自動驗證的攔截器的位置一樣,都是在 AbpBootstrapper 對象被構造的時候進行註冊。

public class AbpBootstrapper : IDisposable
{
    private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
    {
        // ... 其他代碼

        if (!options.DisableAllInterceptors)
        {
            AddInterceptorRegistrars();
        }
    }

    // ... 其他代碼

    // 添加各種攔截器
    private void AddInterceptorRegistrars()
    {
        ValidationInterceptorRegistrar.Initialize(IocManager);
        AuditingInterceptorRegistrar.Initialize(IocManager);
        EntityHistoryInterceptorRegistrar.Initialize(IocManager);
        UnitOfWorkRegistrar.Initialize(IocManager);
        AuthorizationInterceptorRegistrar.Initialize(IocManager);
    }

    // ... 其他代碼
}

轉到 AuditingInterceptorRegistrar 的具體實現可以發現,他在內部針對於審計日誌攔截器的註入是區分了類型的。

internal static class AuditingInterceptorRegistrar
{
    public static void Initialize(IIocManager iocManager)
    {
        iocManager.IocContainer.Kernel.ComponentRegistered += (key, handler) =>
        {
            // 如果審計日誌配置類沒有被註入,則直接跳過
            if (!iocManager.IsRegistered<IAuditingConfiguration>())
            {
                return;
            }

            var auditingConfiguration = iocManager.Resolve<IAuditingConfiguration>();

            // 判斷當前 DI 所註入的類型是否應該為其綁定審計日誌攔截器
            if (ShouldIntercept(auditingConfiguration, handler.ComponentModel.Implementation))
            {
                handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(AuditingInterceptor)));
            }
        };
    }
    
    // 本方法主要用於判斷當前類型是否符合綁定攔截器的條件
    private static bool ShouldIntercept(IAuditingConfiguration auditingConfiguration, Type type)
    {
        // 首先判斷當前類型是否在配置類的註冊類型之中,如果是,則進行攔截器綁定
        if (auditingConfiguration.Selectors.Any(selector => selector.Predicate(type)))
        {
            return true;
        }

        // 當前類型如果擁有 Audited 特性,則進行攔截器綁定
        if (type.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true))
        {
            return true;
        }

        // 如果當前類型內部的所有方法當中有一個方法擁有 Audited 特性,則進行攔截器綁定
        if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
        {
            return true;
        }

        // 都不滿足則返回 false,不對當前類型進行綁定
        return false;
    }
}

可以看到在判斷是否綁定攔截器的時候,Abp 使用了 auditingConfiguration.Selectors 的屬性來進行判斷,那麼預設 Abp 為我們添加了哪些類型是必定有審計日誌的呢?

通過代碼追蹤,我們來到了 AbpKernalModule 類的內部,在其預載入方法裡面有一個 AddAuditingSelectors() 的方法,該方法的作用就是添加了一個針對於應用服務類型的一個選擇器對象。

public sealed class AbpKernelModule : AbpModule
{
    public override void PreInitialize()
    {
        // ... 其他代碼

        AddAuditingSelectors();

        // ... 其他代碼
    }

    // ... 其他代碼

    private void AddAuditingSelectors()
    {
        Configuration.Auditing.Selectors.Add(
            new NamedTypeSelector(
                "Abp.ApplicationServices",
                type => typeof(IApplicationService).IsAssignableFrom(type)
            )
        );
    }

    // ... 其他代碼
}

我們先看一下 NamedTypeSelector 的一個作用是什麼,其基本類型定義由一個 stringFunc<Type, bool> 組成,十分簡單,重點就出在這個斷言委托上面。

public class NamedTypeSelector
{
    // 選擇器名稱
    public string Name { get; set; }
    
    // 斷言委托
    public Func<Type, bool> Predicate { get; set; }

    public NamedTypeSelector(string name, Func<Type, bool> predicate)
    {
        Name = name;
        Predicate = predicate;
    }
}

回到最開始的地方,當 Abp 為 Selectors 添加了一個名字為 "Abp.ApplicationServices" 的類型選擇器。其斷言委托的大體意思就是傳入的 type 參數是繼承自 IApplicationService 介面的話,則返回 true,否則返回 false

這樣在程式啟動的時候,首先註入類型的時候,會首先進入上文所述的攔截器綁定類當中,這個時候會使用 Selectors 內部的類型選擇器來調用這個集合內部的斷言委托,只要這些選擇器對象有一個返回 true,那麼就直接與當前註入的 type 綁定攔截器。

2.代碼分析

2.1 過濾器代碼分析

首先查看這個過濾器的整體類型結構,一個標準的過濾器,肯定要實現 IAsyncActionFilter 介面。從下麵的代碼我們可以看到其註入了 IAbpAspNetCoreConfiguration 和一個 IAuditingHelper 對象。這兩個對象的作用分別是判斷是否記錄日誌,另一個則是用來真正寫入日誌所使用的。

public class AbpAuditActionFilter : IAsyncActionFilter, ITransientDependency
{
    // 審計日誌組件配置對象
    private readonly IAbpAspNetCoreConfiguration _configuration;
    // 真正用來寫入審計日誌的工具類
    private readonly IAuditingHelper _auditingHelper;

    public AbpAuditActionFilter(IAbpAspNetCoreConfiguration configuration, IAuditingHelper auditingHelper)
    {
        _configuration = configuration;
        _auditingHelper = auditingHelper;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // ... 代碼實現
    }
    
    // ... 其他代碼
}

接著看 AbpAuditActionFilter() 方法內部的實現,進入這個過濾器的時候,通過 ShouldSaveAudit() 方法來判斷是否要寫審計日誌。

之後呢與 DTO 自動驗證的過濾器一樣,通過 AbpCrossCuttingConcerns.Applying() 方法為當前的對象增加了一個標識,用來告訴攔截器說我已經處理過了,你就不要再重覆處理了。

再往下就是創建審計信息,執行具體介面方法,並且如果產生了異常的話,也會存放到審計信息當中。

最後介面無論是否執行成功,還是說出現了異常信息,都會將其性能計數信息同審計信息一起,通過 IAuditingHelper 存儲起來。

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
    // 判斷是否寫日誌
    if (!ShouldSaveAudit(context))
    {
        await next();
        return;
    }

    // 為當前類型打上標識
    using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Auditing))
    {
        // 構造審計信息(AuditInfo)
        var auditInfo = _auditingHelper.CreateAuditInfo(
            context.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(),
            context.ActionDescriptor.AsControllerActionDescriptor().MethodInfo,
            context.ActionArguments
        );

        // 開始性能計數
        var stopwatch = Stopwatch.StartNew();

        try
        {
            // 嘗試調用介面方法
            var result = await next();
            
            // 產生異常之後,將其異常信息存放在審計信息之中
            if (result.Exception != null && !result.ExceptionHandled)
            {
                auditInfo.Exception = result.Exception;
            }
        }
        catch (Exception ex)
        {
            // 產生異常之後,將其異常信息存放在審計信息之中
            auditInfo.Exception = ex;
            throw;
        }
        finally
        {
            // 停止計數,並且存儲審計信息
            stopwatch.Stop();
            auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
            await _auditingHelper.SaveAsync(auditInfo);
        }
    }
}

2.2 攔截器代碼分析

攔截器處理時的總體思路與過濾器類似,其核心都是通過 IAuditingHelper 來創建審計信息和持久化審計信息的。只不過呢由於攔截器不僅僅是處理 MVC 介面,也會處理內部的一些類型的方法,所以針對同步方法與非同步方法的處理肯定會複雜一點。

攔截器呢,我們關心一下他的核心方法 Intercept() 就行了。

public void Intercept(IInvocation invocation)
{
    // 判斷過濾器是否已經處理了過了
    if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing))
    {
        invocation.Proceed();
        return;
    }

    // 通過 IAuditingHelper 來判斷當前方法是否需要記錄審計日誌信息
    if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget))
    {
        invocation.Proceed();
        return;
    }

    // 構造審計信息
    var auditInfo = _auditingHelper.CreateAuditInfo(invocation.TargetType, invocation.MethodInvocationTarget, invocation.Arguments);

    // 判斷方法的類型,同步方法與非同步方法的處理邏輯不一樣
    if (invocation.Method.IsAsync())
    {
        PerformAsyncAuditing(invocation, auditInfo);
    }
    else
    {
        PerformSyncAuditing(invocation, auditInfo);
    }
}

// 同步方法的處理邏輯與 MVC 過濾器邏輯相似
private void PerformSyncAuditing(IInvocation invocation, AuditInfo auditInfo)
{
    var stopwatch = Stopwatch.StartNew();

    try
    {
        invocation.Proceed();
    }
    catch (Exception ex)
    {
        auditInfo.Exception = ex;
        throw;
    }
    finally
    {
        stopwatch.Stop();
        auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
        _auditingHelper.Save(auditInfo);
    }
}

// 非同步方法處理
private void PerformAsyncAuditing(IInvocation invocation, AuditInfo auditInfo)
{
    var stopwatch = Stopwatch.StartNew();

    invocation.Proceed();

    if (invocation.Method.ReturnType == typeof(Task))
    {
        invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithFinally(
            (Task) invocation.ReturnValue,
            exception => SaveAuditInfo(auditInfo, stopwatch, exception)
        );
    }
    else //Task<TResult>
    {
        invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult(
            invocation.Method.ReturnType.GenericTypeArguments[0],
            invocation.ReturnValue,
            exception => SaveAuditInfo(auditInfo, stopwatch, exception)
        );
    }
}

private void SaveAuditInfo(AuditInfo auditInfo, Stopwatch stopwatch, Exception exception)
{
    stopwatch.Stop();
    auditInfo.Exception = exception;
    auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);

    _auditingHelper.Save(auditInfo);
}

這裡非同步方法的處理在很早之前的工作單元攔截器就有過講述,這裡就不再重覆說明瞭。

2.3 核心的 IAuditingHelper

從代碼上我們就可以看到,不論是攔截器還是過濾器都是最終都是通過 IAuditingHelper 對象來儲存審計日誌的。Abp 依舊為我們實現了一個預設的 AuditingHelper ,實現了其介面的所有方法。我們先查看一下這個介面的定義:

public interface IAuditingHelper
{
    // 判斷當前方法是否需要存儲審計日誌信息
    bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false);

    // 根據參數集合創建一個審計信息,一般用於攔截器
    AuditInfo CreateAuditInfo(Type type, MethodInfo method, object[] arguments);

    // 根據一個參數字典類來創建一個審計信息,一般用於 MVC 過濾器
    AuditInfo CreateAuditInfo(Type type, MethodInfo method, IDictionary<string, object> arguments);

    // 同步保存審計信息
    void Save(AuditInfo auditInfo);

    // 非同步保存審計信息
    Task SaveAsync(AuditInfo auditInfo);
}

我們來到其預設實現 AuditingHelper 類型,先看一下其內部註入了哪些介面。

public class AuditingHelper : IAuditingHelper, ITransientDependency
{
    // 日誌記錄器,用於記錄日誌
    public ILogger Logger { get; set; }
    // 用於獲取當前登錄用戶的信息
    public IAbpSession AbpSession { get; set; }
    // 用於持久話審計日誌信息
    public IAuditingStore AuditingStore { get; set; }

    // 主要作用是填充審計信息的客戶端調用信息
    private readonly IAuditInfoProvider _auditInfoProvider;
    // 審計日誌組件的配置相關
    private readonly IAuditingConfiguration _configuration;
    // 在調用 AuditingStore 進行持久化的時候使用,創建一個工作單元
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    // 用於序列化參數信息為 JSON 字元串
    private readonly IAuditSerializer _auditSerializer;

    public AuditingHelper(
        IAuditInfoProvider auditInfoProvider,
        IAuditingConfiguration configuration,
        IUnitOfWorkManager unitOfWorkManager,
        IAuditSerializer auditSerializer)
    {
        _auditInfoProvider = auditInfoProvider;
        _configuration = configuration;
        _unitOfWorkManager = unitOfWorkManager;
        _auditSerializer = auditSerializer;

        AbpSession = NullAbpSession.Instance;
        Logger = NullLogger.Instance;
        AuditingStore = SimpleLogAuditingStore.Instance;
    }

    // ... 其他實現的介面
}

2.3.1 判斷是否創建審計信息

首先分析一下其內部的 ShouldSaveAudit() 方法,整個方法的核心作用就是根據傳入的方法類型來判定是否為其創建審計信息。

其實在這一串 if 當中,你可以發現有一句代碼對方法是否標註了 DisableAuditingAttribute 特性進行了判斷,如果標註了該特性,則不為該方法創建審計信息。所以我們就可以通過該特性來控制自己應用服務類,控制裡面的的介面是否要創建審計信息。同理,我們也可以通過顯式標註 AuditedAttribute 特性來讓攔截器為這個方法創建審計信息。

public bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false)
{
    if (!_configuration.IsEnabled)
    {
        return false;
    }

    if (!_configuration.IsEnabledForAnonymousUsers && (AbpSession?.UserId == null))
    {
        return false;
    }

    if (methodInfo == null)
    {
        return false;
    }

    if (!methodInfo.IsPublic)
    {
        return false;
    }

    if (methodInfo.IsDefined(typeof(AuditedAttribute), true))
    {
        return true;
    }

    if (methodInfo.IsDefined(typeof(DisableAuditingAttribute), true))
    {
        return false;
    }

    var classType = methodInfo.DeclaringType;
    if (classType != null)
    {
        if (classType.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true))
        {
            return true;
        }

        if (classType.GetTypeInfo().IsDefined(typeof(DisableAuditingAttribute), true))
        {
            return false;
        }

        if (_configuration.Selectors.Any(selector => selector.Predicate(classType)))
        {
            return true;
        }
    }

    return defaultValue;
}

2.3.2 創建審計信息

審計信息在創建的時候,就為我們將當前調用介面時的用戶信息存放在了審計信息當中,之後通過 IAuditInfoProviderFill() 方法填充了客戶端 IP 與瀏覽器信息。

public AuditInfo CreateAuditInfo(Type type, MethodInfo method, IDictionary<string, object> arguments)
{
    // 構建一個審計信息對象
    var auditInfo = new AuditInfo
    {
        TenantId = AbpSession.TenantId,
        UserId = AbpSession.UserId,
        ImpersonatorUserId = AbpSession.ImpersonatorUserId,
        ImpersonatorTenantId = AbpSession.ImpersonatorTenantId,
        ServiceName = type != null
            ? type.FullName
            : "",
        MethodName = method.Name,
        // 將參數轉換為 JSON 字元串
        Parameters = ConvertArgumentsToJson(arguments),
        ExecutionTime = Clock.Now
    };

    try
    {
        // 填充客戶 IP 與瀏覽器信息等
        _auditInfoProvider.Fill(auditInfo);
    }
    catch (Exception ex)
    {
        Logger.Warn(ex.ToString(), ex);
    }

    return auditInfo;
}

2.4 審計信息持久化

通過上一小節我們知道了在調用審計信息保存介面的時候,實際上是調用的 IAuditingStore 所提供的 SaveAsync(AuditInfo auditInfo) 方法來持久化這些審計日誌信息的。

如果你沒有集成 Abp.Zero 項目的話,則使用的是預設的實現,就是簡單通過 ILogger 輸出審計信息到日誌當中。

預設有這兩種實現,至於第一種是 Abp 的單元測試項目所使用的。

這裡我們就簡單將一下 AuditingStore 這個實現吧,其實很簡單的,就是註入了一個倉儲,在保存的時候往審計日誌表插入一條數據即可。

這裡使用了 AuditLog.CreateFromAuditInfo() 方法將 AuditInfo 類型的審計信息轉換為資料庫實體,用於倉儲進行插入操作。

public class AuditingStore : IAuditingStore, ITransientDependency
{
    private readonly IRepository<AuditLog, long> _auditLogRepository;

    public AuditingStore(IRepository<AuditLog, long> auditLogRepository)
    {
        _auditLogRepository = auditLogRepository;
    }

    public virtual Task SaveAsync(AuditInfo auditInfo)
    {
        // 向表中插入數據
        return _auditLogRepository.InsertAsync(AuditLog.CreateFromAuditInfo(auditInfo));
    }
}

同樣,這裡建議重新實現一個 AuditingStore,存儲在 Redis 或者其他地方。

3. 後記

前幾天發現 Abp 的團隊有開了一個新坑,叫做 Abp vNext 框架,該框架全部基於 .NET Core 進行開發,而且會針對微服務項目進行專門的設計,有興趣的朋友可以持續關註。

其 GitHub 地址為:https://github.com/abpframework/abp/

官方地址為:https://abp.io/

4.點此跳轉到總目錄


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 操作給定的二叉樹,將其變換為源二叉樹的鏡像。 二叉樹的鏡像定義:源二叉樹 8 / \ 6 10 / \ / \ 5 7 9 11 鏡像二叉樹 8 / \ 10 6 / \ / ... ...
  • 數組的智能指針 使用 數組的智能指針的限制: 1,unique_ptr的數組智能指針,沒有 和 操作,但支持下標操作[] 2,shared_ptr的數組智能指針,有 和 操作,但不支持下標操作[],只能通過get()去訪問數組的元素。 3,shared_ptr的數組智能指針,必須要自定義delete ...
  • 1、lambda 函數比較輕便,即用即仍,很適合需要完成一項功能,但是此功能只在此一處使用,連名字都很隨意的情況下;2、匿名函數,一般用來給 filter, map 這樣的函數式編程服務;3、作為回調函數,傳遞給某些應用,比如消息處理 ...
  • 簡單的介紹一下吧,斯特靈數其實有很多好玩的性質和擴展的。 定義 設$S(n, m)$表示把$n$個 不同的球 放到$m$個相同的盒子里,且不允許盒子為空的方案數 稱$S$為第二類斯特靈數 計算方法 遞推: 考慮第$n$個球放到了哪裡 第一種情況是自己占一個盒子,方案為$S(n 1, m 1)$ 第二 ...
  • 現在是北京時間2018/9/29 21:25:05 我在加班,寫一個記錄,開啟我的新生活! ...
  • 按照的是中文的visual studio,用起來很不方便,因為程式員的都是英文版,平時交流時也是英文的名字 轉換語言時發現只有中文和跟隨windows系統的設置 官方給的文檔看的不是很清楚 查閱資料後總結下步驟: 1、進入到設置--選項--區域設置,發現沒有英文語言 2、進入工具--獲取工具和功能 ...
  • Steeltoe里的分散式追蹤功能與 "Spring Cloud Sleuth" 一樣,支持在日誌中記錄追蹤數據,或者上傳到遠端的服務,比如Zipkin。 Logging 在Steeltoe中使用日誌時需要引入其特有的日誌包 。 之後還需在應用程式啟動時加入日誌提供器。 接下來,引入追蹤包 。 然後 ...
  • .Net平臺下相容.NET Standard 2.0,一個實現以Lambda表達式轉轉換標準SQL語句,使用強類型操作數據的輕量級ORM工具,在減少魔法字串同時,通過靈活的Lambda表達式組合,實現業務數據查詢的多樣性。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...