帶你學夠浪:Go語言基礎系列 – 8分鐘學控制流語句

文章每周持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜索公眾號「 後端技術學堂 」第一時間閱讀(一般比博客早更新一到兩篇)

對於一般的語言使用者來說 ,20% 的語言特性就能夠滿足 80% 的使用需求,剩下在使用中掌握。基於這一理論,Go 基礎系列的文章不會刻意追求面面俱到,但該有知識點都會覆蓋,目的是帶你快跑趕上 Golang 這趟新車。

Hurry up , Let’s go !

控制語句是程序的靈魂,有了它們程序才能完成各種邏輯,今天我們就來學習 Go 中的各種控制語句。

通過本文的學習你將掌握以下知識:

  • if 條件語句
  • for 循環語句
  • switch 語句
  • defer 延遲調用

if 條件語句

與大多數編程語言一樣,if 用於條件判斷,當條件表達式 exprtrue 執行 {} 包裹的消息體語句,否則不執行。

語法是這樣的:

if expr {
    // some code
}

**注意:**語法上和 c 語言不同的是不用在條件表達式 expr 外帶括號,和 python 的語法類似。

當然,如果想在條件不滿足的時候做點啥,就可以 if 后帶 else 語句。語法:

if expr {
    // some code
} else {
    // another code
}

不僅僅是 if

除了可以在 if 中做條件判斷之外,在 Golang 中你甚至可以在 if 的條件表達式前執行一個簡單的語句。

舉個例子:

if x2 := 1; x2 > 10 { 
    fmt.Println("x2 great than 10")
} else {
    fmt.Println("x2 less than 10", x2)
}

上面的例子在 if 語句中先聲明並賦值了 x2,之後對 x2 做條件判斷。

注意:此處在 if 內聲明的變量 x2 作用域僅限於 if 和else 語句。

for循環語句

當需要重複執行的時候需要用到循環語句,Go 中只有 for 這一種循環語句。

標準的for循環語法:

for 初始化語句; 條件表達式; 後置語句 {
    // some code
}

這種語法形式和 C 語言中 for 循環寫法還是很像的,不同的是不用把這三個部分用 () 括起來。循環執行邏輯:

  • 初始化語句:初始循環時執行一次,做一些初始化工作,一般是循環變量的聲明和賦值。
  • 條件表達式:在每次循環前對條件表達式求值操作,若求值結果是
    true 則執行循環體內語句,否則不執行。
  • 後置語句:在每次循環的結尾執行,一般是做循環變量的自增操作。

舉個例子:

sum := 0
for i := 0; i < 10; i++ {
    sum += i // i作用域只在for語句內
    fmt.Println(i, sum)
}

注意:循環變量i 的作用域只在 for 語句內,超出這個範圍就不能使用了。

while循環怎麼寫?

前面說了,Golang 中只有 for 這一種循環語法,那有沒有類似 C 語言中 while 循環的寫法呢?答案是有的:把 for 語句的前後兩部分省略,只留中間的「條件表達式」的 for 語句等價於 while 循環。

像下面這樣:

sum1 := 0
for ;sum1 < 10; { // 可以省略初始化語句和後置語句
    sum1++
    fmt.Println(sum1)
}

上面的示例沒有初始化語句和後置語句,會循環執行 10 次後退出。

當然你要是覺得前後的分號也不想寫了,也可以省略不寫,上面的代碼和下面是等效的:

sum1 := 0
for sum1 < 10 { // 可以省略初始化語句和後置語句,分號也能省略
    sum1++
    fmt.Println(sum1)
}

在 Golang 中死循環可以這樣寫,相當於 C 語言中的 while(true)

 for { // 死循環
  // your code
 }

switch 語句

switch 語句可以簡化多個 if-else 條件判斷寫法,避免代碼看起來雜亂。

可以先定義變量,然後在 switch 中使用這個變量。

 a := 1
 switch a {
 case 1: 
  fmt.Println("case 1") // 不用寫break 執行到這自動跳出
 case 2:
  fmt.Println("case 2")
 default:
  fmt.Printf("unexpect case")
 }
輸出:case 1

從 C 語言過來的朋友一定有這樣的經歷:經常會在 case 語句中漏掉 break 導致程序繼續往下執行,從而產生奇奇怪怪的 bug ,這種問題在 Golang 中不復存在了。

Golang 在每個 case 後面隱式提供 break 語句。 除非以 fallthrough 語句結束,否則分支會自動終止。

 switch a := 1; a { //這裡有分號
 case 1: // case 無需為常量,且取值不必為整數。
  fmt.Println("case 1") // 不用寫break 執行到自動跳出 除非以 fallthrough 語句結束
  fallthrough
 case 2:
  fmt.Println("case 2")
 default:
  fmt.Printf("unexpect case")
 }
輸出:
case 1
case 2

還可以直接在 switch 中定義變量后使用,但是要注意變量定義之後又分號,比如下面這樣:

 switch b :=1; b { //注意這裡有分號
 case 1: 
  fmt.Println("case 1") 
 case 2:
  fmt.Println("case 2")
 default:
  fmt.Printf("unexpect case")
 }

沒有條件的switch

沒有條件的 switch 同 switch true 一樣,只有當 case 中的表達式值為「真」時才執行,這種形式能簡化複雜的 if-else-if else 語法。

下面是用 if 來寫多重條件判斷,這裏寫的比較簡單若是再多幾個 else if 代碼結構看起來會更糟糕。

    a := 1
    if a > 0 {
        fmt.Println("case 1") 
    } else if a < 0 {
        fmt.Println("case 2")   
    } else {
        fmt.Printf("unexpect case")   
    }

如果用上不帶條件的 switch 語句,寫出來就會簡潔很多,像下面這樣。

 a := 1
 switch {    // 相當於switch true
 case a > 0: // 若表達式為「真」則執行 
  fmt.Println("case 1") 
 case a < 0:
  fmt.Println("case 2")
 default:
  fmt.Printf("unexpect case")
 }

defer 語句

defer 語句有延遲調用的效果。具體來說defer後面的函數調用會被壓入堆棧,當外層函數返回才會對壓棧的函數按後進先出順序調用。說起來有點抽象,舉個例子:

package main

import "fmt"

func main() {
 fmt.Println("entry main")
 for i := 0; i < 6; i++ {
  defer fmt.Println(i)
 }
 fmt.Println("exit main")
}

fmt.Println(i) 不會每次立即執行,而是在 main 函數返回之後才依次調用,編譯運行上述程序的輸出:

entry main
exit main  //外層函數返回
5
4
3
2
1
0

上面是簡單的使用示例,實際使用中defer 通常用來釋放函數內部變量,因為它可以在外層函數 return 之後繼續執行一些清理動作。

這在文件類操作異常處理中非常實用,比如用於釋放文件描述符,我們以後會講解這塊應用,總之先記住 defer 延遲調用的特點。

總結

通過本文的學習,我們掌握了 Golang 中基本的控制流語句,利用這些控制語句加上一節介紹的變量等基礎知識,可以構成豐富的程序邏輯,就能用 Golang 來做一些有意思的事情了。

感謝各位的閱讀,文章的目的是分享對知識的理解,技術類文章我都會反覆求證以求最大程度保證準確性,若文中出現明顯紕漏也歡迎指出,我們一起在探討中學習.

今天的技術分享就到這裏,我們下期再見。

創作不易,白票不是好習慣,如果在我這有收穫,動動手指「點贊」「關注」是對我持續創作的最大支持。

微信搜索公眾號「 後端技術學堂 」回復「資料」「1024」有我給你準備的各種編程學習資料。文章每周持續更新,我們下期見!

本文使用 mdnice 排版

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

異步函數async await在wpf都做了什麼?

首先我們來看一段控制台應用代碼:

 class Program
 {
     static async Task Main(string[] args)
     {
        System.Console.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
        var result = await ExampleTask(2);
        System.Console.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
        System.Console.WriteLine(result);
        Console.WriteLine("Async Completed");
     }

     private static async Task<string> ExampleTask(int Second)
     {
        await Task.Delay(TimeSpan.FromSeconds(Second));
        return $"It's Async Completed in {Second} seconds";
     }
 }

