分類
發燒車訊

愛爾蘭擬課拿鐵稅 減少一次性咖啡杯

摘錄自2019年11月7日中央社報導

愛爾蘭氣候行動部長布魯敦(Richard Bruton)今(6日)表示,2021年前將對可拋式咖啡杯課徵所謂的「拿鐵稅」,試圖改變消費者習慣並削減使用一次性塑膠對環境的影響。

愛爾蘭去年連續第3年超出年度溫室氣體排放配額量後,開始對經濟採取削減環境影響的行動,盼透過徵收最高0.25歐元的擬議拿鐵稅,促進咖啡飲用者改攜帶環保杯,進一步推進都柏林在歐盟的法定承諾。

愛爾蘭最早於2002年推出塑膠袋稅,拿鐵稅則是為了鼓勵採取較永續行為而新增的賦稅之一。這類計畫還包括對超市櫃台販賣的較昂貴中量級塑膠袋增收0.25歐元稅金。

儘管某些商家已為攜帶環保杯的顧客打折,但去年當局資助的報告發現,愛爾蘭全國490萬人每年仍丟棄高達2億個一次性咖啡杯。

布魯敦說,因為零售業缺乏回收飲食包裝的基礎設備,可分解咖啡杯也會被課稅。

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

不忍樹木被塑膠袋環繞 前進設計之都米蘭宣導

文:宋瑞文

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

Golang 網絡編程

目錄

  • TCP網絡編程
  • UDP網絡編程
  • Http網絡編程
  • 理解函數是一等公民
  • HttpServer源碼閱讀
    • 註冊路由
    • 啟動服務
    • 處理請求
  • HttpClient源碼閱讀
    • DemoCode
    • 整理思路
    • 重要的struct
    • 流程
    • transport.dialConn
    • 發送請求

TCP網絡編程

存在的問題:

  • 拆包:
    • 對發送端來說應用程序寫入的數據遠大於socket緩衝區大小,不能一次性將這些數據發送到server端就會出現拆包的情況。
    • 通過網絡傳輸的數據包最大是1500字節,當TCP報文的長度 - TCP頭部的長度 > MSS(最大報文長度時)將會發生拆包,MSS一般長(1460~1480)字節。
  • 粘包:
    • 對發送端來說:應用程序發送的數據很小,遠小於socket的緩衝區的大小,導致一個數據包裏面有很多不通請求的數據。
    • 對接收端來說:接收數據的方法不能及時的讀取socket緩衝區中的數據,導致緩衝區中積壓了不同請求的數據。

解決方法:

  • 使用帶消息頭的協議,在消息頭中記錄數據的長度。
  • 使用定長的協議,每次讀取定長的內容,不夠的使用空格補齊。
  • 使用消息邊界,比如使用 \n 分隔 不同的消息。
  • 使用諸如 xml json protobuf這種複雜的協議。

實驗:使用自定義協議

整體的流程:

客戶端:發送端連接服務器,將要發送的數據通過編碼器編碼,發送。

服務端:啟動、監聽端口、接收連接、將連接放在協程中處理、通過解碼器解碼數據。

	//###########################
//######  Server端代碼  ###### 
//###########################

func main() {
	// 1. 監聽端口 2.accept連接 3.開goroutine處理連接
	listen, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	for{
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("Fail listen.Accept : %v", err)
			continue
		}
		go ProcessConn(conn)
	}
}

// 處理網絡請求
func ProcessConn(conn net.Conn) {
	defer conn.Close()
	for  {
		bt,err:=coder.Decode(conn)
		if err != nil {
			fmt.Printf("Fail to decode error [%v]", err)
			return
		}
		s := string(bt)
		fmt.Printf("Read from conn:[%v]\n",s)
	}
}

//###########################
//######  Clinet端代碼  ###### 
//###########################
func main() {
	conn, err := net.Dial("tcp", ":9090")
	defer conn.Close()
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}

	// 將數據編碼併發送出去
	coder.Encode(conn,"hi server i am here");
}

//###########################
//######  編解碼器代碼  ###### 
//###########################
/**
 * 	解碼:
 */
func Decode(reader io.Reader) (bytes []byte, err error) {
	// 先把消息頭讀出來
	headerBuf := make([]byte, len(msgHeader))
	if _, err = io.ReadFull(reader, headerBuf); err != nil {
		fmt.Printf("Fail to read header from conn error:[%v]", err)
		return nil, err
	}
	// 檢驗消息頭
	if string(headerBuf) != msgHeader {
		err = errors.New("msgHeader error")
		return nil, err
	}
	// 讀取實際內容的長度
	lengthBuf := make([]byte, 4)
	if _, err = io.ReadFull(reader, lengthBuf); err != nil {
		return nil, err
	}
	contentLength := binary.BigEndian.Uint32(lengthBuf)
	contentBuf := make([]byte, contentLength)
	// 讀出消息體
	if _, err := io.ReadFull(reader, contentBuf); err != nil {
		return nil, err
	}
	return contentBuf, err
}

/**
 *  編碼
 *  定義消息的格式: msgHeader + contentLength + content
 *  conn 本身實現了 io.Writer 接口
 */
func Encode(conn io.Writer, content string) (err error) {
	// 寫入消息頭
	if err = binary.Write(conn, binary.BigEndian, []byte(msgHeader)); err != nil {
		fmt.Printf("Fail to write msgHeader to conn,err:[%v]", err)
	}
	// 寫入消息體長度
	contentLength := int32(len([]byte(content)))
	if err = binary.Write(conn, binary.BigEndian, contentLength); err != nil {
		fmt.Printf("Fail to write contentLength to conn,err:[%v]", err)
	}
	// 寫入消息
	if err = binary.Write(conn, binary.BigEndian, []byte(content)); err != nil {
		fmt.Printf("Fail to write content to conn,err:[%v]", err)
	}
	return err

客戶端的conn一直不被Close 有什麼表現?

四次揮手各個狀態的如下:

主從關閉方						被動關閉方
established					established
Fin-wait1					
										closeWait
Fin-wait2
Tiem-wait						lastAck
Closed							Closed

如果客戶端的連接手動的關閉,它和服務端的狀態會一直保持established建立連接中的狀態。

MacBook-Pro% netstat -aln | grep 9090
tcp4       0      0  127.0.0.1.9090         127.0.0.1.62348        ESTABLISHED
tcp4       0      0  127.0.0.1.62348        127.0.0.1.9090         ESTABLISHED
tcp46      0      0  *.9090                 *.*                    LISTEN

服務端的conn一直不被關閉 有什麼表現?

客戶端的進程結束后,會發送fin數據包給服務端,向服務端請求斷開連接。

服務端的conn不關閉的話,服務端就會停留在四次揮手的close_wait階段(我們不手動Close,服務端就任務還有數據/任務沒處理完,因此它不關閉)。

客戶端停留在 fin_wait2的階段(在這個階段等着服務端告訴自己可以真正斷開連接的消息)。

MacBook-Pro% netstat -aln | grep 9090
tcp4       0      0  127.0.0.1.9090         127.0.0.1.62888        CLOSE_WAIT
tcp4       0      0  127.0.0.1.62888        127.0.0.1.9090         FIN_WAIT_2
tcp46      0      0  *.9090                 *.*                    LISTEN

什麼是binary.BigEndian?什麼是binary.LittleEndian?

對計算機來說一切都是二進制的數據,BigEndian和LittleEndian描述的就是二進制數據的字節順序。計算機內部,小端序被廣泛應用於現代性 CPU 內部存儲數據;大端序常用於網絡傳輸和文件存儲。

比如:

一個數的二進製表示為 	 0x12345678
BigEndian   表示為: 0x12 0x34 0x56 0x78 
LittleEndian表示為: 0x78 0x56 0x34 0x12

UDP網絡編程

思路:

UDP服務器:1、監聽 2、循環讀取消息 3、回複數據。

UDP客戶端:1、連接服務器 2、發送消息 3、接收消息。

// ################################
// ######## UDPServer #########
// ################################
func main() {
	// 1. 監聽端口 2.accept連接 3.開goroutine處理連接
	listen, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	for{
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("Fail listen.Accept : %v", err)
			continue
		}
		go ProcessConn(conn)
	}
}

// 處理網絡請求
func ProcessConn(conn net.Conn) {
	defer conn.Close()
	for  {
		bt,err:= coder.Decode(conn)
		if err != nil {
			fmt.Printf("Fail to decode error [%v]", err)
			return
		}
		s := string(bt)
		fmt.Printf("Read from conn:[%v]\n",s)
	}
}

// ################################
// ######## UDPClient #########
// ################################
func main() {

	udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 9091,
	})
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}

	_, err = udpConn.Write([]byte("i am udp client"))
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	bytes:=make([]byte,1024)
	num, addr, err := udpConn.ReadFromUDP(bytes)
	if err != nil {
		fmt.Printf("Fail to read from udp error: [%v]", err)
		return
	}
	fmt.Printf("Recieve from udp address:[%v], bytes:[%v], content:[%v]",addr,num,string(bytes))
}

Http網絡編程

思路整理:

HttpServer:1、創建路由器。2、為路由器綁定路由規則。3、創建服務器、監聽端口。 4啟動讀服務。

HttpClient: 1、創建連接池。2、創建客戶端,綁定連接池。3、發送請求。4、讀取響應。

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/login", doLogin)
	server := &http.Server{
		Addr:         ":8081",
		WriteTimeout: time.Second * 2,
		Handler:      mux,
	}
	log.Fatal(server.ListenAndServe())
}

func doLogin(writer http.ResponseWriter,req *http.Request){
	_, err := writer.Write([]byte("do login"))
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
}

HttpClient端

func main() {
	transport := &http.Transport{
    // 撥號的上下文
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 撥號建立連接時的超時時間
			KeepAlive: 30 * time.Second, // 長連接存活的時間
		}).DialContext,
    // 最大空閑連接數
		MaxIdleConns:          100,  
    // 超過最大的空閑連接數的連接,經過 IdleConnTimeout時間後會失效
		IdleConnTimeout:       10 * time.Second, 
    // https使用了SSL安全證書,TSL是SSL的升級版
    // 當我們使用https時,這行配置生效
		TLSHandshakeTimeout:   10 * time.Second, 
		ExpectContinueTimeout: 1 * time.Second,  // 100-continue 狀態碼超時時間
	}

	// 創建客戶端
	client := &http.Client{
		Timeout:   time.Second * 10, //請求超時時間
		Transport: transport,
	}

	// 請求數據
	res, err := client.Get("http://localhost:8081/login")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	defer res.Body.Close()

	bytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	fmt.Printf("Read from http server res:[%v]", string(bytes))
}

理解函數是一等公民

點擊查看在github中函數相關的筆記

在golang中函數是一等公民,我們可以把一個函數當作普通變量一樣使用。

比如我們有個函數HelloHandle,我們可以直接使用它。

func HelloHandle(name string, age int) {
	fmt.Printf("name:[%v] age:[%v]", name, age)
}

func main() {
  HelloHandle("tom",12)
}

閉包

如何理解閉包:閉包本質上是一個函數,而且這個函數會引用它外部的變量,如下例子中的f3中的匿名函數本身就是一個閉包。 通常我們使用閉包起到一個適配的作用。

例1:

// f2是一個普通函數,有兩個入參數
func f2() {
	fmt.Printf("f2222")
}

// f1函數的入參是一個f2類型的函數
func f1(f2 func()) {
	f2()
}

func main() {
  // 由於golang中函數是一等公民,所以我們可以把f2同普通變量一般傳遞給f1
	f1(f2)
}

例2: 在上例中更進一步。f2有了自己的參數, 這時就不能直接把f2傳遞給f1了。

總不能傻傻的這樣吧f1(f2(1,2)) ???

而閉包就能解決這個問題。

// f2是一個普通函數,有兩個入參數
func f2(x int, y int) {
	fmt.Println("this is f2 start")
	fmt.Printf("x: %d y: %d \n", x, y)
	fmt.Println("this is f2 end")
}

// f1函數的入參是一個f2類型的函數
func f1(f2 func()) {
	fmt.Println("this is f1 will call f2")
	f2()
	fmt.Println("this is f1 finished call f2")
}

// 接受一個兩個參數的函數, 返回一個包裝函數
func f3(f func(int,int) ,x,y int) func() {
	fun := func() {
		f(x,y)
	}
	return fun
}

func main() {
	// 目標是實現如下的傳遞與調用
	f1(f3(f2,6,6))
}

實現方法的回調:

下面的例子中實現這樣的功能:就好像是我設計了一個框架,定好了整個框架運轉的流程(或者說是提供了一個編程模版),框架具體做事的函數你根據自己的需求自己實現,我的框架只是負責幫你回調你具體的方法。

// 自定義類型,handler本質上是一個函數
type HandlerFunc func(string, int)

