聽說你的資源被盜用了,那你知道 Nginx 怎麼防盜鏈嗎?

上一篇文章講了 Nginx 中的變量和運行原理,下面就來說一個主要提供變量並修改變量的值的模塊,也就是我們要講的防盜鏈模塊:referer 模塊。

簡單有效的防盜鏈手段

場景

如果做過個人站點的同學,可能會遇到別人盜用自己站點資源鏈接的情況,這就是盜鏈。說到盜鏈就要說一個 HTTP 協議的 頭部,referer 頭部。當其他網站通過 URL 引用了你的頁面,用戶在瀏覽器上點擊 URL 時,HTTP 請求的頭部會通過 referer 頭部將該網站當前頁面的 URL 帶上,告訴服務器本次請求是由誰發起的。

例如,在谷歌中搜索 Nginx 然後點擊鏈接:

在打開的新頁面中查看請求頭會發現,請求頭中包含了 referer 頭部且值是 https://www.google.com/。

像谷歌這種我們是允許的,但是有一些其他的網站想要引用我們自己網站的資源時,就需要做一些管控了,不然豈不是誰都可以拿到鏈接。

目的

這裏目的其實已經很明確了,就是要拒絕非正常的網站訪問我們站點的資源。

思路

  • invalid_referer 變量
    • referer 提供了這個變量,可以用來配置哪些 referer 頭部合法,也就是,你允許哪些網站引用你的資源。

referer 模塊

要實現上面的目的,referer 模塊可得算頭一號,一起看下 referer 模塊怎麼用的。

  • 默認編譯進 Nginx,通過 --without-http_referer_module 禁用

referer 模塊有三個指令,下面看一下。

Syntax: valid_referers none | blocked | server_names | string ...;
Default: —
Context: server, location

Syntax: referer_hash_bucket_size size;
Default: referer_hash_bucket_size 64; 
Context: server, location

Syntax: referer_hash_max_size size;
Default: referer_hash_max_size 2048; 
Context: server, location
  • valid_referers 指令,配置是否允許 referer 頭部以及允許哪些 referer 訪問。
  • referer_hash_bucket_size 表示這些配置的值是放在哈希表中的,指定哈希表的大小。
  • referer_hash_max_size 則表示哈希表的最大大小是多大。

這裏面最重要的是 valid_referers 指令,需要重點來說明一下。

valid_referers 指令

可以同時攜帶多個參數,表示多個 referer 頭部都生效。

參數值

  • none
    • 允許缺失 referer 頭部的請求訪問
  • block:允許 referer 頭部沒有對應的值的請求訪問。例如可能經過了反向代理或者防火牆
  • server_names:若 referer 中站點域名與 server_name 中本機域名某個匹配,則允許該請求訪問
  • string:表示域名及 URL 的字符串,對域名可在前綴或者後綴中含有 * 通配符,若 referer 頭部的值匹配字符串后,則允許訪問
  • 正則表達式:若 referer 頭部的值匹配上了正則,就允許訪問

invalid_referer 變量

  • 允許訪問時變量值為空
  • 不允許訪問時變量值為 1

實戰

下面來看一個配置文件。

server {
	server_name referer.ziyang.com;
    listen 80;

	error_log logs/myerror.log debug;
	root html;
	location /{
		valid_referers none blocked server_names
               		*.ziyang.com www.ziyang.org.cn/nginx/
               		~\.google\.;
		if ($invalid_referer) {
    			return 403;
		}
		return 200 'valid\n';
	}
}

那麼對於這個配置文件而言,以下哪些請求會被拒絕呢?

curl -H 'referer: http://www.ziyang.org.cn/ttt' referer.ziyang.com/
curl -H 'referer: http://www.ziyang.com/ttt' referer.ziyang.com/
curl -H 'referer: ' referer.ziyang.com/
curl referer.ziyang.com/
curl -H 'referer: http://www.ziyang.com' referer.ziyang.com/
curl -H 'referer: http://referer.ziyang.com' referer.ziyang.com/
curl -H 'referer: http://image.baidu.com/search/detail' referer.ziyang.com/
curl -H 'referer: http://image.google.com/search/detail' referer.ziyang.com/

我們需要先來解析一下這個配置文件。valid_referers 指令配置了哪些值呢?

 valid_referers none blocked server_names
        *.ziyang.com www.ziyang.org.cn/nginx/
        ~\.google\.;
  • none:表示沒有 referer 的可以訪問
  • blocked:表示 referer 沒有值的可以訪問
  • server_names:表示本機 server_name 也就是 referer.ziyang.com 可以訪問
  • *.ziyang.com:匹配上了正則的可以訪問
  • www.ziyang.org.cn/nginx/:該頁面發起的請求可以訪問
  • ~\.google\.:google 前後都是正則匹配

下面就實際看下響應:

# 返回 403,沒有匹配到任何規則
  ~ curl -H 'referer: http://www.ziyang.org.cn/ttt' referer.ziyang.com/
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
  ~ curl -H 'referer: http://image.baidu.com/search/detail' referer.ziyang.com/
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
# 匹配到了 *.ziyang.com
  ~ curl -H 'referer: http://www.ziyang.com/ttt' referer.ziyang.com/
valid
  ~ curl -H 'referer: http://www.ziyang.com' referer.ziyang.com/
valid
# 匹配到了 server name
  ~ curl -H 'referer: http://referer.ziyang.com' referer.ziyang.com/
valid
# 匹配到了 blocked
  ~ curl -H 'referer: ' referer.ziyang.com/
valid
# 匹配到了 none
  ~ curl referer.ziyang.com/
valid
# 匹配到了 ~\.google\.
  ~ curl -H 'referer: http://image.google.com/search/detail' referer.ziyang.com/
valid

防盜鏈另外一種解決方案:secure_link 模塊

referer 模塊是一種簡單的防盜鏈手段,必須依賴瀏覽器發起請求才會有效,如果攻擊者偽造 referer 頭部的話,這種方式就失效了。

secure_link 模塊是另外一種解決的方案。

它的主要原理是,通過驗證 URL 中哈希值的方式防盜鏈。

基本過程是這個樣子的:

  • 由服務器(可以是 Nginx,也可以是其他 Web 服務器)生成加密的安全鏈接 URL,返回給客戶端
  • 客戶端使用安全 URL 訪問 Nginx,由 Nginx 的 secure_link 變量驗證是否通過

原理如下:

  • 哈希算法是不可逆的
  • 客戶端只能拿到執行過哈希算法的 URL
  • 僅生成 URL 的服務器,驗證 URL 是否安全的 Nginx,這兩者才保存原始的字符串
  • 原始字符串通常由以下部分有序組成:
    • 資源位置。如 HTTP 中指定資源的 URI,防止攻擊者拿到一個安全 URI 后可以訪問任意資源
    • 用戶信息。如用戶的 IP 地址,限制其他用戶盜用 URL
    • 時間戳。使安全 URL 及時過期
    • 密鑰。僅服務器端擁有,增加攻擊者猜測出原始字符串的難度

模塊:

  • ngx_http_secure_link_module
    • 未編譯進 Nginx,需要通過 –with-http_secure_link_module 添加
  • 變量
    • secure_link
    • secure_link_expires
Syntax: secure_link expression;
Default: —
Context: http, server, location

Syntax: secure_link_md5 expression;
Default: —
Context: http, server, location

Syntax: secure_link_secret word;
Default: —
Context: location

變量值及帶過期時間的配置示例

  • secure_link
    • 值為空字符串:驗證不通過
    • 值為 0:URL 過期
    • 值為 1:驗證通過
  • secure_link_expires
    • 時間戳的值

命令行生成安全鏈接

  • 生成 md5
echo -n '時間戳URL客戶端IP密鑰' | openssl md5 -binary | openssl base64 | tr +/ - | tr -d =
  • 構造請求 URL
/test1.txt?md5=md5生成值&expires=時間戳(如 2147483647)

Nginx 配置

  • secure_link $arg_md5,$arg_expires;
    • secure_link 後面必須跟兩個值,一個是參數中的 md5,一個是時間戳
  • secure_link_md5 “$secure_link_expires$uri$remote_addr secret”;
    • 按照什麼樣的順序構造原始字符串

實戰

下面是一個實際的配置文件,我這裏就不做演示了,感興趣的可以自己做下實驗。

