主頁 > .NET開發 > .NET6+Quartz實作定時任務

.NET6+Quartz實作定時任務

2023-03-27 09:54:28 .NET開發

在實際作業中,經常會有一些需要定時操作的業務,如:定時發郵件,定時統計資訊等內容,那么如何實作才能使得我們的專案整齊劃一呢?本文通過一些簡單的小例子,簡述在.Net6+Quartz實作定時任務的一些基本操作,及相關知識介紹,僅供學習分享使用,如有不足之處,還請指正,

什么是定時任務?

定時任務,也叫任務調度,是指在一定的載體上,根據具體的觸發規則,執行某些操作,所以定時任務需要滿足三個條件:載體(Scheduler),觸發規則(Trigger),具體業務操作(Job),如下所示:

什么是Quartz?

Quartz 是一個開源的作業調度框架,它完全由 Java 寫成,并設計用于 J2SE 和 J2EE 應用中,它提供了巨大的靈 活性而不犧牲簡單性,你能夠用它來為執行一個作業而創建簡單的或復雜的調度,它有很多特征,如:資料庫支持,集群,插件,EJB 作業預構 建,JavaMail 及其它,支持 cron-like 運算式等等,雖然Quartz最初是為Java撰寫的,但是目前已經有.Net版本的Quartz,所以在.Net中應用Quartz已經不再是奢望,而是輕而易舉的事情了,

Github上開源網址為:https://github.com/quartznet

 

關于Quartz的快速入門和API檔案,可以參考:https://www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html

 

涉及知識點

在Quartz框架中,主要介面和API如下所示:

 

 其中IScheduler,ITrigger , IJob 三者之間的關系,如下所示:

 

 

 Quartz安裝

為了方便,本示例創建一個基于.Net6.0的控制臺應用程式,在VS2022中,通過Nuget包管理器進行安裝,如下所示:

 

創建一個簡單的定時器任務

要開發一個簡單,完整且能運行的定時器任務,步驟如下所示:

1. 創建作業單元Job