// 閉包
func (f HandlerFunc) Serve(name string, age int) {
	f(name, age)
}

// 具體的處理函數
func HelloHandle(name string, age int) {
	fmt.Printf("name:[%v] age:[%v]", name, age)
}

func main() {
  // 把HelloHandle轉換進自定義的func中
	handlerFunc := HandlerFunc(HelloHandle)
  // 本質上會去回調HelloHandle方法
	handlerFunc.Serve("tom", 12)
  
  // 上面兩行效果 == 下面這行
  // 只不過上面的代碼是我在幫你回調,下面的是你自己主動調用
  HelloHandle("tom",12)
}

HttpServer源碼閱讀

註冊路由

直觀上看註冊路由這一步,就是它要做的就是將在路由器url pattern和開發者提供的func關聯起來。 很容易想到,它裏面很可能是通過map實現的。


func main() {
	// 創建路由器
	// 為路由器綁定路由規則
	mux := http.NewServeMux()
	mux.HandleFunc("/login", doLogin)
	...
}

func doLogin(writer http.ResponseWriter,req *http.Request){
	_, err := writer.Write([]byte("do login"))
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
}

姑且將ServeMux當作是路由器。我們使用http包下的 NewServerMux 函數創建一個新的路由器對象,進而使用它的HandleFunc(pattern,func)函數完成路由的註冊。

跟進NewServerMux函數,可以看到,它通過new函數返回給我們一個ServeMux結構體。

func NewServeMux() *ServeMux {
  return new(ServeMux) 
}

這個ServeMux結構體長下面這樣:在這個ServeMux結構體中我們就看到了這個維護pattern和func的map

type ServeMux struct {
	mu    sync.RWMutex 
	m     map[string]muxEntry
	hosts bool // whether any patterns contain hostnames
}

這個muxEntry長下面這樣:

type muxEntry struct {
	h       Handler
	pattern string
}

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

看到這裏問題就來了,上面我們手動註冊進路由器中的僅僅是一個有規定參數的方法,到這裏怎麼成了一個Handle了?我們也沒有說去手動的實現Handler這個接口,也沒有重寫ServeHTTP函數啊, 在golang中實現一個接口不得像下面這樣搞嗎?**

type Handle interface {
	Serve(string, int, string)
}

type HandleImpl struct {

}

func (h HandleImpl)Serve(string, int, string){

}

帶着這個疑問看下面的方法:

	// 由於函數是一等公民,故我們將doLogin函數同普通變量一樣當做入參傳遞進去。
 	mux.HandleFunc("/login", doLogin)

  func doLogin(writer http.ResponseWriter,req *http.Request){
    ...
	}

跟進去看 HandleFunc 函數的實現:

首先:HandleFunc函數的第二個參數是接收的函數的類型和doLogin函數的類型是一致的,所以doLogin能正常的傳遞進HandleFunc中。

其次:我們的關注點應該是下面的HandlerFunc(handler)

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

跟進這個HandlerFunc(handler) 看到下圖,真相就大白於天下了。golang以一種優雅的方式悄無聲息的為我們完成了一次適配。這麼看來上面的HandlerFunc(handler)並不是函數的調用,而是doLogin轉換成自定義類型。這個自定義類型去實現了Handle接口(因為它重寫了ServeHTTP函數)以閉包的形式完美的將我們的doLogin適配成了Handle類型。

在往下看Handle方法:

第一:將pattern和handler註冊進map中

第二:為了保證整個過程的併發安全,使用鎖保護整個過程。

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

	if pattern[0] != '/' {
		mux.hosts = true
	}

啟動服務

概覽圖:

和java對比着看,在java一組複雜的邏輯會被封裝成一個class。在golang中對應的就是一組複雜的邏輯會被封裝成一個結構體。

對應HttpServer肯定也是這樣,http服務器在golang的實現中有自己的結構體。它就是http包下的Server。

它有一系列描述性屬性。如監聽的地址、寫超時時間、路由器。

	server := &http.Server{
		Addr:         ":8081",
		WriteTimeout: time.Second * 2,
		Handler:      mux,
	}
	log.Fatal(server.ListenAndServe())

我們看它啟動服務的函數:server.ListenAndServe()

實現的邏輯是使用net包下的Listen函數,獲取給定地址上的tcp連接。

再將這個tcp連接封裝進 tcpKeepAliveListenner 結構體中。

在將這個tcpKeepAliveListenner丟進Server的Serve函數中處理

// ListenAndServe 會監聽開發者給定網絡地址上的tcp連接,當有請求到來時,會調用Serve函數去處理這個連接。
// 它接收到所有連接都使用 TCP keep-alives相關的配置
// 
// 如果構造Server時沒有指定Addr,他就會使用默認值: “:http”
// 
// 當Server ShutDown或者是Close,ListenAndServe總是會返回一個非nil的error。
// 返回的這個Error是 ErrServerClosed
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
  // 底層藉助於tcp實現
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

// tcpKeepAliveListener會為TCP設置一個keep-alive 超時時長。
// 它通常被 ListenAndServe 和 ListenAndServeTLS使用。
// 它保證了已經dead的TCP最終都會消失。
type tcpKeepAliveListener struct {
	*net.TCPListener
}

接着去看看Serve方法,上一個函數中獲取到了一個基於tcp的Listener,從這個Listener中可以不斷的獲取出新的連接,下面的方法中使用無限for循環完成這件事。conn獲取到后將連接封裝進httpConn,為了保證不阻塞下一個連接到到來,開啟新的goroutine處理這個http連接。

func (srv *Server) Serve(l net.Listener) error {
  // 如果有一個包裹了 srv 和 listener 的鈎子函數,就執行它
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}
	
  // 將tcp的Listener封裝進onceCloseListener,保證連接不會被關閉多次。
	l = &onceCloseListener{Listener: l}
	defer l.Close()
 
  // http2相關的配置
	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)
	
  // 如果沒有接收到請求睡眠多久
	var tempDelay time.Duration     // how long to sleep on accept failure
	baseCtx := context.Background() // base is always background, per Issue 16220
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
  // 開啟無限循環,嘗試從Listenner中獲取連接。
	for {
		rw, e := l.Accept()
    // accpet過程中發生錯屋
		if e != nil {
			select {
        // 如果從server的doneChan中可以獲取內容,返回Server關閉了
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
      // 如果發生了 net.Error 並且是臨時的錯誤就睡5毫秒,再發生錯誤睡眠的時間*2,上線是1s
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return e
		}
    // 如果沒有發生錯誤,清空睡眠的時間
		tempDelay = 0
    // 將接收到連接封裝進httpConn
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
    // 開啟一條新的協程處理這個連接
		go c.serve(ctx)
	}
}

處理請求

c.serve(ctx)中就會去解析http相關的報文信息~,將http報文解析進Request結構體中。

部分代碼如下:

		// 將 server 包裹為 serverHandler 的實例,執行它的 ServeHTTP 方法,處理請求,返迴響應。
		// serverHandler 委託給 server 的 Handler 或者 DefaultServeMux(默認路由器)
		// 來處理 "OPTIONS *" 請求。
		serverHandler{c.server}.ServeHTTP(w, w.req)
// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
	srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  // 如果沒有定義Handler就使用默認的
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
  // 處理請求,返迴響應。
	handler.ServeHTTP(rw, req)
}

可以看到,req中包含了我們前面說的pattern,叫做RequestUri,有了它下一步就知道該回調ServeMux中的哪一個函數。

HttpClient源碼閱讀

DemoCode

func main() {
	// 創建連接池
	// 創建客戶端,綁定連接池
	// 發送請求
	// 讀取響應
	transport := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 連接超時
			KeepAlive: 30 * time.Second, // 長連接存活的時間
		}).DialContext,
    // 最大空閑連接數
		MaxIdleConns:          100,             
    // 超過最大空閑連接數的連接會在IdleConnTimeout后被銷毀
		IdleConnTimeout:       10 * time.Second, 
		TLSHandshakeTimeout:   10 * time.Second, // tls握手超時時間
		ExpectContinueTimeout: 1 * time.Second,  // 100-continue 狀態碼超時時間
	}

	// 創建客戶端
	client := &http.Client{
		Timeout:   time.Second * 10, //請求超時時間
		Transport: transport,
	}

	// 請求數據,獲得響應
	res, err := client.Get("http://localhost:8081/login")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	defer res.Body.Close()
  // 處理數據
	bytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	fmt.Printf("Read from http server res:[%v]", string(bytes))
}

整理思路

http.Client的代碼其實是很多的,全部很細的過一遍肯定也會難度,下面可能也是只能提及其中的一部分。

首先明白一件事,我們編寫的HttpClient是在干什麼?(雖然這個問題很傻,但是總得問一下)是在發送Http請求。

一般我們在開發的時候,更多的編寫的是HttpServer的代碼。是在處理Http請求, 而不是去發送Http請求,Http請求都是是前端通過ajax經由瀏覽器發送到後端的。

其次,Http請求實際上是建立在tcp連接之上的,所以如果我們去看http.Client肯定能找到net.Dial("tcp",adds)相關的代碼。

那也就是說,我們要看看,http.Client是如何在和服務端建立連接、發送數據、接收數據的。

重要的struct

http.Client中有機幾個比較重要的struct,如下

http.Client結構體中封裝了和http請求相關的屬性,諸如 cookie,timeout,redirect以及Transport。

type Client struct {
	Transport RoundTripper
	CheckRedirect func(req *Request, via []*Request) error
	Jar CookieJar
	Timeout time.Duration
}

Tranport實現了RoundTrpper接口:

 type RoundTripper interface {   
  // 1、RoundTrip會去執行一個簡單的 Http Trancation,併為requestt返回一個響應
  // 2、RoundTrip不會嘗試去解析response
  // 3、注意:只要返回了Reponse,無論response的狀態碼是多少,RoundTrip返回的結果:err == nil 
  // 4、RoundTrip將請求發送出去后,如果他沒有獲取到response,他會返回一個非空的err。
  // 5、同樣,RoundTrip不會嘗試去解析諸如重定向、認證、cookie這種更高級的協議。 
  // 6、除了消費和關閉請求體之外,RoundTrip不會修改request的其他字段
  // 7、RoundTrip可以在一個單獨的gorountine中讀取request的部分字段。一直到ResponseBody關閉之前,調用者都不能取消,或者重用這個request
  // 8、RoundTrip始終會保證關閉Body(包含在發生err時)。根據實現的不同,在RoundTrip關閉前,關閉Body這件事可能會在一個單獨的goroutine中去做。這就意味着,如果調用者想將請求體用於後續的請求,必須等待知道發生Close
  // 9、請求的URL和Header字段必須是被初始化的。 
	RoundTrip(*Request) (*Response, error)
}

看上面RoundTrpper接口,它裏面只有一個方法RoundTrip,方法的作用就是執行一次Http請求,發送Request然後獲取Response。

RoundTrpper被設計成了一個支持併發的結構體。

Transport結構體如下:

type Transport struct {
	idleMu     sync.Mutex
   // user has requested to close all idle conns
	wantIdle   bool
  // Transport的作用就是用來建立一個連接,這個idleConn就是Transport維護的空閑連接池。
	idleConn   map[connectMethodKey][]*persistConn // most recently used at end
	idleConnCh map[connectMethodKey]chan *persistConn
}

其中的connectMethodKey也是結構體:

type connectMethodKey struct {
  // proxy 代理的URL,當他不為空時,就會一直使用這個key 
  // scheme 協議的類型, http https
  // addr 代理的url,也就是下游的url
	proxy, scheme, addr string
}

persistConn是一個具體的連接實例,包含連接的上下文。

type persistConn struct {
  // alt可選地指定TLS NextProto RoundTripper。 
  // 這用於今天的HTTP / 2和以後的將來的協議。 如果非零,則其餘字段未使用。
	alt RoundTripper
	t         *Transport
	cacheKey  connectMethodKey
	conn      net.Conn
	tlsState  *tls.ConnectionState
  // 用於從conn中讀取內容
	br        *bufio.Reader       // from conn
  // 用於往conn中寫內容
	bw        *bufio.Writer       // to conn
	nwrite    int64               // bytes written
  // 他是個chan,roundTrip會將readLoop中的內容寫入到reqch中
	reqch     chan requestAndChan 
  // 他是個chan,roundTrip會將writeLoop中的內容寫到writech中
	writech   chan writeRequest  
	closech   chan struct{}       // closed when conn closed

另外補充一個結構體:Request,他用來描述一次http請求的實例,它定義於http包request.go, 裏面封裝了對Http請求相關的屬性

type Request struct {
   Method string
   URL *url.URL
   Proto      string // "HTTP/1.0"
   ProtoMajor int    // 1
   ProtoMinor int    // 0
   Header Header
   Body io.ReadCloser
   GetBody func() (io.ReadCloser, error)
   ContentLength int64
   TransferEncoding []string
   Close bool
   Host string
   Form url.Values
   PostForm url.Values
   MultipartForm *multipart.Form
   Trailer Header
   RemoteAddr string
   RequestURI string
   TLS *tls.ConnectionState
   Cancel <-chan struct{}
   Response *Response
   ctx context.Context
}

這幾個結構體共同完成如下圖所示http.Client的工作流程

流程

我們想發送一次Http請求。首先我們需要構造一個Request,Request本質上是對Http協議的描述(因為大家使用的都是Http協議,所以將這個Request發送到HttpServer后,HttpServer能識別並解析它)。

// 從這行代碼開始往下看
	res, err := client.Get("http://localhost:8081/login")

// 跟進Get
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)

