Python 爬蟲從入門到進階之路(四)

之前的文章我們做了一個簡單的例子爬取了百度首頁的 html,我們用到的是 urlopen 來打開請求,它是一個特殊的opener(也就是模塊幫我們構建好的)。但是基本的 urlopen() 方法不支持代理、cookie等其他的HTTP/HTTPS高級功能,所以我們需要用到 Python 的 opener 來自定義我們的請求內容。

具體步驟:

  1. 使用相關的 Handler處理器 來創建特定功能的處理器對象;
  2. 然後通過 build_opener()方法使用這些處理器對象,創建自定義opener對象;
  3. 使用自定義的opener對象,調用open()方法發送請求。

我們先來回顧一下使用 urlopen 獲取百度首頁的 html 代碼實例:

 1 # 導入urllib 庫
 2 import urllib.request
 3 
 4 # url 作為Request()方法的參數,構造並返回一個Request對象
 5 request = urllib.request.Request("http://www.baidu.com")
 6 # Request對象作為urlopen()方法的參數,發送給服務器並接收響應
 7 response = urllib.request.urlopen(request)
 8 # 類文件對象支持 文件對象的操作方法,如read()方法讀取文件全部內容,返回字符串
 9 html = response.read().decode("utf-8")
10 # 打印字符串
11 print(html)

接下來我們看一下使用 opener 的處理方式:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler()
 5 
 6 # 構建一個HTTPSHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler()
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

 

在上面的第一段代碼中,我們是通過直接  import urllib.request   來導入我們需要的包,這樣當我們要使用時需要   urllib.request   來使用,第二段代碼我們是通過  from urllib import request  來導入我們需要的包,這樣當我們使用時直接  request 來使用就可以了。

第一段代碼在前面的文章中我們已經說過了,這裏就不多做解釋了。

第二段代碼中,我們使用了 opener 的方法來處理我們的請求,這樣我們就可以對代理,cookie 等做進一步的操作,後續文章會講到。最終結果如下:

在  http_handler = request.HTTPHandler() 中,我們還可以添加一個  debuglevel=1 參數,會將 Debug Log 打開,這樣程序在執行的時候,會把收包和發包的報頭在屏幕上自動打印出來,方便調試,有時可以省去抓包的工作。

代碼如下:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler(debuglevel=1)
 5 
 6 # 構建一個HTTPHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler(debuglevel=1)
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

輸出結果如下:

可以看出在響應結果的時候會為我們打印輸出一些請求信息。

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

一次線上Redis類轉換異常排查引發的思考

之前同事反饋說線上遇到Redis反序列化異常問題,異常如下:

XxxClass1 cannot be cast to XxxClass2

已知信息如下:

  • 該異常不是必現的,偶爾才會出現;
  • 出現該異常后重啟應用或者過一會就好了;
  • 序列化協議使用了hessian。

因為偶爾出現,首先看了報異常那塊業務邏輯是不是有問題,看了一遍也發現什麼問題。看了下對應日誌,發現是在Redis讀超時之後才出現的該異常,因此懷疑redis client操作邏輯那塊導致的(公司架構組對redis做了一層封裝),發現獲取/釋放redis連接如下代碼:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedisPool.returnResourceObject(jedis);
10     }
11 }

初步認定原因為:發生了讀寫超時的連接,直接歸還給連接池,下次使用該連接時讀取到了上一次Redis返回的數據。因此本地驗證下,示例代碼如下:

 1 @Data
 2 @NoArgsConstructor
 3 @AllArgsConstructor
 4 static class Person implements Serializable {
 5     private String name;
 6     private int age;
 7 }
 8 @Data
 9 @NoArgsConstructor
10 @AllArgsConstructor
11 static class Dog implements Serializable {
12     private String name;
13 }
14 
15 public static void main(String[] args) throws Exception {
16     JedisPoolConfig config = new JedisPoolConfig();
17     config.setMaxTotal(1);
18     JedisPool jedisPool = new JedisPool(config, "192.168.193.133", 6379, 2000, "123456");
19 
20     Jedis jedis = jedisPool.getResource();
21     jedis.set("key1".getBytes(), serialize(new Person("luoxn28", 26)));
22     jedis.set("key2".getBytes(), serialize(new Dog("tom")));
23     jedisPool.returnResourceObject(jedis);
24 
25     try {
26         jedis = jedisPool.getResource();
27         Person person = deserialize(jedis.get("key1".getBytes()), Person.class);
28         System.out.println(person);
29     } catch (Exception e) {
30         // 發生了異常之後,未對該連接做任何處理
31         System.out.println(e.getMessage());
32     } finally {
33         if (jedis != null) {
34             jedisPool.returnResourceObject(jedis);
35         }
36     }
37 
38     try {
39         jedis = jedisPool.getResource();
40         Dog dog = deserialize(jedis.get("key2".getBytes()), Dog.class);
41         System.out.println(dog);
42     } catch (Exception e) {
43         System.out.println(e.getMessage());
44     } finally {
45         if (jedis != null) {
46             jedisPool.returnResourceObject(jedis);
47         }
48     }
49 }

連接超時時間設置2000ms,為了方便測試,可以在redis服務器上使用gdb命令斷住redis進程(如果redis部署在Linux系統上的話,還可以使用iptable命令在防火牆禁止某個回包),比如在執行 jedis.get("key1".getBytes() 代碼前,對redis進程使用gdb命令斷住,那麼就會導致讀取超時,然後就會觸發如下異常:

Person cannot be cast to Dog

既然已經知道了該問題原因並且本地復現了該問題,對應解決方案是,在發生異常時歸還給連接池時關閉該連接即可(jedis.close內部已經做了判斷),代碼如下:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedis.close();
10     }
11 }

至此,該問題解決。注意,因為使用了hessian序列化(其包含了類型信息,類似的有Java本身序列化機制),所有會報類轉換異常;如果使用了json序列化(其只包含對象屬性信息),反序列化時不會報異常,只不過因為不同類的屬性不同,會導致反序列化后的對象屬性為空或者屬性值混亂,使用時會導致問題,並且這種問題因為沒有報異常所以更不容易發現。

 

既然說到了Redis的連接,要知道的是,Redis基於RESP(Redis Serialization Protocol)協議來通信,並且通信方式是停等方式,也就說一次通信獨佔一個連接直到client讀取到返回結果之後才能釋放該連接讓其他線程使用。小夥伴們可以思考一下,Redis通信能否像dubbo那樣使用單連接+序列號(標識單次通信)通信方式呢?理論上是可以的,不過由於RESP協議中並沒有一個”序列號”的字段,所以直接靠原生的通信方法來實現是不現實的。不過我們可以通過echo命令傳遞並返回”序列號”+正常的讀寫方式來實現,這裏要保證二者執行的原子性,可以通過lua腳本或者事務來實現,事務方式如下:

MULTI
ECHO "唯一序列號"
GET key1
EXEC

然後客戶端收到的結果是一個 [ "唯一序列號", "value1" ]的列表,你可以根據前一項識別出這是你發送的哪個請求。

為什麼Redis通信方式並沒有採用類似於dubbo這種通信方式呢,個人認為有以下幾點:

  • 使用停等這種通信方式實現簡單,並且協議字段盡可能緊湊;
  • Redis都是內存操作,處理性能較強,停等協議不會造成客戶端等待時間較長;
  • 目前來看,通信方式這塊不是Redis使用上的性能瓶頸,這一點很重要。

 

推薦閱讀:

  • 別再問我ConcurrentHashMap了
  • 分佈式鎖設計與實現

  • ConcurrentHashMap竟然也有死循環問題?

  • 你的ThreadLocal線程安全么

 歡迎小夥伴掃描以下二維碼閱讀更多精彩好文。

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

高級Java工程師必備 —– 深入分析 Java IO (一)BIO

BIO編程

最原始BIO

網絡編程的基本模型是C/S模型,即兩個進程間的通信。

服務端提供IP和監聽端口,客戶端通過連接操作想服務端監聽的地址發起連接請求,通過三次握手連接,如果連接成功建立,雙方就可以通過套接字進行通信。

傳統的同步阻塞模型開發中,ServerSocket負責綁定IP地址,啟動監聽端口;Socket負責發起連接操作。連接成功后,雙方通過輸入和輸出流進行同步阻塞式通信。
最原始BIO通信模型圖:

存在的問題:

  • 同一時間,服務器只能接受來自於客戶端A的請求信息;雖然客戶端A和客戶端B的請求是同時進行的,但客戶端B發送的請求信息只能等到服務器接受完A的請求數據后,才能被接受。(acceptor只有在接受完client1的請求后才能接受client2的請求)
  • 由於服務器一次只能處理一個客戶端請求,當處理完成並返回后(或者異常時),才能進行第二次請求的處理。很顯然,這樣的處理方式在高併發的情況下,是不能採用的。

一請求一線程BIO

那有沒有方法改進呢? ,答案是有的。改進后BIO通信模型圖:

此種BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後為每個客戶端創建一個新的線程進行鏈路處理沒處理完成后,通過輸出流返回應答給客戶端,線程銷毀。即典型的一請求一應答通宵模型。

代碼演示