創建任務需要實作IJob介面,如下所示:

 1 using Quartz;
 2 using System.Diagnostics;
 3 
 4 namespace DemoQuartz.QuartzA.Job
 5 {
 6     /// <summary>
 7     /// 測驗任務,實作IJob介面
 8     /// </summary>
 9     public class TestJob : IJob
10     {
11         public TestJob()
12         {
13             Console.WriteLine("執行建構式");//表示每一次計劃執行,都是一次新的實體
14         }
15 
16         public Task Execute(IJobExecutionContext context)
17         {
18             return Task.Run(() =>
19              {
20                  Console.WriteLine($"******************************");
21                  Console.WriteLine($"測驗資訊{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
22                  Console.WriteLine($"******************************");
23                  Console.WriteLine();
24              });
25         }
26     }
27 }

2. 創建時間軸Scheduler

時間軸也是任務執行的載體,可以通過StdSchedulerFactory進行獲取,如下所示:

1 //創建計劃單元(時間軸,載體)
2 StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
3 var scheduler = await schedulerFactory.GetScheduler();
4 await scheduler.Start();

3. 創建觸發規則Trigger

觸發規則就是那些時間點執行任務,可通過TriggerBuilder進行構建,如下所示:

1 //Trigger時間觸發機制
2 var trigger = TriggerBuilder.Create()
3     .WithIdentity("TestTrigger","TestGroup")
4     //.StartNow() //立即執行
5     .WithSimpleSchedule(w=>w.WithIntervalInSeconds(5).WithRepeatCount(5))//.RepeatForever()//無限回圈
6     //.WithCronSchedule("5/10 * * * * ?") //通過Cron運算式定制時間觸發規則, 示例表示從5開始,每隔10秒一次
7     .Build();

4. 創建任務描述

任務描述定義了具體的任務名稱,分組等內容,可通過JobBuilder進行構建,如下所示:

1 //Job詳細描述
2 var jobDetail = JobBuilder.Create<TestJob>()
3     .WithDescription("這是一個測驗Job")
4     .WithIdentity("TestJob", "TestGroup")
5     .Build();

5. 建立三者聯系

通過載體,將規則和作業單元串聯起來,如下所示:

1 //把時間和任務通過載體關聯起來
2 await scheduler.ScheduleJob(jobDetail, trigger);

6. 簡單示例測驗

通過運行程式,示例結果如下所示:

 

傳遞引數

在Quartz框架下,如果需要給執行的Job傳遞引數,可以通過兩種方式:

jobDetail.JobDataMap,作業描述時通過JobDataMap傳遞引數,

trigger.JobDataMap, 時間觸發時通過JobDataMap傳遞引數,

在Job作業單元中,可以通過Context中對應的JobDataMap獲取引數,

傳遞引數,如下所示:

 1 //傳遞引數
 2 jobDetail.JobDataMap.Add("name", "Alan");
 3 jobDetail.JobDataMap.Add("age", 20);
 4 jobDetail.JobDataMap.Add("sex", true);
 5 
 6 
 7 //trigger同樣可以傳遞引數
 8 trigger.JobDataMap.Add("like1", "meimei");
 9 trigger.JobDataMap.Add("like2", "football");
10 trigger.JobDataMap.Add("like3", "sing");

獲取引數,如下所示:

 1 //獲取引數
 2 var name = context.JobDetail.JobDataMap.GetString("name");
 3 var age = context.JobDetail.JobDataMap.GetInt("age");
 4 var sex = context.JobDetail.JobDataMap.GetBoolean("sex") ? "" : "";
 5 
 6 var like1 = context.Trigger.JobDataMap.GetString("like1");
 7 var like2 = context.Trigger.JobDataMap.GetString("like2");
 8 var like3 = context.Trigger.JobDataMap.GetString("like3");
 9 
10 //context.MergedJobDataMap.GetString("aa");//注意如果使用MergedJobDataMap,JobDetail和Trigger中用到相同的Key,則后面設定的會覆寫前面設定的,

注意:如果使用MergedJobDataMap,JobDetail和Trigger中用到相同的Key,則后面設定的會覆寫前面設定的,

任務特性

假如我們的定時任務,執行一次需要耗時比較久,而且后一次執行需要等待前一次完成,并且需要前一次執行的結果作為參考,那么就需要設定任務的任性,因為默認情況下,作業單元在每一次運行都是一個新的實體,相互之間獨立運行,互不干擾,所以如果需要存在一定的關聯,就要設定任務的特性,主要有兩個,如下所示:

  • [PersistJobDataAfterExecution]//在執行完成后,保留JobDataMap資料
  • [DisallowConcurrentExecution]//不允許并發執行,即必須等待上次完成后才能執行下一次

 

 

 以上兩個特性,只需要標記在任務對應的類上即可,標記上后,只需要往對應的JobDataMap中添加值即可,

監聽器

在Quartz框架下,有三種監聽器,分別是:時間軸監聽器ISchedulerListener,觸發規則監聽器ITriggerListener,任務監聽器IJobListener,要實作對應監聽器,實作對應介面即可,實作監聽器步驟:

1. 創建監聽器

根據不同的需要,可以創建不同的監聽器,如下所示:

時間軸監聽器SchedulerListener

  1 public class TestSchedulerListener : ISchedulerListener
  2 {
  3     public Task JobAdded(IJobDetail jobDetail, CancellationToken cancellationToken = default)
  4     {
  5         return Task.Run(() => {
  6             Console.WriteLine("Test Job is added.");
  7         });
  8     }
  9 
 10     public Task JobDeleted(JobKey jobKey, CancellationToken cancellationToken = default)
 11     {
 12         return Task.Run(() => {
 13             Console.WriteLine("Test Job is deleted.");
 14         });
 15     }
 16 
 17     public Task JobInterrupted(JobKey jobKey, CancellationToken cancellationToken = default)
 18     {
 19         return Task.Run(() => {
 20             Console.WriteLine("Test Job is Interrupted.");
 21         });
 22     }
 23 
 24     public Task JobPaused(JobKey jobKey, CancellationToken cancellationToken = default)
 25     {
 26         return Task.Run(() => {
 27             Console.WriteLine("Test Job is paused.");
 28         });
 29     }
 30 
 31     public Task JobResumed(JobKey jobKey, CancellationToken cancellationToken = default)
 32     {
 33         return Task.Run(() => {
 34             Console.WriteLine("Test Job is resumed.");
 35         });
 36     }
 37 
 38     public Task JobScheduled(ITrigger trigger, CancellationToken cancellationToken = default)
 39     {
 40         return Task.Run(() => {
 41             Console.WriteLine("Test Job is scheduled.");
 42         });
 43     }
 44 
 45     public Task JobsPaused(string jobGroup, CancellationToken cancellationToken = default)
 46     {
 47         return Task.Run(() => {
 48             Console.WriteLine("Test Jobs is paused.");
 49         });
 50     }
 51 
 52     public Task JobsResumed(string jobGroup, CancellationToken cancellationToken = default)
 53     {
 54         return Task.Run(() => {
 55             Console.WriteLine("Test Jobs is resumed.");
 56         });
 57     }
 58 
 59     public Task JobUnscheduled(TriggerKey triggerKey, CancellationToken cancellationToken = default)
 60     {
 61         return Task.Run(() => {
 62             Console.WriteLine("Test Jobs is un schedulered.");
 63         });
 64     }
 65 
 66     public Task SchedulerError(string msg, SchedulerException cause, CancellationToken cancellationToken = default)
 67     {
 68         return Task.Run(() => {
 69             Console.WriteLine("Test scheduler is error.");
 70         });
 71     }
 72 
 73     public Task SchedulerInStandbyMode(CancellationToken cancellationToken = default)
 74     {
 75         return Task.Run(() => {
 76             Console.WriteLine("Test scheduler is standby mode.");
 77         });
 78     }
 79 
 80     public Task SchedulerShutdown(CancellationToken cancellationToken = default)
 81     {
 82         return Task.Run(() => {
 83             Console.WriteLine("Test scheduler is shut down.");
 84         });
 85     }
 86 
 87     public Task SchedulerShuttingdown(CancellationToken cancellationToken = default)
 88     {
 89         return Task.Run(() => {
 90             Console.WriteLine("Test scheduler is shutting down.");
 91         });
 92     }
 93 
 94     public Task SchedulerStarted(CancellationToken cancellationToken = default)
 95     {
 96         return Task.Run(() => {
 97             Console.WriteLine("Test scheduleer is started.");
 98         });
 99     }
100 
101     public Task SchedulerStarting(CancellationToken cancellationToken = default)
102     {
103         return Task.Run(() => {
104             Console.WriteLine("Test scheduler is starting.");
105         });
106     }
107 
108     public Task SchedulingDataCleared(CancellationToken cancellationToken = default)
109     {
110         return Task.Run(() => {
111             Console.WriteLine("Test scheduling is cleared.");
112         });
113     }
114 
115     public Task TriggerFinalized(ITrigger trigger, CancellationToken cancellationToken = default)
116     {
117         return Task.Run(() => {
118             Console.WriteLine("Test trigger is finalized.");
119         });
120     }
121 
122     public Task TriggerPaused(TriggerKey triggerKey, CancellationToken cancellationToken = default)
123     {
124         return Task.Run(() => {
125             Console.WriteLine("Test trigger is paused.");
126         });
127     }
128 
129     public Task TriggerResumed(TriggerKey triggerKey, CancellationToken cancellationToken = default)
130     {
131         return Task.Run(() => {
132             Console.WriteLine("Test trigger is resumed.");
133         });
134     }
135 
136     public Task TriggersPaused(string? triggerGroup, CancellationToken cancellationToken = default)
137     {
138         return Task.Run(() => {
139             Console.WriteLine("Test triggers is paused.");
140         });
141     }
142 
143     public Task TriggersResumed(string? triggerGroup, CancellationToken cancellationToken = default)
144     {
145         return Task.Run(() => {
146             Console.WriteLine("Test triggers is resumed.");
147         });
148     }
149 }

觸發規則監聽器TriggerListener

 1 /// <summary>
 2 /// 觸發器監聽
 3 /// </summary>
 4 public class TestTriggerListener : ITriggerListener
 5 {
 6     public string Name => "TestTriggerListener";
 7 
 8     public Task TriggerComplete(ITrigger trigger, IJobExecutionContext context, SchedulerInstruction triggerInstructionCode, CancellationToken cancellationToken = default)
 9     {
10         //任務完成
11         return Task.Run(() => {
12             Console.WriteLine("Test trigger is complete.");
13         
14         });
15     }
16 
17     public Task TriggerFired(ITrigger trigger, IJobExecutionContext context, CancellationToken cancellationToken = default)
18     {
19         return Task.Run(() => {
20             Console.WriteLine("Test trigger is fired.");
21 
22         });
23     }
24 
25     public Task TriggerMisfired(ITrigger trigger, CancellationToken cancellationToken = default)
26     {
27         return Task.Run(() => {
28             Console.WriteLine("Test trigger is misfired.");
29 
30         });
31     }
32 
33     public Task<bool> VetoJobExecution(ITrigger trigger, IJobExecutionContext context, CancellationToken cancellationToken = default)
34     {
35         return Task.Run(() => {
36             Console.WriteLine("Test trigger is veto.");
37             return false;//是否終止
38         });
39     }
40 }

JobListener任務監聽器

 1 /// <summary>
 2 /// TestJob監聽器
 3 /// </summary>
 4 public class TestJobListener : IJobListener
 5 {
 6     public string Name => "TestJobListener";
 7 
 8     public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
 9     {
10         //任務被終止時
11         return Task.Run(() => {
12             Console.WriteLine("Test Job is vetoed.");
13         });
14     }
15 
16     public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
17     {
18         //任務被執行時
19         return Task.Run(() => {
20             Console.WriteLine("Test Job is to be executed.");
21         });
22     }
23 
24     public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default)
25     {
26         //任務已經執行
27         return Task.Run(() => {
28             Console.WriteLine("Test Job was executed.");
29         });
30     }
31 }