// 跟進Do
	func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
 } 

// 跟進do,do函數中有下面的邏輯,可以看到執行完send后已經拿到返回值了。所以我們得繼續跟進send方法
  if resp, didTimeout, err = c.send(req, deadline); err != nil 

// 跟進send方法,可以看到send中還有一send方法,入參分別是:request,tranpost,deadline
// 到現在為止,我們沒有看到有任何和服務端建立連接的動作發生,但是構造的req和擁有連接池的tranport已經見面了~
	resp, didTimeout, err = send(req, c.transport(), deadline)

// 繼續跟進這個send方法,看到了調用了rt的RoundTrip方法。
// 這個rt就是我們編寫HttpClient代碼時創建的,綁定在http.Client上的tranport實例。
// 這個RoundTrip方法的作用我們在上面已經說過了,最直接的作用就是:發送request 並獲取response。
	resp, err = rt.RoundTrip(req)

但是RoundTrip他是個定義在RoundTripper接口中的抽象方法,我們看代碼肯定是要去看具體的實現嘛
這裏可以使用斷點調試法:在上面最後一行上打上斷點,會進入到他的具體實現中。從圖中可以看到具體的實現在roundtrip中。

RoundTrip中調用的函數是我們自定義的transport的roundTrip函數, 跟進去如下:

緊接着我們需要一個conn,這個conn我們通過Transport可以獲取到。conn的類型為persistConn。

// roundTrip函數中又一個無限for循環
for {
    // 檢查請求的上下文是否關閉了
		select {
		case <-ctx.Done():
			req.closeBody()
			return nil, ctx.Err()
		default:
		}

    // 對傳遞進來的req進行了有一層的封裝,封裝后的這個treq可以被roundTrip修改,所以每次重試都會新建
		treq := &transportRequest{Request: req, trace: trace}
		cm, err := t.connectMethodForRequest(treq)
		if err != nil {
			req.closeBody()
			return nil, err
		}

    // 到這裏真的執行從tranport中獲取和對應主機的連接,這個連接可能是http、https、http代理、http代理的高速緩存, 但是無論如何我們都已經準備好了向這個連接發送treq
    // 這裏獲取出來的連接就是我們在上文中提及的persistConn
		pconn, err := t.getConn(treq, cm)
		if err != nil {
			t.setReqCanceler(req, nil)
			req.closeBody()
			return nil, err
		}

		var resp *Response
		if pconn.alt != nil {
			// HTTP/2 path.
			t.decHostConnCount(cm.key()) // don't count cached http2 conns toward conns per host
			t.setReqCanceler(req, nil)   // not cancelable with CancelRequest
			resp, err = pconn.alt.RoundTrip(req)
		} else {
      
      // 調用persistConn的roundTrip方法,發送treq並獲取響應。
			resp, err = pconn.roundTrip(treq)
		}
		if err == nil {
			return resp, nil
		}
		if !pconn.shouldRetryRequest(req, err) {
			// Issue 16465: return underlying net.Conn.Read error from peek,
			// as we've historically done.
			if e, ok := err.(transportReadFromServerError); ok {
				err = e.err
			}
			return nil, err
		}
		testHookRoundTripRetried()

		// Rewind the body if we're able to.  (HTTP/2 does this itself so we only
		// need to do it for HTTP/1.1 connections.)
		if req.GetBody != nil && pconn.alt == nil {
			newReq := *req
			var err error
			newReq.Body, err = req.GetBody()
			if err != nil {
				return nil, err
			}
			req = &newReq
		}
	}

整理思路:然後看上面代碼中獲取conn和roundTrip的實現細節。

我們需要一個conn,這個conn可以通過Transport獲取到。conn的類型為persistConn。但是不管怎麼樣,都得先獲取出 persistConn,才能進一步完成發送請求再得到服務端到響應。

然後關於這個persistConn結構體其實上面已經提及過了。重新貼在下面