服務端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServer {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               SocketServerThread socketServerThread = new SocketServerThread(socket);
               new Thread(socketServerThread).start();
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServer.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThread implements Runnable {
   private Socket socket;
   public SocketServerThread (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

客戶端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.concurrent.CountDownLatch;

public class BioSocketClient{
   public static void main(String[] args) throws Exception {
       Integer clientNumber = 20;
       CountDownLatch countDownLatch = new CountDownLatch(clientNumber);

       // 分別開始啟動這20個客戶端,併發訪問
       for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) {
           ClientRequestThread client = new ClientRequestThread(countDownLatch, index);
           new Thread(client).start();
       }

       // 這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (BioSocketClient.class) {
           BioSocketClient.class.wait();
       }
   }
}



/**
* 一個ClientRequestThread線程模擬一個客戶端請求。
* @author keep_trying
*/
class ClientRequestThread implements Runnable {


   private CountDownLatch countDownLatch;

   /**
    * 這個線程的編號
    * @param countDownLatch
    */
   private Integer clientIndex;

   /**
    * countDownLatch是java提供的同步計數器。
    * 當計數器數值減為0時,所有受其影響而等待的線程將會被激活。這樣保證模擬併發請求的真實性
    * @param countDownLatch
    */
   public ClientRequestThread(CountDownLatch countDownLatch , Integer clientIndex) {
       this.countDownLatch = countDownLatch;
       this.clientIndex = clientIndex;
   }

   @Override
   public void run() {
       Socket socket = null;
       OutputStream clientRequest = null;
       InputStream clientResponse = null;

       try {
           socket = new Socket("localhost",8083);
           clientRequest = socket.getOutputStream();
           clientResponse = socket.getInputStream();

           //等待,直到SocketClientDaemon完成所有線程的啟動,然後所有線程一起發送請求
           this.countDownLatch.await();

           //發送請求信息
           clientRequest.write(("這是第" + this.clientIndex + " 個客戶端的請求。 over").getBytes());
           clientRequest.flush();

           //在這裏等待,直到服務器返回信息
          System.out.println("第" + this.clientIndex + "個客戶端的請求發送完成,等待服務器返回信息");
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           int realLen;
           String message = "";
           //程序執行到這裏,會一直等待服務器返回信息(注意,前提是in和out都不能close,如果close了就收不到服務器的反饋了)
           while((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) {
               message += new String(contextBytes , 0 , realLen);
           }
           //String messageEncode = new String(message , "UTF-8");
           message = URLDecoder.decode(message, "UTF-8");
           System.out.println("第" + this.clientIndex + "個客戶端接收到來自服務器的信息:" + message);
       } catch (Exception e) {

       } finally {
           try {
               if(clientRequest != null) {
                   clientRequest.close();
               }
               if(clientResponse != null) {
                   clientResponse.close();
               }
           } catch (IOException e) {

           }
       }
   }
}   

存在的問題:

  • 雖然在服務器端,請求的處理交給了一個獨立線程進行,但是操作系統通知accept()的方式還是單個的。也就是,實際上是服務器接收到數據報文後的“業務處理過程”可以多線程,但是數據報文的接受還是需要一個一個的來(acceptor只有在接受完client1的請求后才能接受client2的請求),下文會驗證。
  • 在linux系統中,可以創建的線程是有限的。我們可以通過cat /proc/sys/kernel/threads-max命令查看可以創建的最大線程數。當然這個值是可以更改的,但是線程越多,CPU切換所需的時間也就越長,用來處理真正業務的需求也就越少。
  • 另外,如果您的應用程序大量使用長連接的話,線程是不會關閉的。這樣系統資源的消耗更容易失控。

偽異步I/O編程

為了改進這種一連接一線程的模型,我們可以使用線程池來管理這些線程,實現1個或多個線程處理N個客戶端的模型(但是底層還是使用的同步阻塞I/O),通常被稱為“偽異步I/O模型“。

偽異步I/O模型圖:

代碼演示

只給出服務端,客戶端和上面相同

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServerThreadPool {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  
   //線程池 懶漢式的單例  
   private static ExecutorService executorService = Executors.newFixedThreadPool(60);  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               //當然業務處理過程可以交給一個線程(這裏可以使用線程池),並且線程的創建是很耗資源的。
               //最終改變不了.accept()只能一個一個接受socket的情況,並且被阻塞的情況
               SocketServerThreadPool socketServerThreadPool = new SocketServerThreadPool(socket);
               executorService.execute(socketServerThreadPool);
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServerThreadPool.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThreadPool implements Runnable {
   private Socket socket;
   public SocketServerThreadPool (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

服務器端的執行效果

在 Socket socket = serverSocket.accept(); 處打了斷點,有20個客戶端同時發出請求,可服務端還是一個一個的處理,其它線程都處於阻塞狀態

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

阻塞的問題根源

 那麼重點的問題並不是“是否使用了多線程、或是線程池”,而是為什麼accept()、read()方法會被阻塞。API文檔中對於 serverSocket.accept() 方法的使用描述:

Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.

服務器線程發起一個accept動作,詢問操作系統 是否有新的socket套接字信息從端口xx發送過來。

注意,是詢問操作系統。也就是說socket套接字的IO模式支持是基於操作系統的,那麼自然同步IO/異步IO的支持就是需要操作系統級別的了。如下圖:

 如果操作系統沒有發現有套接字從指定的端口xx來,那麼操作系統就會等待。這樣serverSocket.accept()方法就會一直等待。這就是為什麼accept()方法為什麼會阻塞:它內部的實現是使用的操作系統級別的同步IO。

  • 阻塞IO 和 非阻塞IO
    這兩個概念是程序級別的。主要描述的是程序請求操作系統IO操作后,如果IO資源沒有準備好,那麼程序該如何處理的問題:前者等待;後者繼續執行(並且使用線程一直輪詢,直到有IO資源準備好了)
  • 同步IO 和非同步IO
    這兩個概念是操作系統級別的。主要描述的是操作系統在收到程序請求IO操作后,如果IO資源沒有準備好,該如何處理相應程序的問題:前者不響應,直到IO資源準備好以後;後者返回一個標記(好讓程序和自己知道以後的數據往哪裡通知),當IO資源準備好以後,再用事件機制返回給程序。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

【讀後感1】SQL2008技術內幕- SQL邏輯查詢處理

引言觀點

1. 編程語言日新月異,但是從沒有人否定sql 在現代編程中的巨大作用和 持續的可利用性。SQL以對人類友好的閱讀體驗提供數據查詢能力( 相比其他編程語言 ), 同時在各種數據庫平台中,基礎SQL元素是相同或大同小異的,

從我們最早接觸的SQL,Mysql到公司大數據impala 支持SQL, Es也提供類似SQL的查詢, 阿里提出SQLFlow AI框架, SQL的生命力極其頑強。

2. 在我近6年的開發生涯中,確實覺得SQL語言沒有得到開發者足夠的重視,尤其是流行的ORM概念使得了編寫SQL機會越來越少,使用ORM映射框架是需要一些代碼的, 另外ORM只能用於基礎的關係型二維查詢,對於複雜的查詢無能為力,部分工作可通過巧妙的SQL查詢,存儲過程,觸發器來完成。

3. SQL編程有許多獨特之處: 面向集合的思維方式、 查詢元素的邏輯處理順序、三值邏輯(three value logic),理解不透的話在實際編寫SQL時會產生很多錯誤的寫法、性能低下的代碼。

1987年SQL稱為ISO標準,ANSI宣布該語言發音為“ess kyoo ell”, 但由於歷史原因,很多專業人士還是將SQL發音成sequel,而且從英文習慣上,sequel發音更為流暢。 互聯網如此之大,容得下不同的聲音。

結合《SQL2008 技術內幕 T-SQL查詢》和工作經驗,提煉出Web開發者需要熟練掌握以下SQL查詢。

  • SQL邏輯查詢處理

  • SQL 面向集合的思維方式

不敢妄自宣稱是高級編程經驗, 只是認為Web開發者應該Cover這些常見SQL用法。  

SQL邏輯查詢處理

  開發者、數據分析師每天都在寫【SELECT 列a,聚合函數 FROM 表名 WHERE 過濾條件 GROUP BY 列a HAVING 篩選條件】這樣的查詢語句。

  SQL與其他語言不同的最明顯特徵是代碼的處理順序,大多數編程語言中,代碼是按照編寫順序來處理的,但在SQL中第一個要處理的子句是FROM子句,儘管SELECT語句第一個出現,但基本都在最後處理。

       每一步都會生成一個虛擬表,該虛擬表會作為下一步的輸入, 這些虛擬表對於調用者(客戶端應用程序或者外部查詢)都是不可用的,只有最後一步生成的虛擬表才會返回給調用者,這種形態可對比LINQ理解。

 

①FROM        FROM階段負責標識表或要查詢的表,如果指定了表運算符(JOIN, APPLY,PIVOT,UNPIVOT ),還要進行表運算符的處理。

              例如:表聯接運算中涉及的階段是 笛卡爾積、ON篩選器和 添加外部行,FROM階段生成虛擬表VT1.

②WHERE           這個階段根據在WHERE子句中出現的謂詞對VT1中進行篩選,只有讓謂詞計算結果為TRUE的行,才會插入VT2中。

③GROUP BY     按照GROUP BY 子句中指定的列名列表,對VT2中的行進行分組,生成VT3, 最終每個分組只有一個結果行。

④HAVING          根據HAVING子句中出現的謂詞,對VT3中行記錄進行篩選,只有讓謂詞結果為TRUE的行記錄,才會進入VT4, Having 篩選器是唯一可用於分組數據的篩選器。

⑤SELECT    處理SELECT子句中字段(某些字段可能進行一些操作,形成新的字段),形成虛擬表VT5

⑥ORDER BY  根據ORDER BY子句中指定的列名列表,對VT5 中行進行排序,輸出最後結果。

 

着重理解:

  • 第一步的FROM表運算, 一般情況下是TABLE、TempTable,CTE, 還有可能是表運算符(我們常用的是聯接運算符), 所以不能單純認為FROM後面是一個表結構。

  • 表聯接運算符  ON篩選器 與 WHERE有所不同,若採用OUTER JOIN, 應用ON篩選出來的結果不一定是此階段最終結果,因為涉及【添加外部行】, 而WHERE過濾出的結果是此階段的最終結果。 

  • GROUP BY x,y 意味着將(x,y)作為一個整體來分組

  • 有SELECT 和WHERE的時候,先執行WHERE,再執行SELECT,這樣就很容易理解以下SQL的業務含義:

SELECT page_original_url,server_session_id,access_order-1 as access_order FROM PageViewMeasure WHERE access_order >= 2
--- 查詢過濾出access_order>=2的基礎數據集,然後將(原列值-1)重命名為原列名,重命名的用法業務上也許是為了形成新的SQL聯接

SELECT keyword_id,Coalesce(full_keywords,keywords)  as  not_nullField,profile_id,session_server_time,count (*) over () as Count FROM pageview 
WHERE  profile_id =5254 and keyword_id != '-' and day =20181008  and  not_nullField !='-'   
ORDER BY session_server_time 
--- SQL報錯:Could not resolve column/field reference: 'not_nullfield' 也容易理解了:先執行where, 執行where的時候not_nullField字段還沒有形成
  • ROW_NUMBER() OVER(PARTITION BY UserId ORDER BY PageViewServerTime)排名函數中ORDER BY 與SQL語句最後的ORDER BY 同時存在,哪個ORDER BY起最終排序作用?

SELECT page_original_url as name,page_view_server_time, ROW_NUMBER() OVER(PARTITION BY page_original_url ORDER BY page_view_server_time ) as partition_rank ,wd3_page_duration
  FROM pageview WHERE profile_id=5198 AND day between 20190616 and 20190621   
  ORDER  BY wd3_page_duration desc 
  LIMIT 100

  可以認為 ROW_NUMBER() OVER(PARTITION BY col1 ORDER BY col2) as rank 本質上還是產生一個列值,實際是對應以上的第⑤步,因此SQL最後的ORDER BY起最終排序作用,例證如下:

           某些轉載文章寫有: 以上over函數里的分組及排序的執行晚於“where,group by,order by”的執行 ,這樣的結論是錯誤的

  • 若存在LIMIT子句,則LIMIT子句必須在ORDER BY 語法之後

 

      上圖來自《SQL技術內幕T-SQL查詢》邏輯查詢處理一章

 

 這裏拋出一個困惑點:

  在FROM子句中,若存在JOIN表運算符, 可能會按照 【計算笛卡爾積】 【應用ON篩選】【添加外部行】的順序來完成 JOIN的過程, 但是試想一下: 如果兩個表都為大表,先計算笛卡爾積,再篩選 豈不很費內存,

  我也搜索了很多資料,某些資料認為先進行【ON篩選】再進行【JOIN】運算:

https://www.cnblogs.com/liuzhendong/archive/2011/10/27/2226805.html

https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2008/ms189499(v=sql.100)

我更願意相信《SQL技術內幕T-SQL查詢》書中所言:

本章描述的某些邏輯處理步驟可能看起來非常低效,但要記住, 在實踐中,
查詢的實際物理處理可能與邏輯處理有很大不同

在SQL Server 中負責生成實際工作計劃的組件是查詢優化器,以何種順序訪問表、使用什麼訪問方法和索引,應用哪種聯接算法等都是查詢優化器來決定的,優化器會生成多個有效執行計劃並選擇一個開銷最低的計劃。

邏輯查詢處理中各個階段都有其特定的順序,而優化器缺經常可以在它生成的物理執行計劃中走捷徑。

 我們思考一個簡單的SQL:

SELECT * FROM pageview LEFT JOIN  share  ON  pageview.share_pv_id = share.page_view_id
 WHERE pageview.profile_id =5313 AND pageview.day  between 20190615 and 20190624

若實際物理查詢按照上面描述的 邏輯查詢處理, 先進行 FROM 子句中的 LEFT JOIN 計算,再進行 WHERE過濾, 根本無法查出(在FROM子句可能內存就爆滿了)

  現在我們能夠查詢出來,能夠印證 實際物理查詢確實與邏輯查詢處理有很大不同。 

PS: 以上是個人從現象上推斷書中理論,對於實際物理查詢處理並沒有理論支持,若網友們有相關資料,可留言給我。

 

作者: JulianHuang

感謝您的認真閱讀,如有問題請大膽斧正;覺得有用,請下方或加關注。

本文歡迎轉載,但請保留此段聲明,且在文章頁面明顯位置註明本文的作者及原文鏈接。

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

SpringBoot第十七篇:定時任務

作者:追夢1819
原文:https://www.cnblogs.com/yanfei1819/p/11076555.html
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!

引言

  相信大家對定時任務很熟悉,其重要性也不言而喻。定時發短信、定時批量操作、定時統計數據等,都離不開定時任務。本文將講解定時任務在 SpringBoot 項目中的應用。

版本信息

  • JDK:1.8
  • SpringBoot :2.0.1.RELEASE
  • maven:3.3.9
  • IDEA:2019.1.1
  • quartz:2.3.0

定時任務實現方式

JDK自帶的Timer

  Timer 是Java 自帶的定時任務類。可以用作比較簡單的定時任務。通常用的不多。下面以一個小的示例展示其用法。

SpringBoot集成的schedule

這種方式是 SpringBoot 集成的,使用很簡單。

首先,引入 SpringBoot 的基礎 jar:

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

然後再啟動類中添加註解 @EnableScheduling 即可開啟 SpringBoot 定時任務:

@SpringBootApplication
@EnableScheduling
public class TimedTaskDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(TimedTaskDemoApplication.class, args);
    }
}

下面根據 @Scheduled 的不同屬性創建幾個任務:

任務一:

@Component
public class FirstTask {
    /**
     * cron 表達式
     */
//    @Scheduled(cron = "0/2 * * * * *")
    @Scheduled(cron="${cron.schedule}")
    public void run(){
        System.out.println("這是創建的第一個定時任務");
    }
}

作幾點說明:

  1. cron 表達式是 @Scheduled 的屬性之一,其值可以直接設置為 cron 表達式;
  2. @Scheduled(cron="${cron.schedule}") 是動態讀取 application.properties 配置文件中的 cron 表達式。例如項目中的一個需求是每天凌晨0點執行,但是對於測試人員來說,不可能等到凌晨測試。動態讀取可以幫助解決該問題。

任務二:

@Component
public class SecondTask {
    /**
     * 上一次執行完畢時間點之後多長時間再執行(ms)
     */
    @Scheduled(fixedDelay = 2000)
    public void run(){
        System.out.println("這是創建的第二個定時任務");
    }
}

任務三:

@Component
public class ThirdTask {
    /**
     * 與fixedDelay功能相同,上一次執行完畢時間點之後多長時間再執行(ms),區別是:1、時間是字符串;2、支持佔位符
     */
    // @Scheduled(fixedDelayString = "2000")
    @Scheduled(fixedDelayString = "${time.fixedDelay}")
    public void run(){
        System.out.println("這是創建的第三個定時任務");
    }
}

上面的三個任務列舉了 @Scheduled 註解的三個參數。其實除此之外,查看 @Scheduled 的源碼可知,還有其餘的幾個參數:

  • fixedRate:上一次開始執行時間點之後多長時間再執行;
  • fixedRateString:上一次開始執行時間點之後多長時間再執行;
  • fixedRateString:與fixedRate 意思相同,只是使用字符串的形式。唯一不同的是支持佔位符;
  • initialDelay:第一次延遲多長時間后再執行;
  • initialDelayString:與 initialDelay 意思相同,只是使用字符串的形式。唯一不同的是支持佔位符;

整合Quartz

  如果以上的方式都無法滿足項目的需求,則可以試試 Quartz 調度框架。它功能的強大以及使用無需多說了。此處我們看看 Quartz 在 SpringBoot 中的使用。

創建項目,引入 Quartz 調度框架啟動器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

  需要注意版本信息,如果 SpringBoot 版本是2.0以後的版本,直接引入 Quartz 啟動器即可。但是如果是2.0以前的版本,需要引入以下 jar 包:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

下面創建任務了:

public class QuartzTask extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(new Date());
    }
}

任務需要繼承 QuartzJobBean 抽象類,並重寫 executeInternal 方法。

第三步,創建 quartz 配置類,添加 @Configuration 註解:

@Configuration
public class QuartzConfig {
    @Bean
    public JobDetail testQuartzTask() {
        return JobBuilder.newJob(QuartzTask.class).withIdentity("quartztask").storeDurably().build();
    }
    @Bean
    public Trigger testQuartzTrigger2() {
        //cron方式,每隔5秒執行一次
        return TriggerBuilder.newTrigger().forJob(testQuartzTask())
                .withIdentity("quartztask")
                .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
                .build();
    }
}

  上面的示例是使用 cron 表達式,當然也可以是固定時間間隔。本節是闡述 SpringBoot 和 Quartz 的整合,不作 Quartz 的詳細使用。感興趣的讀者可以登錄 Quartz 的官網 或者中文官網自行研究。

總結

  定時任務的實現方式有很多種,除了上面說到的幾種方式,還有利用線程池實現定時任務,有的系統是通過 Liunx 實現定時任務。總之,定時任務的實現方式多種多樣,其方式要根據項目的實際情況而選。切不可為了實現而實現。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

記:使用IScroll.js 開發picker日曆組件遇到的問題及經驗總結

IScroll中文文檔

第一個問題: 邊界留白

  

  就是這種,上邊界(最小),下邊界(最大)有兩個列表的位置是不能選擇的。解決的辦法是:

  

  在HTML中,添加空白節點就行了。

 

第二個問題:初始化之後的滾動停止的事件的第二個參數問題。  

var myScroll = new IScroll('#wrapper');
myScroll.on('scrollEnd', function(){
    //這裏一定不要寫成es6箭頭函數
    //要執行的代碼
    //這個函數沒有參數
});

(1) 第二的個參數,是個函數。它沒有參數,而且不要寫成,不要寫成,不要寫成箭頭的形式。因為這函數裏面的this,是綁定的一些有用信息,比如:this.y是當前滾動的距離等。還有哪些信息可以看 文檔中的 滾動條信息 這一欄。如果寫成ES6的形式,那this指向就變了,這樣就獲取不了所需的信息。

(2) 第二個參數是沒有形參的。即沒有任何可使用的參數。

 

第三個問題:定義了snap選項,但是滾動有偏差

  開發的日曆選擇組件picker是使用rem單位自適應的,雖然在配置項中 有個options.snap,官方說可以對齊到固定的位置和元素,但是在使用自適應單位的情況中,這個配置並沒有展現出真正的效果,滾動的時候一定會出現偏差。

  那怎麼解決這個自適應的問題呢?由於是在滾動結束之後,位置才出現的偏差。那麼我就在滾動結束之後立馬調用修正位置的函數就行了。

  我是在vue中使用的。所以定義下面的函數,因為有 年,月,日,時四個滾動項。所以需要判斷是哪一個正在滾動

fixPos: function(target,num) {
    var step = Math.abs(Math.round(target.y / this.itemHeight));
    var maxYearLen = this.yearArr.length;
    var maxMonthLen = this.monthArr.length;
    var maxDayLen = this.dayArr.length;
    var maxHourLen = this.hourArr.length;
    switch(num){
        case 0:
            step = step > maxYearLen ? maxYearLen - 1 : step;
            break;
        case 1:
            step = step > maxMonthLen ? maxMonthLen - 1 : step;
            break;
        case 2:
            step = step > maxDayLen ? maxDayLen - 1 : step;
            break;
        case 3:
            step = step > maxHourLen ? maxHourLen - 1 : step;
            break;
    }
    var fixPos = step * this.itemHeight;//重新計算較為精確的位置
    target.y = fixPos;//重置原來的滾動距離 this.selectArr[num] = step;//這是保存每個列表滾動的索引值
    target.scrollTo(0, -fixPos);//這是滾動到修正後的位置
},

(1) 大致的思路就是:首先用當前滾動的距離,來除以滾動內容中,每個列表的高度。然後取最近似的值,就是當前應該滾動的列表的個數。

(2) 如果出現突然滾動到最底部,這時候需要滾動的個數大於了滾動內容的最大列表個數,那麼就糾正一下個數為最大列表數 – 1。

(3) 然後設置較為精確的滾動距離。再滾動到指定的位置。

(4) this.itemHeight是在created生命周期的時候就聲明的 this.itemHeight = (document.body.offsetWidth / 750) * 100 * 0.8;  相當於在375px寬度下,每個列表就是40px的高度。在iPhone5 320px下,就是34.133334了。

  調用的時候: 

var yearScroll = new IScroll('#calendarYear');
var that = this;
yearScroll.on('scrollEnd', function() {
    that.fixPos(this, 0);
})

  

第四個問題:使用自適應單位時最大滾動距離不準確

  這個問題和第二個問題類似,解決的方法:

fixMaxScrollY: function(target, num) {
    var yearLen = this.yearArr.length - 1;
    var monthLen = this.monthArr.length - 1;
    var dayLen = this.dayArr.length - 1;
    var hourLen = this.hourArr.length - 1;
    switch(num) {
        case 0:
            target.maxScrollY = -(Math.round(yearLen * this.itemHeight));
            break;
        case 1:
            target.maxScrollY = -(Math.round(monthLen * this.itemHeight));
            break;
        case 2:
            target.maxScrollY = -(Math.round(dayLen * this.itemHeight));
            break;
        case 3:
            target.maxScrollY = -(Math.round(hourLen * this.itemHeight));
            break;
    }
},

(1)  實例化后的滾動對象,有個最大滾動值maxScrollY,主要也是根據滾動內容的列表長度來重置最大滾動距離

(2)  因為有四個滾動的內容項,所以需要傳入當前是第幾個滾動的內容。

  調用的時候:

var yearScroll = new IScroll('#calendarYear');
this.fixMaxScrollY(yearScroll, 0);

 

第五個問題: 日曆組件的內容是在點擊某個按鈕之後再觸發显示的,最初是隱藏。但就是這個原因,導致显示出來的內容滾動不了

  解決的辦法是:使用 xxx.refresh() 刷新函數。這個函數具體的說明可以看文檔中 刷新 這個選項內容

  在控制組件显示的函數中,調用刷新的方法。

this.$nextTick(function(){
    this.scrollTarget[0].refresh();
    this.scrollTarget[1].refresh();
    this.scrollTarget[2].refresh();
    this.scrollTarget[3].refresh();
    //如果默認隱藏,則必須修復滾動的位置
    this.scrollTarget[0].scrollTo(0, -this.itemHeight * this.selectArr[0]);
    this.scrollTarget[1].scrollTo(0, -this.itemHeight * this.selectArr[1]);
    this.scrollTarget[2].scrollTo(0, -this.itemHeight * this.selectArr[2]);
    this.scrollTarget[3].scrollTo(0, -this.itemHeight * this.selectArr[3]);
})

(1) scrollTarget 是初始化滾動實例之後,保存的滾動實例。因為多次會用到。

(2) 因為有年,月,日,時四個滾動內容,所以要刷新四個滾動器。

(3) 如果日曆有最初的滾動位置,那麼也會出現不能跳到指定的位置的問題。所以,也需要初始化最初始的位置。

(4) selectArr 是滾動器滾動的索引值。比如我月份是1月到12月,當前滾動到了6月,那麼此時selectArr[1] 就是5 。

(5) 官方是推薦用setTimeout來使用刷新,但是我使用的是vue來開發的,所以,這裏用vm.$nextTick()來代替setTimeout。

 

第六個問題: 日曆組件復用時,只能初始化滾動第一個日曆組件,而且第一個日曆組件滾動還是有問題。

  這個問題產生的原因很簡單:因為 IScroll 在初始化實例的時候, var myScroll = new IScroll(‘.wrapper’);  它這個css選擇器使用的是querySelector 而不是 querySelectorAll,所以iScroll只會作用到選擇器選中元素的第一個。如果你需要對多個對象使用iScroll,你需要構建自己的循環機制。這是官方的說法。那麼怎樣建立循環機制呢?難道我要在初始化的時候還要循環去 new IScroll(xxx)創建實例嗎?

  其實沒必要。只需要改一改源碼就行了。

  我使用的是 iscroll-lite 這個版本。這裏面 在定義 IScroll 這個函數的時候有這段代碼

this.wrapper = typeof el == 'string' ? document.querySelector(el) : el;

  它每次初始化時,只選擇了 el  滾動對象的第一個元素,那麼,我只需要傳入當前是第幾個日曆組件,再改成querySelectorAll就行了。即:

this.wrapper = typeof el == 'string' ? document.querySelectorAll(el)[childIndex] : el;

  然後在這個定義的 IScroll 函數的參數中,再增加一個參數,表示第幾個元素。

  然後在初始化滾動實例的時候:

var options = {
    snap: '.calendar-scroll-item', //對齊的位置,相當於自動糾正每次移動的距離
    //scrollbars: true,//是否显示滾動條
}
//初始化滾動
var yearScroll = new IScroll('.calendar-scroll>.calendarYear', options , this.curIndex);

  當然這個時候,css選擇器就不要用 id 了。

  然後在 組件的 prop 裏面添加一個屬性。

curIndex:{
    type:Number,
    default:0
}

(1) 默認只使用一個組件,即不傳這個prop 的話就是默認初始化第一個組件的滾動內容。

  使用組件的時候:引入,註冊等步驟就省略了

<calendar :cur-index='0'/>
<calendar :cur-index='1'/>
<calendar :cur-index='2'/>

   這樣不管使用多少個,都能正常初始化滾動了。而且互不影響 。注意,如果傳的是数字,需要v-bind 告訴vue這不是字符串,是表達式。不然傳過去的是字符串

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Appium+python自動化(九)- 定位元素工具(義結金蘭的兩位異性兄弟)(超詳解)

簡介

   環境搭建好了,其他方面的知識也準備的差不多了,那麼我們就開始下一步元素定位,元素定位宏哥主要介紹如何使用uiautomatorviewer,通過定位到頁面上的元素,然後進行相應的點擊等操作. 此外在介紹另一款工具:Inspector。

uiautomatorviewer是android-sdk自帶的一個元素定位工具,非常簡單好用,使用uiautomatorviewer,你可以檢查一個應用的UI來查看應用的布局和組件以及相關的屬性。

  那麼宏哥先來給各位小夥伴們介紹大哥–uiautomatorviewer,再來介紹小弟–Inspector。

一、大哥–uiautomatorviewer

  大哥人物簡介:

  大哥是皇族後裔,但是比劉皇叔強多了。人家有一個好爹–Google。

  Android 4.1發布的,uiautomator是用來做UI測試的。也就是普通的手工測試,點擊每個控件元素 看看輸出的結果是否符合預期。比如 登陸界面 分別輸入正確和錯誤的用戶名密碼然後點擊登陸按鈕看看是否能否登陸以及是否有錯誤提示等。

功能性或者黑盒UI測試不需要測試人員了解程序如何實現的,只需要驗證各種操作的結果是否符合預期即可。這樣的測試可以分離團隊的開發人員和測試人員。大家各干各的沒有太多的交集。

  常用的UI測試方式就是人工驗證啦,就是測試人員拿着各種手機分別安裝要測試的程序然後看看是否能正確完成各種預定的功能。但是這種驗證方式是非常耗時間的,每次回歸都要全部驗證一邊,並且還容易出現人為錯誤。比較高效和可靠的UI測試方式就是自動化測試。自動化UI測試創建測試代碼來執行測試任務,各種測試任務分別覆蓋不同的使用場景,然後使用測試框架來運行這些測試任務。而uiautomator 就是你的自動化UI測試工具。

Android SDK在4.1中提供了如下工具來支持UI自動化測試:

  • uiautomatorviewer – 一個圖形界面工具來掃描和分析應用的UI控件。
  • uiautomator – 一個測試的Java庫,包含了創建UI測試的各種API和執行自動化測試的引擎。

要大哥出山也就是使用該工具,需要滿足如下條件:

  • Android SDK Tools, Revision 21 or higher
  • Android SDK Platform, API 16 or higher

一、啟動uiautomatorviewer.bat

兩種啟動方法:

第一種方法:

1、打開目錄D:\software\android-sdk-windows\tools

 

2、雙擊啟動,啟動之後出現如下界面

 


 
第二種方法:

1、如果不喜歡雙擊啟動的話,也可以在cmd裏面通過指令啟動

先cd到tools目錄下,然後輸入uiautomatorviewer.bat回車后啟動服務

二、連接手機

1、cmd打開輸入adb devices,確認手機已連上(以夜神模擬器為例)

 

2、打開手機淘寶頁面,讓屏幕處於點亮狀態

 

 

3、點左上角安卓機器人按鈕Devices Screenshot按鈕刷新頁面

 

 

三、定位元素

1、移動鼠標到需要定位的元素上,如搜索輸入框

 
 

2、右下角可以看到元素對應的屬性

text:好沙發需要好坐墊

resource-id:com.taobao.taobao:id/home_searchedit

class:android.widget.EditText

四、點搜索框

1、前面一篇啟動app后,休眠五秒,等待頁面加載完成

2、通過id來定位到搜索框,然後點擊

 

 

五、參考代碼

# coding=utf-8
# 1.先設置編碼,utf-8可支持中英文,如上,一般放在第一行

# 2.註釋:包括記錄創建時間,創建人,項目名稱。
'''
Created on 2019-6-24
@author: 北京-宏哥   QQ交流群:707699217
Project:學習和使用定位元素工具
'''
# 3.導入模塊
from appium import webdriver
import time
desired_caps = {}
desired_caps['platformName'] = 'Android'   #android的apk還是IOS的ipa
desired_caps['platformVersion'] = '8.0'  #android系統的版本號
desired_caps['deviceName'] = '127.0.0.1:62001'    #手機設備名稱,通過adb devices  查看
desired_caps['appPackage'] = 'com.taobao.taobao'  #apk的包名
desired_caps['appActivity'] = 'com.taobao.tao.welcome.Welcome'  #apk的launcherActivity
#desired_caps['unicodeKeyboard'] = True   #使用unicodeKeyboard的編碼方式來發送字符串
#desired_caps['resetKeyboard'] = True   #將鍵盤給隱藏起來
driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps) #啟動服務器地址,後面跟的是手機信息
# 休眠五秒等待頁面加載完成
time.sleep(5)