輸出結果

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:4,Is Thread Pool:True
It's Async Completed in 2 seconds
Async Completed

如果這段代碼在WPF運行,猜猜會輸出啥?

      private async void Async_Click(object sender, RoutedEventArgs e)
      {
          Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
          var result= await ExampleTask(2);
          Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
          Debug.WriteLine(result);
          Debug.WriteLine("Async Completed");   
      }

      private async Task<string> ExampleTask(int Second)
      {
          await Task.Delay(TimeSpan.FromSeconds(Second));
          return $"It's Async Completed in {Second} seconds";
      }

輸出結果:

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:1,Is Thread Pool:False
It's Async Completed in 2 seconds
Async Completed

這時候你肯定是想說,小朋友,你是否有很多問號????,我們接下看下去

一.SynchronizationContext(同步上下文)

首先我們知道async await 異步函數本質是狀態機,我們通過反編譯工具dnspy,看看反編譯的兩段代碼是否有不同之處:

控制台應用:

internal class Program
{
    [DebuggerStepThrough]
	private static Task Main(string[] args)
	{
		Program.<Main>d__0 <Main>d__ = new Program.<Main>d__0();
		<Main>d__.args = args;
		<Main>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
		<Main>d__.<>1__state = -1;
		<Main>d__.<>t__builder.Start<Program.<Main>d__0>(ref <Main>d__);
		return <Main>d__.<>t__builder.Task;
	}
    
	[DebuggerStepThrough]
	private static Task<string> ExampleTask(int Second)
	{
		Program.<ExampleTask>d__1 <ExampleTask>d__ = new Program.<ExampleTask>d__1();
		<ExampleTask>d__.Second = Second;
		<ExampleTask>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
		<ExampleTask>d__.<>1__state = -1;
		<ExampleTask>d__.<>t__builder.Start<Program.<ExampleTask>d__1>(ref <ExampleTask>d__);
		return <ExampleTask>d__.<>t__builder.Task;
	}

	[DebuggerStepThrough]
	private static void <Main>(string[] args)
	{
	        Program.Main(args).GetAwaiter().GetResult();
	}
}

WPF:

public class MainWindow : Window, IComponentConnector
{

	public MainWindow()
	{
	       this.InitializeComponent();
	}

	[DebuggerStepThrough]
	private void Async_Click(object sender, RoutedEventArgs e)
	{
		MainWindow.<Async_Click>d__1 <Async_Click>d__ = new MainWindow.<Async_Click>d__1();
		<Async_Click>d__.<>4__this = this;
		<Async_Click>d__.sender = sender;
		<Async_Click>d__.e = e;
		<Async_Click>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
		<Async_Click>d__.<>1__state = -1;
		<Async_Click>d__.<>t__builder.Start<MainWindow.<Async_Click>d__1>(ref <Async_Click>d__);
	}

	[DebuggerStepThrough]
	private Task<string> ExampleTask(int Second)
	{
	        MainWindow.<ExampleTask>d__3 <ExampleTask>d__ = new MainWindow.<ExampleTask>d__3();
		<ExampleTask>d__.<>4__this = this;
		<ExampleTask>d__.Second = Second;
		<ExampleTask>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
		<ExampleTask>d__.<>1__state = -1;
		<ExampleTask>d__.<>t__builder.Start<MainWindow.<ExampleTask>d__3>(ref <ExampleTask>d__);
		return <ExampleTask>d__.<>t__builder.Task;
	}

	[DebuggerNonUserCode]
	[GeneratedCode("PresentationBuildTasks", "4.8.1.0")]
	public void InitializeComponent()
	{
		bool contentLoaded = this._contentLoaded;
		if (!contentLoaded)
		{
		     this._contentLoaded = true;
		     Uri resourceLocater = new Uri("/WpfApp1;component/mainwindow.xaml", UriKind.Relative);
		     Application.LoadComponent(this, resourceLocater);
		}
	}
	private bool _contentLoaded;
}

我們可以看到完全是一致的,沒有任何區別,為什麼編譯器生成的代碼是一致的,卻會產生不一樣的結果,我們看看創建和啟動狀態機代碼部分的實現:

public static AsyncVoidMethodBuilder Create()
{
	SynchronizationContext synchronizationContext = SynchronizationContext.Current;
	if (synchronizationContext != null)
	{
		synchronizationContext.OperationStarted();
	}
	return new AsyncVoidMethodBuilder
	{
		_synchronizationContext = synchronizationContext
	};
}

[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Start<[Nullable(0)] TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	AsyncMethodBuilderCore.Start<TStateMachine>(ref stateMachine);
}

[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	if (stateMachine == null)
	{
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
	}
	Thread currentThread = Thread.CurrentThread;
	Thread thread = currentThread;
	ExecutionContext executionContext = currentThread._executionContext;
	ExecutionContext executionContext2 = executionContext;
	SynchronizationContext synchronizationContext = currentThread._synchronizationContext;
	try
	{
	     stateMachine.MoveNext();//狀態機執行代碼
	}
	finally
	{
	     SynchronizationContext synchronizationContext2 = synchronizationContext;
	     Thread thread2 = thread;
	     if (synchronizationContext2 != thread2._synchronizationContext)
	     {
		  thread2._synchronizationContext = synchronizationContext2;
	     }
	     ExecutionContext executionContext3 = executionContext2;
	     ExecutionContext executionContext4 = thread2._executionContext;
	     if (executionContext3 != executionContext4)
	     {
		 ExecutionContext.RestoreChangedContextToThread(thread2, executionContext3, executionContext4);
	     }
	}
}

在這裏總結下:

  • 創建狀態機的Create函數通過SynchronizationContext.Current獲取到當前同步執行上下文
  • 啟動狀態機的Start函數之後通過MoveNext函數執行我們的異步方法
  • 這裏還有一個小提示,不管async函數裏面有沒有await,都會生成狀態機,只是MoveNext函數執行同步方法,因此沒await的情況下避免將函數標記為async,會損耗性能

同樣的這裏貌似沒能獲取到原因,但是有個很關鍵的地方,就是Create函數為啥要獲取當前同步執行上下文,之後我從MSDN找到關於SynchronizationContext
的介紹,有興趣的朋友可以去閱讀以下,以下是各個.NET框架使用的SynchronizationContext:

SynchronizationContext 默認
WindowsFormsSynchronizationContext WindowsForm
DispatcherSynchronizationContext WPF/Silverlight
AspNetSynchronizationContext ASP.NET

我們貌似已經一步步接近真相了,接下來我們來看看DispatcherSynchronizationContext

二.DispatcherSynchronizationContext

首先來看看DispatcherSynchronizationContext類的比較關鍵的幾個函數實現:

public DispatcherSynchronizationContext(Dispatcher dispatcher, DispatcherPriority priority)
{
     if (dispatcher == null)
     {
         throw new ArgumentNullException("dispatcher");
     }
     Dispatcher.ValidatePriority(priority, "priority");
     _dispatcher = dispatcher;
     _priority = priority;
     SetWaitNotificationRequired();
 }

//同步執行
public override void Send(SendOrPostCallback d, object state)
{
     if (BaseCompatibilityPreferences.GetInlineDispatcherSynchronizationContextSend() && _dispatcher.CheckAccess())
     {
         _dispatcher.Invoke(DispatcherPriority.Send, d, state);
     }
     else
     {
          _dispatcher.Invoke(_priority, d, state);
     }
}

//異步執行
public override void Post(SendOrPostCallback d, object state)
{
     _dispatcher.BeginInvoke(_priority, d, state);
}

我們貌似看到了熟悉的東西了,Send函數調用Dispatcher的Invoke函數,Post函數調用Dispatcher的BeginInvoke函數,那麼是否WPF執行異步函數之後會調用這裏的函數嗎?我用dnspy進行了調試:

我通過調試之後發現,當等待執行完整個狀態機的之後,也就是兩秒后跳轉到該Post函數,那麼,我們可以將之前的WPF那段代碼大概可以改寫成如此:

private async void Async_Click(object sender, RoutedEventArgs e)
{
    //async生成狀態機的Create函數。獲取到UI主線程的同步執行上下文
    DispatcherSynchronizationContext synchronizationContext = (DispatcherSynchronizationContext)SynchronizationContext.Current;
    
    //UI主線程執行
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    
    //開始在狀態機的MoveNext執行該異步操作
    var result= await ExampleTask(2);
    
    //等待兩秒,異步執行完成,再在同步上下文異步執行
    synchronizationContext.Post((state) =>
    {
         //模仿_dispatcher.BeginInvoke
         Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
         Debug.WriteLine(result);
         Debug.WriteLine("Async Completed");  
     },"Post");           
 }

輸出結果:

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:1,Is Thread Pool:False
It's Async Completed in 2 seconds
Async Completed

也就是asyn負責生成狀態機和執行狀態機,await將代碼分為兩部分,一部分是異步執行狀態機部分,一部分是異步執行完之後,通過之前拿到的DispatcherSynchronizationContext,再去異步執行接下來的部分。我們可以通過dnspy調試DispatcherSynchronizationContext的 _dispatcher字段的Thread屬性,知道Thread為UI主線程,而同步界面UI控件的時候,也就是通過Dispatcher的BeginInvoke函數去執行同步的

三.Task.ConfigureAwait

Task有個ConfigureAwait方法,是可以設置是否對Task的awaiter的延續任務執行原始上下文,也就是為true時,是以一開始那個UI主線程的DispatcherSynchronizationContext執行Post方法,而為false,則以await那個Task裏面的DispatcherSynchronizationContext執行Post方法,我們來驗證下:

我們將代碼改為以下:

private async void Async_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    var result= await ExampleTask(2).ConfigureAwait(false);
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    Debug.WriteLine(result);
    Debug.WriteLine($"Async Completed");
}

輸出:

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:4,Is Thread Pool:True
It's Async Completed in 2 seconds
Async Completed

結果和控制台輸出的一模一樣,且通過dnspy斷點調試依舊進入到DispatcherSynchronizationContext的Post方法,因此我們也可以證明我們上面的猜想,而且默認ConfigureAwait的參數是為true的,我們還可以將異步結果賦值給UI界面的Text block:

private async void Async_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    var result= await ExampleTask(2).ConfigureAwait(false);
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    this.txt.Text = result;//修改部分
    Debug.WriteLine($"Async Completed");
}

拋出異常:

調用線程無法訪問此對象,因為另一個線程擁有該對象

補充
推薦林大佬的一篇文章,也講的也簡潔透徹C# dotnet 自己實現一個線程同步上下文

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

自己動手實現深度學習框架-7 RNN層–GRU, LSTM

目標

        這個階段會給cute-dl添加循環層,使之能夠支持RNN–循環神經網絡. 具體目標包括:

  1. 添加激活函數sigmoid, tanh.
  2. 添加GRU(Gate Recurrent Unit)實現.
  3. 添加LSTM(Long Short-term Memory)實現.
  4. 使用基於GRU和LSTM的RNN模型擬合一個正餘弦疊加函數.

RNN原理

原始的RNN

        RNN模型用來捕捉序列數據的特徵. 給定一個長度為T的輸入系列\(X=(x_1, x_2, .., X_T)\), RNN層輸出一個長度為T的序列\(H=(h_1, h_2, …, H_T)\), 對於任意時間步t, 可以表示為:

\[H_t = δ(X_tW_x + H_{t-1}W_h + b), \quad t = 2, 3, .., T \]

        函數δ是sigmoid函數:

\[δ = \frac{1}{1 + e^{-x}} \]

        \(H_t\)包含了前面第1到t-1步的所有信息。 和CNN層類似, CNN層在空間上共享參數, RNN層在時間步上共享參數\(W_x, W_h, b\).

        RNN層中隱藏層的數量為T-2, 如果T較大(超過10), 反向傳播是很容易出現梯度爆炸. GRU和LSTM就是為了解決這個問題而誕生, 這兩種模型,可以讓RNN能夠支持長度超過1000的輸入序列。

GRU

        GRU使用了不同功能的門控單元, 分別捕捉序列上不同時間跨度的的依賴關係。每個門控單元都會都有獨立的參數, 這些參數在時間步上共享。

        GRU的門控單元有:

        \(R_t = δ(X_tW^r_x + H_{t-1}W^r_h + b^r)\), 重置門用於捕捉短期依賴關係.

        \(U_t = δ(X_tW^u_x + H_{t-1}W^u_h + b^u)\), 更新門用於捕捉長期依賴關係

        \(\bar{H}_t = tanh(X_t\bar{W}_x + (R_t * H_{t-1})\bar{W}_h + \bar{b})\)

        除此之外, 還有一個輸出單元:

        \(H_t = U_t * H_{t-1} + (1-U_t)*\bar{H}_t\)

LSTM

        LSTM的設計思路和GRU類似, 同樣使用了多個門控單元:

        \(I_t = δ(X_tW^i_x + H_{t-1}W^i_h + b^i)\), 輸入門,過濾記憶門的輸出.

        \(F_t = δ(X_tW^f_x + H_{t-1}W^f_h + b^f)\), 遺忘門, 過濾前面時間步的記憶.

        \(O_t = δ(X_tW^o_x + H_{t-1}W^o_h + b^o)\), 輸出門, 過濾當前時間步的記憶.

        \(M_t = tanh(X_tW^m_x + H_{t-1}W^m_h + b^m)\), 記憶門.

        它還有自己獨有的記憶單元和輸出單元:

        \(\bar{M}_t = F_t * \bar{M}_{t-1} + I_t * M_t\)

        \(H_t = O_t * tanh(\bar{M}_t)\)

RNN實現

        設計要求:

  1. RNN層中的隱藏層的數量是基於序列長度的,輸入序列有多長, RNN層應生成對應數量的隱藏層。
  2. RNN層在時間步上共享參數, 從前面的描述可以看出, 只有門控單元有參數,因此門控單元應獨立實現。
  3. 任意一個時間步上的層都依賴上一個時間步的輸出,在正向傳播和反向傳播過程中都需要上一個時間步的輸出, 每個門控單元都使用棧保存上一個時間步的輸出.
  4. 默認情況下RNN層輸出所有時間步的輸出。但有時只需要最後一個時間步的輸出, 這種情況下使用過濾層, 只向下一層傳播最後一個時間步的輸出。
  5. 使用門控單元實現GRU和LSTM

RNN基礎類的實現

RNN類

        文件: cutedl/rnn_layers.py, 類名: RNN

        這個類是RNN層基類, 它主要功能是控制向前傳播和向後傳播的主流程.

        初始化參數:

  '''
  out_units 輸出單元數
  in_units 輸入單元數
  stateful 保留當前批次的最後一個時間步的狀態作為下一個批次的輸入狀態, 默認False不保留

  RNN 的輸入形狀是(m, t, in_units)
  m: batch_size
  t: 輸入系列的長度
  in_units: 輸入單元數頁是輸入向量的維數

  輸出形狀是(m, t, out_units)
  '''
  def __init__(self, out_units, in_units=None, stateful=False, activation='linear'):

        向前傳播

def forward(self, in_batch, training):
    m, T, n = in_batch.shape
    out_units = self.__out_units
    #所有時間步的輸出
    hstatus = np.zeros((m, T, out_units))
    #上一步的輸出
    pre_hs = self.__pre_hs
    if pre_hs is None:
        pre_hs = np.zeros((m, out_units))

    #隱藏層循環過程, 沿時間步執行
    for t in range(T):
        hstatus[:, t, :] = self.hiden_forward(in_batch[:,t,:], pre_hs, training)
        pre_hs = hstatus[:, t, :]

    self.__pre_hs = pre_hs
    #pdb.set_trace()
    if not self.stateful:
        self.__pre_hs = None

    return hstatus

        反向傳播