type persistConn struct {
  // alt可選地指定TLS NextProto RoundTripper。 
  // 這用於今天的HTTP / 2和以後的將來的協議。 如果非零,則其餘字段未使用。
	alt RoundTripper
  
  conn      net.Conn
	t         *Transport
	br        *bufio.Reader  // 用於從conn中讀取內容
	bw        *bufio.Writer  // 用於往conn中寫內容
  // 他是個chan,roundTrip會將readLoop中的內容寫入到reqch中
	reqch     chan requestAndChan 
  // 他是個chan,roundTrip會將writeLoop中的內容寫到writech中
  
	nwrite    int64               // bytes written
	cacheKey  connectMethodKey
	tlsState  *tls.ConnectionState
	writech   chan writeRequest  
	closech   chan struct{}       // closed when conn closed

跟進 t.getConn(treq, cm)代碼如下:

	// 先嘗試從空閑緩衝池中取得連接
  // 所謂的空閑緩衝池就是Tranport結構體中的: idleConn map[connectMethodKey][]*persistConn 
  // 入參位置的cm如下:
  /* type connectMethod struct {
      // 代理的url,如果沒有代理的話,這個值為nil
			proxyURL     *url.URL 
			
			// 連接所使用的協議 http、https
			targetScheme string
      
	    // 如果proxyURL指定了http代理或者是https代理,並且使用的協議是http而不是https。
	    // 那麼下面的targetAddr就會不包含在connect method key中。
	    // 因為socket可以復用不同的targetAddr值
			targetAddr string
	}*/
	t.getIdleConn(cm);

	// 空閑緩衝池有的空閑連接的話返回conn,否則進行如下的select
	select {
    // todo 這裏我還不確定是在干什麼,目前猜測是這樣的:每個服務器能打開的socket句柄是有限的
    // 每次來獲取鏈接的時候,我們就計數+1。當整體的句柄在Host允許範圍內時我們不做任何干涉~
		case <-t.incHostConnCount(cmKey):
			// count below conn per host limit; proceed
    
    // 重新嘗試從空閑連接池中獲取連接,因為可能有的連接使用完后被放回連接池了
		case pc := <-t.getIdleConnCh(cm):
			if trace != nil && trace.GotConn != nil {
				trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()})
			}
			return pc, nil
    // 請求是否被取消了
		case <-req.Cancel:
			return nil, errRequestCanceledConn
    // 請求的上下文是否Done掉了
		case <-req.Context().Done():
			return nil, req.Context().Err()
		case err := <-cancelc:
			if err == errRequestCanceled {
				err = errRequestCanceledConn
			}
			return nil, err
		}

	// 開啟新的gorountine新建連接一個連接
	go func() {
    /**
    *	新建連接,方法底層封裝了tcp client dial相關的邏輯
    *	conn, err := t.dial(ctx, "tcp", cm.addr())
    *	以及根據不同的targetScheme構建不同的request的邏輯。
    */
    // 獲取到persistConn
		pc, err := t.dialConn(ctx, cm)
    // 將persistConn寫到chan中
		dialc <- dialRes{pc, err}
	}()

	// 再嘗試從空閑連接池中獲取
  idleConnCh := t.getIdleConnCh(cm)
	select {
  // 如果上面的go協程撥號成功了,這裏就能取出值來
	case v := <-dialc:
		// Our dial finished.
		if v.pc != nil {
			if trace != nil && trace.GotConn != nil && v.pc.alt == nil {
				trace.GotConn(httptrace.GotConnInfo{Conn: v.pc.conn})
			}
			return v.pc, nil
		}
		// Our dial failed. See why to return a nicer error
		// value.
    // 將Host的連接-1
		t.decHostConnCount(cmKey)
		select {
    ...

transport.dialConn

下面代碼中的cm長這樣

// dialConn是Transprot的方法
// 入參:context上下文, connectMethod
// 出參:persisnConn
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) {
	// 構建將要返回的 persistConn
  pconn := &persistConn{
		t:             t,
		cacheKey:      cm.key(),
		reqch:         make(chan requestAndChan, 1),
		writech:       make(chan writeRequest, 1),
		closech:       make(chan struct{}),
		writeErrCh:    make(chan error, 1),
		writeLoopDone: make(chan struct{}),
	}
	trace := httptrace.ContextClientTrace(ctx)
	wrapErr := func(err error) error {
		if cm.proxyURL != nil {
			// Return a typed error, per Issue 16997
			return &net.OpError{Op: "proxyconnect", Net: "tcp", Err: err}
		}
		return err
	}
  
  // 判斷cm中使用的協議是否是https
	if cm.scheme() == "https" && t.DialTLS != nil {
		var err error
		pconn.conn, err = t.DialTLS("tcp", cm.addr())
		if err != nil {
			return nil, wrapErr(err)
		}
		if pconn.conn == nil {
			return nil, wrapErr(errors.New("net/http: Transport.DialTLS returned (nil, nil)"))
		}
		if tc, ok := pconn.conn.(*tls.Conn); ok {
			// Handshake here, in case DialTLS didn't. TLSNextProto below
			// depends on it for knowing the connection state.
			if trace != nil && trace.TLSHandshakeStart != nil {
				trace.TLSHandshakeStart()
			}
			if err := tc.Handshake(); err != nil {
				go pconn.conn.Close()
				if trace != nil && trace.TLSHandshakeDone != nil {
					trace.TLSHandshakeDone(tls.ConnectionState{}, err)
				}
				return nil, err
			}
			cs := tc.ConnectionState()
			if trace != nil && trace.TLSHandshakeDone != nil {
				trace.TLSHandshakeDone(cs, nil)
			}
			pconn.tlsState = &cs
		}
	} else {
    // 如果不是https協議就來到這裏,使用tcp向httpserver撥號,獲取一個tcp連接。
		conn, err := t.dial(ctx, "tcp", cm.addr())
		if err != nil {
			return nil, wrapErr(err)
		}
    // 將獲取到tcp連接交給我們的persistConn維護
		pconn.conn = conn
    
    // 處理https相關邏輯
		if cm.scheme() == "https" {
			var firstTLSHost string
			if firstTLSHost, _, err = net.SplitHostPort(cm.addr()); err != nil {
				return nil, wrapErr(err)
			}
			if err = pconn.addTLS(firstTLSHost, trace); err != nil {
				return nil, wrapErr(err)
			}
		}
	}

	// Proxy setup.
	switch {
  // 如果代理URL為空,不做任何處理  
	case cm.proxyURL == nil:
		// Do nothing. Not using a proxy.
  //   
	case cm.proxyURL.Scheme == "socks5":
		conn := pconn.conn
		d := socksNewDialer("tcp", conn.RemoteAddr().String())
		if u := cm.proxyURL.User; u != nil {
			auth := &socksUsernamePassword{
				Username: u.Username(),
			}
			auth.Password, _ = u.Password()
			d.AuthMethods = []socksAuthMethod{
				socksAuthMethodNotRequired,
				socksAuthMethodUsernamePassword,
			}
			d.Authenticate = auth.Authenticate
		}
		if _, err := d.DialWithConn(ctx, conn, "tcp", cm.targetAddr); err != nil {
			conn.Close()
			return nil, err
		}
	case cm.targetScheme == "http":
		pconn.isProxy = true
		if pa := cm.proxyAuth(); pa != "" {
			pconn.mutateHeaderFunc = func(h Header) {
				h.Set("Proxy-Authorization", pa)
			}
		}
	case cm.targetScheme == "https":
		conn := pconn.conn
		hdr := t.ProxyConnectHeader
		if hdr == nil {
			hdr = make(Header)
		}
		connectReq := &Request{
			Method: "CONNECT",
			URL:    &url.URL{Opaque: cm.targetAddr},
			Host:   cm.targetAddr,
			Header: hdr,
		}
		if pa := cm.proxyAuth(); pa != "" {
			connectReq.Header.Set("Proxy-Authorization", pa)
		}
		connectReq.Write(conn)

		// Read response.
		// Okay to use and discard buffered reader here, because
		// TLS server will not speak until spoken to.
		br := bufio.NewReader(conn)
		resp, err := ReadResponse(br, connectReq)
		if err != nil {
			conn.Close()
			return nil, err
		}
		if resp.StatusCode != 200 {
			f := strings.SplitN(resp.Status, " ", 2)
			conn.Close()
			if len(f) < 2 {
				return nil, errors.New("unknown status code")
			}
			return nil, errors.New(f[1])
		}
	}

	if cm.proxyURL != nil && cm.targetScheme == "https" {
		if err := pconn.addTLS(cm.tlsHost(), trace); err != nil {
			return nil, err
		}
	}

	if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" {
		if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok {
			return &persistConn{alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil
		}
	}

	if t.MaxConnsPerHost > 0 {
		pconn.conn = &connCloseListener{Conn: pconn.conn, t: t, cmKey: pconn.cacheKey}
	}
  
  // 初始化persistConn的bufferReader和bufferWriter
	pconn.br = bufio.NewReader(pconn) // 可以從上面給pconn維護的tcpConn中讀數據
	pconn.bw = bufio.NewWriter(persistConnWriter{pconn})// 可以往上面pconn維護的tcpConn中寫數據 
  
  // 新開啟兩條和persistConn相關的go協程。
	go pconn.readLoop()
	go pconn.writeLoop()
	return pconn, nil
}

上面的兩條goroutine 和 br bw共同完成如下圖的流程

發送請求

發送req的邏輯在http包的下的tranport包中的func (t *Transport) roundTrip(req *Request) (*Response, error) {}函數中。

如下:

	// 發送treq
	resp, err = pconn.roundTrip(treq)

	// 跟進roundTrip
  // 可以看到他將一個writeRequest結構體類型的實例寫入了writech中
	// 而這個writech會被上圖中的writeLoop消費,藉助bufferWriter寫入tcp連接中,完成往服務端數據的發送。
	pc.writech <- writeRequest{req, writeErrCh, continueCh}

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

STM32內存受限情況下攝像頭驅動方式與圖像裁剪的選擇

1、STM32圖像接收接口

使用stm32芯片,128kB RAM,512kB Rom,資源有限,接攝像頭採集圖像,這種情況下,內存利用制約程序設計。

STM32使用DCMI接口讀取攝像頭,協議如下。行同步信號指示了一行數據完成,場同步信號指示了一幀圖像傳輸完成。所以出現了兩種典型的數據接收方式,按照行信號一行一行處理,按照場信號一次接收一副圖像。

 

2、按行讀取

以網絡上流行的野火的demo為例,使用行中斷,用DMA來讀取一行數據。

//記錄傳輸了多少行
static uint16_t line_num =0;
//DMA傳輸完成中斷服務函數
void DMA2_Stream1_IRQHandler(void)
{
  if ( DMA_GetITStatus(DMA2_Stream1,DMA_IT_TCIF1) == SET )
  {
   /*行計數*/
  line_num++;
  if (line_num==img_height)
  {
  /*傳輸完一幀,計數複位*/
  line_num=0;
  }
  /*DMA 一行一行傳輸*/
  OV2640_DMA_Config(FSMC_LCD_ADDRESS+(lcd_width*2*(lcd_height-line_num-1)),img_width*2/4);
  DMA_ClearITPendingBit(DMA2_Stream1,DMA_IT_TCIF1);
  }
}

 //幀中斷服務函數,使用幀中斷重置line_num,可防止有時掉數據的時候DMA傳送行數出現偏移
void DCMI_IRQHandler(void)
{
  if ( DCMI_GetITStatus (DCMI_IT_FRAME) == SET )
  {
  /*傳輸完一幀,計數複位*/
  line_num=0;
  DCMI_ClearITPendingBit(DCMI_IT_FRAME);
  }
}

DMA中斷服務函數中主要是使用了一個靜態變量line_num來記錄已傳輸了多少行數據,每進一次DMA中斷時自加1,由於進入一次中斷就代表傳輸完一行數據,所以line_num的值等於lcd_height時(攝像頭輸出的數據行數),表示傳輸完一幀圖像,line_num複位為0,開始另一幀數據的傳輸。line_num計數完畢后利用前面定義的OV2640_DMA_Config函數配置新的一行DMA數據傳輸,它利用line_num變量計算顯存地址的行偏移,控制DCMI數據被傳送到正確的位置,每次傳輸的都是一行像素的數據量。

當DCMI接口檢測到攝像頭傳輸的幀同步信號時,會進入DCMI_IRQHandler中斷服務函數,在這個函數中不管line_num原來的值是什麼,它都把line_num直接複位為0,這樣下次再進入DMA中斷服務函數的時候,它會開始新一幀數據的傳輸。這樣可以利用DCMI的硬件同步信號,而不只是依靠DMA自己的傳輸計數,這樣可以避免有時STM32內部DMA傳輸受到阻塞而跟不上外部攝像頭信號導致的數據錯誤。

圖像按幀讀取比按行讀取效率更高,那麼為什麼要按行讀取呢?上面的例子是把圖像送到LCD,如果是送到內存,按幀讀取就需要芯片有很大的內存空間。以752*480的分辨率為例,需要360kB的RAM空間,遠遠超出了芯片RAM的大小。部分應用不需要攝像頭全尺寸的圖像,只需要中心區域,比如為了避免畸變影響一般只用圖像中間的部分,那麼按行讀取就有一個好處,讀到一行后,可以把不需要的丟棄,只保留中間部分的圖像像素。

那麼問題來了?為什麼不直接配置攝像頭的屬性,來實現只讀取圖像的中間部分呢,全部讀取出來然後在arm的內存中裁剪丟棄不要的像素,第一浪費了讀取時間,第二浪費了讀取的空間。更優的做法是直接配置攝像頭sensor,使用sensor的裁剪功能輸出需要的像素區域。

 

3、圖像裁剪–使用STM32 crop功能裁剪

STM32F4系列的DCMI接口支持裁剪功能,對攝像頭輸出的像素點進行截取,不需要的像素部分不被DCMI傳入內存,從硬件接口一側就丟棄了。

HAL_DCMI_EnableCrop(&hdcmi);
HAL_DCMI_ConfigCrop(&hdcmi, CAM_ROW_OFFSET, CAM_COL_OFFSET, IMG_ROW-1, IMG_COL-1);

裁剪的本質如下所述,從接收到的數據里選擇需要的矩形區域。所以STM32 DCMI裁剪功能可以完成節約內存,只選取需要的圖像存入內存的作用。

此方法相比於一次讀一行,然後丟棄首尾部分后把需要的區域圖像像素存入buffer后再讀下一行,避免了時序錯誤,代碼簡潔了,DCMI硬件計數丟掉不要的像素,也提高了程序可靠性、可讀性。

成也蕭何敗也蕭何,如上面所述,STM32的crop完成了選取特定區域圖像的功能,那麼也要付出代價,它是從接收到的圖像數據里進行選擇的,這意味着那些不需要的數據依然會傳輸到MCU一側,只不過MCU的DCMI對數據進行計數是忽略了它而已,那麼問題就來了,哪些不需要的數據的傳輸會帶來什麼問題呢?

有圖為證,下圖是使用了STM32 crop裁剪的時序圖,通道1啟動採集IO置高,frame中斷里拉低,由於使用dma傳輸,那麼被crop裁剪后dma計數的數據量變少,所以DCMI frame中斷能在行數據傳輸完成前到達,通道1高電平部分就代表一有效分辨率的幀的採集時間。通道2 曝光信號管腳,通道3是行掃描信號。其中通道1下降沿到通道3下降沿4.5ms。代表單片機已經收到crop指定尺寸的圖像,採集有效區域(crop區域)的圖像完成,但是line信號沒有結束還有很多行沒傳輸,即CMOS和DCMI接口要傳輸752*480圖像還沒完成。

 舉例說明,如果使用752*480分辨率採集圖像,你只取中間的360*360視野,有效分辨率是360*360,但是總線上的數據依然是752*480,所以幀率無法提高,多餘的數據按說就不應該傳輸出來,如何破解,問題追到這裏,STM32芯片已經無能為力了,接下來需要在CMOS一側發力了。

 

4、圖像裁剪–配置CMOS寄存器裁剪

下圖是MT9V034 攝像頭芯片的寄存器手冊,Reg1–4配置CMOS的行列起點和寬度高度。

修改寄存器后,攝像頭CMOS就不再向外傳輸多餘的數據,被裁剪丟棄的數據也不會反應在接口上,所以STM32 DCMI接收到的數據都是需要保留的有效區數據,極大地減少了數據輸出,提高了傳輸效率。本人也在STM324芯片上,實現了220*220分辨率120幀的連續採集。

下面是序圖,通道1高電平代表開始採集和一幀結束,不同於使用STM32 的crop裁剪,使用CMOS寄存器裁剪有效窗口,使得幀結束時行信號也同時結束,後續沒有任何需要傳輸的行數據。

 

5、一幀數據一次性傳輸

一幀數據一次全部讀入到MCU的方式,其實是最簡單的驅動編寫方式,缺點就是太占內存,但是對於沒有壓縮功能的cmos芯片來說,一般都無力實現。對部分有jpg壓縮功能的cmos芯片而言,比如OV2640可以使用這種方式,一次性讀出一幀圖像。

__align(4) u32 jpeg_buf[jpeg_buf_size];    //JPEG buffer
//JPEG 格式
const u16 jpeg_img_size_tbl[][2]=
{
    176,144,    //QCIF
    160,120,    //QQVGA
    352,288,    //CIF
    320,240,    //QVGA
    640,480,    //VGA
    800,600,    //SVGA
    1024,768,    //XGA
    1280,1024,    //SXGA
    1600,1200,    //UXGA
}; 

//DCMI 接收數據
void DCMI_IRQHandler(void)
{
  if(DCMI_GetITStatus(DCMI_IT_FRAME)==SET)// 一幀數據
  {
    jpeg_data_process();  
    DCMI_ClearITPendingBit(DCMI_IT_FRAME); 
  }
}

 

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

這一次搞懂Spring代理創建及AOP鏈式調用過程

@

目錄

  • 前言
  • 正文
    • 基本概念
    • 代理對象的創建
    • 小結
    • AOP鏈式調用
    • AOP擴展知識
      • 一、自定義全局攔截器Interceptor
      • 二、循環依賴三級緩存存在的必要性
      • 三、如何在Bean創建之前提前創建代理對象
  • 總結

前言

AOP,也就是面向切面編程,它可以將公共的代碼抽離出來,動態的織入到目標類、目標方法中,大大提高我們編程的效率,也使程序變得更加優雅。如事務、操作日誌等都可以使用AOP實現。這種織入可以是在運行期動態生成代理對象實現,也可以在編譯期類加載時期靜態織入到代碼中。而Spring正是通過第一種方法實現,且在代理類的生成上也有兩種方式:JDK Proxy和CGLIB,默認當類實現了接口時使用前者,否則使用後者;另外Spring AOP只能實現對方法的增強。

正文

基本概念

AOP的術語很多,雖然不清楚術語我們也能很熟練地使用AOP,但是要理解分析源碼,術語就需要深刻體會其含義。

  • 增強(Advice):就是我們想要額外增加的功能
  • 目標對象(Target):就是我們想要增強的目標類,如果沒有AOP,我們需要在每個目標對象中實現日誌、事務管理等非業務邏輯
  • 連接點(JoinPoint):程序執行時的特定時機,如方法執行前、后以及拋出異常后等等。
  • 切點(Pointcut):連接點的導航,我們如何找到目標對象呢?切點的作用就在於此,在Spring中就是匹配表達式。
  • 引介(Introduction):引介是一種特殊的增強,它為類添加一些屬性和方法。這樣,即使一個業務類原本沒有實現某個接口,通過AOP的引介功能,我們可以動態地為該業務類添加接口的實現邏輯,讓業務類成為這個接口的實現類。
  • 織入(Weaving):即如何將增強添加到目標對象的連接點上,有動態(運行期生成代理)、靜態(編譯期、類加載時期)兩種方式。
  • 代理(Proxy):目標對象被織入增強后,就會產生一個代理對象,該對象可能是和原對象實現了同樣的一個接口(JDK),也可能是原對象的子類(CGLIB)。
  • 切面(Aspect、Advisor):切面由切點和增強組成,包含了這兩者的定義。

代理對象的創建

在熟悉了AOP術語后,下面就來看看Spring是如何創建代理對象的,是否還記得上一篇提到的AOP的入口呢?在AbstractAutowireCapableBeanFactory類的applyBeanPostProcessorsAfterInitialization方法中循環調用了BeanPostProcessorpostProcessAfterInitialization方法,其中一個就是我們創建代理對象的入口。這裡是Bean實例化完成去創建代理對象,理所當然應該這樣,但實際上在Bean實例化之前調用了一個resolveBeforeInstantiation方法,這裏實際上我們也是有機會可以提前創建代理對象的,這裏放到最後來分析,先來看主入口,進入到AbstractAutoProxyCreator類中:

	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		//創建當前bean的代理,如果這個bean有advice的話,重點看
		// Create proxy if we have advice.
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		//如果有切面,則生成該bean的代理
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			//把被代理對象bean實例封裝到SingletonTargetSource對象中
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

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

先從緩存中拿,沒有則調用wrapIfNecessary方法創建。在這個方法裏面主要看兩個地方:getAdvicesAndAdvisorsForBeancreateProxy。簡單一句話概括就是先掃描后創建,問題是掃描什麼呢?你可以先結合上面的概念思考下,換你會怎麼做。進入到子類AbstractAdvisorAutoProxyCreatorgetAdvicesAndAdvisorsForBean方法中:

	protected Object[] getAdvicesAndAdvisorsForBean(
			Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

		//找到合格的切面
		List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
		if (advisors.isEmpty()) {
			return DO_NOT_PROXY;
		}
		return advisors.toArray();
	}

	protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		//找到候選的切面,其實就是一個尋找有@Aspectj註解的過程,把工程中所有有這個註解的類封裝成Advisor返回
		List<Advisor> candidateAdvisors = findCandidateAdvisors();

		//判斷候選的切面是否作用在當前beanClass上面,就是一個匹配過程。現在就是一個匹配
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			//對有@Order@Priority進行排序
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

findEligibleAdvisors方法中可以看到有兩個步驟,第一先找到所有的切面,即掃描所有帶有@Aspect註解的類,並將其中的切點(表達式)增強封裝為切面,掃描完成后,自然是要判斷哪些切面能夠連接到當前Bean實例上。下面一步步來分析,首先是掃描過程,進入到AnnotationAwareAspectJAutoProxyCreator類中:

	protected List<Advisor> findCandidateAdvisors() {
		// 先通過父類AbstractAdvisorAutoProxyCreator掃描,這裏不重要
		List<Advisor> advisors = super.findCandidateAdvisors();
		// 主要看這裏
		if (this.aspectJAdvisorsBuilder != null) {
			advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
		}
		return advisors;
	}

這裏委託給了BeanFactoryAspectJAdvisorsBuilderAdapter類,並調用其父類的buildAspectJAdvisors方法創建切面對象:

	public List<Advisor> buildAspectJAdvisors() {
		List<String> aspectNames = this.aspectBeanNames;

		if (aspectNames == null) {
			synchronized (this) {
				aspectNames = this.aspectBeanNames;
				if (aspectNames == null) {
					List<Advisor> advisors = new ArrayList<>();
					aspectNames = new ArrayList<>();
					//獲取spring容器中的所有bean的名稱BeanName
					String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
							this.beanFactory, Object.class, true, false);
					for (String beanName : beanNames) {
						if (!isEligibleBean(beanName)) {
							continue;
						}
						Class<?> beanType = this.beanFactory.getType(beanName);
						if (beanType == null) {
							continue;
						}
						//判斷類上是否有@Aspect註解
						if (this.advisorFactory.isAspect(beanType)) {
							aspectNames.add(beanName);
							AspectMetadata amd = new AspectMetadata(beanType, beanName);
							if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
								// 當@Aspect的value屬性為""時才會進入到這裏
								// 創建獲取有@Aspect註解類的實例工廠,負責獲取有@Aspect註解類的實例
								MetadataAwareAspectInstanceFactory factory =
										new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);

								//創建切面advisor對象
								List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
								if (this.beanFactory.isSingleton(beanName)) {
									this.advisorsCache.put(beanName, classAdvisors);
								}
								else {
									this.aspectFactoryCache.put(beanName, factory);
								}
								advisors.addAll(classAdvisors);
							}
							else {
								MetadataAwareAspectInstanceFactory factory =
										new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
								this.aspectFactoryCache.put(beanName, factory);
								advisors.addAll(this.advisorFactory.getAdvisors(factory));
							}
						}
					}
					this.aspectBeanNames = aspectNames;
					return advisors;
				}
			}
		}
		return advisors;
	}