driver.find_element_by_id("com.taobao.taobao:id/home_searchedit").click()

# driver.quit()

 

六、元素定位

1、appium的webdriver提供了11種元素定位方法,在selenium的基礎上擴展了三個,可以在pycharm裏面輸入driver.find_element_by然後會自動匹配出來

 

2、多的三種如下,在後面的會詳細介紹

driver.find_element_by_accessibility_id()

driver.find_element_by_android_uiautomator()

driver.find_element_by_ios_uiautomation()

(第三個是ios的可以暫時不用管)

二、小弟–Inspector

   小弟人物簡介:

  小弟不是很牛叉,但是小弟的存在必定有小弟存在的必要,之前看了好多文章都要放棄小弟,我覺地不要放棄,沒準小弟可以替大哥大哥擋刀呢,呵呵,說笑了,畢竟過河的卒子可以當車!!!

  Appium Inspector是appium自帶的一個元素定位工具,前邊介紹了如何使用uiautomatorviewer進行元素定位。這裏就學習Appium Inspector是如何定位的。

  Appium Inspector 對於測試的同學不得不說是一個神器,可以查詢到我們需要定位的元素的 id 等各種信息 ,還可以 錄製用戶行為,翻譯成腳本。逆天的是可以翻譯成多種語言的腳本。這並不意味着測試的同學可以不用學習語言和編程了,因為機器翻譯出的腳本一般很難一次執行成功,其中很可能還需要經過人工修改后才能順利執行,但是這已經可以為測試腳本的編寫帶來很大的遍歷,所以 Appium Inspector 還是值得我們去學習和使用。

