基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了標籤模塊和友情鏈接模塊的所有功能,本篇來繼續完成博客最後的模塊,文章的管理。

文章列表 & 刪除

先將分頁查詢的列表給整出來,這塊和首頁的分頁列表是類似的,就是多了個Id字段。

先添加兩條路由規則。

@page "/admin/posts"
@page "/admin/posts/{page:int}"

新建返回數據默認QueryPostForAdminDto.cs。

//QueryPostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class QueryPostForAdminDto
    {
        /// <summary>
        /// 年份
        /// </summary>
        public int Year { get; set; }

        /// <summary>
        /// Posts
        /// </summary>
        public IEnumerable<PostBriefForAdminDto> Posts { get; set; }
    }
}

//PostBriefForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class PostBriefForAdminDto : PostBriefDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}

然後添加所需的參數:當前頁碼、限制條數、總頁碼、文章列表返回數據模型。

/// <summary>
/// 當前頁碼
/// </summary>
[Parameter]
public int? page { get; set; }

/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;

/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;

/// <summary>
/// 文章列表數據
/// </summary>
private ServiceResult<PagedList<QueryPostForAdminDto>> posts;

然後在初始化函數OnInitializedAsync()中調用API獲取文章數據.

/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    // 設置默認值
    page = page.HasValue ? page : 1;

    await RenderPage(page);
}

/// <summary>
/// 點擊頁碼重新渲染數據
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
    // 獲取數據
    posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostForAdminDto>>>($"/blog/admin/posts?page={page}&limit={Limit}");

    // 計算總頁碼
    TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}

在初始化中判斷page參數,如果沒有值給他設置一個默認值1。RenderPage(int? page)方法是調用API返回數據,並計算出總頁碼值。

最後在頁面上進行數據綁定。

<AdminLayout>
    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap archive">
            <NavLink style="float:right" href="/admin/post"><h3>~~~ 新增文章 ~~~</h3></NavLink>
            @if (posts.Success && posts.Result.Item.Any())
            {
                @foreach (var item in posts.Result.Item)
                {
                    <h3>@item.Year</h3>
                    @foreach (var post in item.Posts)
                    {
                        <article class="archive-item">
                            <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(post.Id))"></NavLink>
                            <NavLink title="編輯" @onclick="@(async () => await Common.NavigateTo($"/admin/post/{post.Id}"))"></NavLink>
                            <NavLink target="_blank" class="archive-item-link" href="@("/post" + post.Url)">@post.Title</NavLink>
                            <span class="archive-item-date">@post.CreationTime</span>
                        </article>
                    }
                }
                <nav class="pagination">
                    @for (int i = 1; i <= TotalPage; i++)
                    {
                        var _page = i;

                        if (page == _page)
                        {
                            <span class="page-number current">@_page</span>
                        }
                        else
                        {
                            <a class="page-number" @onclick="@(() => RenderPage(_page))" href="/admin/posts/@_page">@_page</a>
                        }
                    }
                </nav>
            }
            else
            {
                <ErrorTip />
            }
        </div>
    }
</AdminLayout>

HTML內容放在組件AdminLayout中,當 posts 沒加載完數據的時候显示加載組件<Loading />。

在頁面上循環遍歷文章數據和翻頁頁碼,每篇文章標題前面添加兩個按鈕刪除和編輯,同時單獨加了一個新增文章的按鈕。

刪除文章調用DeleteAsync(int id)方法,需要傳遞參數,當前文章的id。

新增和編輯按鈕都跳轉到”/admin/post”頁面,當編輯的時候將id也傳過去即可,路由規則為:”/admin/post/{id}”。

刪除文章“方法如下:

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task DeleteAsync(int id)
{
    // 彈窗確認
    bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這篇該死的文章嗎");

    if (confirmed)
    {
        var response = await Http.DeleteAsync($"/blog/post?id={id}");

        var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

        if (result.Success)
        {
            await RenderPage(page);
        }
    }
}

刪除之前進行二次確認,避免誤刪,當確認刪除之後調用刪除文章API,最後重新渲染數據即可。

新增 & 更新文章

完成了後台文章列表的查詢和刪除,現在整個博客模塊功能就差新增和更新文章了,勝利就在前方,沖啊。

這塊的開發工作耗費了我太多時間,因為想使用 markdown 來寫文章,找了一圈下來沒有一個合適的組件,所以退而求次只能選擇現有的markdown編輯器來實現了。

我這裏選擇了開源的編輯器Editor.md,有需要的可以去 Github 自己下載,https://github.com/pandao/editor.md 。

將下載的資源包解壓放在 wwwroot 文件夾下,默認是比較大的,而且還有很多示例文件,我已經將其精簡了一番,可以去我 Github 下載使用。

先來看下最終的成品效果吧。

是不是感覺還可以,廢話不多說,接下里告訴大家如何實現。

在 Admin 文件夾下添加post.razor組件,設置路由,並且引用一個樣式文件,在頁面中引用樣式文件好像不太符合標準,不過無所謂了,這個後台就自己用,而且還就這一個頁面用得到。

@page "/admin/post"
@page "/admin/post/{id:int}"

<link href="./editor.md/css/editormd.css" rel="stylesheet" />

<AdminLayout>
    ...
</AdminLayout>

把具體HTML內容放在組件AdminLayout中。

因為新增和編輯放在同一個頁面上,所以當id參數不為空的時候需要添加一個id參數,同時默認一進來就讓頁面显示加載中的組件,當頁面和數據加載完成后在显示具體的內容,所以在指定一個布爾類型的是否加載參數isLoading。

我們的編輯器主要依賴JavaScript實現的,所以這裏不可避免要使用到JavaScript了。

在app.js中添加幾個全局函數。