def backward(self, gradient):
      m, T, n = gradient.shape

      in_units = self.__in_units
      grad_x = np.zeros((m, T, in_units))
      #pdb.set_trace()
      #從最後一個梯度開始反向執行.
      for t in range(T-1, -1, -1):
          grad_x[:,t,:], grad_hs = self.hiden_backward(gradient[:,t,:])
          #pdb.set_trace()
          if t - 1 >= 0:
              gradient[:,t-1,:] = gradient[:,t-1,:] + grad_hs

      #pdb.set_trace()
      return grad_x

sigmoid和tanh激活函數

sigmoid及其導數

\[sigmoid = \frac{1}{1+e^{-x}} \]

\[\frac{d}{dx}sigmoid = sigmoid(1-sigmoid) \]

tanh及其導數

\[tanh = \frac{e^x – e^{-x}}{e^x + e^{-x}} \]

\[\frac{d}{dx}tanh = 1 – tanh^2 \]

門控單元實現

        文件: cutedl/rnn_layers.py, 類名: GateUint

        門控單元是RNN層基礎的參數單元. 和Dense層類似,它是Layer的子類,負責學習和使用參數。但在學習和使用參數的方式上有很大的不同:

  • Dense有兩個參數矩陣, GateUnit有3個參數矩陣.
  • Dense在一次反向傳播過程中只使用當前的梯度學習參數,而GateUnit會累積每個時間步的梯度。

        下面我們會主要看一下GateUnit特別之處的代碼.

        在__ init__方法中定義參數和棧:

    #3個參數
    self.__W = None #當前時間步in_batch權重參數
    self.__Wh = None #上一步輸出的權重參數
    self.__b = None #偏置量參數

    #輸入棧
    self.__hs = []  #上一步輸出
    self.__in_batchs = [] #當前時間步的in_batch

        正向傳播:

  def forward(self, in_batch, hs, training):
      W = self.__W.value
      b = self.__b.value
      Wh = self.__Wh.value

      out = in_batch @ W + hs @ Wh + b

      if training:
          #向前傳播訓練時把上一個時間步的輸出和當前時間步的in_batch壓棧
          self.__hs.append(hs)
          self.__in_batchs.append(in_batch)

          #確保反向傳播開始時參數的梯度為空
          self.__W.gradient = None
          self.__Wh.gradient = None
          self.__b.gradient = None

      return self.activation(out)

        反向傳播:

def backward(self, gradient):
    grad = self.activation.grad(gradient)

    W = self.__W.value
    Wh = self.__Wh.value
    pre_hs = self.__hs.pop()
    in_batch = self.__in_batchs.pop()

    grad_in_batch = grad @ W.T
    grad_W = in_batch.T @ grad
    grad_hs = grad @ Wh.T
    grad_Wh = pre_hs.T @ grad
    grad_b = grad.sum(axis=0)

    #反向傳播計算
    if self.__W.gradient is None:
        #當前批次第一次
        self.__W.gradient = grad_W
    else:
        #累積當前批次的所有梯度
        self.__W.gradient = self.__W.gradient + grad_W

    if self.__Wh.gradient is None:
        self.__Wh.gradient = grad_Wh
    else:
        self.__Wh.gradient = self.__Wh.gradient +  grad_Wh

    if self.__b.gradient is None:
        self.__b.gradient = grad_b
    else:
        self.__b.gradient = self.__b.gradient + grad_b

    return grad_in_batch, grad_hs

GRU實現

        文件: cutedl/rnn_layers.py, 類名: GRU

        隱藏單初始化:

def set_parent(self, parent):
    super().set_parent(parent)

    out_units = self.out_units
    in_units = self.in_units

    #pdb.set_trace()
    #重置門
    self.__g_reset = GateUnit(out_units, in_units)
    #更新門
    self.__g_update = GateUnit(out_units, in_units)
    #候選輸出門
    self.__g_cddout = GateUnit(out_units, in_units, activation='tanh')

    self.__g_reset.set_parent(self)
    self.__g_update.set_parent(self)
    self.__g_cddout.set_parent(self)

    #重置門乘法單元
    self.__u_gr = MultiplyUnit()
    #輸出單元
    self.__u_out = GRUOutUnit()

        向前傳播:

  def hiden_forward(self, in_batch, pre_hs, training):
      gr = self.__g_reset.forward(in_batch, pre_hs, training)
      gu = self.__g_update.forward(in_batch, pre_hs, training)
      ugr = self.__u_gr.forward(gr, pre_hs, training)
      cddo = self.__g_cddout.forward(in_batch, ugr, training)

      hs = self.__u_out.forward(gu, pre_hs, cddo, training)

      return hs

        反向傳播:

def hiden_backward(self, gradient):

    grad_gu, grad_pre_hs, grad_cddo = self.__u_out.backward(gradient)
    #pdb.set_trace()
    grad_in_batch, grad_ugr = self.__g_cddout.backward(grad_cddo)

    #計算梯度的過程中需要累積上一層輸出的梯度
    grad_gr, g_pre_hs = self.__u_gr.backward(grad_ugr)
    grad_pre_hs = grad_pre_hs + g_pre_hs

    g_in_batch, g_pre_hs = self.__g_update.backward(grad_gu)
    grad_in_batch = grad_in_batch + g_in_batch
    grad_pre_hs = grad_pre_hs + g_pre_hs

    g_in_batch, g_pre_hs = self.__g_reset.backward(grad_gr)
    grad_in_batch = grad_in_batch + g_in_batch
    grad_pre_hs = grad_pre_hs + g_pre_hs

    #pdb.set_trace()
    return grad_in_batch, grad_pre_hs    

LSTM實現

        文件: cutedl/rnn_layers.py, 類名: LSTM

        隱藏單元初始化:

def set_parent(self, layer):
    super().set_parent(layer)

    in_units = self.in_units
    out_units = self.out_units

    #輸入門
    self.__g_in = GateUnit(out_units, in_units)
    #遺忘門
    self.__g_forget = GateUnit(out_units, in_units)
    #輸出門
    self.__g_out = GateUnit(out_units, in_units)
    #記憶門
    self.__g_memory = GateUnit(out_units, in_units, activation='tanh')

    self.__g_in.set_parent(self)
    self.__g_forget.set_parent(self)
    self.__g_out.set_parent(self)
    self.__g_memory.set_parent(self)

    #記憶單元
    self.__memory_unit =LSTMMemoryUnit()
    #輸出單元
    self.__out_unit = LSTMOutUnit()

        向前傳播:

def hiden_forward(self, in_batch, hs, training):
    g_in = self.__g_in.forward(in_batch, hs, training)
    #pdb.set_trace()
    g_forget = self.__g_forget.forward(in_batch, hs, training)
    g_out = self.__g_out.forward(in_batch, hs, training)
    g_memory = self.__g_memory.forward(in_batch, hs, training)

    memory = self.__memory_unit.forward(g_forget, g_in, g_memory, training)
    cur_hs = self.__out_unit.forward(g_out, memory, training)

    return cur_hs

        反向傳播:

def hiden_backward(self, gradient):
    #pdb.set_trace()
    grad_out, grad_memory = self.__out_unit.backward(gradient)
    grad_forget, grad_in, grad_gm = self.__memory_unit.backward(grad_memory)

    grad_in_batch, grad_hs = self.__g_memory.backward(grad_gm)
    tmp1, tmp2 = self.__g_out.backward(grad_out)
    grad_in_batch += tmp1
    grad_hs += tmp2

    tmp1, tmp2 = self.__g_forget.backward(grad_forget)
    grad_in_batch += tmp1
    grad_hs += tmp2

    tmp1, tmp2 = self.__g_in.backward(grad_in)
    grad_in_batch += tmp1
    grad_hs += tmp2

    return grad_in_batch, grad_hs

驗證

        接下來, 驗證示例將會構建一個簡單的RNN模型, 使用該模型擬合一個正餘弦疊加函數:

#採樣函數
def sample_function(x):
    y = 3*np.sin(2 * x * np.pi) + np.cos(x * np.pi) + np.random.uniform(-0.05,0.05,len(x))
    return y

        訓練數據集和測試數據集在這個函數的不同定義域區間內樣. 訓練數據集的採樣區間為[1, 200.01), 測試數據集的採樣區間為[200.02, 240.002). 模型任務是預測這個函數值的序列.

        示例代碼在examples/rnn/fit_function.py文件中.