一、設置appium

1、先不要啟動appium,因為啟動以後就不能操作appium設置界面,必須在未啟動的時候進行操作

2、點開android setting界面(機器人圖標)

3、勾選Application Path,添加被測app的路徑

4、Devices name處添加設備名稱(adb devices查看到的)

 

(注意:通過這種方法也可查看到apk的包名和Launch Activity)

 

 

二、開啟appium

1、手機確保連接電腦(adb devices查看)(仍然以夜神模擬器為例)

 

 

2、點appium右上角三角形圖標,會啟動服務

 

 

3、這時候可以看到手機上安裝淘寶應用,並會啟動淘寶

4、點appium右上角的搜索圖標

 

 

5、點Inspector Window界面的 Refresh 按鈕刷新界面

 

 

三、Ispector Window

1、手機上打開需要單位元素的界面,然後點Refresh按鈕刷新

2、左邊菜單樹,可以挨個點開

3、如果想單位界面上的“是”和“否”按鈕,從菜單樹就可以看到這兩個元素的結構

四、查看屬性

1、選中左側菜單樹對應的元素,在右下角查看對應屬性

 

 

五、常見異常

1、在使用過程中,你會發現經常會報以下這個錯誤

2、每次啟動都會給你手機上重新安裝一次應用

3、並且不能用鼠標指定某個元素,沒有uiautomatorviewer使用方便