這個方法裏面首先從IOC中拿到所有Bean的名稱,並循環判斷該類上是否帶有@Aspect註解,如果有則將BeanName和Bean的Class類型封裝到BeanFactoryAspectInstanceFactory中,並調用ReflectiveAspectJAdvisorFactory.getAdvisors創建切面對象:

	public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
		//從工廠中獲取有@Aspect註解的類Class
		Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		//從工廠中獲取有@Aspect註解的類的名稱
		String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
		validate(aspectClass);

		// 創建工廠的裝飾類,獲取實例只會獲取一次
		MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
				new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

		List<Advisor> advisors = new ArrayList<>();

		//這裏循環沒有@Pointcut註解的方法
		for (Method method : getAdvisorMethods(aspectClass)) {

			//非常重要重點看看
			Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
			Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
			advisors.add(0, instantiationAdvisor);
		}

		//判斷屬性上是否有引介註解,這裏可以不看
		for (Field field : aspectClass.getDeclaredFields()) {
			//判斷屬性上是否有DeclareParents註解,如果有返回切面
			Advisor advisor = getDeclareParentsAdvisor(field);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		return advisors;
	}

	private List<Method> getAdvisorMethods(Class<?> aspectClass) {
		final List<Method> methods = new ArrayList<>();
		ReflectionUtils.doWithMethods(aspectClass, method -> {
			// Exclude pointcuts
			if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
				methods.add(method);
			}
		});
		methods.sort(METHOD_COMPARATOR);
		return methods;
	}

根據Aspect的Class拿到所有不帶@Pointcut註解的方法對象(為什麼是不帶@Pointcut註解的方法?仔細想想不難理解),另外要注意這裏對method進行了排序,看看這個METHOD_COMPARATOR比較器:

	private static final Comparator<Method> METHOD_COMPARATOR;

	static {
		Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
				new InstanceComparator<>(
						Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
				(Converter<Method, Annotation>) method -> {
					AspectJAnnotation<?> annotation =
						AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
					return (annotation != null ? annotation.getAnnotation() : null);
				});
		Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
		METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
	}

關注InstanceComparator構造函數參數,記住它們的順序,這就是AOP鏈式調用中同一個@Aspect類中Advice的執行順序。接着往下看,在getAdvisors方法中循環獲取到的methods,分別調用getAdvisor方法,也就是根據方法逐個去創建切面:

	public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
			int declarationOrderInAspect, String aspectName) {

		validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());

		//獲取pointCut對象,最重要的是從註解中獲取表達式
		AspectJExpressionPointcut expressionPointcut = getPointcut(
				candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
		if (expressionPointcut == null) {
			return null;
		}

		//創建Advisor切面類,這才是真正的切面類,一個切面類裏面肯定要有1、pointCut 2、advice
		//這裏pointCut是expressionPointcut, advice 增強方法是 candidateAdviceMethod
		return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
				this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
	}

	private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
			Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
			
	private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
		//從候選的增強方法裏面 candidateAdviceMethod  找有有註解
		//Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class
		//並把註解信息封裝成AspectJAnnotation對象
		AspectJAnnotation<?> aspectJAnnotation =
				AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
		if (aspectJAnnotation == null) {
			return null;
		}

		//創建一個PointCut類,並且把前面從註解裏面解析的表達式設置進去
		AspectJExpressionPointcut ajexp =
				new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
		ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
		if (this.beanFactory != null) {
			ajexp.setBeanFactory(this.beanFactory);
		}
		return ajexp;
	}

之前就說過切面的定義,是切點和增強的組合,所以這裏首先通過getPointcut獲取到註解對象,然後new了一個Pointcut對象,並將表達式設置進去。然後在getAdvisor方法中最後new了一個InstantiationModelAwarePointcutAdvisorImpl對象:

	public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
			Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
			MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

		this.declaredPointcut = declaredPointcut;
		this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
		this.methodName = aspectJAdviceMethod.getName();
		this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
		this.aspectJAdviceMethod = aspectJAdviceMethod;
		this.aspectJAdvisorFactory = aspectJAdvisorFactory;
		this.aspectInstanceFactory = aspectInstanceFactory;
		this.declarationOrder = declarationOrder;
		this.aspectName = aspectName;

		if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
			// Static part of the pointcut is a lazy type.
			Pointcut preInstantiationPointcut = Pointcuts.union(
					aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

			// Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
			// If it's not a dynamic pointcut, it may be optimized out
			// by the Spring AOP infrastructure after the first evaluation.
			this.pointcut = new PerTargetInstantiationModelPointcut(
					this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
			this.lazy = true;
		}
		else {
			// A singleton aspect.
			this.pointcut = this.declaredPointcut;
			this.lazy = false;
			//這個方法重點看看,創建advice對象
			this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
		}
	}

這個就是我們的切面類,在其構造方法的最後通過instantiateAdvice創建了Advice對象。注意這裏傳進來的declarationOrder參數,它就是循環method時的序號,其作用就是賦值給這裏的declarationOrder屬性以及Advice的declarationOrder屬性,在後面排序時就會通過這個序號來比較,因此Advice的執行順序是固定的,至於為什麼要固定,後面分析完AOP鏈式調用過程自然就明白了。

	public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
			MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

		//獲取有@Aspect註解的類
		Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		validate(candidateAspectClass);

		//找到candidateAdviceMethod方法上面的註解,並且包裝成AspectJAnnotation對象,這個對象中就有註解類型
		AspectJAnnotation<?> aspectJAnnotation =
				AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
		if (aspectJAnnotation == null) {
			return null;
		}
		
		AbstractAspectJAdvice springAdvice;

		//根據不同的註解類型創建不同的advice類實例
		switch (aspectJAnnotation.getAnnotationType()) {
			case AtPointcut:
				if (logger.isDebugEnabled()) {
					logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
				}
				return null;
			case AtAround:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAroundAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtBefore:
				//實現了MethodBeforeAdvice接口,沒有實現MethodInterceptor接口
				springAdvice = new AspectJMethodBeforeAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtAfter:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAfterAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtAfterReturning:
				//實現了AfterReturningAdvice接口,沒有實現MethodInterceptor接口
				springAdvice = new AspectJAfterReturningAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
				if (StringUtils.hasText(afterReturningAnnotation.returning())) {
					springAdvice.setReturningName(afterReturningAnnotation.returning());
				}
				break;
			case AtAfterThrowing:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAfterThrowingAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
				if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
					springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
				}
				break;
			default:
				throw new UnsupportedOperationException(
						"Unsupported advice type on method: " + candidateAdviceMethod);
		}

		// Now to configure the advice...
		springAdvice.setAspectName(aspectName);
		springAdvice.setDeclarationOrder(declarationOrder);
		String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
		if (argNames != null) {
			springAdvice.setArgumentNamesFromStringArray(argNames);
		}

		//計算argNames和類型的對應關係
		springAdvice.calculateArgumentBindings();

		return springAdvice;
	}

這裏邏輯很清晰,就是拿到方法上的註解類型,根據類型創建不同的增強Advice對象:AspectJAroundAdvice、AspectJMethodBeforeAdvice、AspectJAfterAdvice、AspectJAfterReturningAdvice、AspectJAfterThrowingAdvice。完成之後通過calculateArgumentBindings方法進行參數綁定,感興趣的可自行研究。這裏主要看看幾個Advice的繼承體系:

可以看到有兩個Advice是沒有實現MethodInterceptor接口的:AspectJMethodBeforeAdvice和AspectJAfterReturningAdvice。而MethodInterceptor有一個invoke方法,這個方法就是鏈式調用的核心方法,但那兩個沒有實現該方法的Advice怎麼處理呢?稍後會分析。
到這裏切面對象就創建完成了,接下來就是判斷當前創建的Bean實例是否和這些切面匹配以及對切面排序。匹配過程比較複雜,對理解主流程也沒什麼幫助,所以這裏就不展開分析,感興趣的自行分析(AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply())。下面看看排序的過程,回到AbstractAdvisorAutoProxyCreator.findEligibleAdvisors方法:

	protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		//找到候選的切面,其實就是一個尋找有@Aspectj註解的過程,把工程中所有有這個註解的類封裝成Advisor返回
		List<Advisor> candidateAdvisors = findCandidateAdvisors();

		//判斷候選的切面是否作用在當前beanClass上面,就是一個匹配過程。。現在就是一個匹配
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			//對有@Order@Priority進行排序
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

sortAdvisors方法就是排序,但這個方法有兩個實現:當前類AbstractAdvisorAutoProxyCreator和子類AspectJAwareAdvisorAutoProxyCreator,應該走哪個呢?

通過類圖我們可以肯定是進入的AspectJAwareAdvisorAutoProxyCreator類,因為AnnotationAwareAspectJAutoProxyCreator的父類是它。

	protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
		List<PartiallyComparableAdvisorHolder> partiallyComparableAdvisors = new ArrayList<>(advisors.size());
		for (Advisor element : advisors) {
			partiallyComparableAdvisors.add(
					new PartiallyComparableAdvisorHolder(element, DEFAULT_PRECEDENCE_COMPARATOR));
		}
		List<PartiallyComparableAdvisorHolder> sorted = PartialOrder.sort(partiallyComparableAdvisors);
		if (sorted != null) {
			List<Advisor> result = new ArrayList<>(advisors.size());
			for (PartiallyComparableAdvisorHolder pcAdvisor : sorted) {
				result.add(pcAdvisor.getAdvisor());
			}
			return result;
		}
		else {
			return super.sortAdvisors(advisors);
		}
	}

