轉載自台大風險社會與政策研究中心;陳喬琪 編譯
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※別再煩惱如何寫文案,掌握八大原則!
※產品缺大量曝光嗎?你需要的是一流包裝設計!
作者:HelloGitHub-追夢人物
我們的博客有一個側邊欄功能,分別列出博客文章的分類列表、標籤列表、歸檔時間列表,通過點擊側邊欄對應的條目,還可以進入相應的頁面。例如點擊某個分類,博客將跳轉到該分類下全部文章列表頁面。這些數據的展示都需要開發對應的接口,以便前端調用獲取數據。
分類列表、標籤列表實現比較簡單,我們這裏給出接口的設計規範,大家可以使用前幾篇教程中學到的知識點輕鬆實現(具體實現可參考 GtiHub 上的源代碼)。
分類列表接口: /categories/
標籤列表接口:/tags/
歸檔日期列表的接口實現稍微複雜一點,因為我們需要從已有文章中歸納文章發表日期。事實上,我們在上一部教程 HelloDjango – Django博客教程(第二版)的 頁面側邊欄:使用自定義模板標籤 已經講解了如何獲取歸檔日期列表,只是當時返回的歸檔日期列表直接用於模板的渲染,而這裏我們需要將歸檔日期列表序列化后通過 API 接口返回。
具體來說,獲取博客文章發表時間歸檔列表的方法是調用查詢集(QuerySet)的 dates 方法,提取記錄中的日期。核心代碼就一句:
Post.objects.dates('created_time', 'month', order='DESC')
這裏 Post.objects.dates 方法會返回一個列表,列表中的元素為每一篇文章(Post)的創建日期(已去重),日期都是 Python 的 date 對象,精確到月份,降序排列。
有了返回的歸檔日期列表,接下來就實現相應的 API 接口視圖函數:
blog/views.py
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.serializers import DateField
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
# ...
@action(
methods=["GET"], detail=False, url_path="archive/dates", url_name="archive-date"
)
def list_archive_dates(self, request, *args, **kwargs):
dates = Post.objects.dates("created_time", "month", order="DESC")
date_field = DateField()
data = [date_field.to_representation(date) for date in dates]
return Response(data=data, status=status.HTTP_200_OK)
注意這裏我們涉及到了幾個以前沒有詳細講解過的用法。
一是 action 裝飾器,它用來裝飾一個視圖集中的方法,被裝飾的方法會被 django-rest-framework 的路由自動註冊為一個 API 接口。
回顧一下我們之前在使用視圖集 viewset 時提到過 action(動作)的概念,django-rest-framework 預定義了幾個標準的動作,分別為 list 獲取資源列表,retrieve 獲取單個資源、update 和 partial_update 更新資源、destroy 刪除資源,這些 action 具體的實現方法,分別由 mixins 模塊中的混入類提供。例如 用類視圖實現首頁 API 中我們介紹過 mixins.ListModelMixin,這個混入類提供了 list 動作對應的標準實現,即 list 方法。視圖集中所有以上提及的以標準動作命名的方法,都會被 django-rest-framework 的路由自動註冊為標準的 API 接口。
django-rest-framework 默認只能識別標準命名的視圖集方法並將其註冊為 API,但我們可以添加更多非標準的 action,而為了讓 django-rest-framework 能夠識別這些方法,就需要使用 action 裝飾器進行裝飾。
其實我們可以簡單地將 action 裝飾的方法看作是一個視圖函數的實現,因此可以看到方法傳入的第一個參數為 request 請求對象,函數體就是這個視圖函數需要執行的邏輯,顯然,方法最終必須要返回一個 HTTP 響應對象。
action 裝飾器通常用於在視圖集中添加額外的接口實現。例如這裏我們已有了 PostViewSet 視圖集,標準的 list 實現了獲取文章資源列表的邏輯。我們想添加一個獲取文章歸檔日期列表的接口,因此添加了一個 list_archive_dates 方法,並使用 action 進行裝飾。通常如果要在視圖集中添加額外的接口實現,可以使用如下的模板代碼:
@action(
methods=["allowed http method name"],
detail=False or True,
url_path="url/path",
url_name="url name"
)
def method_name(self, request, *args, **kwargs):
# 接口邏輯的具體實現,返回一個 Response
通常 action 裝飾器以下 4 個參數都會設置:
methods:一個列表,指定訪問這個接口時允許的 HTTP 方法(GET、POST、PUT、PATCH、DELETE)
detail:True 或者 False。設置為 True,自動註冊的接口 URL 中會添加一個 pk 路徑參數(請看下面的示例),否則不會。
url_path:自動註冊的接口 URL。
url_name:接口名,主要用於通過接口名字反解對應的 URL。
當然,我們還可以在 action 中設置所有 ViewSet 類所支持的類屬性,例如 serializer_class、pagination_class、permission_classes 等,用於覆蓋類視圖中設置的屬性值。
以上是 action 用法的一個基本介紹,現在來分析一下 list_archive_dates 這個 action 來加深理解。
methods 參數指定接口需要通過 GET 方法訪問,detail 為 False,url_path 設置為 archive/dates,因此最終自動生成的接口路由就是 /posts/archive/dates/。如果我們設置 detail 為 True,那麼生成的接口路由就是 /posts/<int:pk>/archive/dates/,生成的 URL 中就會多一個 pk 路徑參數。
list_archive_dates 具體的實現邏輯中,以下幾點需要注意:
一是獨立使用序列化字段(Field)。之前序列化字段都是在序列化器(Serializer)裏面使用的,因為通常來說接口需要序列化一個對象的多個字段。而這個接口中只需要序列化一個時間字段(類型為 Python 標準庫中的 datetime.date),所以沒必要單獨定義一個序列化器了,直接拿 django-rest-framework 提供的用於序列化時間類型的 DateField 就可以了。用法也很簡單,實例化序列化字段,調用其 to_representation 方法,將需要序列化的值傳入即可(其實序列化器在序列對象的多個字段時,內部也是分別調用對應序列化字段的 to_representation 方法)。
我們通過列表推導式生成一個序列化后的歸檔日期列表,這個列表是可被序列化的。接着我們在接口返回一個 Response, Response 將序列化后的結果包裝返回(保存在 data 屬性中),django-rest-framework 會進一步幫我們把這個 Response 中包含的數據解析為合適的格式(例如 JSON)。
status=status.HTTP_200_OK 指定這個接口返回的狀態碼,HTTP_200_OK 是一個預定義的常數,即 200。django-rest-framework 將常用 HTTP 請求的狀態碼常數預定義 status 模塊里,使用預定義的變量而不是直接使用数字的好處一是增強代碼可讀性,二是減少硬編碼。
由於 PostViewSet 視圖集已經通過 django-rest-framework 的路由進行了註冊,因此 list_archive_dates 也會被連帶着自動註冊為一個接口。啟動開發服務器,訪問 /posts/archive/dates/,就可以看到返回的文章歸檔日期列表。