switchEditorTheme: function () {
    editor.setTheme(localStorage.editorTheme || 'default');
    editor.setEditorTheme(localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default');
    editor.setPreviewTheme(localStorage.editorTheme || 'default');
},
renderEditor: async function () {
	await this._loadScript('./editor.md/lib/zepto.min.js').then(function () {
	    func._loadScript('./editor.md/editormd.js').then(function () {
	        editor = editormd("editor", {
	            width: "100%",
	            height: 700,
	            path: './editor.md/lib/',
	            codeFold: true,
	            saveHTMLToTextarea: true,
	            emoji: true,
	            atLink: false,
	            emailLink: false,
	            theme: localStorage.editorTheme || 'default',
	            editorTheme: localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default',
	            previewTheme: localStorage.editorTheme || 'default',
	            toolbarIcons: function () {
	                return ["bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "h1", "h2", "h3", "h4", "h5", "h6", "list-ul", "list-ol", "hr", "link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "html-entities", "emoji", "watch", "preview", "fullscreen", "clear", "||", "save"]
	            },
	            toolbarIconsClass: {
	                save: "fa-check"
	            },
	            toolbarHandlers: {
	                save: function () {
	                    func._shoowBox();
	                }
	            },
	            onload: function () {
	                this.addKeyMap({
	                    "Ctrl-S": function () {
	                        func._shoowBox();
	                    }
	                });
	            }
	        });
	    });
	});
},
_shoowBox: function () {
    DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');
},
_loadScript: async function (url) {
    let response = await fetch(url);
    var js = await response.text();
    eval(js);
}

renderEditor主要實現了動態加載JavaScript代碼,將markdown編輯器渲染出來。這裏不多說,都是Editor.md示例裏面的代碼。

為了兼容暗黑色主題,這裏還加了一個切換編輯器主題的JavaScript方法,switchEditorTheme。

_shoowBox就厲害了,這個方法是調用的.NET組件中的方法,前面我們用過了在Blazor中調用JavaScript,這裏演示了JavaScript中調用Blazor中的組件方法。

現在將所需的幾個參數都添加到代碼中。

/// <summary>
/// 定義一個委託方法,用於組件實例方法調用
/// </summary>
private static Func<Task> action;

/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;

/// <summary>
/// 修改時的文章Id
/// </summary>
[Parameter]
public int? Id { get; set; }

/// <summary>
/// 格式化的標籤
/// </summary>
private string tags { get; set; }

/// <summary>
/// 默認显示加載中
/// </summary>
private bool isLoading = true;

/// <summary>
/// 文章新增或者修改輸入參數
/// </summary>
private PostForAdminDto input;

/// <summary>
/// API返回的分類列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;

大家看看註釋就知道參數是做什麼的了。

現在我們在初始化函數中將所需的數據通過API獲取到。

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    action = ChangeOpenStatus;

    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    if (Id.HasValue)
    {
        var post = await Http.GetFromJsonAsync<ServiceResult<PostForAdminDto>>($"/blog/admin/post?id={Id}");

        if (post.Success)
        {
            var _post = post.Result;
            input = new PostForAdminDto
            {
                Title = _post.Title,
                Author = _post.Author,
                Url = _post.Url,
                Html = _post.Html,
                Markdown = _post.Markdown,
                CategoryId = _post.CategoryId,
                Tags = _post.Tags,
                CreationTime = _post.CreationTime
            };

            tags = string.Join(",", input.Tags);
        }
    }
    else
    {
        input = new PostForAdminDto()
        {
            Author = "阿星Plus",
            CreationTime = DateTime.Now
        };
    }

    categories = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");

    // 渲染編輯器
    await Common.InvokeAsync("window.func.renderEditor");

    // 關閉加載
    isLoading = !isLoading;
}

action是一個異步的委託,在初始化中執行了ChangeOpenStatus方法,這個方法等會說,然後獲取localStorage中token的值。

通過參數Id是否有值來判斷當前是新增文章還是更新文章,如果有值就是更新文章,這時候需要根據id去將文章的數據拿到賦值給PostForAdminDto對象展示在頁面上,如果沒有可以添加幾個默認值給PostForAdminDto對象。

因為文章需要分類和標籤的數據,同時這裏將分類的數據也查出來,標籤默認是List列表,將其轉換成字符串類型。

但完成上面操作后,調用JavaScript方法renderEditor渲染渲染編輯器,最後關閉加載,显示頁面。

現在來看看頁面。

<AdminLayout>
    @if (isLoading)
    {
        <Loading />
    }
    else
    {
        <div class="post-box">
            <div class="post-box-item">
                <input type="text" placeholder="標題" autocomplete="off" @bind="@input.Title" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="作者" autocomplete="off" @bind="@input.Author" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div class="post-box-item">
                <input type="text" placeholder="URL" autocomplete="off" @bind="@input.Url" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="時間" autocomplete="off" @bind="@input.CreationTime" @bind:format="yyyy-MM-dd HH:mm:sss" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div id="editor">
                <textarea style="display:none;">@input.Markdown</textarea>
            </div>

            <Box OnClickCallback="@SubmitAsync" Open="@Open" ButtonText="發布">
                <div class="box-item">
                    <b>分類:</b>
                    @if (categories.Success && categories.Result.Any())
                    {
                        @foreach (var item in categories.Result)
                        {
                            <label><input type="radio" name="category" value="@item.Id" @onchange="@(() => { input.CategoryId = item.Id; })" checked="@(item.Id == input.CategoryId)" />@item.CategoryName</label>
                        }
                    }
                </div>
                <div class="box-item"></div>
                <div class="box-item">
                    <b>標籤:</b>
                    <input type="text" @bind="@tags" @bind:event="oninput" />
                </div>
            </Box>
        </div>
    }
</AdminLayout>

添加了四個input框,分別用來綁定標題、作者、URL、時間,<div id="editor"></div>中為編輯器所需。

然後我這裏還是把之前的彈窗組件搞出來了,執行邏輯不介紹了,在彈窗組件中自定義显示分類和標籤的內容,將獲取到的分類和標籤綁定到具體位置。

每個分類都是一個radio標籤,並且對應一個點擊事件,點哪個就把當前分類的Id賦值給PostForAdminDto對象。

所有的input框都使用@bind和@bind:event綁定數據和獲取數據。

Box彈窗組件這裏自定義了按鈕文字,ButtonText="發布"。

/// <summary>
/// 改變Open狀態,通知組件渲染
/// </summary>
private async Task ChangeOpenStatus()
{
    Open = true;

    var markdown = await Common.InvokeAsync<string>("editor.getMarkdown");
    var html = await Common.InvokeAsync<string>("editor.getHTML");

    if (string.IsNullOrEmpty(input.Title) || string.IsNullOrEmpty(input.Url) ||
        string.IsNullOrEmpty(input.Author) || string.IsNullOrEmpty(markdown) ||
        string.IsNullOrEmpty(html))
    {
        await Alert();
    }

    input.Html = html;
    input.Markdown = markdown;

    StateHasChanged();
}

/// <summary>
/// 暴漏給JS執行,彈窗確認框
/// </summary>
[JSInvokable("showbox")]
public static void ShowBox()
{
    action.Invoke();
}
/// <summary>
/// alert提示
/// </summary>
/// <returns></returns>
private async Task Alert()
{
    Open = false;

    await Common.InvokeAsync("alert", "\n好像漏了點什麼吧");
    return;
}

現在可以來看看ChangeOpenStatus方法了,這個是改變當前彈窗狀態的一個方法。為什麼需要這個方法呢?

因為在Blazor中JavaScript想要調用組件內的方法,方法必須是靜態的,那麼只能通過這種方式去實現了,在靜態方法是不能夠直接改變彈窗的狀態值的。

其實也可以不用這麼麻煩,因為我在編輯器上自定義了一個按鈕,為了好看一些所以只能曲折一點,嫌麻煩的可以直接在頁面上搞個按鈕執行保存數據邏輯也是一樣的。