server {
	server_name securelink.ziyang.com;
    listen 80;
	error_log  logs/myerror.log  info;
	default_type text/plain;
	location /{
		secure_link $arg_md5,$arg_expires;
        secure_link_md5 "$secure_link_expires$uri$remote_addr secret";

        if ($secure_link = "") {
            return 403;
        }

        if ($secure_link = "0") {
            return 410;
        }

		return 200 '$secure_link:$secure_link_expires\n';
	}

	location /p/ {
        secure_link_secret mysecret2;

        if ($secure_link = "") {
            return 403;
        }

        rewrite ^ /secure/$secure_link;
	}

	location /secure/ {
		alias html/;
    	internal;
	}
}

僅對 URI 進行哈希的簡單辦法

除了上面這種相對複雜的方式防盜鏈,還有一種相對簡單的防盜鏈方式,就是只對 URI 進行哈希,這樣當 URI 傳

  • 將請求 URL 分為三個部分:/prefix/hash/link
  • Hash 生成方式:對 “link 密鑰” 做 md5 哈希
  • 用 secure_link_secret secret; 配置密鑰

命令行生成安全鏈接

  • 原請求
    • link
  • 生成的安全請求
    • /prefix/md5/link
  • 生成 md5
    • echo -n 'linksecret' | openssl md5 –hex

Nginx 配置

  • secure_link_secret secret;

這個防盜鏈的方法比較簡單,那麼具體是怎麼用呢?大家都在網上下載過資源對吧,不管是电子書還是軟件,很多網站你點擊下載的時候往往會彈出另外一個頁面去下載,這個新的頁面其實就是請求的 Nginx 生成的安全 URL。如果這個 URL 被拿到的話,其實還是可以用的,所以需要經常的更新密鑰來確保 URL 不會被盜用。

今天這篇文章詳細講了防盜鏈的具體用法,最近的這兩篇文章都是說的已有的變量用法,下一篇文章講一下怎麼生成新的變量。

本文首發於我的個人博客:iziyang.github.io,所有配置文件我已經放在了 Nginx 配置文件,大家可以自取。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

04 . Docker安全與Docker底層實現

Docker安全

Docker安全性時,主要考慮三個方面

# 1. 由內核的名字空間和控制組機制提供的容器內在安全
# 2. Docker程序(特別是服務端)本身的抗攻擊性
# 3. 內核安全性的加強機制對容器安全性的影響
內核命名空間

Docker容器和LXC容器很相似,所提供的安全特性也差不多。當用docker run啟動一個容器時,在後台Docker為容器創建了一個獨立的命名空間和控制組集合。

命名空間提供了最基礎也是最直接的隔離,在容器中運行的進程不會被運行在主機上的進程和其它容器發 現和作用。

每個容器都有自己獨有的網絡棧,意味着它們不能訪問其他容器的sockets或接口。不過,如果主機系統上做了相應的設置,容器可以像跟主機交互一樣的和其他容器交互。當指定公共端口或使用links來連接2個容器時,容器就可以相互通信了(可以根據配置來限制通信的策略).

從網絡架構的角度來看,所有的容器通過本地主機的網橋接口相互通信,就像物理機器通過物理交換機通信一樣。

那麼,內核中實現命名空間和私有網絡的代碼是否足夠成熟?

內核名字空間從2.6.15版本(2008年7月發布)之後被引入,數年間,這些機制的可靠性在諸多大型生產系統中被實踐驗證。

實際上,名字空間的想法和設計提出的時間要更早,最初是為了在內核中引入一種機制來實現 OpenVZ 的特性。而OpenVZ項目早在2005年就發布了,其設計和實現都已經十分成熟。

控制組

控制組是Linux容器機制的另外一個關鍵組件,負責實現資源的審計和限制.

它提供了很多有用的特性;以及確保各個容器可以公平地分享主機的內存、CPU、磁盤IO等資源;當然,更重要的是,控制組確保了當容器內的資源使用產生壓力時不會連累主機系統。

儘管控制組不負責隔離容器之間相互訪問、處理數據和進程,它在防止拒絕服務(DDOS)攻擊方面是必不可少的。尤其是在多用戶的平台(比如公有或私有的PaaS)上,控制組十分重要。例如,當某些應用程序表現異常的時候,可以保證一致地正常運行和性能。

控制組始於2006年,內核從2.6.24版本開始被引入.

Docker服務端的防護

運行一個容器或應用程序的核心是通過Docker服務端。Docker服務的運行目前需要root權限,因此其安全性十分關鍵。

首先,確保只有可信的用戶才可以訪問Docker服務。Docker允許用戶在主機和容器間共享文件夾,同時不需要限制容器的訪問權限,這就容易讓容器突破資源限制。例如,惡意用戶啟動容器的時候將主機的根目錄/映射到容器的 /host目錄中,那麼容器理論上就可以對主機的文件系統進行任意修改了。這聽起來很瘋狂?但是事實上幾乎所有虛擬化系統都允許類似的資源共享,而沒法禁止用戶共享主機根文件系統到虛擬機系統

這將會造成很嚴重的安全後果。因此,當提供容器創建服務時(例如通過一個web服務器),要更加註意進行參數的安全檢查,防止惡意的用戶用特定參數來創建一些破壞性的容器

為了加強對服務端的保護,Docker的REST API(客戶端用來跟服務端通信)在0.5.2之後使用本地的Unix套接字機制替代了原先綁定在 127.0.0.1 上的TCP套接字,因為後者容易遭受跨站腳本攻擊。現在用戶使用Unix權限檢查來加強套接字的訪問安全。

用戶仍可以利用HTTP提供REST API訪問。建議使用安全機制,確保只有可信的網絡或VPN,或證書保 護機制(例如受保護的stunnel和ssl認證)下的訪問可以進行。此外,還可以使用HTTPS和證書來加強 保護。

最近改進的Linux名字空間機制將可以實現使用非root用戶來運行全功能的容器。這將從根本上解決了容器和主機之間共享文件系統而引起的安全問題。

終極目標是改進 2 個重要的安全特性:

  • 將容器的root用戶映射到本地主機上的非root用戶,減輕容器和主機之間因權限提升而引起的安全問題;
  • 允許Docker服務端在非root權限下運行,利用安全可靠的子進程來代理執行需要特權權限的操作。這些子進程將只允許在限定範圍內進行操作,例如僅僅負責虛擬網絡設定或文件系統管理、配置操作等。
  • 最後,建議採用專用的服務器來運行Docker 和相關的管理服務(例如管理服務比如ssh監控和進程監控、管理工具nrpe、collectd等)。其它的業務服務都放到容器中去運行。
內核能力機制

能力機制(Capability)是Linux內核一個強大的特性,可以提供細粒度的權限訪問控制。 Linux內核自2.2版本起就支持能力機制,它將權限劃分為更加細粒度的操作能力,既可以作用在進程上,也可以作用在文件上。

例如,一個Web服務進程只需要綁定一個低於1024的端口的權限,並不需要root權限。那麼它只需要被授權 net_bind_service能力即可。此外,還有很多其他的類似能力來避免進程獲取root權限。

默認情況下,Docker啟動的容器被嚴格限制只允許使用內核的一部分能力.

使用能力機制對加強Docker容器的安全有很多好處。通常,在服務器上會運行一堆需要特權權限的進程,包括有ssh、cron、syslogd、硬件管理工具模塊(例如負載模塊)、網絡配置工具等等。容器跟這些進程是不同的,因為幾乎所有的特權進程都由容器以外的支持系統來進行管理。

# 1. ssh訪問被主機上ssh服務來管理;
# 2. cron通常應該作為用戶進程執行執行,權限交給使用他服務的應用來處理;
# 3. 日誌系統可由Docker或第三方服務管理;
# 4. 硬件管理無關緊要,容器中也就無需執行udevd以及類似服務;
# 5. 網絡管理也都在主機上設置,除非特殊需求,容器不需要對網絡進行配置.

從上面的例子可以看出,大部分情況下,容器並不需要真正的root權限,容器只需要少數的能力即可.為了加強安全,容器可以禁用一些沒必要的權限:

# 1. 完全禁止任何mount操作.
# 2. 禁止直接訪問本地主機的套接字.
# 3. 禁止訪問一些文件系統的操作,比如創建新的設備,修改文件屬性等.
# 4. 禁止模塊加載.