使用GRU構建的模型

def fit_gru():
    model = Model([
                rnn.GRU(32, 1),
                nn.Filter(),
                nn.Dense(32),
                nn.Dense(1, activation='linear')
            ])
    model.assemble()
    fit('gru', model)

訓練報告:

使用LSTM構建的模型

def fit_lstm():
    model = Model([
                rnn.LSTM(32, 1),
                nn.Filter(),
                nn.Dense(2),
                nn.Dense(1, activation='linear')
            ])
    model.assemble()
    fit('lstm', model)

訓練報告:

總結

        這個階段,框架新增了RNN的兩個最常見的實現:GRU和LSTM, 相應地增加了它需要的激活函數. cute-dl已經具備了構建最基礎RNN模型的能力。通過驗證發現, GRU模型和LSTM模型在簡單任務上都表現出了很好的性能。會添加嵌入層,使框架能夠構建文本分類任務的模型,然後在imdb-review(電影評價)數據集上進行驗證.

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

【Spring註解驅動開發】使用@Scope註解設置組件的作用域

寫在前面

Spring容器中的組件默認是單例的,在Spring啟動時就會實例化並初始化這些對象,將其放到Spring容器中,之後,每次獲取對象時,直接從Spring容器中獲取,而不再創建對象。如果每次從Spring容器中獲取對象時,都要創建一個新的實例對象,該如何處理呢?此時就需要使用@Scope註解設置組件的作用域。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

本文內容概覽

  • @Scope註解概述
  • 單實例bean作用域
  • 多實例bean作用域
  • 單實例bean作用域如何創建對象?
  • 多實例bean作用域如何創建對象?
  • 單實例bean注意的事項
  • 多實例bean注意的事項
  • 自定義Scope的實現

@Scope註解概述

@Scope註解能夠設置組件的作用域,我們先來看@Scope註解類的源碼,如下所示。

package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
	@AliasFor("scopeName")
	String value() default "";
    /**
	 * Specifies the name of the scope to use for the annotated component/bean.
	 * <p>Defaults to an empty string ({@code ""}) which implies
	 * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}.
	 * @since 4.2
	 * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
	 * @see ConfigurableBeanFactory#SCOPE_SINGLETON
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
	 * @see #value
	 */
	@AliasFor("value")
	String scopeName() default "";
    
	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}

從源碼中可以看出,在@Scope註解中可以設置如下值。

ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

很明顯,在@Scope註解中可以設置的值包括ConfigurableBeanFactory接口中的SCOPE_PROTOTYPE和SCOPE_SINGLETON,以及WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION。這些都是什麼鬼?別急,我們來一個個查看。

首先,我們進入到ConfigurableBeanFactory接口中,發現在ConfigurableBeanFactory類中存在兩個常量的定義,如下所示。

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
	String SCOPE_SINGLETON = "singleton";
	String SCOPE_PROTOTYPE = "prototype";
    /*****************此處省略N多行代碼*******************/
}

沒錯,SCOPE_SINGLETON就是singleton,SCOPE_PROTOTYPE就是prototype。

那麼,WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION又是什麼鬼呢?就是說,當我們使用了Web容器來運行Spring應用時,在@Scope註解中可以設置WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION的值,而SCOPE_REQUEST的值就是request,SCOPE_SESSION的值就是session。

綜上,在@Scope註解中的取值如下所示。

  • singleton:表示組件在Spring容器中是單實例的,這個是Spring的默認值,Spring在啟動的時候會將組件進行實例化並加載到Spring容器中,之後,每次從Spring容器中獲取組件時,直接將實例對象返回,而不必再次創建實例對象。從Spring容器中獲取對象,小夥伴們可以理解為從Map對象中獲取對象。
  • prototype:表示組件在Spring容器中是多實例的,Spring在啟動的時候並不會對組件進行實例化操作,而是每次從Spring容器中獲取組件對象時,都會創建一個新的實例對象並返回。
  • request:每次請求都會創建一個新的實例對象,request作用域用在spring容器的web環境中。
  • session:在同一個session範圍內,創建一個新的實例對象,也是用在web環境中。
  • application:全局web應用級別的作用於,也是在web環境中使用的,一個web應用程序對應一個bean實例,通常情況下和singleton效果類似的,不過也有不一樣的地方,singleton是每個spring容器中只有一個bean實例,一般我們的程序只有一個spring容器,但是,一個應用程序中可以創建多個spring容器,不同的容器中可以存在同名的bean,但是sope=aplication的時候,不管應用中有多少個spring容器,這個應用中同名的bean只有一個。

其中,request和session作用域是需要Web環境支持的,這兩個值基本上使用不到,如果我們使用Web容器來運行Spring應用時,如果需要將組件的實例對象的作用域設置為request和session,我們通常會使用request.setAttribute(“key”,object)和session.setAttribute(“key”, object)的形式來將對象實例設置到request和session中,通常不會使用@Scope註解來進行設置。

單實例bean作用域

首先,我們在io.mykit.spring.plugins.register.config包下創建PersonConfig2配置類,在PersonConfig2配置類中實例化一個Person對象,並將其放置在Spring容器中,如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Scope註解設置的作用域
 */
@Configuration
public class PersonConfig2 {

    @Bean("person")
    public Person person(){
        return new Person("binghe002", 18);
    }
}

接下來,在SpringBeanTest類中創建testAnnotationConfig2()測試方法,在testAnnotationConfig2()方法中,創建ApplicationContext對象,創建完畢后,從Spring容器中按照id獲取兩個Person對象,並打印兩個對象是否是同一個對象,代碼如下所示。

@Test
public void testAnnotationConfig2(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    //從Spring容器中獲取到的對象默認是單實例的
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
    System.out.println(person1 == person2);
}

由於對象在Spring容器中默認是單實例的,所以,Spring容器在啟動時就會將實例對象加載到Spring容器中,之後,每次從Spring容器中獲取實例對象,直接將對象返回,而不必在創建新對象實例,所以,此時testAnnotationConfig2()方法會輸出true。如下所示。

這也驗證了我們的結論:對象在Spring容器中默認是單實例的,Spring容器在啟動時就會將實例對象加載到Spring容器中,之後,每次從Spring容器中獲取實例對象,直接將對象返回,而不必在創建新對象實例。

多實例bean作用域

修改Spring容器中組件的作用域,我們需要藉助於@Scope註解,此時,我們將PersonConfig2類中Person對象的作用域修改成prototype,如下所示。

@Configuration
public class PersonConfig2 {

    @Scope("prototype")
    @Bean("person")
    public Person person(){
        return new Person("binghe002", 18);
    }
}

其實,使用@Scope設置作用域就等同於在XML文件中為bean設置scope作用域,如下所示。

此時,我們再次運行SpringBeanTest類的testAnnotationConfig2()方法,此時,從Spring容器中獲取到的person1對象和person2對象還是同一個對象嗎?

通過輸出結果可以看出,此時,輸出的person1對象和person2對象已經不是同一個對象了。

單實例bean作用域何時創建對象?

接下來,我們驗證下在單實例作用域下,Spring是在什麼時候創建對象的呢?

首先,我們將PersonConfig2類中的Person對象的作用域修改成單實例,並在返回Person對象之前打印相關的信息,如下所示。

@Configuration
public class PersonConfig2 {
    @Scope
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

接下來,我們在SpringBeanTest類中創建testAnnotationConfig3()方法,在testAnnotationConfig3()方法中,我們只創建Spring容器,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
}

此時,我們運行SpringBeanTest類中的testAnnotationConfig3()方法,輸出的結果信息如下所示。

從輸出的結果信息可以看出,Spring容器在創建的時候,就將@Scope註解標註為singleton的組件進行了實例化,並加載到Spring容器中。

接下來,我們運行SpringBeanTest類中的testAnnotationConfig2(),結果信息如下所示。

說明,Spring容器在啟動時,將單實例組件實例化之後,加載到Spring容器中,以後每次從容器中獲取組件實例對象,直接返回相應的對象,而不必在創建新對象。