這裏排序主要是委託給PartialOrder進行的,而在此之前將所有的切面都封裝成了PartiallyComparableAdvisorHolder對象,注意傳入的DEFAULT_PRECEDENCE_COMPARATOR參數,這個就是比較器對象:

	private static final Comparator<Advisor> DEFAULT_PRECEDENCE_COMPARATOR = new AspectJPrecedenceComparator();

所以我們直接看這個比較器的compare方法:

	public int compare(Advisor o1, Advisor o2) {
		int advisorPrecedence = this.advisorComparator.compare(o1, o2);
		if (advisorPrecedence == SAME_PRECEDENCE && declaredInSameAspect(o1, o2)) {
			advisorPrecedence = comparePrecedenceWithinAspect(o1, o2);
		}
		return advisorPrecedence;
	}

	private final Comparator<? super Advisor> advisorComparator;
	public AspectJPrecedenceComparator() {
		this.advisorComparator = AnnotationAwareOrderComparator.INSTANCE;
	}

第一步先通過AnnotationAwareOrderComparator去比較,點進去看可以發現是對實現了PriorityOrderedOrdered接口以及標記了PriorityOrder註解的非同一個@Aspect類中的切面進行排序。這個和之前分析BeanFacotryPostProcessor類是一樣的原理。而對同一個@Aspect類中的切面排序主要是comparePrecedenceWithinAspect方法:

	private int comparePrecedenceWithinAspect(Advisor advisor1, Advisor advisor2) {
		boolean oneOrOtherIsAfterAdvice =
				(AspectJAopUtils.isAfterAdvice(advisor1) || AspectJAopUtils.isAfterAdvice(advisor2));
		int adviceDeclarationOrderDelta = getAspectDeclarationOrder(advisor1) - getAspectDeclarationOrder(advisor2);

		if (oneOrOtherIsAfterAdvice) {
			// the advice declared last has higher precedence
			if (adviceDeclarationOrderDelta < 0) {
				// advice1 was declared before advice2
				// so advice1 has lower precedence
				return LOWER_PRECEDENCE;
			}
			else if (adviceDeclarationOrderDelta == 0) {
				return SAME_PRECEDENCE;
			}
			else {
				return HIGHER_PRECEDENCE;
			}
		}
		else {
			// the advice declared first has higher precedence
			if (adviceDeclarationOrderDelta < 0) {
				// advice1 was declared before advice2
				// so advice1 has higher precedence
				return HIGHER_PRECEDENCE;
			}
			else if (adviceDeclarationOrderDelta == 0) {
				return SAME_PRECEDENCE;
			}
			else {
				return LOWER_PRECEDENCE;
			}
		}
	}

	private int getAspectDeclarationOrder(Advisor anAdvisor) {
		AspectJPrecedenceInformation precedenceInfo =
			AspectJAopUtils.getAspectJPrecedenceInformationFor(anAdvisor);
		if (precedenceInfo != null) {
			return precedenceInfo.getDeclarationOrder();
		}
		else {
			return 0;
		}
	}

這裏就是通過precedenceInfo.getDeclarationOrder拿到在創建InstantiationModelAwarePointcutAdvisorImpl對象時設置的declarationOrder屬性,這就驗證了之前的說法(實際上這裏排序過程非常複雜,不是簡單的按照這個屬性進行排序)。
當上面的一切都進行完成后,就該創建代理對象了,回到AbstractAutoProxyCreator.wrapIfNecessary,看關鍵部分代碼:

	//如果有切面,則生成該bean的代理
	if (specificInterceptors != DO_NOT_PROXY) {
		this.advisedBeans.put(cacheKey, Boolean.TRUE);
		//把被代理對象bean實例封裝到SingletonTargetSource對象中
		Object proxy = createProxy(
				bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
		this.proxyTypes.put(cacheKey, proxy.getClass());
		return proxy;
	}

注意這裏將被代理對象封裝成了一個SingletonTargetSource對象,它是TargetSource的實現類。

	protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {

		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}

		//創建代理工廠
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

		if (!proxyFactory.isProxyTargetClass()) {
			if (shouldProxyTargetClass(beanClass, beanName)) {
				//proxyTargetClass 是否對類進行代理,而不是對接口進行代理,設置為true時,使用CGLib代理。
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

		//把advice類型的增強包裝成advisor切面
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

		////用來控制代理工廠被配置后,是否還允許修改代理的配置,默認為false
		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		//獲取代理實例
		return proxyFactory.getProxy(getProxyClassLoader());
	}

這裏通過ProxyFactory對象去創建代理實例,這是工廠模式的體現,但在創建代理對象之前還有幾個準備動作:需要判斷是JDK代理還是CGLIB代理以及通過buildAdvisors方法將擴展的Advice封裝成Advisor切面。準備完成則通過getProxy創建代理對象:

	public Object getProxy(@Nullable ClassLoader classLoader) {
		//根據目標對象是否有接口來判斷採用什麼代理方式,cglib代理還是jdk動態代理
		return createAopProxy().getProxy(classLoader);
	}

	protected final synchronized AopProxy createAopProxy() {
		if (!this.active) {
			activate();
		}
		return getAopProxyFactory().createAopProxy(this);
	}

	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

首先通過配置拿到對應的代理類:ObjenesisCglibAopProxy和JdkDynamicAopProxy,然後再通過getProxy創建Bean的代理,這裏以JdkDynamicAopProxy為例:

	public Object getProxy(@Nullable ClassLoader classLoader) {
		//advised是代理工廠對象
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

這裏的代碼你應該不陌生了,就是JDK的原生API,newProxyInstance方法傳入的InvocationHandler對象是this,因此,最終AOP代理的調用就是從該類中的invoke方法開始。至此,代理對象的創建就完成了,下面來看下整個過程的時序圖:

小結

代理對象的創建過程整體來說並不複雜,首先找到所有帶有@Aspect註解的類,並獲取其中沒有@Pointcut註解的方法,循環創建切面,而創建切面需要切點增強兩個元素,其中切點可簡單理解為我們寫的表達式,增強則是根據@Before、@Around、@After等註解創建的對應的Advice類。切面創建好后則需要循環判斷哪些切面能對當前的Bean實例的方法進行增強並排序,最後通過ProxyFactory創建代理對象。

AOP鏈式調用

熟悉JDK動態代理的都知道通過代理對象調用方法時,會進入到InvocationHandler對象的invoke方法,所以我們直接從JdkDynamicAopProxy的這個方法開始:

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		MethodInvocation invocation;
		Object oldProxy = null;
		boolean setProxyContext = false;

		//從代理工廠中拿到TargetSource對象,該對象包裝了被代理實例bean
		TargetSource targetSource = this.advised.targetSource;
		Object target = null;

		try {
			//被代理對象的equals方法和hashCode方法是不能被代理的,不會走切面
			.......
			
			Object retVal;

			// 可以從當前線程中拿到代理對象
			if (this.advised.exposeProxy) {
				// Make invocation available if necessary.
				oldProxy = AopContext.setCurrentProxy(proxy);
				setProxyContext = true;
			}

			//這個target就是被代理實例
			target = targetSource.getTarget();
			Class<?> targetClass = (target != null ? target.getClass() : null);
			
			//從代理工廠中拿過濾器鏈 Object是一個MethodInterceptor類型的對象,其實就是一個advice對象
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

			//如果該方法沒有執行鏈,則說明這個方法不需要被攔截,則直接反射調用
			if (chain.isEmpty()) {
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				retVal = invocation.proceed();
			}

			// Massage return value if necessary.
			Class<?> returnType = method.getReturnType();
			if (retVal != null && retVal == target &&
					returnType != Object.class && returnType.isInstance(proxy) &&
					!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
				retVal = proxy;
			}
			return retVal;
		}
		finally {
			if (target != null && !targetSource.isStatic()) {
				// Must have come from TargetSource.
				targetSource.releaseTarget(target);
			}
			if (setProxyContext) {
				// Restore old proxy.
				AopContext.setCurrentProxy(oldProxy);
			}
		}
	}

這段代碼比較長,我刪掉了不關鍵的地方。首先來看this.advised.exposeProxy這個屬性,這在@EnableAspectJAutoProxy註解中可以配置,當為true時,會將該代理對象設置到當前線程的ThreadLocal對象中,這樣就可以通過AopContext.currentProxy拿到代理對象。這個有什麼用呢?我相信有經驗的Java開發都遇到過這樣一個BUG,在Service實現類中調用本類中的另一個方法時,事務不會生效,這是因為直接通過this調用就不會調用到代理對象的方法,而是原對象的,所以事務切面就沒有生效。因此這種情況下就可以從當前線程的ThreadLocal對象拿到代理對象,不過實際上直接使用@Autowired注入自己本身也可以拿到代理對象。
接下來就是通過getInterceptorsAndDynamicInterceptionAdvice拿到執行鏈,看看具體做了哪些事情:

	public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
			Advised config, Method method, @Nullable Class<?> targetClass) {

		AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
		//從代理工廠中獲得該被代理類的所有切面advisor,config就是代理工廠對象
		Advisor[] advisors = config.getAdvisors();
		List<Object> interceptorList = new ArrayList<>(advisors.length);
		Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
		Boolean hasIntroductions = null;

		for (Advisor advisor : advisors) {
			//大部分走這裏
			if (advisor instanceof PointcutAdvisor) {
				// Add it conditionally.
				PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
				//如果切面的pointCut和被代理對象是匹配的,說明是切面要攔截的對象
				if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
					MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
					boolean match;
					if (mm instanceof IntroductionAwareMethodMatcher) {
						if (hasIntroductions == null) {
							hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
						}
						match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
					}
					else {
						//接下來判斷方法是否是切面pointcut需要攔截的方法
						match = mm.matches(method, actualClass);
					}
					//如果類和方法都匹配
					if (match) {

						//獲取到切面advisor中的advice,並且包裝成MethodInterceptor類型的對象
						MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
						if (mm.isRuntime()) {
							for (MethodInterceptor interceptor : interceptors) {
								interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
							}
						}
						else {
							interceptorList.addAll(Arrays.asList(interceptors));
						}
					}
				}
			}
			//如果是引介切面
			else if (advisor instanceof IntroductionAdvisor) {
				IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
				if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
					Interceptor[] interceptors = registry.getInterceptors(advisor);
					interceptorList.addAll(Arrays.asList(interceptors));
				}
			}
			else {
				Interceptor[] interceptors = registry.getInterceptors(advisor);
				interceptorList.addAll(Arrays.asList(interceptors));
			}
		}

		return interceptorList;
	}

這也是個長方法,看關鍵的部分,因為之前我們創建的基本上都是InstantiationModelAwarePointcutAdvisorImpl對象,該類是PointcutAdvisor的實現類,所以會進入第一個if判斷里,這裏首先進行匹配,看切點當前對象以及該對象的哪些方法匹配,如果能匹配上,則調用getInterceptors獲取執行鏈:

	private final List<AdvisorAdapter> adapters = new ArrayList<>(3);
	public DefaultAdvisorAdapterRegistry() {
		registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
		registerAdvisorAdapter(new AfterReturningAdviceAdapter());
		registerAdvisorAdapter(new ThrowsAdviceAdapter());
	}

	public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
		List<MethodInterceptor> interceptors = new ArrayList<>(3);
		Advice advice = advisor.getAdvice();
		//如果是MethodInterceptor類型的,如:AspectJAroundAdvice
		//AspectJAfterAdvice
		//AspectJAfterThrowingAdvice
		if (advice instanceof MethodInterceptor) {
			interceptors.add((MethodInterceptor) advice);
		}

		//處理 AspectJMethodBeforeAdvice  AspectJAfterReturningAdvice
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				interceptors.add(adapter.getInterceptor(advisor));
			}
		}
		if (interceptors.isEmpty()) {
			throw new UnknownAdviceTypeException(advisor.getAdvice());
		}
		return interceptors.toArray(new MethodInterceptor[0]);
	}

這裏我們可以看到如果是MethodInterceptor的實現類,則直接添加到鏈中,如果不是,則需要通過適配器去包裝后添加,剛好這裡有MethodBeforeAdviceAdapterAfterReturningAdviceAdapter兩個適配器對應上文兩個沒有實現MethodInterceptor接口的類。最後將Interceptors返回。

if (chain.isEmpty()) {
	Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
	retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
	// We need to create a method invocation...
	invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
	// Proceed to the joinpoint through the interceptor chain.
	retVal = invocation.proceed();
}