這樣,就算攻擊者在容器中取得了root權限,也不能獲得本地主機的較高權限,能進行的破壞也有限.

默認認情況下,Docker採用 白名單 機制,禁用必需功能之外的其它權限。 當然,用戶也可以根據自身需求來為Docker容器啟用額外的權限。

其他安全特性.

除了能力機制之外,還可以利用一些現有的安全機制來增強docker的安全性,例如TOMOYO,AppArmor,SELinux,GRSEC等.

Docker 當前默認只開啟了能力機制,用戶可以採用多種方案來加強Docker主機的安全,例如:

  1. 在內核中啟用GRSEC和PAX,這將增加很多編譯和運行時的安全檢查,通過地址隨機化避免惡意探測等,並且,啟用該特性不需要Docker進行任何配置.

  2. 使用一些有增強安全特性的容器模板,比如帶AppArmor的模板和Redhat帶Selinux策略的模板.這些模板提供了額外的安全特性.

  3. 用戶可以自定義訪問控制機制來定製安全策略.

跟其他添加Docker容器的第三方工具一樣(比如網絡拓撲和文件系統共享),有很多類似的機制,在不改變Docker內核情況下就可以加固現有的容器.

小結

總體來說,Docker容器還是十分安全的,特別是在容器不使用root權限來運行進程的話.

另外,用戶可以使用現有工具,比如Apparmor,SELinux,GRSEC來增強安全性,甚至自己在內核中實現更複雜的安全機制.

Docker底層實現

Docker底層的核心技術包括Linux上的命名空間(Namespaces)、控制組(ControlGroups),Union文件系統(Union file systems)和容器格式(Container format).

我們知道,傳統的虛擬機通過在宿主主機中運行hypervisor來模擬一套完整的硬件環境提供給虛擬機的操作系統.虛擬機系統看到的環境是可限制的,也是彼此隔離的,這種直接的做法實現了對資源完整的封裝,但很多時候往往意味着系統資源的浪費,例如,以宿主機和虛擬機系統都為linux系統為例,虛擬機中運行的應用其實是可以利用宿主機系統中的運行環境。

我們知道,在操作系統中,包括內核、文件系統、網絡、PID、UID、IPC、內存、硬盤、CPU等等,所有的資源都是應用進程直接共享的,要想實現虛擬化,除了要實現對內存、CPU、網絡IO、硬盤IO、存儲空間等的限制外,還要實現文件系統、網絡、PID、UID、IPC等等的相互隔離,前者相對容易實現一些,後者則需要宿主機系統的深入支持.

隨着Linux系統對於命名空間功能的完善實現,程序員可以實現上面的所有需要,讓某些進程在彼此隔離的命名空間中運行,大家雖然都共用一個內核和某些運行時環境(l例如一些系統命令和系統等),但是彼此卻看不到,大家都以為系統中只有自己的存在,這種機制就是容器,利用命名空間來做權限的隔離控制,利用cgroups來做資源分配.

容器的基本架構

Dcoker採用了c/s架構,包括客戶端和服務端,Docker守護進程(Daemon)作為服務端接受來自客戶端的請求,並處理這些請求(創建、運行、分發容器).

客戶端和服務端既可以運行在一個機器上,也可以通過socket或者RESTful API來進行通信.

Docker守護進程一般在宿主主機後台運行,等待來自客戶端的消息.

Docker客戶端則為用戶提供一系列可執行的命令,用戶用這些命令實現跟Docker守護進程交互.

命名空間

命名空間是Linux內核一個強大的特性,每個容器都有自己單獨的命名空間,運行在其中的應用都像是在獨立的操作系統運行一樣,命名空間保證了容器之間彼此互不影響.

pid命名空間

不同用戶的進程就是通過pid命名空間隔離開的,且不同命名空間可以有相同的pid,所有的LXC進程在Docker中的父進程為Docker進程,每個LXC進程具有不同的命名空間,同時由於嵌套,因此可以很方便的實現嵌套的Docker容器.

net命名空間

有了pid命名空間,每個命名空間的pid能夠實現相互隔離,但是網絡端口還是共享host的端口,網絡隔離是通過net命名空間實現的,每個net命名空間有單獨的網絡設備,IP地址,路由表,/proc/net目錄,這樣每個容器的網絡就能隔離開來,Docker默認採用veth的方式,將容器中的虛擬網卡host上的一個Docker網橋docker0連接在一起.

IPC命名空間

容器中進程交互採用了Linux常見的進程交互方法,包括信號量,消息隊列和共享內存等,然而同VM不同的是,容器的進程交互實際山還是host上具有相同pid命名空間的進程交互,因此需要在IPC資源中申請加入命名空間信息,每個IPC資源有一個唯一的32位id。

mnt命名空間

類似chroot,將一個進程放到一個特定的目錄執行,mnt命名空間允許不同命名空間的進程看到的文件結構不同,這樣每個命名空間中的進程所看到的文件目錄就被隔離開了,同chroot不同,每個命名空間的容器在/proc/mounts的信息只包含所在命名空間的mount point。

uts命名空間

UTS命名空間允許每個容器擁有獨立的hostname和domain name,使其在網絡上可以被視作一個獨立的節點而非主機上的一個進程.

每個容器可以有不同的用戶和組id,也就是說可以在容器內用容器內部的用戶執行程序而非主機上的用戶.

控制組

控制組(cgroups)是一個Linux內核的一個特性,主要用來對資源進行隔離、限制、審計等,只有能控制分配到容器的資源,才能避免當多個容器同時運行時對系統資源的競爭.

控制組技術最早由Google的程序員在2006年提出,Linux內核從2.6.24開始支持.

控制組可以提供對容器的內存、CPU、磁盤IO等資源的限制和審計管理.

聯合文件系統

聯合文件系統(UnionFS)是一種分層、輕量級並且高性能的文件系統,他支持對文件系統的修改作為一次提交來一層層的疊加,同時可以將不同目錄掛載到同一個虛擬文件系統.

聯合文件系統是Docker鏡像的基礎,鏡像可以通過分層來進行繼承,基於基礎鏡像(沒有父鏡像),可以製作各種具體的應用鏡像.

另外,不同Docker容器就可以共享一些基礎的文件系統層,同時加上自己獨有的改動層,大大提高了存儲的效率.

Docker中使用的AUFS就是一種聯合文件系統,AUFS支持為每一個成員目錄(類似Git的分支)設定只讀(readonly)、讀寫(readwrite)和寫出(whiteout-able)權限,同時AUFS里有一個類似分層的概念,對只讀權限的分支可以邏輯上進行增量的修改(不影響只讀部分的).

Docker目前支持的聯合文件系統包括OverlayFS,AUFS,BtrFS,VFS,ZFS和Device Mapper。

有可能的情況下,推薦使用overlay2存儲驅動,overlay2是目前Docker默認的存儲驅動,以前是aufs,可以通過配置以上提到的其他類型存儲驅動.

容器格式

最初,Docker採用了LXC中的容器格式,從0.7版本開始以後去除LXC,轉而使用自行開發的libcontainer,從1.11開始,進一步演進為runC和containerd。

網絡實現

Docker的網絡實現其實就是利用了Linux上的網絡命名空間和虛擬網絡設備(特別是vethpair).

基本原理

首先,要實現網絡通信,機器需要至少一個網絡接口(物理接口或虛擬接口)來收發數據包,此外,如果不同子網之間要進行通信,需要路由機制.

Docker中的網絡接口默認都是虛擬的接口,虛擬接口的優勢之一就是轉發效率較高,Linux通過在內核中進行數據複製來實現虛擬接口之間的數據轉發,發送接口的發送緩存中的數據包直接複製到接收接口的接受緩存中,對於本地系統和容器內系統來看就像是一個正常的以太網卡,只是他不需要真正同外部網絡設備通信,速度要快很多.

Docker容器網絡就是利用了這項技術,他在本地主機和容器內分別創建一個虛擬接口,並讓他們彼此連通(這樣的一對接口叫做veth pair).

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

Java 異常處理的十個建議

前言

Java異常處理的十個建議,希望對大家有幫助~

本文已上傳github:

https://github.com/whx123/JavaHome

公眾號:撿田螺的小男孩