(要是以上三種問題你都遇到了,怎麼辦???那就放棄吧!!!)​

相信很多人在使用過程中,都會遇到中文無法輸入問題,宏哥在後續文章里會手把手教大家如何輸入中文。

三、小結

Android SDK中的UIAutomator中本身是不支持網頁中的UI元素定位,下面介紹幾種常用的定位app內部的網頁的UI元素的方法。

一、使用chrome瀏覽器調試移動端網頁

       這是使用最多的一種方法。首頁確保自己的手機已經跟電腦連接且處於開發者模式。

       打開PC端的谷歌瀏覽器,輸入chrome://inspect/#devices,會看到下圖所示界面:

      

     點擊需要測試的網頁下方的inspect,且保證手機界面停留在那個頁面。

      

 

      這樣打開后是不是就熟悉了,點擊上方紅色標記的就可以操作進行定位了~~~

 

 二、Android手機的鋪助功能帶有TalkBack

        之前一直用的是谷歌的方法,但是最近發現使用talkback可以直接用UIAutomator定位元素。  

        Android系統一般自帶一個TalkeBack功能。但這裏千萬要注意:打開之後整個系統的操作都變得不同了!!滑動界面需要兩個手指,單擊變成雙擊。打開之後用UiAutomatorViewer獲取webview界面的內容,你就會發現原來webview裏面獲取不到的ui元素,現在已經可以可以獲取到了。即使現在你關掉TalkBack,也能獲取到,除非重啟手機才會回到不能獲取的狀態,所以開啟后我們可以立即關閉,以方便操作。有些手機沒有talkBack的可以去應用市場下載。

       功能路徑:
       原生系統:設置 – 輔助功能(Accessibility) – TalkBack
       華為EMUI系統:設置 – 高級設置 – 輔助功能 – TalkBack

三、宏哥在小弟哪裡遇到的問題

 

查看appium日誌的error發現沒有設備名字報錯

 

將夜神模擬器的名字加上就成功了

宏哥不能保證所整理都符合大家的口味,但我能保證每一篇都是用心去寫和用心去整理,我始終認同“分享的越多,你的價值增值越大”,歡迎大家關注我的博客和個人公眾號的技術分享。在分享中進步,越努力越幸運,期待我們都有美好的明天!

支持宏哥的朋友們和宏哥的宏粉記得點波推薦哦,您的肯定就是我進步的動力。鄙人先在這裏給您道謝了,謝您嘞~~

個人公眾號(因為許多文章都被搬到別人的公眾號里了,還是原創,所以宏哥果斷也開一個公眾號。打算與博客園文章同步,希望大家隨時隨地學習與進步):

微信群(因為有人給我發短消息說公司不讓用QQ,就幫忙建立一個微信群,歡迎加入討論和交流)

 

我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2k3h2stkvscgc

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

實際項目中,看 ECharts 和 HighCharts 渲染性能對比,表面看衣裝,本質看內功!!!

     最近做項目,使用的是echarts显示圖表數據,但是數據量比較多的時候,有卡頓的情況。後來同事拿echarts和HighCharts做了對比,僅供大家參考。同時感謝同事做的工作。

一、查詢1天的源數據,屬性1、屬性2、屬性3、屬性4

       Echarts查詢3.61s,渲染0.786s(約8.6M數據)

       HighCharts查詢3.10s,渲染0.768s(約8.7M數據)

二、查詢7天的源數據,屬性1、屬性2、屬性3、屬性4

      Echarts查詢21.67秒,渲染5.38秒。(約60.7M數據)。渲染完成后

      HighCharts查詢22.18s,渲染5.39s。(約60.5M數據)

三、查詢14天的源數據,屬性1、屬性2、屬性3、屬性4

      Echarts渲染不出來,瀏覽器崩潰。

      HighCharts查詢41.42s,渲染10.82s。(約121M數據)