使用JSInvokableAttribute需要在_Imports.razor中添加命名空間@using Microsoft.JSInterop。

ChangeOpenStatus中獲取到文章內容:HTML和markdown,賦值給PostForAdminDto對象,要先進行判斷頁面上的幾個參數是否有值,沒值的話給出提示執行Alert()方法,最後使用StateHasChanged()通知組件其狀態已更改。

Alert方法就是調用原生的JavaScriptalert方法,給出一個提示。

ShowBox就是暴漏給JavaScript的方法,使用DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');進行調用。

那麼現在一切都正常進行的情況下,點擊編輯器上自定義的保存按鈕,頁面上值不為空的情況下就會彈出我們的彈窗組件Box。

最後在彈窗組件的回調方法中執行新增文章還是更新文章。

/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
    if (string.IsNullOrEmpty(tags) || input.CategoryId == 0)
    {
        await Alert();
    }

    input.Tags = tags.Split(",");

    var responseMessage = new HttpResponseMessage();

    if (Id.HasValue)
        responseMessage = await Http.PutAsJsonAsync($"/blog/post?id={Id}", input);
    else
        responseMessage = await Http.PostAsJsonAsync("/blog/post", input);

    var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
    if (result.Success)
    {
        await Common.NavigateTo("/admin/posts");
    }
}

打開彈窗后執行回調事件之前還是要判斷值是否為空,為空的情況下還是給出alert提示,此時將tags標籤還是轉換成List列表,根據Id是否有值去執行新增數據或者更新數據,最終成功后跳轉到文章列表頁。

本片到這裏就結束了,主要攻克了在Blazor中使用Markdown編輯器實現新增和更新文章,這個系列差不多就快結束了,預計還有2篇的樣子,感謝各位的支持。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

Spring系列.AOP原理簡析

Spring AOP使用簡介

Spring的兩大核心功能是IOC和AOP。當我們使用Spring的AOP功能時是很方便的。只需要進行下面的配置即可。

@Component
@Aspect
public class MyAspect {

//PointCut匹配的方法必須是Spring中bean的方法
//Pointcut可以有下列方式來定義或者通過&& || 和!的方式進行組合.
//下面定義的這些切入點就可以通過&& ||組合

private static Logger logger = LoggerFactory.getLogger(MyAspect.class);

//*:代表方法的返回值可以是任何類型
//整個表達式匹配controller包下面任何的的echo方法,方法入參樂意是任意
@Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(..))")
public void pointCut1(){}

@Before("pointCut1()")
public void befor(){
    logger.info("前置通知vvvv...");
    logger.info("我要做些事情...");
}
}

然後再開啟註解

//自動選擇合適的AOP代理
//傳統xml這樣配置:<aop:aspectj-autoproxy/>

//exposeProxy = true屬性設置成true,意思是將動態生成的代理類expose到AopContext的ThreadLocal線程
//可以通過AopContext.currentProxy();獲取到生成的動態代理類。

//proxyTargetClass屬性設置動態代理使用JDK動態代理還是使用CGlib代理,設置成true是使用CGlib代理,false的話是使用JDK動態代理

//注意:如果使用Spring Boot的話,下面的配置可以不需要。AopAutoConfiguration這個自動配置類中已經自動開啟了AOP
//默認使用CGLIB動態代理,Spring Boot配置的優先級高於下面的配置

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = false)
public class AopConfig {

}

通上面的配置,當我們調用controller包下面的任何類的echo方法時就會觸發前置通知。其實這個說法不是很準確。因為我們調用的類已經不是我們自己寫的類了。而是Spring框架通過動態代理生成的類。

稍微了解一點Spring AOP的同學都會知道Spring的AOP是通過動態代理實現的。那Spring是怎麼生成動態代理類,並將Advice織入代理類的呢?整個流程是怎樣的呢?下面就分析下Spring生成動態代理類的過程。

需要說明下的是,本博客旨在梳理整個AOP動態代理的過程,細節方面需要大家自己去看。

@EnableAspectJAutoProxy幹了些啥

如果讓你從頭開始研究下AOP的原理,你是不是一頭霧水,根本不知道從何入手。但其實看Spring的代碼有個小技巧:如果你要研究一個功能,可以從開啟這個功能的Enable註解開始看。Spring的很多功能都是通過Enable註解開啟的,所以這些註解肯定和這些功能相關。

那麼這邊我們可以從@EnableAspectJAutoProxy這個註解開始着手,看下這個註解做了些什麼操作。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    //設置為true的話就一直使用cglib動態代理
    //設置為false的話,對於接口使用jdk動態代理,對於類使用cglib代理
	boolean proxyTargetClass() default false;
	boolean exposeProxy() default false;
}

看到上面的@Impoer註解,我們很自然就會想到去看AspectJAutoProxyRegistrar這個類。

//AspectJAutoProxyRegistrar源代碼
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * 主要作用也就是註冊AnnotationAwareAspectJAutoProxyCreator
     */
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //註冊AnnotationAwareAspectJAutoProxyCreator的BeanDefinition
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            //給上面註冊的BeanDefinition中添加兩個屬相proxyTargetClass和exposeProxy
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }
    }

}

我們可以看到上面的類中也沒幹什麼特別的事情,就註冊了一個BeanDefinition。如果我們點進去看下AnnotationAwareAspectJAutoProxyCreator這個類的源代碼會發現這個類竟然實現了InstantiationAwareBeanPostProcessor這個接口。熟悉Spring尿性的朋友會敏銳的感覺到Spring可能是在postProcessBeforeInstantiation或者postProcessAfterInstantiation這些方法中對Bean進行動態代理的。

“大膽假設,小心求證”,讓我們帶着這個猜想去看看AnnotationAwareAspectJAutoProxyCreator到底幹了些什麼?

AnnotationAwareAspectJAutoProxyCreator生成動態代理類

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
    Object cacheKey = getCacheKey(beanClass, beanName);

    if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
        if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
        }
        if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
        }
    }
    // 一般不指定CustomTargetSource,所以不會進入這段代碼,所以關鍵代碼在
    // postProcessAfterInitialization中
    // Create proxy here if we have a custom TargetSource.
    // Suppresses unnecessary default instantiation of the target bean:
    // The TargetSource will handle target instances in a custom fashion.
    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
    if (targetSource != null) {
        if (StringUtils.hasLength(beanName)) {
            this.targetSourcedBeans.add(beanName);
        }
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
        Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    return null;
}

下面是創建動態代理類的關鍵代碼。

@Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                //這邊是創建代碼類的關鍵代碼
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }

        // Create proxy if we have advice.
        //獲取當前Bean配置的advice,這步是關鍵
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            //創建代理
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }

        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