一、盡量不要使用e.printStackTrace(),而是使用log打印。

反例:

try{
  // do what you want  
}catch(Exception e){
  e.printStackTrace();
}

正例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦,{}",e);
}

理由:

  • printStackTrace()打印出的堆棧日誌跟業務代碼日誌是交錯混合在一起的,通常排查異常日誌不太方便。
  • e.printStackTrace()語句產生的字符串記錄的是堆棧信息,如果信息太長太多,字符串常量池所在的內存塊沒有空間了,即內存滿了,那麼,用戶的請求就卡住啦~

二、catch了異常,但是沒有打印出具體的exception,無法更好定位問題

反例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦");
}

正例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦,{}",e);
}

理由:

  • 反例中,並沒有把exception出來,到時候排查問題就不好查了啦,到底是SQl寫錯的異常還是IO異常,還是其他呢?所以應該把exception打印到日誌中哦~

三、不要用一個Exception捕捉所有可能的異常

反例:

public void test(){
    try{
        //…拋出 IOException 的代碼調用
        //…拋出 SQLException 的代碼調用
    }catch(Exception e){
        //用基類 Exception 捕捉的所有可能的異常,如果多個層次都這樣捕捉,會丟失原始異常的有效信息哦
        log.info(“Exception in test,exception:{}”, e);
    }
}

正例:

public void test(){
    try{
        //…拋出 IOException 的代碼調用
        //…拋出 SQLException 的代碼調用
    }catch(IOException e){
        //僅僅捕捉 IOException
        log.info(“IOException in test,exception:{}”, e);
    }catch(SQLException e){
        //僅僅捕捉 SQLException
        log.info(“SQLException in test,exception:{}”, e);
    }
}

理由:

  • 用基類 Exception 捕捉的所有可能的異常,如果多個層次都這樣捕捉,會丟失原始異常的有效信息哦

四、記得使用finally關閉流資源或者直接使用try-with-resource

反例:

FileInputStream fdIn = null;
try {
    fdIn = new FileInputStream(new File("/jay.txt"));
    //在這裏關閉流資源?有沒有問題呢?如果發生異常了呢?
    fdIn.close();
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}

正例1:

需要使用finally關閉流資源,如下

FileInputStream fdIn = null;
try {
    fdIn = new FileInputStream(new File("/jay.txt"));
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}finally {
    try {
        if (fdIn != null) {
            fdIn.close();
        }
    } catch (IOException e) {
        log.error(e);
    }
}

正例2:

當然,也可以使用JDK7的新特性try-with-resource來處理,它是Java7提供的一個新功能,它用於自動資源管理。

  • 資源是指在程序用完了之後必須要關閉的對象。
  • try-with-resources保證了每個聲明了的資源在語句結束的時候會被關閉
  • 什麼樣的對象才能當做資源使用呢?只要實現了java.lang.AutoCloseable接口或者java.io.Closeable接口的對象,都OK。