四、內存佔用對比:7天數據

      Echarts渲染后瀏覽器佔用內存約3280M。

      HighCharts渲染后瀏覽器佔用內存約637M。

五、操作對比

圖表類型

1天數據

7天數據

14天數據

Echarts

卡頓

無法操作

瀏覽器崩潰

HighCharts

流暢

流暢

流暢

 

雲端操作系統:http://www.ineuos.net

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

SpringBoot啟動流程分析(二):SpringApplication的run方法

SpringBoot系列文章簡介

SpringBoot源碼閱讀輔助篇:

  Spring IoC容器與應用上下文的設計與實現

SpringBoot啟動流程源碼分析:

  1. SpringBoot啟動流程分析(一):SpringApplication類初始化過程
  2. SpringBoot啟動流程分析(二):SpringApplication的run方法
  3. SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
  4. SpringBoot啟動流程分析(四):IoC容器的初始化過程
  5. SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
  6. SpringBoot啟動流程分析(六):IoC容器依賴注入

筆者註釋版Spring Framework與SpringBoot源碼git傳送門:請不要吝嗇小星星

  1. spring-framework-5.0.8.RELEASE
  2. SpringBoot-2.0.4.RELEASE

一、前言

  前一篇博客介紹了 SpringApplication 類的實例化過程,本章總結SpringBoot啟動流程最重要的部分run方法。通過rrun方法梳理出SpringBoot啟動的流程,然後後面的博客再一步步的分析啟動流程中各個步驟所做的具體的工作。深入分析後會發現SpringBoot也就是給Spring包了一層皮,事先替我們準備好Spring所需要的環境及一些基礎,具體通過源碼一步步深入分析後會發現Spring是真的很偉大。當然跟代碼的時候越深入越容易陷進去進而發現有些東西沒法通過博客詳細的梳理出來。當然在這個過程中還是立足於我們對SpringBoot的使用來說明源碼所做的工作。知其然才能知其所以然。加油

 

二、SpringBoot啟動流程梳理

  首先擺上run方法的源碼

 1 /**
 2  * Run the Spring application, creating and refreshing a new
 3  * {@link ApplicationContext}.
 4  *
 5  * @param args the application arguments (usually passed from a Java main method)
 6  * @return a running {@link ApplicationContext}
 7  *
 8  * 運行spring應用,並刷新一個新的 ApplicationContext(Spring的上下文)
 9  * ConfigurableApplicationContext 是 ApplicationContext 接口的子接口。在 ApplicationContext
10  * 基礎上增加了配置上下文的工具。 ConfigurableApplicationContext是容器的高級接口
11  */
12 public ConfigurableApplicationContext run(String... args) {
13     //記錄程序運行時間
14     StopWatch stopWatch = new StopWatch();
15     stopWatch.start();
16     // ConfigurableApplicationContext Spring 的上下文
17     ConfigurableApplicationContext context = null;
18     Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
19     configureHeadlessProperty();
20     //從META-INF/spring.factories中獲取監聽器
21     //1、獲取並啟動監聽器
22     SpringApplicationRunListeners listeners = getRunListeners(args);
23     listeners.starting();
24     try {
25         ApplicationArguments applicationArguments = new DefaultApplicationArguments(
26                 args);
27         //2、構造應用上下文環境
28         ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
29         //處理需要忽略的Bean
30         configureIgnoreBeanInfo(environment);
31         //打印banner
32         Banner printedBanner = printBanner(environment);
33         ///3、初始化應用上下文
34         context = createApplicationContext();
35         //實例化SpringBootExceptionReporter.class,用來支持報告關於啟動的錯誤
36         exceptionReporters = getSpringFactoriesInstances(
37                 SpringBootExceptionReporter.class,
38                 new Class[]{ConfigurableApplicationContext.class}, context);
39         //4、刷新應用上下文前的準備階段
40         prepareContext(context, environment, listeners, applicationArguments, printedBanner);
41         //5、刷新應用上下文
42         refreshContext(context);
43         //刷新應用上下文後的擴展接口
44         afterRefresh(context, applicationArguments);
45         //時間記錄停止
46         stopWatch.stop();
47         if (this.logStartupInfo) {
48             new StartupInfoLogger(this.mainApplicationClass)
49                     .logStarted(getApplicationLog(), stopWatch);
50         }
51         //發布容器啟動完成事件
52         listeners.started(context);
53         callRunners(context, applicationArguments);
54     } catch (Throwable ex) {
55         handleRunFailure(context, ex, exceptionReporters, listeners);
56         throw new IllegalStateException(ex);
57     }
58 
59     try {
60         listeners.running(context);
61     } catch (Throwable ex) {
62         handleRunFailure(context, ex, exceptionReporters, null);
63         throw new IllegalStateException(ex);
64     }
65     return context;
66 }

 

  具體的每一行代碼的含義請看註釋,我們在這先總結一下啟動過程中的重要步驟:(筆者傾向於將應用上下文同容器區分開來)

第一步:獲取並啟動監聽器
第二步:構造應用上下文環境
第三步:初始化應用上下文
第四步:刷新應用上下文前的準備階段
第五步:刷新應用上下文
第六步:刷新應用上下文後的擴展接口

  OK,下面SpringBoot的啟動流程分析,我們就根據這6大步驟進行詳細解讀。最總要的是第四,五步。我們會着重的分析。

 

三、第一步:獲取並啟動監聽器

  事件機制在Spring是很重要的一部分內容,通過事件機制我們可以監聽Spring容器中正在發生的一些事件,同樣也可以自定義監聽事件。Spring的事件為Bean和Bean之間的消息傳遞提供支持。當一個對象處理完某種任務后,通知另外的對象進行某些處理,常用的場景有進行某些操作后發送通知,消息、郵件等情況。

1 private SpringApplicationRunListeners getRunListeners(String[] args) {
2     Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};
3     return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
4             SpringApplicationRunListener.class, types, this, args));
5 }

  在這裏面是不是看到一個熟悉的方法:getSpringFactoriesInstances(),可以看下下面的註釋,前面的博文我們已經詳細介紹過該方法是怎麼一步步的獲取到META-INF/spring.factories中的指定的key的value,獲取到以後怎麼實例化類的。

 1 /**
 2  * 通過指定的classloader 從META-INF/spring.factories獲取指定的Spring的工廠實例
 3  * @param type
 4  * @param parameterTypes
 5  * @param args
 6  * @param <T>
 7  * @return
 8  */
 9 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
10                                                       Class<?>[] parameterTypes, Object... args) {
11     ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
12     // Use names and ensure unique to protect against duplicates
13     //通過指定的classLoader從 META-INF/spring.factories 的資源文件中,
14     //讀取 key 為 type.getName() 的 value
15     Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
16     //創建Spring工廠實例
17     List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
18             classLoader, args, names);
19     //對Spring工廠實例排序(org.springframework.core.annotation.Order註解指定的順序)
20     AnnotationAwareOrderComparator.sort(instances);
21     return instances;
22 }

   回到refresh方法,debug這個代碼 SpringApplicationRunListeners listeners = getRunListeners(args); 看一下獲取的是哪個監聽器:

  EventPublishingRunListener監聽器是Spring容器的啟動監聽器。

   listeners.starting(); 開啟了監聽事件。

 

四、第二步:構造應用上下文環境

  應用上下文環境包括什麼呢?包括計算機的環境,Java環境,Spring的運行環境,Spring項目的配置(在SpringBoot中就是那個熟悉的application.properties/yml)等等。

  首先看一下prepareEnvironment()方法。

 1 private ConfigurableEnvironment prepareEnvironment(
 2         SpringApplicationRunListeners listeners,
 3         ApplicationArguments applicationArguments) {
 4     // Create and configure the environment
 5     //創建並配置相應的環境
 6     ConfigurableEnvironment environment = getOrCreateEnvironment();
 7     //根據用戶配置,配置 environment系統環境
 8     configureEnvironment(environment, applicationArguments.getSourceArgs());
 9     // 啟動相應的監聽器,其中一個重要的監聽器 ConfigFileApplicationListener 就是加載項目配置文件的監聽器。
10     listeners.environmentPrepared(environment);
11     bindToSpringApplication(environment);
12     if (this.webApplicationType == WebApplicationType.NONE) {
13         environment = new EnvironmentConverter(getClassLoader())
14                 .convertToStandardEnvironmentIfNecessary(environment);
15     }
16     ConfigurationPropertySources.attach(environment);
17     return environment;
18 }

   看上面的註釋,方法中主要完成的工作,首先是創建並按照相應的應用類型配置相應的環境,然後根據用戶的配置,配置系統環境,然後啟動監聽器,並加載系統配置文件。

 

4.1、 ConfigurableEnvironment environment = getOrCreateEnvironment(); 

  看看getOrCreateEnvironment()幹了些什麼。

 1 private ConfigurableEnvironment getOrCreateEnvironment() {
 2     if (this.environment != null) {
 3         return this.environment;
 4     }
 5     //如果應用類型是 SERVLET 則實例化 StandardServletEnvironment
 6     if (this.webApplicationType == WebApplicationType.SERVLET) {
 7         return new StandardServletEnvironment();
 8     }
 9     return new StandardEnvironment();
10 }

   通過代碼可以看到根據不同的應用類型初始化不同的系統環境實例。前面咱們已經說過應用類型是怎麼判斷的了,這裏就不在贅述了。

  

  從上面的繼承關係可以看出,StandardServletEnvironment是StandardEnvironment的子類。這兩個對象也沒什麼好講的,當是web項目的時候,環境上會多一些關於web環境的配置。

 

4.2、 configureEnvironment(environment, applicationArguments.getSourceArgs()); 