返回到invoke方法后,如果執行鏈為空,說明該方法不需要被增強,所以直接反射調用原對象的方法(注意傳入的是TargetSource封裝的被代理對象);反之,則通過ReflectiveMethodInvocation類進行鏈式調用,關鍵方法就是proceed

	private int currentInterceptorIndex = -1;
	
	public Object proceed() throws Throwable {
		//如果執行鏈中的advice全部執行完,則直接調用joinPoint方法,就是被代理方法
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}

		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			InterceptorAndDynamicMethodMatcher dm =
					(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
			Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
			if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);
			}
			else {
				return proceed();
			}
		}
		else {
			//調用MethodInterceptor中的invoke方法
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

這個方法的核心就在兩個地方:invokeJoinpointinterceptorOrInterceptionAdvice.invoke(this)。當增強方法調用完后就會通過前者調用到被代理的方法,否則則是依次調用Interceptorinvoke方法。下面就分別看看每個Interceptor是怎麼實現的。

  • AspectJAroundAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		if (!(mi instanceof ProxyMethodInvocation)) {
			throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
		}
		ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
		ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
		JoinPointMatch jpm = getJoinPointMatch(pmi);
		return invokeAdviceMethod(pjp, jpm, null, null);
	}
  • MethodBeforeAdviceInterceptor -> AspectJMethodBeforeAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
		return mi.proceed();
	}

	public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
		invokeAdviceMethod(getJoinPointMatch(), null, null);
	}
  • AspectJAfterAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		finally {
			invokeAdviceMethod(getJoinPointMatch(), null, null);
		}
	}
  • AfterReturningAdviceInterceptor -> AspectJAfterReturningAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		Object retVal = mi.proceed();
		this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
		return retVal;
	}

	public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable {
		if (shouldInvokeOnReturnValueOf(method, returnValue)) {
			invokeAdviceMethod(getJoinPointMatch(), returnValue, null);
		}
	}
  • AspectJAfterThrowingAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		catch (Throwable ex) {
			if (shouldInvokeOnThrowing(ex)) {
				invokeAdviceMethod(getJoinPointMatch(), null, ex);
			}
			throw ex;
		}
	}

這裏的調用順序是怎樣的呢?其核心就是通過proceed方法控制流程,每執行完一個Advice就會回到proceed方法中調用下一個Advice。可以思考一下,怎麼才能讓調用結果滿足如下圖的執行順序

以上就是AOP的鏈式調用過程,但是這隻是只有一個切面類的情況,如果有多個@Aspect類呢,這個調用過程又是怎樣的?其核心思想和“棧”一樣,就是“先進后出,後進先出”。

AOP擴展知識

一、自定義全局攔截器Interceptor

在上文創建代理對象的時候有這樣一個方法:

	protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) {
		//自定義MethodInterceptor.拿到setInterceptorNames方法注入的Interceptor對象
		Advisor[] commonInterceptors = resolveInterceptorNames();

		List<Object> allInterceptors = new ArrayList<>();
		if (specificInterceptors != null) {
			allInterceptors.addAll(Arrays.asList(specificInterceptors));
			if (commonInterceptors.length > 0) {
				if (this.applyCommonInterceptorsFirst) {
					allInterceptors.addAll(0, Arrays.asList(commonInterceptors));
				}
				else {
					allInterceptors.addAll(Arrays.asList(commonInterceptors));
				}
			}
		}

		Advisor[] advisors = new Advisor[allInterceptors.size()];
		for (int i = 0; i < allInterceptors.size(); i++) {
			//對自定義的advice要進行包裝,把advice包裝成advisor對象,切面對象
			advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i));
		}
		return advisors;
	}

這個方法的作用就在於我們可以擴展我們自己的Interceptor,首先通過resolveInterceptorNames方法獲取到通過setInterceptorNames方法設置的Interceptor,然後調用DefaultAdvisorAdapterRegistry.wrap方法將其包裝為DefaultPointcutAdvisor對象並返回:

	public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
		if (adviceObject instanceof Advisor) {
			return (Advisor) adviceObject;
		}
		if (!(adviceObject instanceof Advice)) {
			throw new UnknownAdviceTypeException(adviceObject);
		}
		Advice advice = (Advice) adviceObject;
		if (advice instanceof MethodInterceptor) {
			return new DefaultPointcutAdvisor(advice);
		}
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				return new DefaultPointcutAdvisor(advice);
			}
		}
		throw new UnknownAdviceTypeException(advice);
	}

	public DefaultPointcutAdvisor(Advice advice) {
		this(Pointcut.TRUE, advice);
	}

需要注意DefaultPointcutAdvisor構造器裏面傳入了一個Pointcut.TRUE,表示這種擴展的Interceptor是全局的攔截器。下面來看看如何使用:

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        System.out.println("自定義攔截器");
        return invocation.proceed();
    }
}

首先寫一個類實現MethodInterceptor 接口,在invoke方法中實現我們的攔截邏輯,然後通過下面的方式測試,只要UserService 有AOP攔截就會發現自定義的MyMethodInterceptor也生效了。

    public void costomInterceptorTest() {
        AnnotationAwareAspectJAutoProxyCreator bean = applicationContext.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        bean.setInterceptorNames("myMethodInterceptor ");

        UserService userService = applicationContext.getBean(UserService.class);
        userService.queryUser("dark");
    }

但是如果換個順序,像下面這樣:

    public void costomInterceptorTest() {

        UserService userService = applicationContext.getBean(UserService.class);

        AnnotationAwareAspectJAutoProxyCreator bean = applicationContext.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        bean.setInterceptorNames("myMethodInterceptor ");

        userService.queryUser("dark");
    }

這時自定義的全局攔截器就沒有作用了,這是為什麼呢?因為當執行getBean的時候,如果有切面匹配就會通過ProxyFactory去創建代理對象,注意Interceptor是存到這個Factory對象中的,而這個對象和代理對象是一一對應的,因此調用getBean時,還沒有myMethodInterceptor這個對象,自定義攔截器就沒有效果了,也就是說要想自定義攔截器生效,就必須在代理對象生成之前註冊進去。

二、循環依賴三級緩存存在的必要性

在上一篇文章我分析了Spring是如何通過三級緩存來解決循環依賴的問題的,但你是否考慮過第三級緩存為什麼要存在?我直接將bean存到二級不就行了么,為什麼還要存一個ObjectFactory對象到第三級緩存中?這個在學習了AOP之後就很清楚了,因為我們在@Autowired對象時,想要注入的不一定是Bean本身,而是想要注入一個修改過後的對象,如代理對象。在AbstractAutowireCapableBeanFactory.getEarlyBeanReference方法中循環調用了SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference方法,AbstractAutoProxyCreator對象就實現了該方法:

	public Object getEarlyBeanReference(Object bean, String beanName) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (!this.earlyProxyReferences.contains(cacheKey)) {
			this.earlyProxyReferences.add(cacheKey);
		}
		// 創建代理對象
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

因此,當我們想要對循壞依賴的Bean做出修改時,就可以像AOP這樣做。

三、如何在Bean創建之前提前創建代理對象

Spring的代理對象基本上都是在Bean實例化完成之後創建的,但在文章開始我就說過,Spring也提供了一個機會在創建Bean對象之前就創建代理對象,在AbstractAutowireCapableBeanFactory.resolveBeforeInstantiation方法中:

	protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
		Object bean = null;
		if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
			// Make sure bean class is actually resolved at this point.
			if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
				Class<?> targetType = determineTargetType(beanName, mbd);
				if (targetType != null) {
					bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
					if (bean != null) {
						bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
					}
				}
			}
			mbd.beforeInstantiationResolved = (bean != null);
		}
		return bean;
	}

	protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
		for (BeanPostProcessor bp : getBeanPostProcessors()) {
			if (bp instanceof InstantiationAwareBeanPostProcessor) {
				InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
				Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
				if (result != null) {
					return result;
				}
			}
		}
		return null;
	}

主要是InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation方法中,這裏又會進入到AbstractAutoProxyCreator類中:

	public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
		TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
		if (targetSource != null) {
			if (StringUtils.hasLength(beanName)) {
				this.targetSourcedBeans.add(beanName);
			}
			Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
			Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		return null;
	}

	protected TargetSource getCustomTargetSource(Class<?> beanClass, String beanName) {
		// We can't create fancy target sources for directly registered singletons.
		if (this.customTargetSourceCreators != null &&
				this.beanFactory != null && this.beanFactory.containsBean(beanName)) {
			for (TargetSourceCreator tsc : this.customTargetSourceCreators) {
				TargetSource ts = tsc.getTargetSource(beanClass, beanName);
				if (ts != null) {
					return ts;
				}
			}
		}

		// No custom TargetSource found.
		return null;
	}

看到這裏大致應該明白了,先是獲取到一個自定義的TargetSource對象,然後創建代理對象,所以我們首先需要自己實現一個TargetSource類,這裏直接繼承一個抽象類,getTarget方法則返回原始對象:

public class MyTargetSource extends AbstractBeanFactoryBasedTargetSource {
    @Override
    public Object getTarget() throws Exception {
        return getBeanFactory().getBean(getTargetBeanName());
    }
}

但這還不夠,上面首先判斷了customTargetSourceCreators!=null,而這個屬性是個數組,可以通過下面這個方法設置進來:

	public void setCustomTargetSourceCreators(TargetSourceCreator... targetSourceCreators) {
		this.customTargetSourceCreators = targetSourceCreators;
	}

所以我們還要實現一個TargetSourceCreator類,同樣繼承一個抽象類實現,並只對userServiceImpl對象進行攔截:

public class MyTargetSourceCreator extends AbstractBeanFactoryBasedTargetSourceCreator {
    @Override
    protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource(Class<?> beanClass, String beanName) {

        if (getBeanFactory() instanceof ConfigurableListableBeanFactory) {
            if(beanName.equalsIgnoreCase("userServiceImpl")) {
                return new MyTargetSource();
            }
        }

        return null;
    }
}

createBeanFactoryBasedTargetSource方法是在AbstractBeanFactoryBasedTargetSourceCreator.getTargetSource中調用的,而getTargetSource就是在上面getCustomTargetSource中調用的。以上工作做完后,還需要將其設置到AnnotationAwareAspectJAutoProxyCreator對象中,因此需要我們注入這個對象:

@Configuration
public class TargetSourceCreatorBean {

    @Autowired
    private BeanFactory beanFactory;

   @Bean
    public AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator() {
        AnnotationAwareAspectJAutoProxyCreator creator = new AnnotationAwareAspectJAutoProxyCreator();
        MyTargetSourceCreator myTargetSourceCreator = new MyTargetSourceCreator();
        myTargetSourceCreator.setBeanFactory(beanFactory);
        creator.setCustomTargetSourceCreators(myTargetSourceCreator);
        return creator;
    }
}

這樣,當我們通過getBean獲取userServiceImpl的對象時,就會優先生成代理對象,然後在調用執行鏈的過程中再通過TargetSource.getTarget獲取到被代理對象。但是,為什麼我們在getTarget方法中調用getBean就能拿到被代理對象呢?
繼續探究,通過斷點我發現從getTarget進入時,在resolveBeforeInstantiation方法中返回的bean就是null了,而getBeanPostProcessors方法返回的Processors中也沒有了AnnotationAwareAspectJAutoProxyCreator對象,也就是沒有進入到AbstractAutoProxyCreator.postProcessBeforeInstantiation方法中,所以不會再次獲取到代理對象,那AnnotationAwareAspectJAutoProxyCreator對象是在什麼時候移除的呢?
帶着問題,我開始反推,發現在AbstractBeanFactoryBasedTargetSourceCreator類中有這樣一個方法buildInternalBeanFactory

	protected DefaultListableBeanFactory buildInternalBeanFactory(ConfigurableBeanFactory containingFactory) {
		DefaultListableBeanFactory internalBeanFactory = new DefaultListableBeanFactory(containingFactory);

		// Required so that all BeanPostProcessors, Scopes, etc become available.
		internalBeanFactory.copyConfigurationFrom(containingFactory);

		// Filter out BeanPostProcessors that are part of the AOP infrastructure,
		// since those are only meant to apply to beans defined in the original factory.
		internalBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor ->
				beanPostProcessor instanceof AopInfrastructureBean);

		return internalBeanFactory;
	}