try (FileInputStream inputStream = new FileInputStream(new File("jay.txt")) {
    // use resources   
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}

理由:

  • 如果不使用finally或者try-with-resource,當程序發生異常,IO資源流沒關閉,那麼這個IO資源就會被他一直佔著,這樣別人就沒有辦法用了,這就造成資源浪費。

五、捕獲異常與拋出異常必須是完全匹配,或者捕獲異常是拋異常的父類

反例:

//BizException 是 Exception 的子類
public class BizException extends Exception {}
//拋出父類Exception
public static void test() throws Exception {}

try {
    test(); //編譯錯誤
} catch (BizException e) { //捕獲異常子類是沒法匹配的哦
    log.error(e);
}

正例:

//拋齣子類Exception
public static void test() throws BizException {}

try {
    test();
} catch (Exception e) {
    log.error(e);
}

六、捕獲到的異常,不能忽略它,至少打點日誌吧

反例:

public static void testIgnoreException() throws Exception {
    try {       
        // 搞事情
    } catch (Exception e) {     //一般不會有這個異常
        
    }
}

正例:

public static void testIgnoreException() {
    try {
        // 搞事情
    } catch (Exception e) {     //一般不會有這個異常
        log.error("這個異常不應該在這裏出現的,{}",e); 
    }
}

理由:

  • 雖然一個正常情況都不會發生的異常,但是如果你捕獲到它,就不要忽略呀,至少打個日誌吧~

七、注意異常對你的代碼層次結構的侵染(早發現早處理)

反例:

public UserInfo queryUserInfoByUserId(Long userid) throw SQLException {
    //根據用戶Id查詢數據庫
}

正例:

public UserInfo queryUserInfoByUserId(Long userid) {
    try{
        //根據用戶Id查詢數據庫
    }catch(SQLException e){
        log.error("查詢數據庫異常啦,{}",e);
    }finally{
        //關閉連接,清理資源
    }
}

理由:

  • 我們的項目,一般都會把代碼分 Action、Service、Dao 等不同的層次結構,如果你是DAO層處理的異常,儘早處理吧,如果往上 throw SQLException,上層代碼就還是要try catch處理啦,這就污染了你的代碼~

八、自定義封裝異常,不要丟棄原始異常的信息Throwable cause

我們常常會想要在捕獲一個異常后拋出另一個異常,並且希望把原始異常的信息保存下來,這被稱為異常鏈。公司的框架提供統一異常處理就用到異常鏈,我們自定義封裝異常,不要丟棄原始異常的信息,否則排查問題就頭疼啦

反例:

public class TestChainException {
    public void readFile() throws MyException{
        try {
            InputStream is = new FileInputStream("jay.txt");
            Scanner in = new Scanner(is);
            while (in.hasNext()) {
                System.out.println(in.next());
            }
        } catch (FileNotFoundException e) {
            //e 保存異常信息
            throw new MyException("文件在哪裡呢");
        }
    }
    public void invokeReadFile() throws MyException{
        try {
            readFile();
        } catch (MyException e) {
            //e 保存異常信息
            throw new MyException("文件找不到");
        }
    }
    public static void main(String[] args) {
        TestChainException t = new TestChainException();
        try {
            t.invokeReadFile();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}
//MyException 構造器
public MyException(String message) {
        super(message);
    }

運行結果如下,沒有了Throwable cause,不好排查是什麼異常了啦

正例:


public class TestChainException {
    public void readFile() throws MyException{
        try {
            InputStream is = new FileInputStream("jay.txt");
            Scanner in = new Scanner(is);
            while (in.hasNext()) {
                System.out.println(in.next());
            }
        } catch (FileNotFoundException e) {
            //e 保存異常信息
            throw new MyException("文件在哪裡呢", e);
        }
    }
    public void invokeReadFile() throws MyException{
        try {
            readFile();
        } catch (MyException e) {
            //e 保存異常信息
            throw new MyException("文件找不到", e);
        }
    }
    public static void main(String[] args) {
        TestChainException t = new TestChainException();
        try {
            t.invokeReadFile();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}
//MyException 構造器
public MyException(String message, Throwable cause) {
        super(message, cause);
    }

九、運行時異常RuntimeException ,不應該通過catch 的方式來處理,而是先預檢查,比如:NullPointerException處理

反例:

try {
  obj.method() 
} catch (NullPointerException e) {
...
}

正例:

if (obj != null){
   ...
}

十、注意異常匹配的順序,優先捕獲具體的異常

注意異常的匹配順序,因為只有第一個匹配到異常的catch塊才會被執行。如果你希望看到,是NumberFormatException異常,就拋出NumberFormatException,如果是IllegalArgumentException就拋出IllegalArgumentException。

反例:

try {
    doSomething("test exception");
} catch (IllegalArgumentException e) {       
    log.error(e);
} catch (NumberFormatException e) {
    log.error(e);
}

正例:

try {
    doSomething("test exception");
} catch (NumberFormatException e) {       
    log.error(e);
} catch (IllegalArgumentException e) {
    log.error(e);
}

理由:

  • 因為NumberFormatException是IllegalArgumentException 的子類,反例中,不管是哪個異常,都會匹配到IllegalArgumentException,就不會再往下執行啦,因此不知道是否是NumberFormatException。所以需要優先捕獲具體的異常,把NumberFormatException放前面~

公眾號

  • 歡迎關注我個人公眾號,交個朋友,一起學習哈~
  • 如果答案整理有錯,歡迎指出哈,感激不盡~

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

Spring源碼系列(一)–詳細介紹bean組件

簡介

spring-bean 組件是 Spring IoC 的核心,我們可以使用它的 beanFactory 來獲取所需的對象,對象的實例化、屬性裝配和初始化等都可以交給 spring 來管理。

針對 spring-bean 組件,我計劃分成兩篇博客來講解。本文會詳細介紹這個組件,包括以下內容。下一篇再具體分析它的源碼。

  1. spring-bean 組件的相關概念:實例化、屬性裝配、初始化、bean、beanDefinition、beanFactory。
  2. bean 組件的使用:註冊bean、獲取bean、屬性裝配、處理器等。

項目環境說明

正文開始前,先介紹下示例代碼使用的環境等。

工程環境

JDK:1.8.0_231

maven:3.6.1

IDE:Spring Tool Suites4 for Eclipse 4.12

Spring:5.2.6.RELEASE

依賴引入

除了引入 spring,這裏還額外引入了日誌和單元測試。

    <properties>
        <spring.version>5.2.6.RELEASE</spring.version>
    </properties>
    
    <dependencies>
        <!-- spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
		<!-- junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- logback -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
    </dependencies>

幾個重要概念

實例化、屬性裝配和初始化

在 spring-bean 組件的設計中,這三個詞完整、有序地描述了生成一個新對象的整個流程,是非常重要的理論基礎。它們的具體含義如下:

  1. 實例化:創建出一個新對象。
  2. 屬性裝配:給對象的成員屬性賦值。
  3. 初始化:調用對象的初始化方法。

下面使用一段代碼來簡單演示下這個流程。

public class UserService implements IUserService {
    
    private UserDao userDao;
    
    
    public UserService() {
        super();
        System.err.println("UserService構造方法被調用");
        System.err.println("        ||");
        System.err.println("        \\/");
    }
    
    public void init() {
        System.err.println("UserService的init方法被調用");
        System.err.println("        ||");
        System.err.println("        \\/");
    }
    
    
    public UserDao getUserDao() {
        return userDao;
    }
    
    public void setUserDao(UserDao userDao) {
        System.err.println("UserService的屬性裝配中");
        System.err.println("        ||");
        System.err.println("        \\/");
        this.userDao = userDao;
    }
    
}

如果我們將這個 bean 交給 spring 管理,獲取 bean 時會在控制台打印以下內容:

什麼是bean

按照官方的說法, bean 是一個由 Spring IoC 容器實例化、組裝和管理的對象。我認為,這種表述是錯誤的,通過registerSingleton方式註冊的 bean,它就不是由 Spring IoC 容器實例化、組裝,所以,更準確的表述應該是這樣:

某個類的對象、FactoryBean 對象、描述對象或 FactoryBean 描述對象,被註冊到了 Spring IoC 容器,這時通過 Spring IoC 容器獲取的這個類的對象就是 bean。

舉個例子,使用了 Spring 的項目中, Controller 對象、Service 對象、DAO 對象等都屬於 bean。

至於什麼是 IoC 容器,在 spring-bean 組件中,我認為,beanFactory 就屬於 IoC 容器。

什麼是beanFactory

從客戶端來看,一個完整的 beanFactory 工廠包含以下基本功能:

  1. 註冊別名。對應下圖的AliasRegistry接口。
  2. 註冊單例對象。對應下圖的SingletonBeanRegistry接口。
  3. 註冊BeanDefinition對象。對應下圖的BeanDefinitionRegistry接口。
  4. 獲取 bean。對應下圖的BeanFactory接口。

在 spring-bean 組件中,DefaultListableBeanFactory就是一個完整的 beanFactory 工廠,也可以說是一個 IoC 容器。接下來的例子將直接使用它來作為 beanFactory。

至於其他的接口,這裏也補充說明下。HierarchicalBeanFactory用於提供父子工廠的支持,ConfigurableBeanFactory用於提供配置 beanFactory 的支持,ListableBeanFactory用於提供批量獲取 bean 的支持(不包含父工廠的 bean),AutowireCapableBeanFactory用於提供實例化、屬性裝配、初始化等一系列管理 bean 生命周期的支持。

什麼是beanDefinition

beanDefinaition 是一個描述對象,用來描述 bean 的實例化、初始化等信息。

在 spring-bean 組件中,beanDefinaition主要包含以下四種:

  1. RootBeanDefinition:beanFactory 中最終用於 createBean 的 beanDefinaition,不允許添加 parentName。在 BeanFactory 中以下三種實現類都會被包裝成RootBeanDefinition用於 createBean。
  2. ChildBeanDefinition:必須設置 parentName 的 beanDefinaition。當某個 Bean 的描述對象和另外一個的差不多時,我們可以直接定義一個ChildBeanDefinition,並設置它的 parentName 為另外一個的 beanName,這樣就不用重新設置一份。
  3. GenericBeanDefinition:通用的 beanDefinaition,可以設置 parentName,也可以不用設置。
  4. AnnotatedGenericBeanDefinition:在GenericBeanDefinition基礎上增加暴露註解數據的方法。

spring-bean 組件提供了BeanDefinitionBuilder用於創建 beanDefinaition,下面的例子會頻繁使用到。

使用例子

入門–簡單地註冊和獲取bean

下面通過一個入門例子來介紹註冊和獲取 bean 的過程。

    @Test
    public void testBase() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
    }

兩種註冊bean的方式

beanFactory 除了支持註冊 beanDefinition,還允許直接註冊 bean 實例,如下。和前者相比,後者的實例化、屬性裝配和初始化都沒有交給 spring 管理。

    @Test
    public void testRegisterWays() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 註冊Bean-- BeanDefinition方式
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 註冊Bean-- Bean實例方式
        beanFactory.registerSingleton("userService2", new UserService());
        
        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
        IUserService userService2 = (IUserService)beanFactory.getBean("userService2");
        System.err.println(userService2.get("userId"));
    }

當然,這種方式僅支持單例 bean 的註冊,多例的就沒辦法了。

註冊多例bean

默認情況下,我們從 beanFactory 獲取到的 bean 都是單例的,即每次 getBean 獲取到的都是同一個對象,實際項目中,有時我們需要獲取到多例的 bean,這個時候就可以通過設置 beanDefinition 的 scope 來處理。如下:

    @Test
    public void testScope() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 註冊Bean-- BeanDefinition方式
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        rootBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean--通過BeanType
        IUserService userService1 = beanFactory.getBean(IUserService.class);
        IUserService userService2 = beanFactory.getBean(IUserService.class);
        assertNotEquals(userService1, userService2);
    }

多種獲取bean的方式

beanFactory 提供了多種方式來獲取 bean 實例,如下。如果同時使用 beanName 和 beanType,獲取到指定 beanName 的 bean 後會進行類型檢查和類型類型,如果都不通過,將會報錯。

    @Test
    public void testGetBeanWays() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean--通過BeanName
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
        // 獲取Bean--通過BeanType
        IUserService userService2 = beanFactory.getBean(IUserService.class);
        System.err.println(userService2.get("userId"));
        // 獲取Bean--通過BeanName+BeanType的方式
        IUserService userService3 = beanFactory.getBean("userService", IUserService.class);
        System.err.println(userService3.get("userId"));
    }

不同形式的beanName

通過 name 獲取 bean,這個 name 包含以下三種形式:

  1. beanName,即註冊 bean 時用的 beanName。這是使用最多的形式,需要注意一點,如果 beanName 對應的 bean 是FactoryBean,並不會返回FactoryBean的實例,而是會返回FactoryBean.getObject方法的返回結果。
  2. alias,即我們通過SimpleAliasRegistry.registerAlias(name, alias)方法註冊到 beanFactory 的別名。這時,需要將 name 解析為 alias 對應的 beanName 來獲取 bean。
  3. ‘&’ + factorybeanName,這時為了獲取FactoryBean的一種特殊格式。
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 註冊Bean--註冊的是一個 FactoryBean
        UserServiceFactoryBean userServiceFactoryBean = new UserServiceFactoryBean();
        beanFactory.registerSingleton("userServiceFactoryBean", userServiceFactoryBean);

        // 註冊BeanName的別名
        beanFactory.registerAlias("userServiceFactoryBean", "userServiceAlias01");

        // 通過BeanName獲取
        assertEquals(userServiceFactoryBean.getObject(), beanFactory.getBean("userServiceFactoryBean"));

        // 通過別名獲取
        assertEquals(userServiceFactoryBean.getObject(), beanFactory.getBean("userServiceAlias01"));

        // 通過&+FactoryBeanName的方式
        assertEquals(userServiceFactoryBean, beanFactory.getBean("&userServiceFactoryBean"));

bean衝突的處理

通過 beanType 的方式獲取 bean,如果存在多個同類型的 bean且無法確定最優先的那一個,就會報錯。

    @Test
    public void testPrimary() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); 
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("UserRegisterBeanDefinition", rootBeanDefinition);
        beanFactory.registerSingleton("UserRegisterSingleton", new User("zzs002", 19));
        beanFactory.registerSingleton("UserRegisterSingleton2", new User("zzs002", 18));
        
        // 獲取Bean--通過BeanType
        User user = beanFactory.getBean(User.class);
        System.err.println(user);
    }