注意到紅框圈出部分,django-rest-framework API 交互後台會識別到額外定義的 action 並將它們展示出來,點擊就可以進入到相應的 API 頁面。
現在,側邊欄所需要的數據接口就開發完成了,接下來實現返回某一分類、標籤或者歸檔日期下的文章列表接口。
在 使用視圖集簡化代碼 我們開發了獲取全部文章的接口。事實上,分類、標籤或者歸檔日期文章列表的 API,本質上還是返回一個文章列表資源,只不過比首頁 API 返回的文章列表資源多了個“過濾”,只過濾出了指定的部分文章而已。對於這樣的場景,我們可以在請求 API 時加上查詢參數,django-rest-framework 解析查詢參數,然後從全部文章列表中過濾出查詢所指定的文章列表再返回。
這在 RESTful API 的設計中肯定是會遇到的,因此第三方庫 django-filter 幫我們實現了上述所說的查詢過濾功能,而且和 django-rest-framework 有很好的集成,我們可以在 django-rest-framework 中非常方便地使用 django-filter。
既然要使用它,當然是先安裝它(已安裝跳過):pipenv install django-filter
接着我們來配置 PostViewSet,為其設置用於過濾返回結果集的一些屬性,代碼如下:
from django_filters.rest_framework import DjangoFilterBackend
from .filters import PostFilter
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
# ...
filter_backends = [DjangoFilterBackend]
filterset_class = PostFilter
非常的簡單,僅僅設置了 filter_backends 和 filterset_class 兩個屬性。其中 filter_backends 設置為 DjangoFilterBackend,這樣 API 在返回結果時, django-rest-framework 會調用設置的 backend(這裡是 DjangoFilterBackend) 的 filter 方法對 get_queryset 方法返回的結果進行進一步的過濾,而 DjangoFilterBackend 會依據 filterset_class(這裡是 PostFilter)中定義的過濾規則來過濾查詢結果集。
當然 PostFilter 還沒有定義,我們來定義它。首先在 blog 應用下創建一個 filters.py 文件,用於存放自定義 filter 的代碼,PostFilter 代碼如下:
from django_filters import rest_framework as drf_filters
from .models import Post
class PostFilter(drf_filters.FilterSet):
created_year = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="year"
)
created_month = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="month"
)
class Meta:
model = Post
fields = ["category", "tags", "created_year", "created_month"]
PostFilter 的定義和序列化器 Serializer 非常類似。
category,tags 兩個過濾字段因為是 Post 模型中定義的字段,因此 django-filter 可以自動推斷其過濾規則,只需要在 Meta.fields 中聲明即可。
歸檔日期下的文章列表,我們設計的接口傳遞 2 個查詢參數:年份和月份。由於這兩個字段在 Post 中沒有定義,Post 記錄時間的字段為 created_time,因此我們需要显示地定義查詢規則,定義的規則是:
查詢參數名 = 查詢參數值的類型(查詢的模型字段,查詢表達式)
例如示例中定義的 created_year 查詢參數,查詢參數值的類型為 number,即数字,查詢的模型字段為 created_time,查詢表達式是 year。當用戶傳遞 created_year 查詢參數時,django-filter 實際上會將以上定義的規則翻譯為如下的 ORM 查詢語句:
Post.objects.filter(created_time__year=created_year傳遞的值)
現在回到 API 交互後台,先進到 /post/ 接口下,默認返回了全部文章列表。可以看到右上角多了個過濾器(紅框圈出部分)。
點擊會彈出過濾參數輸入的交互面板,在這裏可以交互式地輸入查詢過濾參數的值。
例如選擇如下的過濾參數,得到查詢的 URL 為:
http://127.0.0.1:10000/api/posts/?category=1&tags=1&created_year=2020&created_month=1
這條查詢返回創建於 2020 年 1 月,id 為 1 的分類下,id 為 1 的標籤下的全部文章。
通過不同的查詢參數組合,就可以得到不同的文章資源列表了。
關注公眾號加入交流群
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※帶您來了解什麼是 USB CONNECTOR ?
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!
※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※教你寫出一流的銷售文案?
談到服務註冊,首先我們先了解一下服務註冊時使用的三種方式,也代表了不同的服務生命周期:
1 AddTransient 2 AddScoped 3 AddSingleton
AddSingleton生命周期最長,其生命周期範圍描述為:從應用程序啟動到應用程序結束。在第一次請求時會創建一個實例,之後的每次請求都會使用同一個實例。
AddTransient生命周期最短,在服務請求時會創建一個實例,服務請求結束生命周期即結束,之後的每一次請求都會創建不同的實例。
AddSingleton生命周期介於上述兩者之間,這裏用客戶端請求會話的概念來描述比較清晰一點,它也是在服務請求時創建實例,但是在同一個會話周期內,之後的每次請求都會使用同一個實例,直至會話結束才會創建新的實例。
ASP.Net Core框架支持我們以如下方式註冊我們自己的服務。
services.AddScoped<ITest, Test>();
其中第一個泛型類型(如:ITest)表示將要從容器中請求的類型(通常是一個接口)。第二個泛型類型(如:Test)表示將由容器實例化並且用於完成這些請求的具體實現類。
具體我們一起看下面的例子:
首先,我們創建一個需要實現查詢功能的服務接口ITest
public interface ITest { Task<string> Get(); }
然後,我們創建功能類Test實現這個接口
1 public class Test : ITest 2 { 3 private readonly ILogger logger; 4 public Test(ILogger<Test> _logger) 5 { 6 logger = _logger; 7 } 8 public Task<string> Get() 9 { 10 logger.LogInformation("自定義服務查詢"); 11 return Task.FromResult("Hello World"); 12 } 13 }
最後,我們需要我們自己的服務註冊到容器中。
public void ConfigureServices(IServiceCollection services) { services.AddScoped<ITest, Test>(); }
以上我們便簡單完成了自定義服務的註冊。
隨後我這裏創建了一個Controller用以使用該服務。
1 [Route("api/[controller]")] 2 [ApiController] 3 public class ValuesController : ControllerBase 4 { 5 //聲明服務 6 private readonly ITest service; 7 8 /// <summary> 9 /// 通過構造函數的方式注入自定義服務類 10 /// </summary> 11 /// <param name="_service"></param> 12 public ValuesController(ITest _service) 13 { 14 service = _service; 15 } 16 17 /// <summary> 18 /// 調用服務中實現的Get方法 19 /// </summary> 20 /// <returns></returns> 21 [HttpGet] 22 public Task<string> Get() 23 { 24 return service.Get(); 25 } 26 }
ASP.Net Core框架默認支持我們以構造函數的方式注入我們的服務以使用。
我想寫到這裏,大家也會有疑問,如果我們有很多service,這樣一個個註冊寫起來代碼很低效,這裏我們簡單給大家介紹一種批量註冊的方式:
這裏我們創建了一個批量註冊服務派生類:
1 public static class ServiceExtensions 2 { 3 /// <summary> 4 /// 批量註冊程序集下的服務類 5 /// </summary> 6 /// <param name="services"></param> 7 public static IServiceCollection AddBatchServices(this IServiceCollection services) 8 { 9 //根據指定程序集名稱獲取待註冊服務 10 var batchServices = GetConfigureClass("WebApiApplication"); 11 foreach (var type in batchServices) 12 { 13 type.Value.ToList().ForEach(i => 14 { 15 //註冊服務類 16 services.AddScoped(i, type.Key); 17 }); 18 } 19 return services; 20 } 21 22 /// <summary> 23 /// 根據程序集名稱獲取自定義服務 24 /// </summary> 25 /// <param name="assembly"></param> 26 /// <returns></returns> 27 public static Dictionary<Type, Type[]> GetConfigureClass(string assembly) 28 { 29 Dictionary<Type, Type[]> dic = new Dictionary<Type, Type[]>(); 30 if (!string.IsNullOrEmpty(assembly)) 31 { 32 //獲取程序集對應的類型 33 Assembly dll = Assembly.LoadFrom(assembly); 34 List<Type> lstType = dll.GetTypes().ToList(); 35 lstType.ForEach(x => 36 { 37 //篩選滿足條件的服務類 38 if (x.IsClass && x.GetInterfaces().Length > 0) 39 { 40 dic.Add(x, x.GetInterfaces()); 41 } 42 }); 43 } 44 return dic; 45 } 46 }
然後我們ConfigureServices方法中註冊:
public void ConfigureServices(IServiceCollection services) { //批量註冊 services.AddBatchServices(); }
對於批量註冊,ASP.Net Core允許我們更換默認的IOC容器,感興趣的同學可以試試AutoFac容器支持的程序集掃描式註冊。
註冊我們自己的服務,往往在項目開發過程中是必要的,希望以上簡單的分享能給需要的小夥伴們帶來一點收貨。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※為什麼 USB CONNECTOR 是電子產業重要的元件?
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※台北網頁設計公司全省服務真心推薦
※想知道最厲害的網頁設計公司"嚨底家"!
※推薦評價好的iphone維修中心
作者:小傅哥
博客:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!
你感受到的容易,一定有人為你承擔不容易
這句話更像是描述生活的,許許多多的磕磕絆絆總有人為你提供躲雨的屋檐和避風的港灣。其實編程開發的團隊中也一樣有人只負責CRUD中的簡單調用,去使用團隊中高級程序員開發出來的核心服務和接口。這樣的編程開發對於初期剛進入程序員行業的小夥伴來說鍛煉鍛煉還是不錯的,但隨着開發的日子越來越久一直做這樣的事情就很難得到成長,也想努力的去做一些更有難度的承擔,以此來增強個人的技術能力。
沒有最好的編程語言,語言只是工具
刀槍棍棒、斧鉞鈎叉、包子油條、盒子麻花,是語言。五郎八卦棍、十二路彈腿、洪家鐵線拳,是設計。記得恭弘=叶 恭弘問里有一句台詞是:金山找:今天我北方拳術,輸給你南方拳術了。恭弘=叶 恭弘問:你錯了,不是南北拳的問題,是你的問題。所以當你編程開發寫的久了,就不會再特別在意用的語言,而是為目標服務,用最好的設計能力也就是編程的智慧做出做最完美的服務。這也就是編程人員的價值所在!
設計與反設計以及過渡設計
設計模式是解決程序中不合理、不易於擴展、不易於維護的問題,也是幹掉大部分ifelse的利器,在我們常用的框架中基本都會用到大量的設計模式來構建組件,這樣也能方便框架的升級和功能的擴展。但!如果不能合理的設計以及亂用設計模式,會導致整個編程變得更加複雜難維護,也就是我們常說的;反設計、過渡設計。而這部分設計能力也是從實踐的項目中獲取的經驗,不斷的改造優化摸索出的最合理的方式,應對當前的服務體量。
bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)| 工程 | 描述 |
|---|---|
| itstack-demo-design-10-00 | 場景模擬工程;模擬一個提供接口服務的SpringBoot工程 |
| itstack-demo-design-10-01 | 使用一坨代碼實現業務需求 |
| itstack-demo-design-10-02 | 通過設計模式開發為中間件,包裝通用型核心邏輯 |
外觀模式也叫門面模式,主要解決的是降低調用方的使用接口的複雜邏輯組合。這樣調用方與實際的接口提供方提供方提供了一个中間層,用於包裝邏輯提供API接口。有些時候外觀模式也被用在中間件層,對服務中的通用性複雜邏輯進行中間件層包裝,讓使用方可以只關心業務開發。
那麼這樣的模式在我們的所見產品功能中也經常遇到,就像幾年前我們註冊一個網站時候往往要添加很多信息,包括;姓名、昵稱、手機號、QQ、郵箱、住址、單身等等,但現在註冊成為一個網站的用戶只需要一步即可,無論是手機號還是微信也都提供了這樣的登錄服務。而對於服務端應用開發來說以前是提供了一個整套的接口,現在註冊的時候並沒有這些信息,那麼服務端就需要進行接口包裝,在前端調用註冊的時候服務端獲取相應的用戶信息(從各個渠道),如果獲取不到會讓用戶後續進行補全(營銷補全信息給獎勵),以此來拉動用戶的註冊量和活躍度。
在本案例中我們模擬一個將所有服務接口添加白名單的場景
在項目不斷壯大發展的路上,每一次發版上線都需要進行測試,而這部分測試驗證一般會進行白名單開量或者切量的方式進行驗證。那麼如果在每一個接口中都添加這樣的邏輯,就會非常麻煩且不易維護。另外這是一類具備通用邏輯的共性需求,非常適合開發成組件,以此來治理服務,讓研發人員更多的關心業務功能開發。
一般情況下對於外觀模式的使用通常是用在複雜或多個接口進行包裝統一對外提供服務上,此種使用方式也相對簡單在我們平常的業務開發中也是最常用的。你可能經常聽到把這兩個接口包裝一下,但在本例子中我們把這種設計思路放到中間件層,讓服務變得可以統一控制。
itstack-demo-design-10-00
└── src
├── main
│ ├── java
│ │ └── org.itstack.demo.design
│ │ ├── domain
│ │ │ └── UserInfo.java
│ │ ├── web
│ │ │ └── HelloWorldController.java
│ │ └── HelloWorldApplication.java
│ └── resources
│ └── application.yml
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
SpringBoot的HelloWorld工程,在工程中提供了查詢用戶信息的接口HelloWorldController.queryUserInfo,為後續擴展此接口的白名單過濾做準備。@RestController
public class HelloWorldController {
@Value("${server.port}")
private int port;
/**
* key:需要從入參取值的屬性字段,如果是對象則從對象中取值,如果是單個值則直接使用
* returnJson:預設攔截時返回值,是返回對象的Json
*
* http://localhost:8080/api/queryUserInfo?userId=1001
* http://localhost:8080/api/queryUserInfo?userId=小團團
*/
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
}
}
userId,查詢用戶信息。後續就需要在這裏擴展白名單,只有指定用戶才可以查詢,其他用戶不能查詢。@SpringBootApplication
@Configuration
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}
}
SpringBoot啟動類。需要添加的是一個配置註解@Configuration,為了後續可以讀取白名單配置。一般對於此種場景最簡單的做法就是直接修改代碼
累加if塊幾乎是實現需求最快也是最慢的方式,快是修改當前內容很快,慢是如果同類的內容幾百個也都需要如此修改擴展和維護會越來越慢。
itstack-demo-design-10-01
└── src
└── main
└── java
└── org.itstack.demo.design
└── HelloWorldController.java
public class HelloWorldController {
public UserInfo queryUserInfo(@RequestParam String userId) {
// 做白名單攔截
List<String> userList = new ArrayList<String>();
userList.add("1001");
userList.add("aaaa");
userList.add("ccc");
if (!userList.contains(userId)) {
return new UserInfo("1111", "非白名單可訪問用戶攔截!");
}
return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
}
}
接下來使用外觀器模式來進行代碼優化,也算是一次很小的重構。
這次重構的核心是使用外觀模式也可以說門面模式,結合SpringBoot中的自定義starter中間件開發的方式,統一處理所有需要白名單的地方。
後續接下來的實現中,會涉及的知識;
itstack-demo-design-10-02
└── src
├── main
│ ├── java
│ │ └── org.itstack.demo.design.door
│ │ ├── annotation
│ │ │ └── DoDoor.java
│ │ ├── config
│ │ │ ├── StarterAutoConfigure.java
│ │ │ ├── StarterService.java
│ │ │ └── StarterServiceProperties.java
│ │ └── DoJoinPoint.java
│ └── resources
│ └── META_INF
│ └── spring.factories
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
門面模式模型結構
public class StarterService {
private String userStr;
public StarterService(String userStr) {
this.userStr = userStr;
}
public String[] split(String separatorChar) {
return StringUtils.split(this.userStr, separatorChar);
}
}
@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {
private String userStr;
public String getUserStr() {
return userStr;
}
public void setUserStr(String userStr) {
this.userStr = userStr;
}
}
application.yml 中添加 itstack.door 的配置信息。@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {
@Autowired
private StarterServiceProperties properties;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
StarterService starterService() {
return new StarterService(properties.getUserStr());
}
}
@Configuration、@ConditionalOnClass、@EnableConfigurationProperties,這一部分主要是與SpringBoot的結合使用。@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {
String key() default "";
String returnJson() default "";
}
@Aspect
@Component
public class DoJoinPoint {
private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);
@Autowired
private StarterService starterService;
@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
public void aopPoint() {
}
@Around("aopPoint()")
public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
//獲取內容
Method method = getMethod(jp);
DoDoor door = method.getAnnotation(DoDoor.class);
//獲取字段值
String keyValue = getFiledValue(door.key(), jp.getArgs());
logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
if (null == keyValue || "".equals(keyValue)) return jp.proceed();
//配置內容
String[] split = starterService.split(",");
//白名單過濾
for (String str : split) {
if (keyValue.equals(str)) {
return jp.proceed();
}
}
//攔截
return returnObject(door, method);
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
return jp.getTarget().getClass();
}
//返回對象
private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
Class<?> returnType = method.getReturnType();
String returnJson = doGate.returnJson();
if ("".equals(returnJson)) {
return returnType.newInstance();
}
return JSON.parseObject(returnJson, returnType);
}
//獲取屬性值
private String getFiledValue(String filed, Object[] args) {
String filedValue = null;
for (Object arg : args) {
try {
if (null == filedValue || "".equals(filedValue)) {
filedValue = BeanUtils.getProperty(arg, filed);
} else {
break;
}
} catch (Exception e) {
if (args.length == 1) {
return args[0].toString();
}
}
}
return filedValue;
}
}
Object doRouter(ProceedingJoinPoint jp),接下來我們分別介紹。@Pointcut(“@annotation(org.itstack.demo.design.door.annotation.DoDoor)”)
定義切面,這裏採用的是註解路徑,也就是所有的加入這個註解的方法都會被切面進行管理。
getFiledValue
獲取指定key也就是獲取入參中的某個屬性,這裏主要是獲取用戶ID,通過ID進行攔截校驗。
returnObject
返回攔截后的轉換對象,也就是說當非白名單用戶訪問時則返回一些提示信息。
doRouter
切面核心邏輯,這一部分主要是判斷當前訪問的用戶ID是否白名單用戶,如果是則放行jp.proceed();,否則返回自定義的攔截提示信息。
這裏的測試我們會在工程:itstack-demo-design-10-00中進行操作,通過引入jar包,配置註解的方式進行驗證。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>itstack-demo-design-10-02</artifactId>
</dependency>
# 自定義中間件配置
itstack:
door:
enabled: true
userStr: 1001,aaaa,ccc #白名單用戶ID,多個逗號隔開
/**
* http://localhost:8080/api/queryUserInfo?userId=1001
* http://localhost:8080/api/queryUserInfo?userId=小團團
*/
@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
}
@DoDoor,也就是我們的外觀模式中間件化實現。 . ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.2.RELEASE)
2020-06-11 23:56:55.451 WARN 65228 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2020-06-11 23:56:55.531 INFO 65228 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-11 23:56:55.533 INFO 65228 --- [ main] o.i.demo.design.HelloWorldApplication : Started HelloWorldApplication in 1.688 seconds (JVM running for 2.934)
白名單用戶訪問
http://localhost:8080/api/queryUserInfo?userId=1001
{"code":"0000","info":"success","name":"蟲蟲:1001","age":19,"address":"天津市南開區旮旯衚衕100號"}
非白名單用戶訪問
http://localhost:8080/api/queryUserInfo?userId=小團團
{"code":"1111","info":"非白名單可訪問用戶攔截!","name":null,"age":null,"address":null}
userId換成小團團,此時返回的信息已經是被攔截的信息。而這個攔截信息正式我們自定義註解中的信息:@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
如果想從頭學起Cypress,可以看下面的系列文章哦
https://www.cnblogs.com/poloyy/category/1768839.html
這一篇着重講點擊操作,一共有三個命令
單擊某個元素
// 單擊某個元素 .click() // 帶參數的單擊 .click(options) // 在某個位置點擊 .click(position) // 在某個位置點擊,且帶參數 .click(position, options) // 根據頁面坐標點擊 .click(x, y) // 根據頁面坐標點擊,且帶參數 .click(x, y, options)
宗旨:先獲取 DOM 元素,再對 DOM 元素操作
每個元素都有九個 position,具體可看下圖
距離 DOM 元素左上角的坐標,x 是橫軸,y 是豎軸
共有四個
// 強制點擊,和所有後續事件 // 即使該元素 “不可操作”,也會觸發點擊操作 cy.get('button').click({ force: true })
總而言之, { force: true } 跳過檢查,它將始終在所需元素處觸發事件
cy.get(‘ ul > li ‘) 共匹配四個 DOM 元素,他們均觸發單擊操作
.click() 命令還可以與 .type() 命令結合使用修飾符來觸發組合鍵操作,以便在單擊時結合鍵盤操作,例如ALT + click
| 修飾符 | 作用 | 別名 |
|---|---|---|
{alt} |
等價於 alt 鍵 | {option} |
| {ctrl} | 等價於 ctrl 鍵 | {control} |
| {shift} | 等價於 shift 鍵 |
雙擊,跟 click() 的語法 & 用法一致,只是變成了雙擊
cy.get("#main1").dblclick()
cy.get("#main1").dblclick("top")
cy.get("#main1").dblclick(15, 15)
右鍵,跟 click() 的語法 & 用法一致,只是變成了右鍵點擊
cy.get("#li1").rightclick()
cy.get("#li1").rightclick("top")
cy.get("#li1").rightclick(15, 15)
執行 .click() 必須是 DOM 元素達到了可操作狀態
.click() 將自動等待元素達到可操作狀態。
.click() 將自動等待後面鏈接的斷言通過
.click() 如果 DOM 元素一直達不到可操作狀態,可能會超時
.click() 如果後面鏈接的斷言一直不通過,可能會超時
在命令日誌中單擊 click 時,控制台console 將輸出以下鼠標事件
結尾
本文是博主基於對蔡超老師的《Cypress 從入門到精通》閱讀理解完后輸出的博文,並附上了自己的理解
對書籍感興趣的,大家可以參考本篇博客:https://www.cnblogs.com/poloyy/p/13052972.html,考慮自身需求進行購買
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=12vd92hxgwgj1
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※Google地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計最專業,超強功能平台可客製化
Jmeter官網對邏輯控制器的解釋是:“Logic Controllers determine the order in which Samplers are processed.”。
意思是說,邏輯控制器可以控制採樣器(samplers)的執行順序。由此可知,控制器需要和採樣器一起使用,否則控制器就沒有什麼意義了。放在控制器下面的所有的採樣器都會當做一個整體,執行時也會一起被執行。
JMeter邏輯控制器可以對元件的執行邏輯進行控制,除僅一次控制器外,其他可以嵌套別的種類的邏輯控制器。
JMeter中的Logic Controller分為兩類:
(1)控制測試計劃執行過程中節點的邏輯執行順序,如:Loop Controller、If Controller等;
(2)對測試計劃中的腳本進行分組、方便JMeter統計執行結果以及進行腳本的運行時控制等,如:Throughput Controller、Transaction Controller。
首先我們來看一下JMeter的邏輯控制器,路徑:線程組(用戶)->添加->邏輯控制器(Logic Controller);我們可以清楚地看到JMeter5中共有17個邏輯控制器,如下圖所示:
如果上圖您看得不是很清楚的話,宏哥總結了一個思維導圖,關於JMeter5的邏輯控制器類型,如下圖所示:
通過以上的了解,我們對邏輯控制器有了一個大致的了解和認識。下面宏哥就給小夥伴或則童鞋們分享講解一些通常在工作中會用到的邏輯控制器。
這一小節,宏哥就由上而下地詳細地講解一下常用的邏輯控制器。由於時間關係,宏哥將這部分分為上、中、下三個部分講解。
在實際工作中,當使用Jmeter進行接口測試或者性能測試時,有時需要根據不同條件做不同的操作,為了解決這個問題,Jmeter提供了IF控制器。顧名思義,IF控制器實現了代碼中IF的功能,通過判斷表達式的True/False來判定是否執行相應的操作。通過條件判斷下邊的節點執行不執行。
1、我們先來看看這個if Controller長得是啥樣子,路徑:線程組 > 添加 > 邏輯控制器 > 如果 (if) 控制器,如下圖所示:
2、關鍵參數說明如下:
Name:名稱,可以隨意設置,甚至為空;
Comments:註釋,可隨意設置,可以為空。
Expression (must evaluate to true or false) :表達式(值必須是true或false),也就是說,在右邊文本框中輸入的條件值必須是true 或 false,(默認情況下)
Interpret Condition as Variable Expression?:默認勾選項,將條件解釋為變量表達式(需要使用__jexl3 or __groovy 表達式)
Evaluate for all children?:條件作用於每個子項(具體理解宏哥會在後邊實戰篇列舉例子說明),判斷條件是否針對所有子節點,默認不勾選,只在if Controller的入口處判斷一次。
注意:敲黑板!!!敲腦殼啦!!!
1、文本框上的黃色感嘆號,就是提示你,建議採用__jexl3 or __groovy 表達式,以提高性能,也就是默認的方式。
2、if 控制器 只能作用於其下的子項
1、默認用法,就是採用__jexl3 or __groovy 表達式if 控制器下有一個 訪問北京宏哥的博客園首頁的取樣器,只有if條件滿足時,才會執行該取樣器。採用默認方式,將條件’北京宏哥’==’北京宏哥’,放入 __jexl3表達式中。如下圖所示:
2、如果不知道表達式如何使用,可使用Jmeter 的函數助手,函數助手圖標 > 選擇_jexl3 > 在值的輸入框輸入’北京宏哥’==’北京宏哥’ > 點擊‘生成’ > 全選Ctrl+C複製 > Ctrl+V粘貼到表達式處 如下圖所示:
3、配置好以後,運行JMeter,選擇HTML,然後查看結果樹,如下圖所示:
1、直接輸入只需要去掉 “Interpret Condition as Variable Expression?” 前面複選框,直接輸入條件: ‘北京宏哥’==’北京宏哥’ 。訪問北京宏哥的博客園的首頁的取樣器將被執行。如下圖所示:
2、配置好以後,運行JMeter,選擇HTML,然後查看結果樹,如下圖所示:
我們在日常工作中在很多的測試場景下,需要根據用戶變量或者上一個取樣器的返回值來進行條件判斷,從而決定是否需要執行某一個的取樣器。
1、首先我們新增一個用戶變量:北京宏哥。條件:北京宏哥 的值為 宏哥 的時候,才執行訪問北京宏哥博客園的首頁的取樣器。如下圖所示:
用戶變量及配置,如下圖所示:
2、IF Controller及配置,或者可以用表達式:${__jexl3(‘${北京宏哥}’==’宏哥’ ,)}。如下圖所示:
1、宏哥在上面的小節中講解和分享了在條件中如何使用變量,我們假設一種測試場景:如果 if 控制器下的取樣器執行后,改變了該變量的值,if 控制器下 其後的取樣器還會被繼續執行嗎?跟隨宏哥一起來看看下面的列子:
2、改變“北京宏哥”變量的值為“北京宏哥”,如下圖所示:
3、JMeter執行過程的邏輯分析:
(1) if 控制器下 有 3 個取樣器,變量 北京宏哥 的初始值為 宏哥,if 控制器的條件為:${__jexl3(“${北京宏哥}”==”宏哥”,)}。
(2)開始執行的時候滿足條件,那麼按理說應該執行 訪問博客園首頁 、訪問北京宏哥的博客園首頁、訪問宏哥的JMeter系列文章 3個取樣器,
(3)但是 訪問北京宏哥的博客園首頁 執行后,將 北京宏哥 的值變了 北京宏哥,已經不能滿足 “${北京宏哥}”==”宏哥” 條件。
(4)所以 訪問宏哥的JMeter系列文章 這個取樣器不會被執行。
4、運行JMeter,查看結果樹,對比運行結果和宏哥分析的一致,如下圖所示:
5、如果這個時候,去掉 Evaluate for all children? 的勾選,會發生什麼呢,大家可以自己動手試試。修改後記得點擊“保存”。下邊是宏哥的執行結果,如下下圖所示:
另外,如果時字符串必須要用引號,變量都認為是字符串的形式,如:${__jexl3(“${北京宏哥}”==”宏哥”,)}。
事務響應時間是我們衡量業務性能的主要指標,事務控制器可以把其他節點下的取樣器執行消耗時間累加在一起,便於統計。同時對每一個取樣器的執行時間進行統計。
如果事務控制器下的取樣器有多個,只有當所有的取樣器都運行成功,整個事務控制器定義的事物才算成功。
用於將Test Plan中的特定部分組織成一個Transaction,JMeter中Transaction的作用在於,可以針對Transaction統計其響應時間、吞吐量等。比如說,一個用戶操作可能需要多個Sampler來模擬,此時使用Transaction Controller,可以更準確地得到該用戶操作的性能指標,如響應時間等。這個時間包含該控制器範圍內的所有處理時間,而不僅僅是採樣器的。
這個就非常有用了。我們前面有提到過事務的概念,有時候我們不關心單個請求的響應時間,而是關心一組相關請求的整體響應時間,怎麼來統計呢?就需要藉助事務這個概念,把這組請求,放到一個事務控制器下面。
1、我們先來看看這個Transaction Controller長得是啥樣子,路徑:線程組 > 添加 > 邏輯控制器 > 事務控制器,如下圖所示:
2、關鍵參數說明如下:
Name:名稱,可以隨意設置,甚至為空;
Comments:註釋,可隨意設置,可以為空;
generate parent sample:選擇是否生成一個父取樣器;
include duration of timer and pre-post processors in generated samle:是否包含定時器,選擇將在取樣器前與后加上延時。(宏哥建議大家不要勾選,否則統計就比較麻煩了,還需要你扣除延時)
1、宏哥列舉一個測試場景:我們需要了解 訪問博客園首頁 訪問北京宏哥的博客園首頁這兩個請求的單個請求的響應時間,那麼就來看看如下實例。
(1)單個請求,那麼不勾選generate parent sample,如下圖所示:
2、運行JMeter,查看聚合報告的單個請求的響應時間,如下圖所示:
1、宏哥列舉一個測試場景:我們需要了解 訪問博客園首頁 訪問北京宏哥的博客園首頁這兩個請求作為一組請求的響應時間,那麼就來看看如下實例。
(1)一組請求,那麼勾選generate parent sample,如下圖所示:
2、運行JMeter,查看聚合報告的一組請求的響應時間,如下圖所示:
循環控制器可以控制在其節點下的元件的執行次數,可以是具體数字,也可以是變量。
1、我們先來看看這個Loop Controller長得是啥樣子,默認循環一次。路徑:線程組 > 添加 > 邏輯控制器 > 循環控制器,如下圖所示:
2、關鍵參數說明如下:
Name:名稱,可以隨意設置,甚至為空;
Comments:註釋,可隨意設置,可以為空;
Forever:勾選上這一項表示一直循環下去。
注意:敲黑板,敲腦殼!!!
如果同時設置了線程組的循環次數和循環控制器的循環次數,那循環控制器的子節點運行的次數為兩個數值相乘的結果。
1、現在宏哥準備兩個請求,設置線程組1個線程,5次loop,下邊有一個請求:訪問北京宏哥的博客園首頁 一個Loop Controller(設置2次loop),下邊有一個請求:訪問博客園首頁
(1)線程組,如下圖所示:
(2)循環控制器,如下圖所示:
2、運行JMeter,查看結果樹,為了清楚地看出結果,宏哥將第一個請求故意配置成失敗的;如下圖所示:
從上邊的結果可以看出:
(1)如果同時設置了線程組的循環次數和循環控制器的循環次數,那循環控制器的子節點運行的次數為兩個數值相乘的結果。
(2)運行順序是:先執行線程組裡的循環,再執行循環控制器里的循環。
While條件控制器,其節點下的元件將一直運行直到While 條件為false。
1、我們先來看看這個While Controller長得是啥樣子,默認循環一次。路徑:線程組 > 添加 > 邏輯控制器 > While控制器,如下圖所示:
2、關鍵參數說明如下:
Name:名稱,可以隨意設置,甚至為空;
Comments:註釋,可隨意設置,可以為空;
Condition:接受變量表達式與變量。條件為 Flase 的時候,才會跳出 While 循環,否則一直執行 While 控制器下的元件。
3、While控制器提供三個常量
(1)Blank:當循環中最後一個取樣器失敗后停止
(2)LAST:當循換前有取樣器失敗,不進入循環
(3)Otherwise:當判斷條件為false時,停止循環
1、不填(空):當 While 控制器下最後一個樣例執行失敗后 跳出循環,如下圖所示:
2、運行JMeter,查看結果樹,(你可以通過鼠標拖動最後失敗的取樣器,移動到第一個或者第二個位置的時候,運行JMeter后,會發現在一直運行);如下圖所示:
LAST :當 While 控制器下最後一個樣例執行失敗后 跳出循環,如果 While 控制器 前一個樣例執行失敗,則不會進入While循環,也就是不會執行While控制器下的樣例。
1、取樣器樹還是上邊的位置和順序。這次我們在While控制器表達式處填寫:LAST,如下圖所示:
2、運行JMeter,查看結果樹,(你可以通過鼠標拖動最後失敗的取樣器,移動到第一個或者第二個位置的時候,運行JMeter后,會發現在一直運行);細心的你可以發現循環只跑一遍,與不填 的結果是一樣的如下圖所示:
3、但是輸入LAST的時候,還會出現一個結果,那就是:如果While 控制器 的前一個樣例執行失敗,則不會進入While 控制器
在While 控制器 前面 添加兩個取樣器:取樣器1 訪問百度,取樣器2 訪問北京宏哥 使取樣器2 訪問北京宏哥 執行失敗。取樣器2必須在While控制器前邊且執行失敗。如下圖所示:
4、運行JMeter,查看結果樹,執行結果發現,取樣器1、取樣器2 執行了,但沒有進入While 控制器,如下圖所示:
自定義條件:值為True 或 False的函數/變量/屬性 表達式;類似前邊講解的IF控制器,宏哥這裏就照貓畫虎的舉個例子。
1、用戶自定義變量,變量名:北京宏哥,變量值:true,如下圖所示:
2、While控制器配置,取到變量的值:${北京宏哥},填寫到表達式的地方,如下圖所示:
3、JMeter執行過程的邏輯分析:
(1)北京宏哥用戶(線程組)下 有 1 個用戶自定義變量,變量 北京宏哥 的值為 true,While控制器的條件為:${北京宏哥} 取到的值始終是 true。
(2)所以一旦開始執行始終滿足條件,那麼按理說就應該一直執行 訪問博客園首頁 、訪問北京宏哥的博客園首頁、訪問宏哥的JMeter系列文章 3個取樣器,
4、運行JMeter,查看結果樹,(運行JMeter后,會發現在一直運行),對比一下,與宏哥的分析是不是高度一致哈;如下圖所示:
好了,今天關於邏輯控制器的上篇就講解到這裏,這一篇主要介紹了 IF控制器、Transaction Controller、Loop Controller和While控制器。
您的肯定就是我進步的動力。如果你感覺還不錯,就請鼓勵一下吧!記得隨手點波 推薦 不要忘記哦!!!
別忘了點 推薦 留下您來過的痕迹
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※教你寫出一流的銷售文案?
synchronized鎖的原理也是大廠面試中經常會涉及的問題,本文主要通過對以下問題進行分析講解,來幫助大家理解synchronized鎖的原理。
1.synchronized鎖是什麼?鎖的對象是什麼?
2.偏向鎖,輕量級鎖,重量級鎖的執行流程是怎樣的?
3.為什麼說是輕量級,重量級鎖是不公平的?
4.重量級鎖為什麼需要自旋操作?
5.什麼時候會發生鎖升級,鎖降級?
6.偏向鎖,輕量鎖,重量鎖的適用場景,優缺點是什麼?
synchronized的英文意思就是同步的意思,就是可以讓synchronized修飾的方法,代碼塊,每次只能有一個線程在執行,以此來實現數據的安全。
一般可以修飾同步代碼塊、實例方法、靜態方法,加鎖對象分別為同步代碼塊塊括號內的對象、實例對象、類。
在實現原理上,
public class SyncTest {
private Object lockObject = new Object();
public void syncBlock(){
//修飾代碼塊,加鎖對象為lockObject
synchronized (lockObject){
System.out.println("hello block");
}
}
//修飾實例方法,加鎖對象為當前的實例對象
public synchronized void syncMethod(){
System.out.println("hello method");
}
//修飾靜態方法,加鎖對象為當前的類
public static synchronized void staticSyncMethod(){
System.out.println("hello method");
}
}
在JVM中,一個Java對象其實由對象頭+實例數據+對齊填充三部分組成,而對象頭主要包含Mark Word+指向對象所屬的類的指針組成(如果是數組對象,還會包含長度)。像下圖一樣:
Mark Word:存儲對象自身的運行時數據,例如hashCode,GC分代年齡,鎖狀態標誌,線程持有的鎖等等。在32位系統佔4字節,在64位系統中佔8字節,所以它能存儲的數據量是有限的,所以主要通過設立是否偏向鎖的標誌位和鎖標誌位用於區分其他位數存儲的數據是什麼,具體請看下圖:
鎖信息都是存在鎖對象的Mark Word中的,當對象狀態為偏向鎖時,Mark Word存儲的是偏向的線程ID;當狀態為輕量級鎖時,Mark Word存儲的是指向線程棧中Lock Record的指針;當狀態為重量級鎖時,Mark Word為指向堆中的monitor對象的指針。
這是網上找到的一個流程圖,可以先看流程圖,結合著文字來了解執行流程
Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,於是引入了偏向鎖。
簡單的來說,就是主要鎖處於偏向鎖狀態時,會在Mark Word中存當前持有偏向鎖的線程ID,如果獲取鎖的線程ID與它一致就說明是同一個線程,可以直接執行,不用像輕量級鎖那樣執行CAS操作來加鎖和解鎖。
線程發現是匿名偏向狀態(也就是鎖對象的Mark Word沒有存儲線程ID),則會用CAS指令,將mark word中的thread id由0改成當前線程Id。如果成功,則代表獲得了偏向鎖,繼續執行同步塊中的代碼。否則,將偏向鎖撤銷,升級為輕量級鎖。
發現鎖對象存儲的線程ID就是當前線程的ID,會往當前線程的棧中添加一條Displaced Mark Word為空的Lock Record中,然後繼續執行同步塊的代碼,因為操縱的是線程私有的棧,因此不需要用到CAS指令;由此可見偏向鎖模式下,當被偏向的線程再次嘗試獲得鎖時,僅僅進行幾個簡單的操作就可以了,在這種情況下,synchronized關鍵字帶來的性能開銷基本可以忽略。
當沒有獲得鎖的線程進入同步塊時,發現當前是偏向鎖狀態,並且存儲的是其他線程ID(也就是其他線程正在持有偏向鎖),則會進入到撤銷偏向鎖的邏輯里,一般來說,會在safepoint中去查看偏向的線程是否還存活
mark word改為無鎖狀態(unlocked)由此可見,偏向鎖升級的時機為:當一個線程獲得了偏向鎖,在執行時,只要有另一個線程嘗試獲得偏向鎖,並且當前持有偏向鎖的線程還在同步塊中執行,則該偏向鎖就會升級成輕量級鎖。
因此偏向鎖的解鎖很簡單,其僅僅將線程的棧中的最近一條lock record的obj字段設置為null。需要注意的是,偏向鎖的解鎖步驟中並不會修改鎖對象Mark Word中的thread id,簡單的說就是鎖對象處於偏向鎖時,Mark Word中的thread id 可能是正在執行同步塊的線程的id,也可能是上次執行完已經釋放偏向鎖的thread id,主要是為了上次持有偏向鎖的這個線程在下次執行同步塊時,判斷Mark Word中的thread id相同就可以直接執行,而不用通過CAS操作去將自己的thread id設置到鎖對象Mark Word中。
這是偏向鎖執行的大概流程:
重量級鎖依賴於底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行,這種切換的代價是非常昂貴的,而在大部分時候可能並沒有多線程競爭,只是這段時間是線程A執行同步塊,另外一段時間是線程B來執行同步塊,僅僅是多線程交替執行,並不是同時執行,也沒有競爭,如果採用重量級鎖效率比較低。以及在重量級鎖中,沒有獲得鎖的線程會阻塞,獲得鎖之後線程會被喚醒,阻塞和喚醒的操作是比較耗時間的,如果同步塊的代碼執行比較快,等待鎖的線程可以進行先進行自旋操作(就是不釋放CPU,執行一些空指令或者是幾次for循環),等待獲取鎖,這樣效率比較高。所以輕量級鎖天然瞄準不存在鎖競爭的場景,如果存在鎖競爭但不激烈,仍然可以用自旋鎖優化,自旋失敗后再升級為重量級鎖。
JVM會為每個線程在當前線程的棧幀中創建用於存儲鎖記錄的空間,我們稱為Displaced Mark Word。如果一個線程獲得鎖的時候發現是輕量級鎖,會把鎖的Mark Word複製到自己的Displaced Mark Word裏面。
然後線程嘗試用CAS操作將鎖的Mark Word替換為自己線程棧中拷貝的鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示Mark Word已經被替換成了其他線程的鎖記錄,說明在與其它線程競爭鎖,當前線程就嘗試使用自旋來獲取鎖。
自旋:不斷嘗試去獲取鎖,一般用循環來實現。
自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。
JDK採用了適應性自旋,簡單來說就是線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。
自旋也不是一直進行下去的,如果自旋到一定程度(和JVM、操作系統相關),依然沒有獲取到鎖,稱為自旋失敗,那麼這個線程會阻塞。同時這個鎖就會升級成重量級鎖。
在釋放鎖時,當前線程會使用CAS操作將Displaced Mark Word的內容複製回鎖的Mark Word裏面。如果沒有發生競爭,那麼這個複製的操作會成功。如果有其他線程因為自旋多次導致輕量級鎖升級成了重量級鎖,那麼CAS操作會失敗,此時會釋放鎖並喚醒被阻塞的線程。
輕量級鎖的加鎖解鎖流程圖:
當多個線程同時請求某個重量級鎖時,重量級鎖會設置幾種狀態用來區分請求的線程:
Contention List:所有請求鎖的線程將被首先放置到該競爭隊列,我也不知道為什麼網上的文章都叫它隊列,其實這個隊列是先進后出的,更像是棧,就是當Entry List為空時,Owner線程會直接從Contention List的隊列尾部(后加入的線程中)取一個線程,讓它成為OnDeck線程去競爭鎖。(主要是剛來獲取重量級鎖的線程是回進行自旋操作來獲取鎖,獲取不到才會進從Contention List,所以OnDeck線程主要與剛進來還在自旋,還沒有進入到Contention List的線程競爭)
Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List,主要是為了減少對Contention List的併發訪問,因為既會添加新線程到隊尾,也會從隊尾取線程。
Wait Set:那些調用wait方法被阻塞的線程被放置到Wait Set。
OnDeck:任何時刻最多只能有一個線程正在競爭鎖,該線程稱為OnDeck。
Owner:獲得鎖的線程稱為Owner
。
!Owner:釋放鎖的線程
流程圖如下:
步驟1是線程在進入Contention List時阻塞等待之前,程會先嘗試自旋使用CAS操作獲取鎖,如果獲取不到就進入Contention List隊列的尾部。
步驟2是Owner線程在解鎖時,如果Entry List為空,那麼會先將Contention List中隊列尾部的部分線程移動到Entry List
步驟3是Owner線程在解鎖時,如果Entry List不為空,從Entry List中取一個線程,讓它成為OnDeck線程,Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交給OnDeck,OnDeck需要重新競爭鎖,JVM中這種選擇行為稱為 “競爭切換”。(主要是與還沒有進入到Contention
List,還在自旋獲取重量級鎖的線程競爭)
步驟4就是OnDeck線程獲取到鎖,成為Owner線程進行執行。
步驟5就是Owner線程調用鎖對象的wait()方法進行等待,會移動到Wait Set中,並且會釋放CPU資源,也同時釋放鎖,
步驟6.就是當其他線程調用鎖對象的notify()方法,之前調用wait方法等待的這個線程才會從Wait Set移動到Entry List,等待獲取鎖。
偏向鎖由於不涉及到多個線程競爭,所以談不上公平不公平,輕量級鎖獲取鎖的方式是多個線程進行自旋操作,然後使用用CAS操作將鎖的Mark Word替換為指向自己線程棧中拷貝的鎖記錄的指針,所以誰能獲得鎖就看運氣,不看先後順序。重量級鎖不公平主要在於剛進入到重量級的鎖的線程不會直接進入Contention List隊列,而是自旋去獲取鎖,所以後進來的線程也有一定的幾率先獲得到鎖,所以是不公平的。
因為那些處於ContetionList、EntryList、WaitSet中的線程均處於阻塞狀態,阻塞操作由操作系統完成(在Linxu下通過pthread_mutex_lock函數)。線程被阻塞后便進入內核(Linux)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能。如果同步塊中代碼比較少,執行比較快的話,後進來的線程先自旋獲取鎖,先執行,而不進入阻塞狀態,減少額外的開銷,可以提高系統吞吐量。
偏向鎖升級為輕量級鎖:
就是有不同的線程競爭鎖時。具體來看就是當一個線程發現當前鎖狀態是偏向鎖,然後鎖對象存儲的Thread id是其他線程的id,並且去Thread id對應的線程棧查詢到的lock record的obj字段不為null(代表當前持有偏向鎖的線程還在執行同步塊)。那麼該偏向鎖就會升級成輕量級鎖。
輕量級鎖升級為重量級鎖:
就是在輕量級鎖中,沒有獲取到鎖的線程進行自旋,自旋到一定次數還沒有獲取到鎖就會進行鎖升級,因為自旋也是佔用CPU的,長時間自旋也是很耗性能的。
鎖降級
因為如果沒有多線程競爭,還是使用重量級鎖會造成額外的開銷,所以當JVM進入SafePoint安全點(可以簡單的認為安全點就是所有用戶線程都停止的,只有JVM垃圾回收線程可以執行)的時候,會檢查是否有閑置的Monitor,然後試圖進行降級。
篇幅有限,下面是各種鎖的優缺點,來自《併發編程的藝術》:
| 鎖 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 | 適用於只有一個線程訪問同步塊場景。 |
| 輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度。 | 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 | 追求響應時間。同步塊執行速度非常快。 |
| 重量級鎖 | 線程競爭不使用自旋,不會消耗CPU。 | 線程阻塞,響應時間緩慢。 | 追求吞吐量。同步塊執行速度較長。 |
參考鏈接:
https://github.com/farmerjohngit/myblog/issues/12
http://redspider.group:4000/article/02/9.html
https://blog.csdn.net/bohu83/article/details/51141836
https://blog.csdn.net/Dev_Hugh/article/details/106577862
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※別再煩惱如何寫文案,掌握八大原則!
※產品缺大量曝光嗎?你需要的是一流包裝設計!
摘錄自2020年8月25日共同網報導
在模里西斯近海發生日本貨輪燃油洩漏事故,在當地的日本國際緊急援助隊25日舉行記者會,稱觸礁的船體後半部分隨波浪擺動,把下方的珊瑚削成細顆粒狀,有可能因此使海水變渾濁,導致其他地方珊瑚死亡。
船體斷為兩截,前半部分已在24日被沉入深海。緊急援助隊強調有必要迅速撤走後半部分,但模里西斯當局難以撈起船體,已決定到11月1日前在事故現場進行解體。
緊急援助隊表示,通常珊瑚礁淺灘的水中能見度約30公尺,但模里西斯近海一些地方的能見度降至約3公尺。據稱,還發生防止油污漂流的圍油欄損傷珊瑚的情況;燃油還漂流到了紅樹林中,地表的油污可能會因人的踩踏而進入土壤中,有必要慎重開展去除油污的工作。
污染治理
國際新聞
模里西斯
珊瑚
油污攔阻作業
漏油污染
紅樹林
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※帶您來了解什麼是 USB CONNECTOR ?
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!
※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※教你寫出一流的銷售文案?
摘錄自2020年8月25日中央社報導
舊金山灣區四周被野火包圍,有害的煙塵讓許多民眾呼吸困難、眼睛發炎,原本有呼吸系統宿疾者病情加劇。舊金山灣區空氣品質管理區(Bay Area Air Quality Management District)指出,野火將有害的煙塵送入天空,讓今天的空氣品質盤旋在不健康的指數。
舊金山多個城市的細懸浮微粒(PM2.5)都達到對健康有威脅的程度,並讓許多民眾難以呼吸。舊金山紀事報網站今(25日)指出,許多居住在火災區附近的醫生通報,有呼吸系統宿疾的患者在上週末病情加劇,也有醫生認為未來幾週內都將陸續出現更多呼吸系統疾病相關的病人。
公衛專家警告,惡化的空氣品質伴隨著武漢肺炎(COVID-19)疫情,會讓原本就有氣喘與肺部疾病的患者更不舒服,同時可能讓染疫的民眾更虛弱。
污染治理
國際新聞
美國
加州
舊金山
空氣污染
空氣品質
火災
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※為什麼 USB CONNECTOR 是電子產業重要的元件?
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※台北網頁設計公司全省服務真心推薦
※想知道最厲害的網頁設計公司"嚨底家"!
※推薦評價好的iphone維修中心