多實例bean作用域何時創建對象?

如果我們將對象的作用域修改成多實例,那什麼時候創建對象呢?

此時,我們將PersonConfig2類的Person對象的作用域修改成多實例,如下所示。

@Configuration
public class PersonConfig2 {

    @Scope("prototype")
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,輸出的結果信息如下所示。

可以看到,終端並沒有輸出任何信息,說明在創建Spring容器時,並不會實例化和加載多實例對象,那多實例對象是什麼時候實例化的呢?接下來,我們在SpringBeanTest類中的testAnnotationConfig3()方法中添加一行獲取Person對象的代碼,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,結果信息如下所示。

從結果信息中,可以看出,當向Spring容器中獲取Person實例對象時,Spring容器實例化了Person對象,並將其加載到Spring容器中。

那麼,問題來了,此時Spring容器是否只實例化一個Person對象呢?我們在SpringBeanTest類中的testAnnotationConfig3()方法中再添加一行獲取Person對象的代碼,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,結果信息如下所示。

從輸出結果可以看出,當對象的Scope作用域為多實例時,每次向Spring容器獲取對象時,都會創建一個新的對象並返回。此時,獲取到的person1和person2就不是同一個對象了,我們也可以打印結果信息來進行驗證,此時在SpringBeanTest類中的testAnnotationConfig3()方法中打印兩個對象是否相等,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
    System.out.println(person1 == person2);
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,結果信息如下所示。

可以看到,當對象是多實例時,每次從Spring容器中獲取對象時,都會創建新的實例對象,並且每個實例對象都不相等。

單實例bean注意的事項

單例bean是整個應用共享的,所以需要考慮到線程安全問題,之前在玩springmvc的時候,springmvc中controller默認是單例的,有些開發者在controller中創建了一些變量,那麼這些變量實際上就變成共享的了,controller可能會被很多線程同時訪問,這些線程併發去修改controller中的共享變量,可能會出現數據錯亂的問題;所以使用的時候需要特別注意。

多實例bean注意的事項

多例bean每次獲取的時候都會重新創建,如果這個bean比較複雜,創建時間比較長,會影響系統的性能,這個地方需要注意。

自定義Scope

如果Spring內置的幾種sope都無法滿足我們的需求的時候,我們可以自定義bean的作用域。

1.如何實現自定義Scope

自定義Scope主要分為三個步驟,如下所示。

(1)實現Scope接口

我們先來看下Scope接口的定義,如下所示。

package org.springframework.beans.factory.config;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.lang.Nullable;

public interface Scope {

    /**
    * 返回當前作用域中name對應的bean對象
    * name:需要檢索的bean的名稱
    * objectFactory:如果name對應的bean在當前作用域中沒有找到,那麼可以調用這個ObjectFactory來創建這個對象
    **/
    Object get(String name, ObjectFactory<?> objectFactory);

    /**
     * 將name對應的bean從當前作用域中移除
     **/
    @Nullable
    Object remove(String name);

    /**
     * 用於註冊銷毀回調,如果想要銷毀相應的對象,則由Spring容器註冊相應的銷毀回調,而由自定義作用域選擇是不是要銷毀相應的對象
     */
    void registerDestructionCallback(String name, Runnable callback);

    /**
     * 用於解析相應的上下文數據,比如request作用域將返回request中的屬性。
     */
    @Nullable
    Object resolveContextualObject(String key);

    /**
     * 作用域的會話標識,比如session作用域將是sessionId
     */
    @Nullable
    String getConversationId();

}

(2)將Scope註冊到容器

需要調用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下這個方法的聲明

/**
* 向容器中註冊自定義的Scope
*scopeName:作用域名稱
* scope:作用域對象
**/
void registerScope(String scopeName, Scope scope);

(3)使用自定義的作用域

定義bean的時候,指定bean的scope屬性為自定義的作用域名稱。

2.自定義Scope實現案例

例如,我們來實現一個線程級別的bean作用域,同一個線程中同名的bean是同一個實例,不同的線程中的bean是不同的實例。

這裏,要求bean在線程中是共享的,所以我們可以通過ThreadLocal來實現,ThreadLocal可以實現線程中數據的共享。

此時,我們在io.mykit.spring.plugins.register.scope包下新建ThreadScope類,如下所示。

package io.mykit.spring.plugins.register.scope;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定義本地線程級別的bean作用域,不同的線程中對應的bean實例是不同的,同一個線程中同名的bean是同一個實例
 */
public class ThreadScope implements Scope {

    public static final String THREAD_SCOPE = "thread";

    private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new HashMap<>();
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get().get(name);
        if (Objects.isNull(bean)) {
            bean = objectFactory.getObject();
            beanMap.get().put(name, bean);
        }
        return bean;
    }

    @Nullable
    @Override
    public Object remove(String name) {
        return this.beanMap.get().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        //bean作用域範圍結束的時候調用的方法,用於bean清理
        System.out.println(name);
    }

    @Nullable
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Nullable
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

在ThreadScope類中,我們定義了一個常量THREAD_SCOPE,在定義bean的時候給scope使用。

接下來,我們在io.mykit.spring.plugins.register.config包下創建PersonConfig3類,並使用@Scope(“thread”)註解標註Person對象的作用域為Thread範圍,如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Scope註解設置的作用域
 */
@Configuration
public class PersonConfig3 {

    @Scope("thread")
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

最後,我們在SpringBeanTest類中創建testAnnotationConfig4()方法,在testAnnotationConfig4()方法中創建Spring容器,並向Spring容器中註冊ThreadScope對象,接下來,使用循環創建兩個Thread線程,並分別在每個線程中獲取兩個Person對象,如下所示。

@Test
public void testAnnotationConfig4(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig3.class);
    //向容器中註冊自定義的scope
    context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());