運行以上方法,將出現 NoUniqueBeanDefinitionException 的異常。

針對上面的這種問題,spring 的處理方法如下:

  1. 檢查是否存在唯一一個通過registerBeanDefinition且isPrimary = true的(存在多個會報錯),存在的話將它作為匹配到的唯一 beanName;
  2. 通過我們註冊的OrderComparator來確定優先值最小的作為唯一 beanName。注意,通過registerSingleton註冊的和通過registerBeanDefinition註冊的,比較的對象是不一樣的,前者比較的對象是 bean 實例,後者比較的對象是 bean 類型,另外,這種方法最好不要存在相同優先級的 bean。

所以,為了解決這種衝突,可以設置BeanDefinition對象的 isPrimary = true,或者為 beanFactory 設置OrderComparator,代碼如下:

    @Test
    public void testPrimary() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 為BeanFactory設置比較器
        beanFactory.setDependencyComparator(new OrderComparator() {

            @Override
            public Integer getPriority(Object obj) {
                return obj.hashCode();
            }
        });

        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        // rootBeanDefinition.setPrimary(true); // 設置BeanDefinition對象為isPrimary

        // 註冊Bean
        beanFactory.registerBeanDefinition("userRegisterBeanDefinition", rootBeanDefinition);
        beanFactory.registerSingleton("userRegisterSingleton", new User("zzs002", 19));
        beanFactory.registerSingleton("userRegisterSingleton2", new User("zzs003", 18));

        // 獲取Bean--通過BeanType
        User user = beanFactory.getBean(User.class);
        System.err.println(user);
    }

使用TypeConverter獲取自定義類型的對象

當我們使用 beanType 來獲取 bean 時,如果獲取到的 bean 不是指定的類型,這時,不會立即報錯,beanFactory 會嘗試使用我們註冊的TypeConverter來強制轉換。而這個類型轉換器我們可以自定義設置,如下。

    @Test
    public void testTypeConverter() {
        
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        // 註冊類型轉換器
        beanFactory.setTypeConverter(new TypeConverterSupport() {
            @SuppressWarnings("unchecked")
            @Override
            public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
                    @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
                // 將User轉換為UserVO
                if(UserVO.class.equals(requiredType) && User.class.isInstance(value)) {
                    User user = (User)value;
                    return (T)new UserVO(user);
                }
                return null;
            }
        });

        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        beanFactory.registerBeanDefinition("User", rootBeanDefinition);

        UserVO bean = beanFactory.getBean("User", UserVO.class);
        Assert.assertTrue(UserVO.class.isInstance(bean));
    }

屬性裝配

beanFactory 在進行屬性裝配時,會讀取 beanDefinition 對象中的PropertyValues中的propertyName=propertyValue,所以,我們想要對 bean 注入什麼參數,只要在定義 beanDefinition 時指定就行。

    @Test
    public void testPopulate() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 定義userService的beanDefinition
        AbstractBeanDefinition userServiceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        // 定義userDao的beanDefinition
        AbstractBeanDefinition userDaoBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserDao.class).getBeanDefinition();
        // 給userService設置裝配屬性
        userServiceBeanDefinition.getPropertyValues().add("userDao", userDaoBeanDefinition);
        // userServiceBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        // userServiceBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);

        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", userServiceBeanDefinition);
        beanFactory.registerBeanDefinition("userDao", userDaoBeanDefinition);

        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        userService.save(null);
    }

運行以上方法,發現 userDao 對象被成功注入到了 userService 對象中!

這裏補充一點,beanFactory 除了通過 beanDefinition 中的PropertyValues獲取 propertyName=propertyValue,還可以讀取 bean 中的屬性來自動定義 propertyName=propertyValue,只要設置 beanDefinition 的 autowireMode 就可以了。

bean 實例化、屬性裝配和初始化的處理器

前面講到,我們將 bean 的實例化、屬性裝配和初始化都交給了 spring 處理,然而,有時我們需要在這些節點對 bean 進行自定義的處理,這時就需要用到 beanPostProcessor。

這裏我簡單演示下如何添加處理器,以及處理器的執行時機,至於處理器的具體實現,我就不多擴展了。

    @Test
    public void testPostProcessor() {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 添加實例化處理器
        beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {
            // 如果這裏我們返回了對象,則beanFactory會將它作為bean直接返回,不再進行bean的實例化、屬性裝配和初始化等操作
            public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
                if(UserService.class.equals(beanClass)) {
                    System.err.println("bean實例化之前的處理。。 --> ");
                }
                return null;
            }

            // 這裏通過返回的布爾值判斷是否需要繼續對bean進行屬性裝配和初始化等操作
            public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("bean實例化之後的處理。。 --> ");
                }
                return true;
            }
        });

        // 添加裝配處理器
        beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {

            // 這裏可以在屬性裝配前對參數列表進行調整
            public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("屬性裝配前對參數列表進行調整 --> ");
                }
                return InstantiationAwareBeanPostProcessor.super.postProcessProperties(pvs, bean, beanName);
            }

        });

        // 添加初始化處理器
        beanFactory.addBeanPostProcessor(new BeanPostProcessor() {

            // 初始化前對bean進行改造
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("初始化前,對Bean進行改造。。 --> ");
                }
                return bean;
            }

            // 初始化后對bean進行改造
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("初始化后,對Bean進行改造。。 --> ");
                }
                return bean;
            }
        });
        // 定義userService的beanDefinition
        AbstractBeanDefinition userServiceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        // 定義userDao的beanDefinition
        AbstractBeanDefinition userDaoBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserDao.class).getBeanDefinition();
        // 給userService添加裝配屬性
        userServiceBeanDefinition.getPropertyValues().add("userDao", userDaoBeanDefinition);
        // 給userService設置初始化方法
        userServiceBeanDefinition.setInitMethodName("init");
        
        // 註冊bean
        beanFactory.registerBeanDefinition("userService", userServiceBeanDefinition);
        beanFactory.registerBeanDefinition("userDao", userDaoBeanDefinition);

        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
    }

運行以上方法,控制台打印出了整個處理流程。實際開發中,我們可以通過設置處理器來改變改造生成的 bean 。

以上,基本介紹完 spring-bean 組件的使用,下篇博客再分析源碼,如果在分析過程中發現有其他特性,也會在這篇博客的基礎上擴展。

相關源碼請移步: spring-beans

本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/13126053.html

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

秋季奶茶米色系穿搭,復古潮流

秋季是一個溫暖的季節,自然是要柔和的色彩來應景。

與夏季盛行的亮色系相比,秋季奶茶米色系穿搭更合心意,分分鐘凹出你想要的風格,復古潮流有韻味。

米色打底衫是秋季不可少的單品,一件好的打底衫,是打造良好着裝的基礎。搭配復古格子西裝,秋季奶茶米色系穿搭,低調有內涵。秋季很百搭的單品小皮鞋,一雙小皮鞋便可為你解決鞋子搭配問題。

近年來複古風盛行,格子元素無處不在。除了格子西裝,格子半身裙也是無數愛美的小仙女們所熱衷的。天轉涼了,依舊可以穿上美美的格子裙,look V領針織衫,復古潮流有韻味。這時候,就可以凹上一頂貝雷帽,更具時尚哦。