1 protected void configureEnvironment(ConfigurableEnvironment environment,
2                                     String[] args) {
3     // 將main 函數的args封裝成 SimpleCommandLinePropertySource 加入環境中。
4     configurePropertySources(environment, args);
5     // 激活相應的配置文件
6     configureProfiles(environment, args);
7 }

  在執行完方法中的兩行代碼后,debug的截圖如下

  如下圖所示,我在spring的啟動參數中指定了參數:–spring.profiles.active=prod(關於這個參數的用法,點我,其實就是啟動多個實例用的)

 

  在configurePropertySources(environment, args);中將args封裝成了SimpleCommandLinePropertySource並加入到了environment中。

  configureProfiles(environment, args);根據啟動參數激活了相應的配置文件。

  話不多說,debug一遍就明白了。

 

4.3、 listeners.environmentPrepared(environment); 

  進入到方法一路跟下去就到了SimpleApplicationEventMulticaster類的multicastEvent()方法。

  

  查看getApplicationListeners(event, type)執行結果,發現一個重要的監聽器ConfigFileApplicationListener。

  先看看這個類的註釋

 1 /**
 2  * {@link EnvironmentPostProcessor} that configures the context environment by loading
 3  * properties from well known file locations. By default properties will be loaded from
 4  * 'application.properties' and/or 'application.yml' files in the following locations:
 5  * <ul>
 6  * <li>classpath:</li>
 7  * <li>file:./</li>
 8  * <li>classpath:config/</li>
 9  * <li>file:./config/:</li>
10  * </ul>
11  * <p>
12  * Alternative search locations and names can be specified using
13  * {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
14  * <p>
15  * Additional files will also be loaded based on active profiles. For example if a 'web'
16  * profile is active 'application-web.properties' and 'application-web.yml' will be
17  * considered.
18  * <p>
19  * The 'spring.config.name' property can be used to specify an alternative name to load
20  * and the 'spring.config.location' property can be used to specify alternative search
21  * locations or specific files.
22  * <p>
23  * 從默認的位置加載配置文件,並將其加入 上下文的 environment變量中
24  */

  這個監聽器默認的從註釋中<ul>標籤所示的幾個位置加載配置文件,並將其加入 上下文的 environment變量中。當然也可以通過配置指定。

  debug跳過 listeners.environmentPrepared(environment); 這一行,查看environment屬性,果真如上面所說的,配置文件的配置信息已經添加上來了。

  

五、第三步:初始化應用上下文

  在SpringBoot工程中,應用類型分為三種,如下代碼所示。

 1 public enum WebApplicationType {
 2     /**
 3      * 應用程序不是web應用,也不應該用web服務器去啟動
 4      */
 5     NONE,
 6     /**
 7      * 應用程序應作為基於servlet的web應用程序運行,並應啟動嵌入式servlet web(tomcat)服務器。
 8      */
 9     SERVLET,
10     /**
11      * 應用程序應作為 reactive web應用程序運行,並應啟動嵌入式 reactive web服務器。
12      */
13     REACTIVE
14 }

  對應三種應用類型,SpringBoot項目有三種對應的應用上下文,我們以web工程為例,即其上下文為AnnotationConfigServletWebServerApplicationContext。

 1 public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework.boot."
 2         + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
 3 public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
 4         + "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
 5 public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
 6         + "annotation.AnnotationConfigApplicationContext";
 7         
 8 protected ConfigurableApplicationContext createApplicationContext() {
 9     Class<?> contextClass = this.applicationContextClass;
10     if (contextClass == null) {
11         try {
12             switch (this.webApplicationType) {
13                 case SERVLET:
14                     contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
15                     break;
16                 case REACTIVE:
17                     contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
18                     break;
19                 default:
20                     contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
21             }
22         } catch (ClassNotFoundException ex) {
23             throw new IllegalStateException(
24                     "Unable create a default ApplicationContext, "
25                             + "please specify an ApplicationContextClass",
26                     ex);
27         }
28     }
29     return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
30 }

  我們先看一下AnnotationConfigServletWebServerApplicationContext的設計。

   關於他的繼承體系,我們在前面的博客中<Spring IoC容器與應用上下文的設計與實現>已經詳細介紹了,在此不再贅述。

  應用上下文可以理解成IoC容器的高級表現形式,應用上下文確實是在IoC容器的基礎上豐富了一些高級功能。

  應用上下文對IoC容器是持有的關係。他的一個屬性beanFactory就是IoC容器(DefaultListableBeanFactory)。所以他們之間是持有,和擴展的關係。

  接下來看GenericApplicationContext類

1 public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
2     private final DefaultListableBeanFactory beanFactory;
3     ...
4     public GenericApplicationContext() {
5         this.beanFactory = new DefaultListableBeanFactory();
6     }
7     ...
8 }

   beanFactory正是在AnnotationConfigServletWebServerApplicationContext實現的接口GenericApplicationContext中定義的。在上面createApplicationContext()方法中的, BeanUtils.instantiateClass(contextClass) 這個方法中,不但初始化了AnnotationConfigServletWebServerApplicationContext類,也就是我們的上下文context,同樣也觸發了GenericApplicationContext類的構造函數,從而IoC容器也創建了。仔細看他的構造函數,有沒有發現一個很熟悉的類DefaultListableBeanFactory,沒錯,DefaultListableBeanFactory就是IoC容器真實面目了。在後面的refresh()方法分析中,DefaultListableBeanFactory是無處不在的存在感。

  debug跳過createApplicationContext()方法。

  如上圖所示,context就是我們熟悉的上下文(也有人稱之為容器,都可以,看個人愛好和理解),beanFactory就是我們所說的IoC容器的真實面孔了。細細感受下上下文和容器的聯繫和區別,對於我們理解源碼有很大的幫助。在系列文章中,我們也是將上下文和容器嚴格區分開來的。

 

  

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

kubernetes高級之pod安全策略

系列目錄

什麼是pod安全策略

pod安全策略是集群級別的用於控制pod安全相關選項的一種資源.PodSecurityPolicy定義了一系列pod相要進行在系統中必須滿足的約束條件,以衣一些默認的約束值.它允許管理員控制以下方面內容

Control Aspect Field Names
以特權運行容器 privileged
使用宿主名稱空間 hostPID, hostIPC
使用宿主網絡和端口 hostNetwork, hostPorts
使用存儲卷類型 volumes
使用宿主機文件系統 allowedHostPaths
flex存儲卷白名單 allowedFlexVolumes
分配擁有 Pod 數據卷的 FSGroup fsGroup
只讀root文件系統 readOnlyRootFilesystem
容器的用戶id和組id runAsUser, runAsGroup, supplementalGroups
禁止提升到root權限 allowPrivilegeEscalation, defaultAllowPrivilegeEscalation
Linux能力 defaultAddCapabilities, requiredDropCapabilities, allowedCapabilities
SELinux上下文 seLinux
允許容器加載的proc類型 allowedProcMountTypes
The AppArmor profile used by containers annotations
The seccomp profile used by containers annotations
The sysctl profile used by containers annotations

啟用pod安全策略

pod安全策略作為可選的(但強烈建議的)admission controller的實現.pod安全策略通過啟用admission controller來實現,但是僅僅啟用而沒有對策略授權則會導致整個集群無法創建pod!

由於pod安全策略api(policy/v1beta1/podsecuritypolicy)獨立於admission controller之外啟用,對於已經存在的集群建議在啟用admission controller之前添加並授權策略.

授權策略

當一個pod安全策略資源被創建(前面說過,psp(PodSecurityPolicy )pod安全策略是一種kubernetes資源),它什麼都不會做.為了使用它,請求操作的用戶或者目標pod的serviceaccount必須通過策略的use動詞來授權.

絕大部分kubernetes pod並不是直接由用戶直接創建的.相反,典型使用場景是它們通過Deployment或者ReplicaSet間接被創建,或者通過控制器管理器的其它模板控制器來創建.對控制器進行策略授權也將對它所創建的所有pod進行策略授權.因此首選的授權方法是對pod的serviceaccount進行策略授權(後面有示例).

通過RBAC授權

RBAC是kubernetes標準的授權模式,並且很容易用於授權安全策略使用.

首先,一個角色(role)或者集群角色(clusterRole)需要被授權使用(use動詞)它想要的策略.對角色的授權類似下面

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: <role name>
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - 一系列要進行授權的資源名稱

然後把集群角色(或角色)與授權的用戶綁定

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: 綁定名稱
roleRef:
  kind: ClusterRole
  name: 角色名稱
  apiGroup: rbac.authorization.k8s.io
subjects:
# Authorize specific service accounts:
- kind: ServiceAccount
  name: 授權的serviceaccount名稱
  namespace: <authorized pod namespace>
# Authorize specific users (not recommended):
- kind: User
  apiGroup: rbac.authorization.k8s.io
  name: 授權的用戶名

如果一個角色綁定(不是集群角色綁定)被使用,它僅對和它處於同一名稱空間下的pod才能進行有效策略授權,這樣同樣適用於用戶和用戶組

# Authorize all service accounts in a namespace:
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:serviceaccounts
# Or equivalently, all authenticated users in a namespace:
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:authenticated

故障排除

控制器管理器必須運行在安全的api端口上,並且不能有超級權限.不然請求就會繞過認證和授權模塊,將導致所有的策略均被允許,並且用戶可以創建特權pod

策略順序

除了限制pod的創建和更新,pod安全策略還用於提供它所控制的諸多字段的默認值.當有多個策略時,pod安全策略根據以下因素來選擇策略

  • 任何成功通過驗證沒有警告的策略將被使用

  • 如果是請求創建pod,則按通過驗證的策略按字母表順序被選用

  • 否則,如果是一個更新請求,將會返回錯誤.因為在更新操作過程中不允許pod變化

示例

以下示例假定你運行的集群開啟了pod安全策略admission controller並且你有集群管理員權限