層層dedug進去我們能看到下面這段代碼,我們口中常說的JDK動態代理和Cglib動態代理就是在這邊生成的。

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                //生成JDK動態代理
                return new JdkDynamicAopProxy(config);
            }
            //生成Cglib動態代理
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }
    /**
     * Determine whether the supplied {@link AdvisedSupport} has only the
     * {@link org.springframework.aop.SpringProxy} interface specified
     * (or no proxy interfaces specified at all).
     */
    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] ifcs = config.getProxiedInterfaces();
        return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
    }

到此,我們已經簡單分析了Spring動態代理類的生成流程。

PS:關於InstantiationAwareBeanPostProcessor接口和BeanPostProcessor接口大家可以自行了解下,這兩個接口是Spring中非常重要的接口。看懂了這兩個接口,Spring很多“神秘”的功能你就能理解了。

簡單總結

通過上面分析,其實我們發現如果不去看AOP動態代理類生成的細節的話,整個Spring AOP的流程還是挺簡單的:

  • @EnableAspectJAutoProxy註解通過AopConfigUtils這個工具類註冊AnnotationAwareAspectJAutoProxyCreator這個類,這個類實現了InstantiationAwareBeanPostProcessor接口,所以會在Bean實例化前後對Bean做一系列額外的操作;
  • AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization中會找出所有和當前Bean相關的Advice,如果找到就創建相應的動態代理類,如果找不到就不生成,返回原始類。

所以整個大流程就這麼簡單。

一些重要類:

  • @EnableAspectJAutoProxy;
  • AspectJAutoProxyRegistrar:註冊AnnotationAwareAspectJAutoProxyCreator
  • AnnotationAwareAspectJAutoProxyCreator:AOP動態代理自動生成的處理類,其他類似的類有AspectJAwareAdvisorAutoProxyCreator和InfrastructureAdvisorAutoProxyCreator等;
  • AopConfigUtils:AOP配置工具類
  • ProxyFactory:代理工廠
  • AopProxy接口:常見實現類ObjenesisCglibAopProxy、JdkDynamicAopProxy

參考

  • https://blog.csdn.net/zhoushimiao1990/article/details/89853368

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Asp.Net Core入門之自定義中間件

什麼是中間件?

這裏引用官方解釋:

       中間件是用於組成應用程序管道來處理請求和響應的組件。管道內的每一個組件都可以選擇是否將請求交給下一個組件、並在管道中調用下一個組件之前和之後執行某些操作。請求委託被用來建立請求管道,請求委託處理每一個 HTTP 請求。

       請求委託通過使用 IApplicationBuilder 類型的 Run、Map 以及 Use 擴展方法來配置,並在 Startup 類中傳給 Configure 方法 。每個單獨的請求委託都可以被指定為一個內嵌匿名方法,或其定義在一個可重用的類中。這些可重用的類被稱作 中間件 或 中間件組件。每個位於請求管道內的中間件組件負責調用管道中下一個組件,或適時短路調用鏈。

       ASP.NET 請求管道由一系列的請求委託所構成,它們一個接着一個被調用,如圖所示(該執行線程按黑色箭頭的順序執行):

 

ASP.Net Core內置很多中間件,如:靜態文件,身份驗證,MVC等。

這裏需要注意的是,中間件添加的順序非常重要,這會影響他們處理請求的順序。如果順序添加不對,很可能其中某个中間件會造成管道短路,不能觸發下一個請求委託,造成系統異常。

自定義中間件

1.自定義中間件處理邏輯。

 

 1     public class MyMiddleWare
 2     {
 3         private readonly ILogger logger;
 4         private readonly RequestDelegate next;
 5         /// <summary>
 6         /// 通過構造函數 注入Logger日誌和Http請求委託
 7         /// </summary>
 8         /// <param name="_logger"></param>
 9         /// <param name="_next"></param>
10         public MyMiddleWare(ILogger<MyMiddleWare> _logger, RequestDelegate _next)
11         {
12             logger = _logger;
13             next = _next;
14         }
15 
16         /// <summary>
17         /// 在調用下一個組件之前和之後執行某些操作
18         /// </summary>
19         /// <param name="context"></param>
20         /// <returns></returns>
21         public async Task Invoke(HttpContext context)
22         {
23             logger.LogInformation("自定義中間件,開始" + DateTime.Now);
24             await next.Invoke(context);
25             logger.LogInformation("自定義中間件,結束" + DateTime.Now);
26         }
27     }

 

2.使用IApplicationBuilder嚮應用程序的請求管道添加中間件

1     public static class ServiceExtensions
2     {
3         public static IApplicationBuilder UserMyMiddleWare(this IApplicationBuilder app)
4         {
5             return app.UseMiddleware<MyMiddleWare>();
6         }
7     }

3.通過Configure方法添加自定義中間件。

Configure 方法用於指定 ASP.NET 應用程序將如何響應每一個 HTTP 請求。中間件需要最終在這裏添加。

 1         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
 2         {
 3             if (env.IsDevelopment())
 4             {
 5                 app.UseDeveloperExceptionPage();
 6             }
 7             //wwroot目錄下靜態文件
 8             app.UseStaticFiles();
 9             //添加自定義中間件
10             app.UserMyMiddleWare();
11         }

 

當然大家也可以在Configure 方法中直接使用 app.UseMiddleware<MyMiddleWare>()進行添加,我這裡是單獨做了一個派生類,方便管理吧。

 

好了,然後我們啟動程序,大家會發現我們剛才添加的中間件處理請求信息:

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※FB行銷專家,教你從零開始的技巧

代碼之間為什麼要加空格?

據我觀察,大多數人寫代碼喜歡在符號兩側加空格,而且還會嘲諷不加空格的人。這些支持者認為,加空格可以讓代碼美觀,增強代碼的可讀性。但是我不喜歡加空格,因為這麼寫對代碼含義沒有影響,浪費空間還很麻煩。我不認為多幾個空格就能讓原本凌亂的代碼清晰起來,反而會讓代碼看起來很鬆散,影響閱讀。

以上,是我在知乎上看到的一個題目,感覺蠻清新的,說得似乎有理有據。就好像在說,據我觀察,大多數人喜歡早晚刷牙,而且還會嘲笑不刷牙的人。這些支持者認為,刷牙有益於牙齒健康,空腔清新。但我不喜歡刷牙,因為我牙齒沒壞,吃嘛嘛香。我不認為刷牙能保護牙齒健康,反而很浪費時間。

對吧?我敢保證,這世界上有不少人不刷牙。說出來不怕大家笑話,我上初中之前真沒刷過牙,就算是到現在,很多農村的孩子仍然沒有刷牙的習慣。

在我寫代碼的前一兩年裡,寫代碼也不習慣加空格,覺得很麻煩,並且加不加空格,真的不影響代碼的運行效果。

// 加空格的寫法
int a = 5;
int b = 3;
a = a + b;

