文章出處

寫在前面

閱讀目錄:

  POCO Controller 你這么厲害,ASP.NET vNext 知道嗎?是的,ASP.NET vNext 曉得,因為這就是它的特性之一,嘿嘿。

  在上一篇《分享我對 ASP.NET vNext 的一些感受,也許多年回過頭看 So Easy!》博文中,簡單介紹了 ASP.NET vNext 的一些特性,文中的最后也做了一個簡單的示例,在示例中 HelloWorldController 控制器像以往我們創建 ASP.NET MVC 控制器一樣,需要繼承 Controller 基類,就像人群中某一種特定的人(公務員),Controller 用來標示這一種人是公務員一樣。后來 dudu 在評論中指出:

  ASP.NET vNext 具有 POCO Controllers 特性,控制器并不需要繼承 Controller,當時試了下確實可以,后來梁兄又指出:

  本人回復:“還沒接觸過 POCO Controller,不做過多評論,后面深入了解下,不過多謝兄臺的指教。:)”,確實對 POCO Controller 并不是很了解,但是讓我對 POCO Controller 充滿了好奇,后來看了下相關知識,發現確實像梁兄所說,簡單的輸出是可以,如果輸出 View,需要自行實現一個 ViewDataDictionary 對象,如果真要做到 POCO 形式結合 View 的話,可能付出的努力要遠大于使用現成的 Controller 繼承?關于這一點,是有些疑問,但我覺得凡事必有存在的價值,只是了解多少而已,POCO Controller 也確實在探索性質階段,關于 POCO Controller 以及相關知識,本篇內容希望起到拋磚引玉的作用。

  以下內容,只是一些個人看法,僅供參考學習,也歡迎討論指教。

POCO 是什么?