在這裏移除掉了所有AopInfrastructureBean的子類,而AnnotationAwareAspectJAutoProxyCreator就是其子類,那這個方法是在哪裡調用的呢?繼續反推:

	protected DefaultListableBeanFactory getInternalBeanFactoryForBean(String beanName) {
		synchronized (this.internalBeanFactories) {
			DefaultListableBeanFactory internalBeanFactory = this.internalBeanFactories.get(beanName);
			if (internalBeanFactory == null) {
				internalBeanFactory = buildInternalBeanFactory(this.beanFactory);
				this.internalBeanFactories.put(beanName, internalBeanFactory);
			}
			return internalBeanFactory;
		}
	}

	public final TargetSource getTargetSource(Class<?> beanClass, String beanName) {
		AbstractBeanFactoryBasedTargetSource targetSource =
				createBeanFactoryBasedTargetSource(beanClass, beanName);
		
		// 創建完targetSource后就移除掉AopInfrastructureBean類型的BeanPostProcessor對象,如AnnotationAwareAspectJAutoProxyCreator
		DefaultListableBeanFactory internalBeanFactory = getInternalBeanFactoryForBean(beanName);

		......
		return targetSource;
	}

至此,關於TargetSource接口擴展的原理就搞明白了。

總結

本篇篇幅比較長,主要搞明白Spring代理對象是如何創建的以及AOP鏈式調用過程,而後面的擴展則是對AOP以及Bean創建過程中一些疑惑的補充,可根據實際情況學習掌握。

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

Java筆試面試總結—try、catch、finally語句中有return 的各類情況

前言

之前在刷筆試題和面試的時候經常會遇到或者被問到 try-catch-finally 語法塊的執行順序等問題,今天就抽空整理了一下這個知識點,然後記錄下來。

正文

本篇文章主要是通過舉例的方式來闡述各種情況,我這裏根據 try-catch-finally 語法塊分為兩種大情況討論:try-catch 語法塊和 try-catch-finally 語句塊,然後再在每種情況里再去具體討論。

一、try-catch 語句塊

我們可以看看下面程序:

public static void main(String[] args) {

    System.out.println(handleException0());
  }

  /**
   * try,catch都有return
   * @return
   */
  private static String handleException0() {
    try{
      System.out.println("try開始");
      String s = null;
      int length = s.charAt(0);
      System.out.println("try結束");
      return "try塊的返回值";
    }catch (Exception e){
      System.out.println("捕獲到了異常");
      return "catch的返回值";
    }
  }

執行結果

try開始
捕獲到了異常
catch的返回值

分析:程序首先執行 try 塊裏面的代碼,try 塊裏面發現有異常,try 塊後面的代碼不會執行(自然也不會return),然後進入匹配異常的那個 catch 塊,然後進入 catch 塊裏面將代碼執行完畢,當執行到 catch 裏面的return 語句的時候,程序中止,然後將此 return 的最終結果返回回去。

二、try-catch-finally 語句塊

這種語法塊我分為了 4 種情況討論,下面進行一一列舉。

1、第一種情況,try 塊裏面有 return 的情況,並且捕獲到異常

例1:

public static void main(String[] args) {
  String result = handleException1();
  System.out.println(result);
}
private static String handleException1() {
  try{
    System.out.println("try開始");
    String str = null;
    int length = str.length();
    System.out.println("try結束");
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例1執行的結果如下

try開始
捕獲到了異常
finally塊執行完畢了
最終的結果

例2:

public static void main(String[] args) {
  String result = handleException2();
  System.out.println(result);
}
private static String handleException2() {
  try{
    System.out.println("try開始");
    String str = null;
    int length = str.length();
    System.out.println("try結束");
    return "try塊的返回值";
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例2的執行結果如下

try開始
捕獲到了異常
finally塊執行完畢了
最終的結果

分析:首先 例1 和 例2 的結果是很顯然的,當遇到異常的時候,直接進入匹配到相對應的 catch 塊,然後繼續執行 finallly 語句塊,最後將 return 結果返回回去。

第二種情況:try塊裏面有return的情況,但是不會捕獲到異常

例3:

思考:下面代碼try語句塊中有return語句,那麼是否執行完try語句塊就直接return退出方法了呢?

public static void main(String[] args) {
  String result = handleException3();
  System.out.println(result);
}
private static String handleException3() {
  try{
  	System.out.println("");
    return "try塊的返回值";
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例3的執行結果如下

finally塊執行完畢了
try塊的返回值

分析:例3的結果其實我們可以通過打斷點的方式去看看程序的具體執行流程,通過打斷點我們可以發現,代碼先執行 try塊 里的代碼,當執行到 return 語句的時候,handleException3方法並沒有立刻結束,而是繼續執行finally塊里的代碼,finally塊里的代碼執行完后,緊接着回到 try 塊的 return 語句,再把最終結果返回回去, handleException 方法執行完畢。

第三種情況:try塊和finally裏面都有return的情況

例4:

public static void main(String[] args) {
    System.out.println(handleException4());
  }

  /**
   * 情況3:try和finally中均有return
   * @return
   */
  private static String handleException4() {
    try{
      System.out.println("");
      return "try塊的返回值";
    }catch (Exception e){
      System.out.println("捕獲到了異常");
    }finally {
      System.out.println("finally塊執行完畢了");
      return "finally的返回值";
    }
  //  return "最終的結果";//不能再有返回值
  }

例4的執行結果

finally塊執行完畢了
finally的返回值

分析:需要注意的是,當 try 塊和 finally 裏面都有 return 的時候,在 try/catch/finally 語法塊之外不允許再有return 關鍵字。我們還是通過在程序中打斷點的方式來看看代碼的具體執行流程。代碼首先執行 try 塊 里的代碼,當執行到 return 語句的時候,handleException4 方法並沒有立刻結束,而是繼續執行 finally 塊里的代碼,當發現 finally 塊里有 return 的時候,直接將 finally 里的返回值(也就是最終結果)返回回去, handleException4 方法執行完畢。

第四種情況:try塊,catch塊,finally塊都有return

例5:

public static void main(String[] args) {
    System.out.println(handleException5());
  }

  /**
   * 情況4:try,catch,finally都有return
   * @return
   */
  private static String handleException5() {
    try{
      System.out.println("try開始");
      int[] array = {1, 2, 3};
      int i = array[10];
      System.out.println("try結束");
      return "try塊的返回值";
    }catch (Exception e){
      e.printStackTrace();//這行代碼其實就是打印輸出異常的具體信息
      System.out.println("捕獲到了異常");
      return "catch的返回值";
    }finally {
      System.out.println("finally塊執行完畢了");
      return "finally的返回值";
    }
//    return "最終的結果";
  }

例5的執行結果

try開始
捕獲到了異常
finally塊執行完畢了
finally的返回值
java.lang.ArrayIndexOutOfBoundsException: 10
at com.example.javabasic.javabasic.ExceptionAndError.TryCatchFinally.handleException5(TryCatchFinally.java:25)
at com.example.javabasic.javabasic.ExceptionAndError.TryCatchFinally.main(TryCatchFinally.java:14)

分析:程序首先執行try塊裏面的代碼,try塊裏面發現有異常,try塊後面的代碼不會執行(自然也不會return),然後進入匹配異常的那個catch塊,然後進入catch塊裏面將代碼執行完畢,當執行到catch裏面的return語句的時候,程序不會馬上終止,而是繼續執行finally塊的代碼,最後執行finally裏面的return,然後將此return的最終結果返回回去。

總結

其實,我們通過以上例子我們可以發現,不管return關鍵字在哪,finally一定會執行完畢。理論上來說try、catch、finally塊中都允許書寫return關鍵字,但是執行優先級較低的塊中的return關鍵字定義的返回值將覆蓋執行優先級較高的塊中return關鍵字定義的返回值。也就是說finally塊中定義的返回值將會覆蓋catch塊、try塊中定義的返回值;catch塊中定義的返回值將會覆蓋try塊中定義的返回值。
再換句話說如果在finally塊中通過return關鍵字定義了返回值,那麼之前所有通過return關鍵字定義的返回值都將失效——因為finally塊中的代碼一定是會執行的。

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

神秘疾病侵襲 加勒比海珊瑚礁群拉警報

摘錄自2019年11月12日中央通訊社綜合報導

短短一年多來,墨西哥加勒比海地區的珊瑚因為遭到一種罕為人知「石珊瑚組織損失症」(SCTLD)侵襲,已經損失30%。這種疾病會造成珊瑚鈣化和死亡。

專家警告,這種疾病可能造成大部分中美洲珊瑚礁(Mesoamerican Reef)死亡。這處龐大的弧狀珊瑚礁群範圍廣達超過1000公里,為墨西哥、貝里斯、瓜地馬拉和宏都拉斯等國家共有。

SCTLD已使加勒比海地區陷入困境,這種疾病可能摧毀環礁地區民眾賴以為生的觀光產業。中美洲珊瑚礁是僅次於澳洲大堡礁的世界第2大珊瑚礁。科學家表示,旅遊業太發達非常有可能讓問題火上加油。

「健康珊瑚礁、健康人民」在墨西哥的協調人員蘇鐸(Melina Soto)說,SCTLD只需幾週時間,就可以殺死需要花費數十年生長起來的珊瑚組織。蘇鐸表示:「如果以這種速度繼續下去,這個生態系統將會在未來的5到10年內崩潰。」

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

稀有越南老鼠鹿 近30年首度現蹤

摘錄自2019年11月12日中央通訊社綜合報導

一種外觀似鹿、大小相當於兔子或貓的小型哺乳類動物銀背鼷鹿(越南老鼠鹿),在失去蹤跡近30年後,近日首度在越南被發現。專家多年來認為此物種已瀕臨絕種。

根據今天發布在「自然生態學與進化」(Nature Ecology and Evolution)期刊的研究報告,紀錄顯示,越南老鼠鹿(Silver-backed Chevrotain,或Mouse deer)最後一次現蹤是在1990年。由於自1990年起即未有發現越南老鼠鹿的紀錄,專家推測此物種已因人類過度獵捕而瀕臨絕種。

由於人口成長和經濟開發對東南亞的樹林構成巨大生存壓力,生物學家堤爾克指出,保育工作必須搶占先機。根據聯合國「政府間生物多樣性與生態系服務科學政策平台」(IPBES)5月發布的報告,人類活動對地球的衝擊,導致高達100萬個物種瀕臨絕種。

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

北京掀新能源車熱:指標申請首破三千 純電動車也需搖號

受4月2日北京宣佈純電動小客車不尾號限行()的利好政策推動,據4月9日北京小客車指標調控管理辦公室公佈的資料顯示,本期搖號個人新能源車指標的申請者首次突破三千,達3874人,而此前申請指標的人數徘徊在2000多人左右。根據今年的指標配置方案,本期分配給個人的新能車指標為3333個,這意味著從這一期開始,在北京購買純電動車也要搖號分配了。   在北京剛剛結束的新能源汽車工作會議上,北京相關新能源小客車採取備案制的方案已經通過,知情人士稱,未來進口純電動汽車也有望通過備案進入免購置稅目錄,這無疑將大大增加北京純電動汽車的供應類型。   而針對充電樁安裝困難問題,北京市科委新能源與新材料處處長許心超透露,現在北京正著手做三件事:一是和百度合作,標識所有充電樁地圖並上線;二是印製北京充電樁地圖,發放給消費者和計程車司機;三是辦充電體驗周活動,聯合所有在京車企,做充電實際體驗。   北京市環保局機動車排放管理處處長李昆生在4月7日透露,未來北京將嚴格控制機動車總量,每年新增小客車指標由24萬輛削減到15萬輛,力爭在2017年底前,全市機動車保有量控制在600萬輛以內,而新能源和清潔能源汽車的應用規模將達到20萬輛。

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

為取得專利,小米持股 Ninebot 收購電動代步車始祖 Segway

  小米投資的電動代步車廠商 Ninebot 今天宣布與小米、順為資本、紅杉資本聯合出資收購電動代步車始祖 Segway,交易目前已全數完成。身為 Ninebot 的股東,這項收購除了意味著小米持續拓展海外市場外,對小米來說最大的收益,便是可取得 Segway 手中大量的專利。   總部位於美國的 Segway 成立於 2001 年,產品主要為雙輪的電動代步車,而 Ninebot 則是中國電動代步車品牌。Segway 在全球每年約只有 1 萬台左右的銷量,對增加 Ninebot 銷售量並沒有太大的直接幫助,據知情人士透露,先前 Segway 曾與包括法拉利、香奈兒等國際知名品牌合作,Ninebot 買下 Segway 主要是衝著其在海外的知名度,更重要的是可取得 Segway 手中握有的專利。   有趣的是,Segway 創辦人 Dean Kamen 曾在 2014 年控告 Ninebot 及其他中國廠商侵犯智慧財產權,美國國際貿易委員會於 11 月受理調查,而今調查結果尚未出爐,Segway 卻先被 Ninebot 收購,成為旗下全資子公司。   此外,Ninebot 執行長高祿峰同時也對外宣布已完成 8,000 萬美元的 A 輪融資,由小米、紅杉資本、順為資本共同出資,並表示未來將開發應用電動代步車、移動互聯網及人車互動技術的產品。     本文全文授權轉載自《科技新報》─〈〉

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

【其他文章推薦】

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

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

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

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

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