// 不加空格的寫法
int a=5;
int b=3;
a=a+b;

想知道為什麼後來我開始刷牙了呢?

記得小學五年級的時候,牙齒疼過一次,真的是喪心病狂地疼,什麼東西也吃不了。那時候,哪有看牙科的,完全沒有,至少在我那時候的成長環境下。

沒辦法,疼了大概有一周的時間,終於打聽到鄰村有一個老先生,他那有秘方,父母就帶我去了。還真的見效,某一種秘制的液體滴在牙齒上,超級冰涼的那種,感覺整個牙齒完全失去了知覺。

有了這段痛苦的記憶,上初中后,看到很多同學都刷牙,於是我也就買了牙刷。初中的條件還是很差勁的,冬天,沒有熱水,洗臉的水和刷牙的水,都是前一天下午飯缸里接的水,放在課桌里。第二天早上到了教室,同學們就互幫互助,端着對方的飯缸倒水洗臉,臉沒凍壞,牙沒凍壞就是萬幸啊。

我在代碼里加空格,和開始刷牙有相似的經歷。

Thisisavarofthings=yourfuckingtype+otherdamnedresult*me;
oh=mygodblessedsweetheart-thesaintangel/cursedcorpsehere;

哦,老天,上面這段代碼里就完全沒有空格,看起來密密麻麻,可讀性差到你感覺自己的雙眼受到了侮辱。

當符號兩側的單詞很簡單的時候,簡單到只有 a 和 b 的時候,的確,不加空格好像看起來更緊湊一些。但考慮到實際的編碼中,命名要有意義,長度都不會太短,這時候,如果沒有空格的輔助,代碼就真的不堪入目了。

Thisisavarofthings = yourfuckingtype + otherdamnedresult * me;
oh = mygodblessedsweetheart - thesaintangel / cursedcorpsehere;

OK,現在加了空格,是不是觀感上就完全不一樣了。單詞與單詞之間,很清楚,很協調,不拖泥帶水。

一般的 IDE 都有自動格式化代碼的快捷鍵,寫完代碼來那麼一下,立馬就會把該有的空格給加上。我現在已經完全習慣手敲空格了,感覺自己很優雅,甚至有點大師範的感覺,畢竟程序員還是要有點追求的,對吧?

說到這,不得不替古人感慨一下,由於紙張稀缺,所以大師們寫出來的文章都是沒有標點符號的,要是沒點文化水平,密密麻麻一團的字擠在一起,真不一定能讀的懂,況且漢語是這麼博大精深。

我們算是幸運的,讀到的文言文都經過了加工,編輯們幫我們加了標點符號,所以看起來不至於那麼吃力。但我就特別討厭一些題目,讓加標點符號斷句的那些,很無聊。

所以,別任性,該加空格就加空格,你寫的代碼可不只是給你一個人看的,負責任點。

換句話說,我也可以寫文章不換行,對吧?寫一行兩行文字真沒必要換行。但段落多了,還不換行,我保證你看不下去。

不過,需要注意一點的是,我看有些視頻課里,有些講課人習慣什麼地方都加空格,我也是有點受不了的,比如說方法名和小括號之間——public void print (),這就有點不合時宜了。

按照規約來說,方法名和小括號之間是不需要空格的,因為沒有空格看起來更自然,更像一個方法,而不是割裂的關鍵字和小括號。

寫代碼不是鑽牛角尖,就像有些人喜歡在 if 語句里使用 == false 一樣——if (isFlase == false) {},他覺得這樣的代碼更具有可讀性,這不是出來搞笑的嗎?

最次,你應該寫成下面這樣子:

if (isTrue) {

} else {

}

這都比在 if 條件中使用 == false 來得讓人舒服。

做程序員嘛,總得有個程序員的樣子,想優雅點的話,把非“!”用上——if (!isFlase),搞那些無病呻吟的代碼,真的很掉檔次。

無規矩不成方圓,你如果不想遵守代碼規約的話,不想團隊協作的話,請隨意處置你的代碼,誰也拿你沒辦法。逃逃逃)

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

※網頁設計最專業,超強功能平台可客製化

Dotnet core基於ML.net的銷售數據預測實踐

ML.net已經進到了1.5版本。作為Microsoft官方的機器學習模型,你不打算用用?

 

一、前言

ML.net可以讓我們很容易地在各種應用場景中將機器學習加入到應用程序中。這是這個框架很重要的一點。

通過ML.net,我們可以使用手中的可用數據,進行預測、分析、檢測,而不需要進行過於複雜的編程。

ML.net的核心,同樣是機器學習模型。它採用同樣的步驟,通過指定算法來訓練模型,將輸入數據轉換為所需的預測數據。

更重要的是,ML.net基於.NET Core,這讓它可以非常簡單地跨平台,在Windows、Linux、MacOS上運行,並成為我們服務端的一部分內容。

 

回到今天的主題。

我們用實際的例子,完成一個通過歷史銷售數據進行單變量時序分析(單譜分析),以預測未來銷量的需求。

    為了防止不提供原網址的轉載,特在這裏加上原文鏈接:https://www.cnblogs.com/tiger-wang/p/13150021.html

二、開發環境&基礎工程

這個Demo的開發環境是:Mac + VS Code + Dotnet Core 3.1.2。

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.1.201
 Commit:    b1768b4ae7

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.15
 OS Platform: Darwin
 RID:         osx.10.15-x64
 Base Path:   /usr/local/share/dotnet/sdk/3.1.201/

Host (useful for support):
  Version: 3.1.3
  Commit:  4a9f85e9f8