初始設置

我們為示例創建一個名稱空間和一個serviceaccount.我們使用這個serviceaccount來模擬一個非管理員用戶

kubectl create namespace psp-example
kubectl create serviceaccount -n psp-example fake-user
kubectl create rolebinding -n psp-example fake-editor --clusterrole=edit --serviceaccount=psp-example:fake-user

為了方便辨認我們使用的賬戶,我們創建兩個別名

alias kubectl-admin='kubectl -n psp-example'
alias kubectl-user='kubectl --as=system:serviceaccount:psp-example:fake-user -n psp-example'

創建一個策略和一個pod

以下定義文件定義了一個簡單pod安全策略(PodSecurityPolicy),這個策略僅僅阻止創建特權pod

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: example
spec:
  privileged: false  # Don't allow privileged pods!
  # The rest fills in some required fields.
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  runAsUser:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  volumes:
  - '*'

我們使用kubectl命令來應用以上文件.

現在,做為一個非特權用戶,我們創建一個簡單pod

kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
  name:      pause
spec:
  containers:
    - name:  pause
      image: k8s.gcr.io/pause
EOF
Error from server (Forbidden): error when creating "STDIN": pods "pause" is forbidden: unable to validate against any pod security policy: []

發生了什麼?儘管pod安全策略已創建,不管是pod的serviceaccount還是fack-user都沒有權限使用這個策略.

kubectl-user auth can-i use podsecuritypolicy/example
no

創建一個rolebing來授權fake-user來使用example策略(example是前面創建的策略的名稱)

但是請注意這裏並不是首選方式!後面的示例將介紹首選的方式

kubectl-admin create role psp:unprivileged \
    --verb=use \
    --resource=podsecuritypolicy \
    --resource-name=example
role "psp:unprivileged" created

kubectl-admin create rolebinding fake-user:psp:unprivileged \
    --role=psp:unprivileged \
    --serviceaccount=psp-example:fake-user
rolebinding "fake-user:psp:unprivileged" created

kubectl-user auth can-i use podsecuritypolicy/example
yes

此時,再重新嘗試創建pod

kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
  name:      pause
spec:
  containers:
    - name:  pause
      image: k8s.gcr.io/pause
EOF
pod "pause" created

這次正如我們期待的一樣,可以正常工作.但是試圖創建特權pod仍然會被阻止(因此策略本身阻止創建特權pod)

kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
  name:      privileged
spec:
  containers:
    - name:  pause
      image: k8s.gcr.io/pause
      securityContext:
        privileged: true
EOF
Error from server (Forbidden): error when creating "STDIN": pods "privileged" is forbidden: unable to validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]

再運行一個其它pod

我們再嘗試創建一個pod,這次有一點不同

ubectl-user run pause --image=k8s.gcr.io/pause
deployment "pause" created

kubectl-user get pods
No resources found.

kubectl-user get events | head -n 2
LASTSEEN   FIRSTSEEN   COUNT     NAME              KIND         SUBOBJECT                TYPE      REASON                  SOURCE                                  MESSAGE
1m         2m          15        pause-7774d79b5   ReplicaSet                            Warning   FailedCreate            replicaset-controller                   Error creating: pods "pause-7774d79b5-" is forbidden: no providers available to validate pod request

從以上可以看到deployment已經成功創建(kubectl run 實際上會創建一個deployment).但是使用kubectl get pod命令卻沒有發現pod被創建.這是為什麼?問題的答案隱藏在replicaset控制器里.Fake-user成功創建的deployment(deployment又成功創建replicaset),但是當replicaset嘗試創建pod的時候,它並沒有被授權使用example定義的策略.

為了解決這個問題,需要把psp:unprivileged角色(前面創建的)綁定到pod的serviceaccount上(前面我們是綁定在了fake-user上).這裏serviceaccount是default(因為我們沒有指定其它用戶)

看到這裏如果你仍然覺得難以理解,可以回頭再看看,還是無法理解的話則需要補充關於角色,用戶和RBAC相關的知識.

kubectl-admin create rolebinding default:psp:unprivileged \
    --role=psp:unprivileged \
    --serviceaccount=psp-example:default
rolebinding "default:psp:unprivileged" created

這時候等待若干分鐘,replicaset的控制器最終會成功創建pod

kubectl-user get pods --watch
NAME                    READY     STATUS    RESTARTS   AGE
pause-7774d79b5-qrgcb   0/1       Pending   0         1s
pause-7774d79b5-qrgcb   0/1       Pending   0         1s
pause-7774d79b5-qrgcb   0/1       ContainerCreating   0         1s
pause-7774d79b5-qrgcb   1/1       Running   0         2s

清理工作

刪除名稱空間以刪除絕大部分示例中用到的資源

kubectl-admin delete ns psp-example
namespace "psp-example" deleted

注意現在剛剛創建的pod安全策略已經沒有了名稱空間,並且需要單獨被清除

kubectl-admin delete psp example
podsecuritypolicy "example" deleted

策略示例

以下是一個最小限制的策略,和不使用pod安生策略admission controller效果一樣

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: privileged
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
spec:
  privileged: true
  allowPrivilegeEscalation: true
  allowedCapabilities:
  - '*'
  volumes:
  - '*'
  hostNetwork: true
  hostPorts:
  - min: 0
    max: 65535
  hostIPC: true
  hostPID: true
  runAsUser:
    rule: 'RunAsAny'
  seLinux:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'

以下的一個示例有限制性策略,需要用戶是一個非特權用戶,阻止pod的權限提升

之所以要求是非特權用戶,因為特權用戶將會繞過限制

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default'
    apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
    seccomp.security.alpha.kubernetes.io/defaultProfileName:  'docker/default'
    apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
spec:
  privileged: false
  # Required to prevent escalations to root.
  allowPrivilegeEscalation: false
  # This is redundant with non-root + disallow privilege escalation,
  # but we can provide it for defense in depth.
  requiredDropCapabilities:
    - ALL
  # Allow core volume types.
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    # Assume that persistentVolumes set up by the cluster admin are safe to use.
    - 'persistentVolumeClaim'
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    # Require the container to run without root privileges.
    rule: 'MustRunAsNonRoot'
  seLinux:
    # This policy assumes the nodes are using AppArmor rather than SELinux.
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'MustRunAs'
    ranges:
      # Forbid adding the root group.
      - min: 1
        max: 65535
  fsGroup:
    rule: 'MustRunAs'
    ranges:
      # Forbid adding the root group.
      - min: 1
        max: 65535
  readOnlyRootFilesystem: false

策略參考

特權的

它決定了pod中的所有容器是否被允許以特權方式運行.默認情況下容器不允許訪問主機的設備,但是特權容器卻被允許訪問.這將允許容器有幾乎和它所在的進程一樣的訪問主機的權利.這將非常有用當容器想要使用主機的功能,比如訪問網絡的設備.

Host名稱空間

HostPID – 控制容器是否可以共享主機的進程id名稱空間

HostIPC – 控制容器是否可以共享主機的IPC名稱空間

HostNetwork – 控制容器是否可以使用所在節點的網絡名稱空間.這將允許pod訪問迴環設備,監聽localhost,並且可以窺探同一節點上其它pod的網絡活動狀況

AllowedHostPaths – 控制允許訪問的宿主機路徑

存儲卷和文件系統

Volumes – 提供了一系列的存儲卷類型白名單.這些允許的值和創建存儲卷時定義的資源類型相對應.想要獲取所有存儲卷類型,可以查看存儲卷類型列表.此外,*可以被用來允許所有的存儲卷類型

以下是推薦的最小化的允許存儲卷類型的安全策略配置

  • configMap
  • downwardAPI
  • emptyDir
  • persistentVolumeClaim
  • secret
  • projected

AllowedHostPaths– 它定義了一個hostPath類型的存儲卷可用的宿主機路徑的白名單.空集群意味着對宿主機的path無使用限制.它被定義為一個包含了一系列對象的單個pathPrefix字段,允許hostpath類型的存儲卷掛載以pathPrefix字段開頭的宿主機路徑.readonly字段意味着必須以readonly方式掛載(即不能寫入,只能讀)

allowedHostPaths:
  # This allows "/foo", "/foo/", "/foo/bar" etc., but
  # disallows "/fool", "/etc/foo" etc.
  # "/foo/../" is never valid.
  - pathPrefix: "/foo"
    readOnly: true # only allow read-only mounts

警告,一個可以無限制訪問宿主機文件系統的容器可以有很多方式提升權限,包括讀取其它容器內的數據,濫用系統服務的密鑰,比如kubecctl

可寫的hostpath目錄存儲卷允許容器寫入到宿主機文件系統,並且可以遍歷pathPrefix以外的文件系統,readOnly: true在kubernetes 1.11+版本以後才能使用,並且在allowedHostPaths必須使用以有效限制訪問特定的pathPrefix

ReadOnlyRootFilesystem – 限制容器必須以只讀的root文件系統運行(沒有可寫層)

特權提升

這個選項控制着容器的allowPrivilegeEscalation選項.這個布爾值直接控制着no_new_privs是否設置到容器運行的進程.它將阻止setuid來改變user ID,並且阻止文件有其它的能力(比如禁止使用ping工具).這個行為需要啟用MustRunAsNonRoot

AllowPrivilegeEscalation– 它決定着容器的安全上下文是否可以設置allowPrivilegeEscalation=true,為true是默認值.設置為false將使得容器所有的子進程沒有比父進程更高的特權

DefaultAllowPrivilegeEscalation,為allowPrivilegeEscalation設置默認值,從上面可以看到,默認的值為true.如果這個行為不是我們期待的,這個字段可以用於把它設置為不允許,但是仍然pod顯式請求allowPrivilegeEscalation

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"