半身格子裙不管怎麼搭配都很好看,上半身可以採取基礎款打底針織衫,外搭大衣。秋季奶茶米色系穿搭,配上休閑鞋,就能輕鬆出門了,舒適又方便。

呢子與半身裙,經典不變的穿搭。尤其是這種米色系搭配,襯托整個人的溫柔氣質,復古潮流有韻味。再來一隻斜挎包點綴,豐富整體的造型。不得不說包包是搭配的加分項,一款合適的包包讓你的整體裝扮更有看點。

相比半身格子裙,我更喜歡弔帶格子裙。可以結合打底衫穿,外披一身米色大衣,保暖又時髦。秋季奶茶米色系穿搭,低調沉穩有內涵。遇上黑色靴子,氣場瞬間便可提升不少,走路自帶風。

這樣的襯衫與針織結合也是不錯的選擇,不同的面料結合,產生柔性美。裙子與外套採用最為簡單的色彩,平平淡淡更讓人舒服。至於鞋子,豆豆鞋,樂福鞋這些都是可以的哦,復古潮流有韻味。想要走在時尚的前端,就需要下一番功夫。衣服要搭得好,鞋子更要穿得巧。

所有的襯衫中,最有特色的當屬碎花襯衫。不同樣式的碎花,大片式小片式均是時髦點。相對來說小碎花更好駕馭,不挑人,復古潮流有韻味。如果想要更具個性,就可以選擇大碎花。米色碎花的繁雜可以結合簡單針織衫搭配,米色就是很好的選擇。

秋季是個穿衣的好季節,提升自身氣質的最佳時期。在這個季節,盡情地嘗試不同的穿衣風格。秋季奶茶米色系穿搭,為你打開新的穿衣模式,復古潮流有韻味。

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※你應該要知道的電子煙懶人包!

※男人為什麼總愛舒壓按摩台北外送茶交流呢?

※老司機曝包養內幕

※那些年被包養的回憶實錄

9套隨性穿搭,讓你氣質依舊在線!

愛美的小仙女,總是很喜歡把自己打扮得美美的,即便沒什麼事,也喜歡穿上好看的衣裳出去溜達一圈~所以日常出街也想扮靚,這9套隨性穿搭,讓你氣質依舊在線,心動的小仙女,就一起來pick一下吧~

第一套:

風衣是我們秋季日常中,出鏡率十分高的單品。想要不一樣的話,就拋棄經典的卡其色,選擇這件深咖色的風衣,內搭淺色單品,塑造出深淺不一的造型,是這9套隨性穿搭中,最能讓人眼前一亮的look。

第二套:

會擔心黑色風衣太沉悶了嗎?內搭選擇白色T恤,就能為你破除那壓抑感了。日常出街也想扮靚,破洞的牛仔褲,休閑的款式,讓造型看起來更有酷感,街頭風滿滿。小白鞋是最百搭的,所以不管造型如何變化,搭一雙小白鞋就對了。

第三套:

經典的卡其色風衣,真的是無法令人拒絕,內搭簡單的條紋T恤,就能讓你美得很氣質。黑色直筒褲,聯手白色帆布鞋,休閑又顯青春文藝感。

第四套:

自帶高級感的灰色風衣,內搭黑色T恤,展現出無限神秘魅力,讓你氣質依舊在線。經典又百搭的牛仔褲,九分的長度,讓你不用翻褲腳,就能秀出纖細的腳踝。腳深色的平底鞋,盡顯你的柔美氣質。

第五套:

撲面而來的中性風,令人忍不住想要為之瘋狂的打call。日常出街也想扮靚,軍綠色的外套配牛仔褲,深淺色的對比,穿出很不一樣的視覺效果。藍色的牛仔褲進行卷邊,細節處就很有時髦感。

第六套:

格紋元素的連衣裙,彰顯出你的溫柔感,外搭淺白色的針織開衫,一瞬間就讓你變得溫柔如水,彷彿江南女子。手捧鮮花,明媚如春。

第七套:

深色的針織開衫,小仙女可以考慮一下,白色T恤當內搭,輕輕鬆松地擁有了時髦感,讓你氣質依舊在線。小仙女可以給自己備上一頂針織帽,焦糖色的色調,不羈又很青春。

第八套:

咖色的皮衣,比起黑色來說,更加有自己的特色。而搭配一條藍色的印花裙,讓人感受到秋季的浪漫氣息。小仙女可以多多的嘗試一下這樣子的穿搭,會讓你變得更加有女人味。

第九套:

格紋襯衫來助力,是這9套隨性穿搭中,最能讓時髦指數瞬間飆升的造型。而搭配黑色的休閑褲,卷邊的設計,很有自己的時髦feel。

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※電子煙有爭議?真相解密

※桃園外送茶莊、喝茶吃宵夜首選

※老司機曝包養內幕

※那些年被包養的回憶實錄

王麗坤穿白色T恤搭淺色牛仔褲,清新打扮

王麗坤是個人頗為喜歡的,很有氣質的這麼一位女星,不過,王麗坤之所以看着氣質這麼好,和她的穿着打扮有着很大的關係,王麗坤的私服總給人一種看着很舒服的既視感。

同樣是職業裝,像王麗坤這樣,選擇一件深色的西裝,在搭配短褲或者短褲,再穿上一雙細高跟,這樣的LOOK更適合女生穿,同時也能凸顯出女生腿的細長,在上班的女生們,知道了嗎?

還有這條裙子也是蠻好看的,看王麗坤穿在身上的是一條淡粉色的裙子,襯得王麗坤看起來頗為甜美和有少女感。這麼多年了,感覺王麗坤是越來越有少女范了呢。這不,又看到王麗坤現身,穿白色T恤搭淺色牛仔褲,還是清新打扮顯氣質一點!看下。

看得出來,王麗坤依舊是素顏,但是,我們也看得到王麗坤的臉上還是那麼光滑乾淨,素顏女神這個稱號可不是白叫的呢。然後,看王麗坤將藍色的鴨舌帽給戴上,可以感受到她的氣色很好。

再看王麗坤的着裝,看王麗坤穿了件白色的字母款短袖T恤,穿出來給人清清爽爽的既視感,背上一個雙肩包之後的她像極了國慶回程的大學女生一樣呢。那麼,在搭配T恤的時候怎麼樣搭配更好看呢?

看王麗坤下搭了一條淺藍色的牛仔褲,白色T恤和淺藍色的褲子可以說是非常經典和基礎的搭配了,但是呢,令人百看不厭,這種小清新的裝扮非常減齡,同時也是大齡女生們的最愛呢。

又看到王麗坤往腳上穿了一雙白色的帆布鞋,帆布鞋是很顯青春氣息的鞋子,和這一身小清新的造型很搭,在讀書的時候,最喜歡穿的就是帆布鞋了。另外,我們還注意到王麗坤的手上還拿了件衣服。

看王麗坤的手上拿着一件格子外套,眼下初秋的時候,溫度有點飄忽不定,多帶一件外套出門還是比較合適的,像這樣的格子外套非常百搭,同時也很好看,帶着出門最為合適了。

好了,王麗坤的出行造型就是這樣,還是之前她一貫喜歡的簡約和清爽風格的穿搭,要是感覺到冷的話,再把外套給穿上就可以了,十分接地氣。那麼,看到王麗坤的這套LOOK你咋看?喜歡王麗坤的私服嗎?

 

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※全台最大電子煙交易平台?

※新竹外送茶交流平台

※老司機曝包養內幕

※那些年被包養的回憶實錄

張子萱秀秋季私服,一件藍色呢大衣內搭針織衫

張子萱做過模特,也出演過影視作品,現在是一名時尚博主,其實,自從張子萱安安靜靜地做時尚博主之後,大家對她的好感度也是直線上升,不得不說,張子萱的衣品是真的很不錯。

要是你去聚會的時候穿以上這套裝扮,選擇一件小香風夾克,內搭小香風的裙子,看起來很有氣質,同時,還相當有名媛范,在人群當中自然就是很出眾,很亮眼的呢。

在秋天的時候怎麼能少得了開衫呢?看張子萱穿了件開衫,下搭粉色的裙子,那種甜美范就出來了,這身可愛的穿着使得張子萱根本就不像是30來歲的人呢。這不,又看到張子萱秀秋季私服,一件藍色呢大衣內搭針織衫,穿回少女的樣子!看下。