.NET Core SDKs installed:
  3.1.201 [/usr/local/share/dotnet/sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

 

首先,在這個環境下建立工程:

  1. 創建Solution
% dotnet new sln -o demo
The template "Solution File" was created successfully.
  1. 這次,我們用Console創建工程
% cd demo
% dotnet new console -o demo
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on demo/demo.csproj...
  Determining projects to restore...
  Restored demo/demo.csproj (in 143 ms).

Restore succeeded.
  1. 把工程加到Solution中
% dotnet sln add demo/demo.csproj

基礎工程搭建完成。

三、引入ML.net庫

為了使用ML.net,我們需要引入Microsoft.ML庫:

% cd demo
% dotnet add package Microsoft.ML

除此之外,本文是基於時序的預測,還需要引入時序庫Microsoft.ML.TimeSeries:

% dotnet add package Microsoft.ML.TimeSeries

 

我們今天用到的算法是單譜分析(SSA)。SSA會將時序分解為一組主要成分, 並將這些成分解釋為信號,對應於趨勢、噪音、季節性及許多其他的因素,然後重新構建這些成分,用來預測未來某個時間的值。

四、準備數據

為了這個DEMO,我準備了一個包含全年365天實際銷售金額的數據。

其中這個數據又分為了兩部分,第一部分是前11個月的數據,用來做訓練,第二部分是12月一個月的數據,用來評估模型。

兩部分數據的鏈接如下:訓練數據,評估數據

兩個數據文件均為CSV文件,數據結構完全相同,下面是一段內容範例:

2018-12-21,17959.0
2018-12-22,19537.03
2018-12-23,20068.0
2018-12-24,20013.0
2018-12-25,21005.0
2018-12-26,16876.0
2018-12-27,15150.0
2018-12-28,15669.0
2018-12-29,25048.0
2018-12-30,25236.0

五、代碼開發

  1. 準備一個輸入模型ModelInput
public class ModelInput
{

    [LoadColumn(0)]
    public DateTime action_time { get; set; }
    [LoadColumn(1)]
    public float count { get; set; }
}

這個模型對應數據文件的結構,分兩個字段,第一個是日期,第二個是對應的銷售金額。

  1. 準備另一個輸出模型ModelOutput
public class ModelOutput
{

    public float[] forecasted_count { get; set; }
    public float[] lower_count { get; set; }
    public float[] upper_count { get; set; }
}

這個模型跟隨預測結果的輸出,其中:

  • forecasted_count – 預測時間段內的預測值
  • lower_count – 預測時間段內預測值的下限
  • upper_count – 預測時間段內預測值的上限

 

  1. 初始化機器學習的實例
MLContext mlContext = new MLContext();

執行所有 ML.NET 操作都是從MLContext類開始,初始化 MLContext將創建一個新的 ML.net 環境,並在模型創建工作流對象之間共享該環境。

  1. 加載數據

ML.net有多種數據的加載方式,可以通過文件、數據庫、JSON/XML、內存中加載數據,甚至可以用自定義的數據庫連接加載數據。

本文的DEMO中,數據在CSV文件中,所以,我們採用下面的方式加載:

static readonly string _data1Path = Path.Combine(Environment.CurrentDirectory, "data1.csv");
static readonly string _data2Path = Path.Combine(Environment.CurrentDirectory, "data2.csv");

static void Main(string[] args)
{
    MLContext mlContext = new MLContext();

    IDataView data1View = mlContext.Data.LoadFromTextFile<ModelInput>(_data1Path, separatorChar: ',', hasHeader: false);
    IDataView data2View = mlContext.Data.LoadFromTextFile<ModelInput>(_data2Path, separatorChar: ',', hasHeader: false);
}

IDataView是數據的承載空間。

  1. 定義時序分析管道
var forecastingPipeline = mlContext.Forecasting.ForecastBySsa(
    outputColumnName: "forecasted_count",
    inputColumnName: "count",
    windowSize: 7,
    seriesLength: 30,
    trainSize: 334,
    horizon: 7,
    confidenceLevel: 0.95f,
    confidenceLowerBoundColumn: "lower_count",
    confidenceUpperBoundColumn: "upper_count");

前面有說過,我們採用單譜分析,所以代碼中我們選擇了mlContext.Forecasting.ForecastBySsa。

解釋一下這裏面的幾個參數:

  • trainSize – 數據樣本的數量,也就是訓練數據的行數(在這個文件中,一行是一個數據樣本,共334行)
  • seriesLength – 從數據樣本按時序採樣時的間隔,這裡是30天
  • windowSize – 樣本周期的天數,這裡是7天
  • horizon – 預測結果的天數
  • confidenceLevel – 上下限的可信度。預測屬於合理猜測,不總是完全準確。
  • 其它幾個參數,對應輸入輸出模型的字段名

 

  1. 訓練模型

管道定義完成,數據加載完成,下面要進行數據訓練。

SsaForecastingTransformer forecaster = forecastingPipeline.Fit(data1View);

跟隨上一節,管道是單譜管道,所以訓練也是單譜訓練SsaForecastingTransformer。

 

程序執行到這裏,數據訓練完成。

  1. 模型評估

模型評估不是必須環節。

模型評估的意義在於:通過評估模型的性能,來調整管道的參數,以達到最佳的預測效果。

模型評估也有多種方式。在這裏,我們採用平均絕對誤差和均方根誤差來做評估依據。

static void Evaluate(IDataView testData, ITransformer model, MLContext mlContext)
{
    IDataView predictions = model.Transform(testData);

    IEnumerable<float> actual =
        mlContext.Data.CreateEnumerable<ModelInput>(testData, true)
            .Select(p => p.count);

    IEnumerable<float> forecast =
        mlContext.Data.CreateEnumerable<ModelOutput>(predictions, true)
            .Select(p => p.forecasted_count[0]);

    var metrics = actual.Zip(forecast, (actualValue, forecastValue) => actualValue - forecastValue);

    var MAE = metrics.Average(error => Math.Abs(error));
    var RMSE = Math.Sqrt(metrics.Average(error => Math.Pow(error, 2)));

    Console.WriteLine("評估結果");
    Console.WriteLine("---------------------");
    Console.WriteLine($"平均絕對誤差: {MAE:F3}");
    Console.WriteLine($"均方根誤差: {RMSE:F3}\n");
}

在這個方法中,我們取評估數據的實際值actual和通過訓練數據生成的預測值forecast,計算兩個誤差並輸出。

在Main中調用此方法:

static void Main(string[] args)
{
        /* 這兒是前邊訓練的代碼,略過 */

        Evaluate(data2View, forecaster, mlContext);
}
static void Evaluate(IDataView testData, ITransformer model, MLContext mlContext)
{
      /* 這兒是評估模型的方法,上面有,略過 */
}

輸出結果類似於以下內容:

評估結果
---------------------
平均絕對誤差: 23.442
均方根誤差: 174.236

兩個指標:

  • 平均絕對誤差 – 度量預測與實際值之間的接近程度。 此值介於 0 到無限大之間。 越接近 0,模型的質量越好。
  • 均方根誤差 – 匯總模型中的錯誤。 此值介於 0 到無限大之間。 越接近 0,模型的質量越好。

 

  1. 預測

訓練模型調整到滿意后,即可開始預測的工作:

var forecastEngine = forecaster.CreateTimeSeriesEngine<ModelInput, ModelOutput>(mlContext);
ModelOutput forecast = forecastEngine.Predict();

這兩行代碼,在內存中加載前邊訓練好的模型,並進行預測操作。預測數據的結果放在forecast中。

對應於分析管道定義中的horizon,預測數據包含7天的預測結果。

  1. 預測結果輸出

放在forecast中的數據,對應模型ModelOutput,可以用在任何地方。

在本文中,我們直接显示到Console:

IEnumerable<string> forecastOutput =
    mlContext.Data.CreateEnumerable<ModelInput>(data2View, reuseRowObject: false)
        .Take(7)
        .Select((ModelInput data, int index) =>
        {
            string action_date = data.action_time.ToString("yyyy-MM-dd");
            float actual_count = data.count;
            float lowerEstimate = Math.Max(0, forecast.lower_count[index]);
            float estimate = forecast.forecasted_count[index];
            float upperEstimate = forecast.upper_count[index];
            return $"日期: {action_date}\n" +
            $"實際值: {actual_count}\n" +
            $"預測下限估值: {lowerEstimate}\n" +
            $"預測估值: {estimate}\n" +
            $"預測上限估值: {upperEstimate}\n";
        });

Console.WriteLine("預測結果");
Console.WriteLine("---------------------");
foreach (var prediction in forecastOutput)
{
    Console.WriteLine(prediction);
}

運行結果類似於以下內容:

預測結果
---------------------
日期: 2018-12-01
實際值: 24566.08
預測下限估值: 16791.379
預測估值: 20394.115
預測上限估值: 23996.852

 

完成!

六、延伸內容

ML.net包含了很多機器學習的內容。其中,我自己認為時序預測是用途很廣的一個部分,可以用在

  • 銷售預測
  • 庫存預警
  • 活動策劃輔助

以及其它諸如天氣、股票、人口等諸多內容上,依靠過去和現在的數據,分析兩者之間的關係,然後利用得到的這個關係去預測未來的數據。

因此,在這個分類中,我的第一篇文章就寫了時序預測。

 

機器學習,核心是各種算法,而算法的基礎是一類數學。這是一個很高的坎。刷算法,線性的部分還好,一旦到了冪次或矩陣,沒有正統的學習,是很難有突破的。而即便刷通了,也只是皮毛性的理解,距離創造算法的大神,還有很長的距離。

所以,退而求其次,對很多人而言,與其花大功夫去研究算法,不如多研究下如何能把現有的算法或工具用好。

 

還有,在應用中,你能用機器學習來預測銷量、預測庫存,有沒有很自豪?是不是很高大上?

 

(全文完)

 

本文的對應代碼,在https://github.com/humornif/Demo-Code/tree/master/0013/demo

 

 

微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此聲明和原文鏈接

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Hexo快速構建個人小站-Hexo初始化和將項目託管在Github(一)

 

背景交代

    相信每個程序員都有自己做過個人網站,博客之類的項目了,但是現在還在維護嗎?反正我前前後后做過2到3個了,維護一段時間后因為一些不可逆的原因(主要是懶)都沒有維護了,購買的一些域名和服務器信息也都過期了,最近玩了一下hexo,發現這個東西挺方便的,基本半個小時就可以搞完,並且如果 完全託管在github上基本就是0成本,用作學習記錄輸出是夠了。

 

1.依賴於nodejs安裝,安裝nodejs和npm

下載地址,可以對照電腦系統版本進行下載安裝:https://nodejs.org/en/download/
現在nodejs的安裝包內置了npm,所以下載安裝完成之後,nodejs和npm都會安裝好

檢查安裝是否成功

安裝成後會显示出對應的版本信息,由於我電腦之前就安裝過了,所以應該不是最新的版本

2.安裝hexo

安裝命令:

sudo npm i -g hexo

直接一步就安裝完成了,然後可以通過hexo -v查看是否安裝成,成功安裝的話,會打印出上面截圖中的一些版本信息

3.hexo初始化博客項目

命令:

hexo init

 

初始化完成之後,看看hexo在文件夾給我生成了哪些文件

 

如果你是一名前端或者nodejs開發者,相信對這些文件再熟悉不過了,還是對上述幾個文件簡單解釋一下:

  • node_modules:存放依賴包信息

  • public:存放生成的頁面

  • scaffolds:存放生成文章的一些模板

  • source:存放用命令創建的各種文章

  • themes:存放博客使用的主題

  • _config.yml:存放整個博客的配置

  • db.json:存放source解析所得到的

  • package.json:存放項目所需模塊項目的配置信息

 

4.將生成的博客項目跑起來

1、清除
hexo clean
2、生成
hexo g
3、啟動服務
hexo server

 

啟動完成后就可以按照提示,打開 http://localhost:4000 來訪問我們在本地初始化好的博客項目了

可以看到我們的項目已經成功跑起來了,成功的邁出了第一步,先暫且按耐住激動的心情,我們繼續!

 

5.在GitHub上創建倉庫用於託管博客項目

打開Github:https://github.com/PeyShine,新建一個倉庫,如圖

倉庫名稱需要配置為:個人名字.github.io,倉庫設置為公開,然後點擊創建就可以

 

6.配置_config.yml

deploy:
   type: git
   repository: https://github.com/PeyShine/PeyShine.github.io.git
   branch: master

  

7.上傳項目 

在此之前請先安裝一個插件

npm install hexo-deployer-git --save

  

部署(上傳到GitHub) 

hexo d 或者 hexo deploy

上傳過程中輸入了用戶名和密碼,如果提前配置好ssh也可以不用輸入

到Github上看一下,是否上傳成功

可以看到文件都已經上傳成功了

8.成功訪問

通過域名“https://peyshine.github.io” 來訪問

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※FB行銷專家,教你從零開始的技巧

全球首創 東京用碳交易替大型建築減碳 六年減下1/4

環境資訊中心記者 陳文姿報導

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

福島核電廠淨化核污水 檢出半衰期1570萬年超標碘129

摘錄自2018年8月20日中央社報導

日本媒體報導,2011年發生核子事故的福島第一核電廠所淨化的核污水,除了氚以外,還被檢測出其他放射性物質,包括半衰期長達1570萬年的碘129。

共同社報導,福島縣知事內堀雅雄今天(20日)表示,有關這個問題,希望中央政府與營運福島第一核電廠的東京電力公司,能仔細說明水質狀況數據,及對環境與當地形象會造成什麼影響。

東電公司表示,2017年度使用放射性廢水處理系統(ALPS)淨化含氚水後的檢測結果顯示,半衰期約1570萬年的碘129,每公升最多檢出62.2貝克,比標準值多9貝克。

關於如何處理持續聚積在福島第一核電廠內的含氚水,基於對人體影響較小等原則,日本政府已設置小委員會全面展開討論,月底也將舉行傾聽國民意見的聽證會;但關於氚以外的放射性物質則幾乎沒被討論。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

利比亞炸藥捕魚猖獗 破壞海洋生態又擾安寧

摘錄自2018年8月20日中央社報導

利比亞首都的黎波里地中海岸的居民週末經常被爆炸巨響嚇醒,許多漁民為增加漁獲量使用炸藥,破壞海洋生態也毫不在意。

利比亞自從2011年爆發動亂造成武器和炸藥泛濫後,炸藥捕魚(或稱「爆破捕魚」)盛行,且免受懲罰。

利比亞自此陷入混亂、暴力充斥,2大敵對陣營爭奪政權,各路民兵搶奪石油財富控制權。專家與官員表示,政府因此未將維護魚類資源和環境列為優先。

根據規定,未經許可進行炸藥捕魚以及使用炸藥都是違法行為,但炸魚的漁民似乎免於受罰。

這些漁民甚至拍攝畫面上傳到網路,顯示爆炸時海水噴濺到半空中,水面浮出許多被炸暈或炸死的魚。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※FB行銷專家,教你從零開始的技巧

校工噴灑年年春致癌 孟山都遭判賠2.89億

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS

美國舊金山一陪審團17日認定,農化大廠孟山都公司需為校工暴露於年年春(Roundup)除草劑後罹癌負責,並支付2.89億美元的賠償金。陪審團認為,孟山都明明知道年年春的主成分嘉磷塞(glyphosate)可致癌,卻沒有警告消費者。

年年春。圖片來源:Mike Mozart(CC BY 2.0)

校工受災  公園、學校廣泛使用嘉磷塞

受害者是舊金山灣區一位校工強森(Dewayne “Lee” Johnson)。強森的律師威斯納(Brent Wisner)說,強森用接在卡車後的大型水箱噴灑地面。風會將農藥吹到他臉上,或是萬一水管破裂,這位校工全身都會暴露於有毒物質。

威斯納解釋,強森在皮疹爆發後聯繫了孟山都公司,但從未接收到年年春可能致癌的警告。 2014年,當時42歲的強森確診非何杰金氏淋巴瘤(non-Hodgkin’s lymphoma)。

在聽審中。

孟山都否認是年年春導致強森罹癌症。

嘉磷塞  最受歡迎農藥

嘉磷塞的作用機制是抑制EPSP合酶作用,阻止植物和細菌製造氨基酸。嘉磷塞的化學結構類似甘氨酸,與ESPS合酶的活性位置結合。植物必須合成生產蛋白質所需的所有氨基酸,因此嘉磷塞抑制EPSP合酶,能使植物細胞缺乏氨基酸,導致植物死亡。

1971年,孟山都公司的法蘭茲博士(John Franz)發現嘉磷塞,1974年嘉磷塞上市。1983年,年年春成為世界上第一個銷售額超過10億美元的農藥,至今仍是農藥界霸主。年年春並不是唯一以嘉磷塞為主成分的農藥。截至2010年,市場上已有750多種嘉磷塞產品。

儘管如此,年年春仍是全世界最常用的除草劑,而這僅是第一個案例,後續還有數千人試圖讓孟山都對年年春的健康影響負責。

世界衛生組織(WHO)三年前就將嘉磷塞歸類為可能的人類致癌物,但年年春仍被廣泛使用,於學校和公園附近噴灑。

拜耳併孟山都 5200人因孟山都產品受害提告

加州陪審團認為,孟山都的年年春和「Ranger Pro」產品對消費者健康具「重大危險」性,孟山都知道或應該要知道潛在的風險。

此案僅是數起類似投訴中的第一起,這些案件後續都可能進入司法程序,對拜耳來說相當棘手。

拜耳在經過兩年的談判後於6月7日以600億美元收購了孟山都公司。總部位於聖路易斯的孟山都,股票再也不在紐約證券交易所上市交易,拜耳現在是孟山都的唯一持有者。孟山都的股東每股收益為128美元。

孟山都表示,截至2月份,其各種產品索賠金額總額高達2.54億美元,並且已知有5200名原告聲稱因接觸嘉磷塞產品而受害。

該公司先前曾表示無法估計訴訟損失。合併後的公司將放棄孟山都公司名,以免受孟山都品牌的法律和公共關係問題影響。

Monsanto Must Pay $289 Million to Roundup Cancer Victim SAN FRANCISCO, California, August 15, 2018 (ENS)

A San Francisco jury has found Monsanto liable for causing a school groundskeeper’s cancer through his exposure to the Monsanto product Roundup and ordered the agricultural giant to pay US$289.2 million in damages.

On Friday the jury found that Monsanto knew glyphosate, the primary ingredient in the company’s signature herbicide Roundup, could cause cancer but failed to warn consumers.

Dewayne “Lee” Johnson, a Bay Area school district groundskeeper, sprayed the grounds from a large tank attached for a truck, said his lawyer Brent Wisner. Wind gusts would blow the herbicide in his face, and when a hose broke, he got the toxic substance all over his body.

Johnson contacted Monsanto after he broke out in a rash, but he was never warned the herbicide could cause cancer, Wisner explained. In 2014, at age 42, Johnson received a diagnosis of non-Hodgkin’s lymphoma.

Monsanto rejected Johnson’s claim that Roundup caused his cancer.

Glyphosate works by inhibiting an enzyme that plants and bacteria use to make amino acids called EPSP synthase. Structurally, glyphosate resembles the chemical structure of the amino acid glycine. Because of its structural similarity to glycine, glyphosate binds the active site of the ESPS synthase enzyme that is critical for the production of aromatic amino acids.

Since plants must synthesize all the amino acids they need for protein production, inhibition of EPSP synthase by glyphosate makes the plant cells to starve for amino acids, causing the plants to die.

Glyphosate was discovered by Dr. John Franz of Monsanto in 1971 and released commercially in 1974. In 1983, RoundUp became the first pesticide with world-wide sales of over $1 billion, a position it has maintained since then.

Today, Monsanto’s Roundup is not the only glyphosate-based product available. Other glyphosate formulations are sold under brand names that include: Bronco, Glifonox, KleenUp, Ranger Pro, Rodeo and Weedoff. As of 2010, more than 750 glyphosate products were on the market.

Still, Roundup is the most commonly used herbicide in the world, and this case is the first of thousands seeking to hold Monsanto responsible for the health impacts of Roundup.

It has been three years since the World Health Organization classified glyphosate as a probable human carcinogen, but Roundup is still used widely and is sprayed near schools and parks.

The California jury’s ruling found that Monsanto’s Roundup and Ranger Pro products posed a “substantial danger” to consumers, and that Monsanto knew, or ought to have known, of the potential risks.

The case is the first of several similar complaints that could go to trial and represents a troublesome issue for Bayer, which closed its US$60 billion acquisition of Monsanto on June 7 after two years of negotiating.

Shares in the St. Louis-based Monsanto are no longer traded on the New York Stock Exchange, with Bayer now the sole owner of Monsanto Company. Monsanto shareholders are being paid US128 per share.

Monsanto said that as of February it had a recorded liability of $254 million relating to various product claims and that it is aware of 5,200 plaintiffs who claim to have been injured by exposure to glyphospate-based products.

The company has previously said it could not estimate losses from the litigation.

The combined company will drop the name Monsanto to avoid identification with the legal and public relations troubles that encumber the Monsanto brand.

※ 全文及圖片詳見:

※ 本文與 行政院農業委員會 林務局  合作刊登

作者

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

延伸閱讀

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

※網頁設計最專業,超強功能平台可客製化