2. 添加監聽

在時間軸上的監聽管理器中進行添加,如下所示:

1 //增加監聽
2 scheduler.ListenerManager.AddJobListener(new TestJobListener());
3 scheduler.ListenerManager.AddTriggerListener(new TestTriggerListener());
4 scheduler.ListenerManager.AddSchedulerListener(new TestSchedulerListener());

日志管理

在Quartz框架中,創建之前會進行日志創建檢測,所以如果需要獲取框架中的日志資訊,可以進行創建實作ILogProvider,如下所示:

 1 public class TestLogProvider : ILogProvider
 2 {
 3     public Logger GetLogger(string name)
 4     {
 5         return (level, func, exception, parameters) =>
 6         {
 7             if (level >= Quartz.Logging.LogLevel.Info && func != null)
 8             {
 9                 Console.WriteLine("[" + DateTime.Now.ToLongTimeString() + "] [" + level + "] " + func(), parameters);
10             }
11             return true;
12         };
13     }
14 
15     public IDisposable OpenMappedContext(string key, object value, bool destructure = false)
16     {
17         throw new NotImplementedException();
18     }
19 
20     public IDisposable OpenNestedContext(string message)
21     {
22         throw new NotImplementedException();
23     }
24 }

然后在當前的Scheduler中,添加日志即可,如下所示:

1 //日志
2 LogProvider.SetCurrentLogProvider(new TestLogProvider());

完整示例

在添加了監聽器,日志,引數傳遞,任務特性后,完整的目錄結構,如下所示:

 

 示例截圖

 

以上就是.Net6.0+Quartz開發控制臺定時任務調度的全部內容,以上任務都是硬編碼的固定程式,包括任務的啟停,那么如果能通過可視化界面來創建以及管理任務,是不是一件很爽的事情呢,這也是后續需要探討的內容,


作者:小六公子
出處:http://www.cnblogs.com/hsiang/
本文著作權歸作者和博客園共有,寫文不易,支持原創,歡迎轉載【點贊】,轉載請保留此段宣告,且在文章頁面明顯位置給出原文連接,謝謝,
關注個人公眾號,定時同步更新技術及職場文章

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/548269.html

標籤:.NET技术

上一篇:abp(net core)+easyui+efcore實作倉儲管理系統——ABP升級7.3上(五十八)

下一篇:將紙殼CMS通知通過WebHook發送到釘釘

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more