在秋冬季節,一件呢外套肯定是少不了的,看張子萱就已經把呢外套給安排上了,只見張子萱穿了件淺藍色的呢外套,非常清純小清新的風格,穿上這件外套的張子萱也減齡10歲都不止呢。

而且,張子萱的這件呢外套是oversize風格的,穿在身上會比較顯大氣和男友力,而且,女生穿這種大碼的呢外套,會給人一種可愛的感覺,所以,張子萱穿這件呢外套是很合適的,那麼,張子萱又會怎麼搭配這件外套呢?

在內搭上,張子萱則是穿了件針織衫,而且是條紋風格的,看起來的話很顯少女感,而且,呢大衣和針織衫的搭配也是超級合適的搭配,秋冬很多女生都喜歡這樣子穿呢,除此之外,張子萱還搭配了什麼呢?

只見張子萱搭配了一條白色的鉛筆褲,還有一雙小白鞋,白色的單品是百搭的單品,和這件淺藍色的大衣搭配在一起自然也是很合適的,從上到下看都給人清爽的既視感,這麼穿可顯年輕了呢。

其實,褲子是非常容易髒的,所以,很多人在選擇褲子的時候,都會選擇顏色比較深一點的,但其實,像這樣的純白色褲子不僅好看,而且和什麼衣服都能搭配在一起,更重要的是,這麼穿能夠穿出女生的清純感。

好了,張子萱的秋裝就是這樣,呢大衣造型的張子萱可美了,加上她這款空氣劉海髮型,已經當媽媽的她美回到了少女的模樣。好了,看到張子萱的私服,知道秋天該怎麼穿了呢?你喜歡張子萱的衣品嗎?

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※網友分享與台中外送茶店搞笑趣事

※你應該要知道的電子煙懶人包!

※老司機曝包養內幕

※那些年被包養的回憶實錄

楊蓉已經把風衣穿上了,簡單但很有范

楊蓉是一位低調但是很有實力的演員,而且,還長得一張“少女臉”,雖然都38歲了,演起小年輕也是一點都不吃力的,雖然長得美,但也需要好衣品來加持,這次,我們就來說說楊蓉的好衣品吧。

既然想要看起來年輕一點,那就需要穿年輕一點的私服,看楊蓉出行的時候,外穿一條綠色的衛衣裙,看起來十分顯清純活力,一點都不像是38歲,初秋的時候穿這樣的衛衣裙非常合適呢。

有的時候,楊蓉也會嘗試一下校園風,就像以上這身裝扮,看楊蓉身穿白色的短袖T恤,再搭配一條牛仔裙,看起來的話頗為清純和小清新。而現在已經是秋天了,楊蓉也是從夏天美到了秋天,這不,看到楊蓉已經把風衣穿上了,再內搭黑色裙子,很簡單但很有范!看下。

看楊蓉留着披肩長發的同時還戴上了一副塑料框的眼鏡,而且這一副眼鏡是豹紋元素的,接着還看到楊蓉戴起了耳墜,這麼一來的話,再看楊蓉的造型就多了幾分女人味呢,我們接着看楊蓉的穿搭。

看楊蓉穿上了一件卡其色的風衣,在秋冬季節,風衣是非常流行的單品,而卡其色則是所有風衣顏色當中的經典顏色,穿出來有一種很有氣質,很有文藝范的既視感。另外,楊蓉這件風衣還特別與眾不同。

看楊蓉的這件風衣的袖子上是針織元素風格的,和普通風衣的袖子有着很大的不同,穿針織袖的風衣除了時髦之外,而且還會比較透氣,在秋高氣爽的季節,穿這樣的單品才會比較舒爽呢。

那麼,楊蓉是怎麼搭配這件風衣的呢?看楊蓉在風衣的裏面內搭黑色裙子,作為百搭的裙子,和卡其色的風衣搭配在一起經典簡約,但是呢,穿出來的氣質卻是相當好的呢。

但是呢,楊蓉的造型上也不缺時髦感,這不,又看到楊蓉肩上的包包,背着的是一款毛茸茸的單肩包,這種包包看起來頗為可愛,與此同時,也凸顯出了楊蓉的少女心,很適合女孩子背着出門呢。

最後,再穿上秋天另外的一件時尚單品,那就是楊蓉腳上的襪靴,看楊蓉穿了一雙黑色襪靴在腳上,整套裝扮就這樣完成了,大齡女生們在選擇私服的時候,參照楊蓉這麼穿才有氣質呢。那麼,看到楊蓉的LOOK,你認為美嗎?

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※高雄外送茶坊行情

※老司機曝包養內幕

※你應該要知道的電子煙懶人包!

※新竹外送茶交流平台

※那些年被包養的回憶實錄

鐵路全面市場化再出發

  中國鐵路總公司改製成立中國國家鐵路集團有限公司,是國企改革向縱深推進的又一重要成果,也是鐵路行業從壟斷向市場化邁出的重要步伐。可以預料,通過這次改革,將為鐵路行業從傳統運輸生產型企業向現代運輸經營型企業轉變打下堅實基礎。

  自2017年以來,鐵路行業先後通過實施鐵路局公司制改革、鐵路總公司機關內設機構改革、所屬非運輸企業公司制改革、鐵路局集團公司內設機構改革,初步完成了從計劃化向市場化、從壟斷向逐步放開以後,不僅企業活力得到了很大增強,企業經營服務能力和服務水平有了較大提高,市場競爭力和盈利能力也有了明顯增強。如京滬高鐵已全面實現盈利,其他方面效益也在不斷改善。

  但是,由於長期積累的矛盾和問題,鐵路行業在市場化水平和服務效率與能力方面,與完全競爭性行業相比,仍有一定差距。在運行效率、資源有效配置、資產合理利用上,還存在較大的問題,在客運方面還沒有把服務與效率、服務與效益緊密結合起來,尤其負債水平還很高,已成鐵路行業可持續發展的重要制約。按鐵路行業現狀,特別是資產負債情況,完全按照自身造血的方式,顯然是效率較低、活力不足、能力不夠、速度較慢的。通過公司制改革,並在此基礎上加快股份制改造,積極引進戰略投資者,尤其是民營資本,有效發揮社會資本的作用,將對鐵路行業的可持續發展產生積極作用,將大大提升鐵路行業的運行效率和服務能力。

  眼下,社會資本急需尋找投資的空間和通道,鐵路行業經過此次改革,為引入社會資本創造了良好條件,也為引入社會資本打開了新通道。現在需要把握好的是,中鐵集團應充分利用好此次改制的良好機會,對集團內部的體制機制、機構設置、業務分類、發展目標、經營思路等做出更加科學、合理、規範、有序的劃分,認真研究下一步改革思路。譬如是在集團推進混合所有制改革還是對內部具體業務板塊推進混合所有制改革,採用怎樣的方式改革,在多大限度範圍內改革,引入怎樣的社會資本,放寬到什麼水平,有什麼具體要求等,都要深入思考、認真謀划。在引入戰略投資者時,到底是以消化債務為主,還是拓展業務為主,能否全面融入市場,需不需要引進外資,內部分配製度如何改革,如何處置和使用閑置資產以及安排人員,都需要有詳細而嚴密的思考。

  鐵路是事關民生的重要行業,也是國民經濟的命脈之一。推動改革向縱深推進,就是要建立更加適應市場化需要的體制機制,讓鐵路人能儘快全面融入市場,讓鐵路人能成為真正具有市場化意識、市場化觀念、市場化能力的人,鐵路行業能建立完全適應市場需求的現代企業制度,真正步入市場化行列,增強企業的經營效率和經營質量,為推動經濟高質量發展標作出更大貢獻。

(文章來源:上海證券報)

(責任編輯:DF380)

【其他文章推薦】

※實木地板、海島型地板、耐磨地板怎麼挑? 木地板三倍價差的秘密!!

※新屋購入,尋找台中室內設計師?是否可先免費估價丈量?

※挑好磚一點都不難!馬賽克磚挑選眉角小撇步!

※想知道北部最多平價、庫存出清的家具工廠推薦在哪裡?

※分享木質地板DIY自行施工教學影片

※推薦沙發修理,老師傅的專業手工!

※家中裝潢適合實木地板嗎?注意五大鋪設狀況