POCO(Plain Old C#/CLR Object),意為:純老式的 C#/CLR 對象,也可以稱為簡單的 C#/CLR 對象,POCO 的概念來自于 POJO(Plain Old Java Object),POJO 的內在含義是指那些沒有從任何類繼承、也沒有實現任何接口,更沒有被其它框架侵入的 C# 對象。

1、為什么會有 POJO?   

  主要是 Java 的開發者被 EJB 的繁雜搞怕了,大家經過反思,又回歸“純潔老式”的 JavaBean,即有無參構造函數,每個字段都有 getter 和 setter 的 java 類。

2、POJO 的意義

  POJO 讓開發者可專注于業務邏輯和脫離框架的單元測試。除此之外, 由于 POJO 并不須要繼承框架的類或實現其接口,開發者能夠極其靈活地搭建繼承結構和建造應用。

  POJO 的意義就在于它的簡單而靈活性,因為它的簡單和靈活,使得 POJO 能夠任意擴展,從而勝任多個場合,也就讓一個模型貫穿多個層成為現實。先寫一個核心 POJO,然后實現業務邏輯接口和持久化接口,就成了 Domain Model; UI 需要使用時,就實現數據綁定接口,變成 VO(View Object)。

3、POJO 與 PO、VO 的區別

  • POJO 是指簡單 java 對象(Plain Old Java Objects、pure old java object 或者 plain ordinary java object)。
  • PO 是指持久對象(persistant object持久對象)。
  • VO 是指值對象或者 View 對象(Value Object、View Object)。注意,本文的 VO 特指 View Object。

  持久對象實際上必須對應數據庫中的 entity,所以和 POJO 有所區別。比如說 POJO 是由 new 創建,由 GC 回收。但是持久對象是 insert 數據庫創建,由數據庫 delete 刪除的。基本上持久對象生命周期和數據庫密切相關。另外持久對象往往只能存在一個數據庫 Connection 之中,Connnection 關閉以后,持久對象就不存在了,而 POJO 只要不被 GC 回收,總是存在的。

  由于存在諸多差別,因此持久對象 PO(Persistent Object)在代碼上肯定和 POJO 不同,起碼 PO 相對于 POJO 會增加一些用來管理數據庫 entity 狀態的屬性和方法。而 ORM 追求的目標就是要 PO 在使用上盡量和 POJO 一致,對于程序員來說,他們可以把 PO 當做 POJO 來用,而感覺不到 PO 的存在。

4、POJO 的擴展

  POJO 僅包含最簡單的字段屬性,沒有多余的東西,它本質上就是一個普通的 JavaBean。但是在 POJO 的基礎上,能夠擴展出不同的對象。

  • 為 POJO 增加了持久化的方法(Insert、Update、Delete……)之后,POJO 就變成了 PO。
  • 為 POJO 增加了數據綁定功能之后,POJO 就變成了 View Object,即 UI Model。
  • 為 POJO 增加業務邏輯的方法(比如單據審核、轉帳……)之后,POJO 就變成了 Domain Model。
  • POJO 還可以當作 DTO 使用。

注:以上內容來自(http://kb.cnblogs.com/page/89750/

5、POCO VS DTO

  理解了上面關于 POCO 的概念理解,如果對 DDD(領域驅動設計)有所了解,就會發現 POCO 和 DTO 某種意義上很相似,上面關于 POCO 的擴展也有指出:POCO 可以當做 DTO 使用,但也只是某種意義上的,其實有很大的區別,可以參考:http://stackoverflow.com/questions/725348/poco-vs-dto,我理解的 POCO,就像原始人類一樣,它可以演化成現在的各個人種,比如白種人、黑種人和黃種人(PO、UI Model、Domain Model等等)。

  雖然 POCO 為原始人類,但是也包含(不必須)人類的狀態或行為,只是這種狀態或行為是最原始的,比如狩獵為生、吃生食物等等,DTO(Data Transfer Objec)是什么?關于 DTO 的概念可以參考:http://www.cnblogs.com/xishuai/p/3691787.html,DTO 是領域驅動設計中的概念,只是數據傳輸對象,不包含任何的行為和狀態,我個人覺得和 POCO 不是一個概念里面的東西,無法進行比較。

  如果非要對 DTO 和 POCO 進行區別的話,上面把 POCO 看做為原始人類,那 DTO 可以看做是原始人類的標本,他們倆只是長得比較像,僅此而已。

Controller 是什么?

  在 ASP.NET MVC Controller(控制器)的職責是:獲取 Model 數據并將 Model 傳遞給 View 對象,通知 View 對象顯示,關于 Controller 概念的官方說明:

Controllers:Controllers are the components that handle user interaction, work with the model, and ultimately select a view to render that displays UI. In an MVC application, the view only displays information; the controller handles and responds to user input and interaction. For example, the controller handles query-string values, and passes these values to the model, which in turn queries the database by using the values.

  可以把 Controller 看做是一個產品加工廠,Model 為原材料,View 為銷售平臺,原材料在產品加工廠經過一定的加工處理后,得到成型的產品,然后放在銷售平臺上進行展示銷售,從這個比喻中可以看出 Controller 的作用,就是協調 Model 和 View,那 Routing(路由)是什么?可以看做是政府的宏觀調控,用來決定加工生產什么產品?由哪家加工廠生產?這些工作都是 Routing 進行宏觀調控的。

  為了進一步方便理解 Controller 在 ASP.NET MVC 所起到的作用,我們分析下 ASP.NET MVC 整個的處理流程,首先,用戶通過 Web 瀏覽器向服務器發送一條 Url 請求,這個請求被 ASP.NET MVC 的路由映射系統截獲。路由映射系統按照映射規則,解析出控制器名 ControllerName、ActionName和各個參數 Parameters,然后,找尋 Controllers 目錄下的 ControllerNameController.cs 這個控制器類,默認情況下,系統總是找尋 Controllers 目錄下的“控制器名+ Controller ”這么一個類,然后,找尋這個類下與 ActionName 同名的方法,找到后,將 Parameters 作為參數傳給這個方法,而后 Action 方法開始執行,完成后返回相應視圖,默認情況下,會返回 Views 目錄下與 ControllerName 同名的目錄下的與 ActionName 同名的 View 文件,并且將 ViewData 傳遞到視圖。

  大致畫了下 ASP.NET MVC 的處理流程:

  上圖只是大致演示了下 ASP.NET MVC 的處理流程,更專業、詳細請參照《Pro ASP.NET MVC Framework》作者 Steven Sanderson 的一張完整請求處理流程圖(Request-Handling Pipeline Poster):

  當我們使用 VS2012 創建 MVC3/MVC4 的時候,如果把控制器中繼承的 Controller 基類去掉,結果如下:

 1 namespace MvcApplication1.Controllers
 2 {
 3     public class HomeController
 4     {
 5         //
 6         // GET: /Home/
 7         public string Index()
 8         {
 9             return "Hello World";
10         }
11     }
12 }

  HomeController 的 IndexAction 方法中返回一段字符串,路由配置是完全正確的,運行結果會是怎樣?你可能會猜到了,對,就是這樣:

  結合上面關于 ASP.NET MVC 的處理流程,你會明白為什么會報“無法找到資源”的異常?而這些同樣的代碼或配置,在 ASP.NET vNext 中卻可以正常使用,這也就是 POCO Controllers 特性的美妙之處。

  請接著往下看。

關于 POCO Controller

  理解了 POCO 和 Controller 概念,理解 POCO Controller 就不會那么困難了,網上關于 POCO Controller 特性的資料實在是少的可憐,MSDN 暫時未找到詳細的說明,如果想要深入的研究,看來只有過段時間把 ASP.NET vNext 搞熟之后了,因為 ASP.NET vNext 支持 POCO Controller 特性,這樣使得代碼更加簡潔,難道好處只是代碼簡潔嗎?其實不然,因為 ASP.NET vNext 開源和支持內置依賴注入(DI),所以你可以研究它的源代碼,在 Controller 中編寫適合自己的控制器,這樣使得你的 vNext 應用程序可擴展性或性能得到進一步的提升,如果可以的話,你甚至編寫屬于自己的一套 Controller,這一切都是從 POCO Controller 演化而來。

  其實如果往長遠一點來想,我個人覺得 POCO Controller 并不簡簡單單是 ASP.NET vNext 的一種特性,它也代表著 ASP.NET vNext 的一種方向,因為跨平臺、開源和依賴注入,使得一切都有可能,雖然 ASP.NET vNext 現在還未正式發布,POCO Controller 也僅僅是一個探路者,但是我覺得這個信號很重要,很重要,也希望大家可以接收得到。

POCO Controller 應用

1,識別 POCO Controller

  言歸正題,因為 POCO Controllers 不從 Microsoft.AspNet.Mvc.Controller 基類派生,ASP.NET vNext 如何識別 POCO Controller?關于這個問題在 stackoverflow 中提問:“How Are POCO Controllers Discovered As Controllers?”,回答如下:

There are some conventions that we use to identify a POCO controller:

  • The assembly must reference MVC(必須引用 MVC)
  • The POCO controller class must have the suffix Controller(POCO controller 必須以“Controller”后綴結尾)

  必須引用 MVC 就是在 project.json 中添加如下如下引用:

2,簡單 POCO Controller

  最簡單的 POCO Controller

 1 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 2 
 3 namespace PocoControllerDemo.Controllers
 4 {
 5     public class HelloWorldController
 6     {
 7         // GET: /<controller>/
 8         public string Index()
 9         {
10             return "Hello World";
11         }
12     }
13 }

  注意 HelloWorldController 并沒有繼承 Controller,這種寫法在 ASP.NET vNext 是可以的,因為未涉及到 Controller 的核心操作如(View、Url、Action等),只是返回一個字符串(內置依賴注入映射默認配置),首先我們看下 DefaultControllerFactory 中的源碼:

 1 // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
 2 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 
 4 using System;
 5 using Microsoft.AspNet.Mvc.Core;
 6 using Microsoft.AspNet.Mvc.ModelBinding;
 7 using Microsoft.Framework.DependencyInjection;
 8 
 9 namespace Microsoft.AspNet.Mvc
10 {
11     public class DefaultControllerFactory : IControllerFactory
12     {
13         private readonly ITypeActivator _activator;
14         private readonly IServiceProvider _serviceProvider;
15 
16         public DefaultControllerFactory(IServiceProvider serviceProvider, ITypeActivator activator)
17         {
18             _serviceProvider = serviceProvider;
19             _activator = activator;
20         }
21 
22         public object CreateController(ActionContext actionContext)
23         {
24             var actionDescriptor = actionContext.ActionDescriptor as ReflectedActionDescriptor;
25             if (actionDescriptor == null)
26             {
27                 throw new ArgumentException(
28                     Resources.FormatDefaultControllerFactory_ActionDescriptorMustBeReflected(
29                         typeof(ReflectedActionDescriptor)),
30                     "actionContext");
31             }
32 
33             var controller = _activator.CreateInstance(
34                 _serviceProvider,
35                 actionDescriptor.ControllerDescriptor.ControllerTypeInfo.AsType());
36 
37             InitializeController(controller, actionContext);
38 
39             return controller;
40         }
41 
42         public void ReleaseController(object controller)
43         {
44             var disposableController = controller as IDisposable;
45 
46             if (disposableController != null)
47             {
48                 disposableController.Dispose();
49             }
50         }
51 
52         private void InitializeController(object controller, ActionContext actionContext)
53         {
54             Injector.InjectProperty(controller, "ActionContext", actionContext);
55 
56             var viewData = new ViewDataDictionary(
57                 _serviceProvider.GetService<IModelMetadataProvider>(),
58                 actionContext.ModelState);
59             Injector.InjectProperty(controller, "ViewData", viewData);
60 
61             var urlHelper = _serviceProvider.GetService<IUrlHelper>();
62             Injector.InjectProperty(controller, "Url", urlHelper);
63 
64             Injector.CallInitializer(controller, _serviceProvider);
65         }
66     }
67 }

3,IActionResultHelper

  InitializeController 方法,用來初始化控制器,可以看出,如果我們沒有添加任何的配置操作,ASP.NET vNext 會通過內置的 IoC 容器注入默認的類型映射,比如上面的 HelloWorldController。主要包含三個類型:ActionContext、ViewDataDictionary 和 IUrlHelper,從單詞上可以簡單看出這三種類型所包含的意義,ViewDataDictionary 視圖操作的數據字典,這個在控制器返回 View 的時候會用到,這個后面有說明,我們看下在 MSDN 中關于 POCO Controller 的簡單示例,項目地址:http://www.asp.net/vnext/overview/aspnet-vnext/overview,示例代碼:

 1 public class HomeController
 2 {
 3     // Helpers can be dependency injected into the controller
 4     public HomeController(IActionResultHelper resultHelper)
 5     {
 6         Result = resultHelper;
 7     }
 8 
 9     private IActionResultHelper Result { get; set; }
10 
11     public ActionResult Index()
12     {
13         return Result.Json(new { message = "Poco controllers!" });
14     }
15 }

  執行結果:

  代碼中主要對 IActionResultHelper 進行構造函數注入,你可以實現屬于自己的 IActionResultHelper 實例,使用 ASP.NET vNext 內置IoC容器進行依賴注入,IActionResultHelper 是什么?IActionResultHelper is a helper for creating action results。就是說 IActionResultHelper 用來創建 Action 結果的契約,Index 方法中返回一個 Json 格式數據,返回類型為ActionResult,Result.Json 也就是 IActionResultHelper.Json,也就是說所有的一切都表明,你可以創建屬于自己的 IActionResultHelper,包含自定義的 IActionResultHelper.Json 類型格式。

  以下是在 MSDN 中找到僅存的關于 POCO Controller 的說明:

The Controller class also gives you convenient access to the HTTP request context, the current principal (IPrincipal), a view bag, and other useful properties. So the Controller class is certainly useful, but it’s not required. For a light-weight controller, you might prefer a POCO controller.

4,IModelMetadataProvider、ViewDataDictionary

  以上是有關 Action 的操作,如果涉及到 View 操作,又是怎么實現?MSDN 太坑爹了,都沒有詳細說明,有位國外哥們和我遇到一樣的問題了,詳細地址:http://geekswithblogs.net/stun/archive/2014/06/04/asp.net-vnext-blog-post-series.aspx,以下來自這位國外哥們的描述:

Today, I want to start this blog post series with a teaser code snippet for those developers familiar with the ASP.NET MVC.
Getting Started with ASP.NET MVC 6 article from ASP.NET website shows how to write a lightweight POCO (plain-old CLR object) MVC Controller class in the upcoming ASP.NET MVC 6.
However, it doesn't show us how to use the IActionResultHelper interface to render a View.
This is how I wrote my POCO MVC Controller based on the https://github.com/aspnet/Home/blob/master/samples/HelloMvc/Controllers/HomeController.cs sample from Github.

  雖然在 MSDN 中關于 View 操作沒有相關說明,但是這位國外哥們搞出來了,不服不行啊,所謂前人種樹,后人乘涼,來看一下這位哥們的解決方案:

 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 using MvcSample.Web.Models;
 4 
 5 namespace MvcSample.Web
 6 {
 7     public class HomeController
 8     {
 9         IActionResultHelper html;
10         IModelMetadataProvider mmp;
11 
12         public HomeController(IActionResultHelper h, IModelMetadataProvider mmp)
13         {
14             this.html = h;
15             this.mmp = mmp;
16         }
17 
18         public IActionResult Index()
19         {
20             var viewData = new ViewDataDictionary<User>(mmp) { Model = User() };
21             return html.View("Index", viewData);
22         }
23 
24         public User User()
25         {
26             return new User { Name = "My name", Address = "My address" };
27         }
28     }
29 }

  可以看出除了 IActionResultHelper 類型映射,還包含 IModelMetadataProvider 類型,關于 IModelMetadataProvider 可以從 DefaultControllerFactory 源碼中得到一些信息,在創建 ViewDataDictionary 實例的時候,需要傳入 IModelMetadataProvider 類型的參數,也就是代碼中的 new ViewDataDictionary<User>(mmp) { Model = User() }; IModelMetadataProvider 需要引入 using Microsoft.AspNet.Mvc.ModelBinding,關于 IModelMetadataProvider 定義,請參照:

 1 using System;
 2 using System.Collections.Generic;
 3 
 4 namespace Microsoft.AspNet.Mvc.ModelBinding
 5 {
 6     public interface IModelMetadataProvider
 7     {
 8         IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType);
 9         ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName);
10         ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType);
11     }
12 }

  html.View("Index", viewData); 使用方式就像我們平常向 View 傳入數據一樣,如果不傳入數據,但是也要傳入 ViewDataDictionary 類型參數,修改 HelloWorldController 控制器代碼:

 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 
 4 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 5 
 6 namespace PocoControllerDemo.Controllers
 7 {
 8     public class HelloWorldController
 9     {
10         private IActionResultHelper actionResultHelper { get; set; }
11         private IModelMetadataProvider modelMetadataProvider { get; set; }
12         // Helpers can be dependency injected into the controller
13         public HelloWorldController(IActionResultHelper resultHelper, IModelMetadataProvider metadataProvider)
14         {
15             actionResultHelper = resultHelper;
16             modelMetadataProvider = metadataProvider;
17         }
18         // GET: /<controller>/
19         public ActionResult Index()
20         {
21             //return Result.Json(new { message = "Poco controllers!" });
22             var viewData = new ViewDataDictionary(modelMetadataProvider);
23             return actionResultHelper.View("Index", viewData);
24         }
25     }
26 }

  Index.cshtml 視圖中只有 html 代碼:“<h1>Hello World</h1>”,運行結果:

5,IActionDiscoveryConventions、IControllerDescriptorFactory

  既然 POCO Controller 都這么厲害了(以上內容),還能不能來點更厲害的呢?當然會有,比如擴展 EndpointActionDiscoveryConventions 和IControllerDescriptorFactory 使得控制器并不需要以“Controller”名稱結尾,可以按照自己的喜好創建配置結束符,雖然功能作用不是很大,但是這些操作在 MVC3/MVC4 中連想都不敢想,也從另一方便看出 POCO Controller 可擴展型是多么的強大,怎么實現?

  首先創建 EndpointActionDiscoveryConventions 類,繼承 DefaultActionDiscoveryConventions(IActionDiscoveryConventions 的默認派生類),在 IActionDiscoveryConventions 接口定義中有個重要的虛方法:IsController,參數類型為:TypeInfo,用來判斷當前 typeInfo 是否為 Controller?示例代碼:

 1 using System;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Mvc;
 4 
 5 namespace PocoControllerDemo
 6 {
 7     public class EndpointActionDiscoveryConventions : DefaultActionDiscoveryConventions
 8     {
 9         public override bool IsController(TypeInfo typeInfo)
10         {
11             var isController = base.IsController(typeInfo);
12             return isController || typeInfo.Name.EndsWith("Endpoint", StringComparison.OrdinalIgnoreCase);
13         }
14     }
15 }

  可以看出 IsController 控制器判斷,以“Endpoint”為結束符的 TypeInfo 返回 True,表示當前 TypeInfo 為 Controller。僅僅實現 Controller 判斷還是不夠的,還要對 IControllerDescriptorFactory 進行配置,示例代碼:

 1 using System;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Mvc;
 4 
 5 namespace PocoControllerDemo
 6 {
 7     public class EndpointControllerDescriptorFactory : IControllerDescriptorFactory
 8     {
 9         public ControllerDescriptor CreateControllerDescriptor(TypeInfo type)
10         {
11             var descriptor = new ControllerDescriptor(type);
12 
13             if (descriptor.Name.EndsWith("Endpoint", StringComparison.Ordinal))
14             {
15                 var nameProp = descriptor.GetType().GetProperty("Name");
16                 var oldName = nameProp.GetValue(descriptor, null) as string;
17                 nameProp.SetValue(descriptor, oldName.ToLowerInvariant().Replace("endpoint", string.Empty));
18             }
19             return descriptor;
20         }
21     }
22 }

  HelloWorldEndpoint 控制器代碼:

 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 
 4 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 5 
 6 namespace PocoControllerDemo.Controllers
 7 {
 8     public class HelloWorldEndpoint
 9     {
10         private IActionResultHelper actionResultHelper { get; set; }
11         private IModelMetadataProvider modelMetadataProvider { get; set; }
12         // Helpers can be dependency injected into the controller
13         public HelloWorldEndpoint(IActionResultHelper resultHelper, IModelMetadataProvider metadataProvider)
14         {
15             actionResultHelper = resultHelper;
16             modelMetadataProvider = metadataProvider;
17         }
18         // GET: /<controller>/
19         public ActionResult Index()
20         {
21             //return Result.Json(new { message = "Poco controllers!" });
22             var viewData = new ViewDataDictionary(modelMetadataProvider);
23             return actionResultHelper.View("Index", viewData);
24         }
25     }
26 }

  注意 HelloWorldEndpoint 并不是 HelloWorldController,Startup 添加 IActionDiscoveryConventions 和 IControllerDescriptorFactory 類型配置:

1             app.UseServices(services =>
2             {
3                 services.AddMvc();
4                 services.AddTransient<IActionDiscoveryConventions, EndpointActionDiscoveryConventions>();
5                 services.AddTransient<IControllerDescriptorFactory, EndpointControllerDescriptorFactory>();
6             });

  運行截圖:

后記

  示例代碼下載:http://pan.baidu.com/s/1gd8AoxH

  ASP.NET vNext 是 .NET 未來發展的一種方向,那我覺得 POCO Controller 就是 ASP.NET vNext 未來發展的一種方向,還是那句話,因為跨平臺、開源和依賴注入,使一切皆有可能,關于 ASP.NET vNext 的相關探討,未完待續。

  再次在 MSDN 中搜“ASP.NET vNext”,雖然過去了才幾天的時間,發現已經又增加了幾百篇的文章,看來全球的程序員對 ASP.NET vNext 還是充滿熱情的,也希望博客園的園友們多多奉獻,更期待 ASP.NET vNext 正式版發布早日到來。

  如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^

  參考資料:


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()