    //使用容器獲取bean
    for (int i = 0; i < 2; i++) { 
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此時,我們運行SpringBeanTest類的testAnnotationConfig4()方法,輸出的結果信息如下所示。

從輸出中可以看到,bean在同樣的線程中獲取到的是同一個bean的實例,不同的線程中bean的實例是不同的。

注意:這裏,我將Person類進行了相應的調整,去掉Lombok的註解,手動寫構造函數和setter與getter方法,如下所示。

package io.mykit.spring.bean;

import java.io.Serializable;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試實體類
 */
public class Person implements Serializable {
    private static final long serialVersionUID = 7387479910468805194L;
    private String name;
    private Integer age;

    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

博斯普魯斯海峽變色 出現正港土耳其藍

摘錄自2020年5月27日中央社報導

「每日晨報」(Daily Sabah)報導,博斯普魯斯海峽(Bosporus)水色自26日起轉變成「土耳其藍」。伊斯坦堡科技大學(Istanbul Technical University)教授托羅斯(Huseyin Toros)認為,東北風是導致「海水變色」主要原因。

托羅斯指出:「單細胞生物被東北風曳引進入博斯普魯斯海峽,海水表面經過折射,轉變成土耳其藍色。在此一大氣環境下的氣流、海平面下的活動、不同微生物、白天陽光變化等因素也可能導致海水顏色產生變化。」他表示,海水將於幾天內恢復「本色」。

美國國家航空暨太空總署(NASA)的衛星於當年5月29日首度補捉到黑海浮游生物激增的圖像。漁夫們相信,海中出現大量浮游生物意味當年鯷魚產量將會大增。但是浮游生物也會消耗水中大量氧氣,從而對其他海洋生物造成傷害。

土地水文
土地利用
國際新聞
土耳其
海水
港口
浮游生物

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

掌握SpringBoot-2.3的容器探針:基礎篇

歡迎訪問我的GitHub

  • 地址:https://github.com/zq2599/blog_demos
  • 內容:原創文章分類匯總,及配套源碼,涉及Java、Docker、K8S、DevOPS等

關於《SpringBoot-2.3容器化技術》系列

《SpringBoot-2.3容器化技術》系列,旨在和大家一起學習實踐2.3版本帶來的最新容器化技術,讓咱們的Java應用更加適應容器化環境,在雲計算時代依舊緊跟主流,保持競爭力;
全系列文章分為主題和輔助兩部分,主題部分如下:

  1. 《體驗SpringBoot(2.3)應用製作Docker鏡像(官方方案)》;
  2. 《詳解SpringBoot(2.3)應用製作Docker鏡像(官方方案)》;
  3. 《掌握SpringBoot-2.3的容器探針:基礎篇》;
  4. 《掌握SpringBoot-2.3的容器探針:深入篇》;
  5. 《掌握SpringBoot-2.3的容器探針:實戰篇》;
  • 輔助部分是一些參考資料和備忘總結,如下:
  1. 《SpringBoot-2.3鏡像方案為什麼要做多個layer》;
  2. 《設置非root賬號不用sudo直接執行docker命令》;
  3. 《開發階段,將SpringBoot應用快速部署到K8S》;

SpringBoot容器探針系列文章簡介

為了讓應用更適應容器化環境,SpringBoot2.3版本推出了新的探針技術,《掌握SpringBoot-2.3的容器探針》系列旨在與您一起學習和實踐這些新技術,分為三個階段:

  1. 基礎篇:即本文,對容器探針的相關知識點進行梳理和學習;
  2. 深入篇:繼續深入學習探針相關的知識點;
  3. 實戰篇:將springboot應用部署在kubernetes環境,並使用最新的探針技術;

探針特性的官方信息

  • 如下圖紅框所示,2.3版本的容器探針特性早在預覽版(v2.3.0.M4)就已經發布:

  • 如今v2.3.0.RELEASE已發布,可以放心的學習和使用該特性了,首先把基礎知識點列出來,確保準備工作OK;

知識點整理

下面是掌握探針技術所需的基礎知識,也是本文的主要內容:

  1. kubernetes的存活探針livenessProbe
  2. kubernetes的就緒探針readinessProbe
  3. SpringBoot的actuator

接下來逐個學習,有了這些知識積累,我們才能更好的閱讀官方資料,開發適合自己業務場景的探針;

kubernetes的存活探針livenessProbe

  1. kubernetes的探針涉及的內容是很多的,這裏只提和SpringBoot相關的部分;
  2. kubelet 使用存活探針livenessProbe來知道什麼時候要重啟容器;
  3. 下圖是kubernetes官網的存活探針示例,幾個關鍵參數已經做了詳細說明:
  1. 可見如果我們的SpringBoot應用發布到kubernetes環境,只要應用還健康,livenessProbe對應的地址就要能響應200-400的返回碼;

kubernetes的就緒探針readinessProbe

  1. 有時候,應用程序會暫時性的不能提供通信服務。例如,應用程序在啟動時可能需要加載很大的數據或配置文件,或是啟動后要依賴等待外部服務。在這種情況下,既不想殺死應用程序,也不想給它發送請求。Kubernetes 提供了就緒探測器來發現並緩解這些情況。容器所在 Pod 上報還未就緒的信息,並且不接受通過 Kubernetes Service 的流量。
  2. 就緒探測器的配置和存活探測器的配置相似,唯一區別就是要使用 readinessProbe字段,而不是 livenessProbe 字段;
  3. 簡單的說,就緒探針正常的容器,k8s就認為是可以對外提供服務的,相應的請求也會被調度到該容器上來;

SpringBoot的actuator

  1. 簡單來說,actuator是用來幫助用戶監控和操作SprinBoot應用的,這些監控和操作都可以通過http請求實現,如下圖,http://localhost:8080/actuator/health 地址返回的是應用的健康狀態:
  1. 下面是常用的actuator地址,訪問不同的地址可以得到不同的信息:
  1. 在SpringBoot-2.3版本中,actuator新增了兩個地址:/actuator/health/liveness/actuator/health/readiness,前者用作kubernetes的存活探針,後者用作kubernetes的就緒探針

畫外音:SpringBoot的探針技術就這點東西?

  1. 文章看到這裏,您可能覺得索然無味:所謂的容器探針特性如此簡單,新增兩個actuator地址留給kubernetes的存活和就緒探針用,只要這兩個地址響應正常,kubernetes就判定該容器正常;
  2. 大多數時候,上述結論並無不妥,SpringBoot官方給出的推薦配置如下圖,我們只要照搬即可:
  1. 冷靜下來仔細思考,有三個問題似乎沒有解決:
  • 首先,SpringBoot為kubernetes提供了兩個actuator項,但是那些並未部署在kubernetes的SringBoot應用呢?用不上這兩項也要對外暴露這兩個服務地址嗎?

  • 其次,就緒探針是什麼時候開始返回200返回碼的?應用啟動階段,業務服務可能需要一段時間才能正常工作,就緒探針要是提前返回了200,那k8s就認為容器可以正常工作了,這時候把外部請求調度過來是無法正常響應的,所以搞清楚就緒探針的狀態變化邏輯很重要;

  • 最後,也是最重要的一點:有的場景下,例如外部依賴服務異常、本地全局異常等情況下,業務不想對外提供服務,等到問題解決后業務又可以對外提供服務了,如果此時我們能自己寫代碼控制就緒探針的返回碼,那就做到了控制kubernetes是否將外部請求調度到此容器上,這可是個很實用的功能!

還需要繼續深入

面對上述三個問題您是否會感慨:看似簡單的容器探針技術,想要用好還需掌握更多知識,接下來的文章中咱們一起努力吧,從知識覆蓋到實戰操練,終究會掌握這門實用技術;

歡迎關注我的公眾號:程序員欣宸

https://github.com/zq2599/blog_demos

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

法國擬開採莫三比克天然氣 環團批製造氣候炸彈

摘錄自2020年6月15日中央社報導

法國石油業巨擘道達爾(Total)正在莫三比克發展開採天然氣的大型計畫,環保組織「地球之友」今(15日)指控法國此舉無異是在當地放置一枚「氣候定時炸彈」。

法新社報導,「地球之友」(Friends of the Earth)一份報告名為「產業的意外之財,莫三比克的詛咒:法國把莫三比克推進氣阱」(A windfall for the industry, a curse for the country: France Thrusts Mozambique into the gas trap)。報告指出,2010年代初在莫三比克的北海岸外海水面下發現巨大的天然氣儲量,開採總投資金額將達600億美元。

「地球之友」指控,「法國這項陰謀是代表法國能源產業與銀行家的經濟利益……迫使另個非洲國家仰賴化石燃料」。

這份報告指出,法國正研擬三個天然氣計畫,「釋放出的溫室氣體可能相當於法國一年溫室氣體排放量的7倍,更是莫三比克目前年排放量的49倍」。

能源議題
能源轉型
國際新聞
法國
天然氣
油氣開採

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

俄羅斯西伯利亞飆高溫 北極圈小鎮破天荒測攝氏38°C

摘錄自2020年6月22日聯合報報導

根據氣象數據網站的資料顯示,過去曾出現攝氏零下68°C極端低溫的俄羅斯西伯利亞小鎮維爾霍揚斯克(Verkhoyansk),竟在昨天測得攝氏38°C高溫。

美聯社報導,彙整俄羅斯氣象數據網站Pogoda iKlimat的資料指出,維爾霍揚斯克鎮20日高溫達到攝氏38°C(華氏100.4°F)。

西伯利亞大部分地區今年出現異常高溫,導致大規模野火重創當地森林。俄羅斯薩哈共和國(Sakha Republic)的維爾霍揚斯克鎮在北極圈內,位於首都莫斯科(Moscow)東北方大約4660公里處。

全球變遷
氣候變遷
國際新聞
俄羅斯
西伯利亞
歷史高溫
全球暖化

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

Pytest單元測試框架——Pytest+Allure+Jenkins的應用

一、簡介

  pytest+allure+jenkins進行接口測試、生成測試報告、結合jenkins進行集成。

  pytest是python的一種單元測試框架,與python自帶的unittest測試框架類似,但是比unittest框架使用起來更簡潔,效率更高

  allure-pytest是python的一個第三方庫。用於連接pytest和allure,使它們可以配合在一起使用。

  allure-pytest基於pytest的原始執行結果生成適用於allure的json格式結果。該json格式結果可以用於後續適用allure生成html結果。

二、安裝  

  1、安裝pytest,命令行或終端中輸入

1 pip install pytest

  2、安裝allure-pytest,安裝成功

1 pip install allure-pytest

  allure-pytest安裝成功后截圖如下。

  3、下載安裝JDK

  官方下載:https://www.oracle.com/java/technologies/javase-jdk11-downloads.html

  安裝與配置不作闡述請諒解  

  4、下載安裝Jenkins

  官方下載:https://www.jenkins.io/

  安裝與配置不作闡述請諒解

三、下載Allure並配置

  下載allure並配置

  1、allure官網下載:https://github.com/allure-framework/allure2/releases

  如下圖所示

  2、allure2下載下來是一個zip的壓縮包,我們要解壓至自己的文件目錄下(可解壓放至項目的測試用例下或python安裝目錄下),自己可找到文件即可。

  3、打開allure2目錄,找到bin目錄,複製bin文件目錄, 然後進行環境變量的配置,設置環境變量的目的就是讓系統無論在哪個目錄下都可以運行allure2。

  4、環境變量設置:(桌面——我的電腦——右鍵屬性——高級系統配置——環境變量——系統變量——Path——編輯環境變量——把我們上面複製的目錄路徑新增至環境變量中即可)

  設置環境變量,如下圖所示。

 

  5、配置好后,打開cmd終端,輸入allure,出現以下幫助文檔,就說明配置成功了。

 四、Allure裝飾器描述

  Allure裝飾器

 五、Pytest+Allure的應用

  上述我們講了一些理論的知識,下面我們就來實戰練習一下吧。進一步理解Pytest+allure如何結合應用的。

  1、新建testcase文件夾,用來存放測試用例,新建test_Demo.py文件,作為pytest的具體測試用例文件。在test_Demo.py文件中輸入以下代碼。

 1 # test_Demo.py
 2 # Creator:wuwei
 3 # Date:2020-06-09
 4 
 5 import pytest
 6 import requests
 7 import allure
 8 import sys
 9 sys.dont_write_bytecode = True
10 
11 @allure.epic('測試描述'.center(30, '*'))
12 @allure.feature('測試模塊')
13 @allure.suite('測試套件')
14 class TestPytestOne():
15     @allure.story('用戶故事描述:用例一')
16     @allure.title('測試標題:用例一')
17     @allure.description('測試用例描述:用例一')
18     @allure.testcase('測試用例地址:https://www.baidu.com/')
19     @allure.tag('測試用例標籤:用例一')
20     def test_one(self):
21         print('執行第一個用例')
22         assert 1 == 1
23 
24     @allure.story('用戶故事描述:用例二')
25     @allure.title('測試標題:用例二')
26     @allure.description('測試用例描述:用例二')
27     @allure.testcase('測試用例地址:https://www.sogou.com/')
28     @allure.tag('測試用例標籤:用例二')
29     def test_two(self,action):
30         print('執行第二個用例')
31         assert True == True
32 
33 # pytest運行
34 if __name__ == "__main__":
35     pytest.main(['-s', '-v', 'test_Demo.py', '-q', '--alluredir', '../reports'])

  2、我們再來創建一個conftest.py,conftest用來共享數據及不同層次之間共享使用的文件,測試用例的前置和後置中一般都可以用到的。

 1 # conftest.py
 2 # Creator:wuwei
 3 # Date:2020-06-09
 4 
 5 import pytest
 6 import sys
 7 sys.dont_write_bytecode = True
 8 
 9 @pytest.fixture()
10 def action():
11     print("測試用例開始".center(30, '*'))
12     yield
13     print("測試用例結束".center(30, '*'))

  3、運行test_Demo.py文件,test_Demo文件中已經pytest+allure的結合,可查看allure的運行結果,可看出在根目錄中生成了一個reports文件夾,其中生成了測試報告的json文件,這裏面的json文件可通過allure生成html的測試報告。
  運行test_Demo.py,終端显示如下圖所示。

   生成的Json格式的測試報告,如下圖所示。

   4、使用allure將json文件生成html的測試報告,定位至項目文件根目錄下,運行以下命令,會在項目根目錄下生成一個名為allure_reports的文件夾,用來存放html測試報告。命令下如所示。

1 allure generate reports -o allure_reports/

  成功運行allure,結果如下圖所示。

  項目根目錄下的allure_reports文件,存放的是allure生成的測試報告。可看出文件下有一個HTML文件,可通過Python的編輯器Pycharm來打開該HTML文件(測試報告),或可通過allure命令來打開該HTML,展示HTML測試報告。如下所示。

  測試報告文件,HTML測試報告如下。

  allure命令打開HTML測試報告。命令如下所示。

1 allure open allure_reports/

  如下圖所示。

   打開生成的HTML測試報告如下圖所示。

 六、Pytest+Allure+Jenkins的應用

  1、Jenkins插件網站上下載allure插件最新版本:

    http://mirrors.jenkins-ci.org/plugins/allure-jenkins-plugin/

  2、Jenkins的安裝我已經在Postman+Newman+Git+Jenkins的篇章中講過了,沒看小夥伴可以看一下那篇文章。確認Jenkins服務是否開啟。確認開啟后,在瀏覽器中輸入:http://localhost:8080/,進入Jenkins配置頁面。

  3、http://localhost:8080/,登錄Jenkins的頁面,在管理Jenkins——插件管理——高級中找到上傳插件。將(1)步驟中下載的.hpi的文件上傳至jenkins上。

  上傳安裝好的allure-jenkins-plugin的插件,安裝完成並成功,是藍色圓點显示,因我已經安裝過一次,會提示已經安裝,重啟Jenkins即可生效。(注意:不是關閉瀏覽器重新打開,而是重啟Jenkins服務

  4、全局變量中配置allure路徑與JDK的路徑,

  配置JDK安裝的路徑,如下圖所示。

  配置allure安裝的路徑,如下圖所示。

  5、新建Item,配置構建后的allure測試報告生成。這裏配置Pytest執行完成之後,生成的allure文件所在的目錄位置。

  項目中生成allure的json測試報告的位置。需與下面構建后操作中的Results的Path文件一致。

  構建后操作的allure生成測試報告的配置,如下圖所示

  6、配置構建命令。就是上述在cmd中運行項目時的命令。如下圖所示。

注意:運行后發現有報錯。“Build step ‘Execute Windows batch command’ marked build as failure”,解決方案,在運行項目的命令后添加exit 0。如下圖所示。

  7、修改運行命令后我們再來運行一下。我們可發現運行后,allure裏面沒任務數據。因為我們還沒設置運行的項目路徑。設置工作空間,打開工作空間目錄,將我們的項目複製到jenkins的工作目錄中。

  我們可將代碼傳至GitHub上,在Jenkins中設置相關Github項目的配置,也可進行Jenkins部署。我在Postman+Newman+Git+Jenkins這篇博客里就應用到了。有興趣的可參考看看這篇Jenkins如何Git項目。在這裏我們使用本地項目來部署。

  測試報告無數據因為工作空間裏面沒有項目配置。

  複製項目至Jenkins工作空間的目錄中。

  8、添加項目后,我們再運行一下,藍點則為運行成功,可看到後面已經生成了allure的測試報告了。可直接點擊後面的alluree圖標跳轉至HTML的測試報告。如下圖所示。

  allure生成的HTML測試報告

八、總結

  上述我們聊了下pytest+allure+jenkins如何結合集成一起使用的,本地啟動jenkins,運行項目,調用allure生成測試報告。也簡單的做了一個小Demo。後期我將結合Requests接口測試和seleniumWeb測試應用至具體項目中。

 

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

想要成家的90后,12萬元究竟能買到什麼好車?

以上車型都是適合年輕人的,即使是使用貸款的方式購買也不會造成太大的壓力,關鍵的一點是這些車型基本可以陪伴你度過初初成家、孩子長大的過程。

以上車型都是適合年輕人的,即使是使用貸款的方式購買也不會造成太大的壓力,關鍵的一點是這些車型基本可以陪伴你度過初初成家、孩子長大的過程。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準