drf序列化組件之視圖家族

一、視圖家族的分類

1.導入分類
from rest_framewok import views, generics, mixins, viewsets

views:視圖類

​ 兩大視圖類:APIView、GenericAPIView

from rest_framework.views import APIView
from rest_framework.generics import GenericAPIView

mixins:視圖工具類

​ 六大視圖工具類: RetrieveModelMixin, ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin

from rest_framework.mixins import RetrieveModelMixin, ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin

generics:工具視圖類

​ 九大工具視圖類:…

from rest_framework import generics

viewsets:視圖集

​ 兩大視圖集基類:ViewSet、GenericViewSet

from rest_framework import viewsets

2.APIVIiew的特性

它繼承了Django的View

​ 1)View:將請求方式與視圖類的同名方法建立映射,完成請求響應

​ 2)APIView:

​ 繼承了View所有的功能;

​ 重寫as_view禁用csrf認證;

​ 重寫dispatch:請求、響應、渲染、異常、解析、三大認證

​ 多了一堆類屬性,可以完成視圖類的局部配置

二、views視圖類的兩大視圖類的用法與區別

APIView:

from rest_framework.views import APIView
from rest_framework.response import Response
from . import models,serializers

# APIView:
class StudentAPIView(APIView):
    def get(self, request, *args, **kwargs):
        # 群查
        stu_query = models.Sudent.objects.all()
        stu_ser = serializers.StudentModelSerializer(stu_query,many=True)
        print(stu_ser)
        return Response(stu_ser.data)

GenericAPIView:

# GenericAPIView:
from rest_framework.generics import GenericAPIView

class StudentGenericAPIView(GenericAPIView):
    queryset = models.Sudent.objects.all()
    serializer_class = serializers.StudentModelSerializer
    def get(self, request, *args, **kwargs):
        # 群查
        # stu_query = models.Sudent.objects.all()
        stu_query = self.get_queryset()
        # stu_ser = serializers.StudentModelSerializer(stu_query,many=True)
        stu_ser = self.get_serializer(stu_query, many=True)

        return Response(stu_ser.data)

區別:

1.GenericAPIView繼承了APIView,所以它可以用APIView所有的功能
2.GenericAPIView內部提供了三個常用方法:
get_object(): 拿到單個準備序列化的對象,用於單查
get_queryset(): 拿到含有多條數據的Queryset對象,用於群查
get_serializer(): 拿到經過序列化的的serializer對象
3.三個常用屬性:
queryset
serializer_class
lookup_url_kwarg

三、視圖工具類Mixin的用法與介紹

以單增和群查為例:

from rest_framework import mixins
class StudentMixinGenericAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView):
    queryset = models.Sudent.objects.all()
    serializer_class = serializers.StudentModelSerializer
    # 群查
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    # 單增
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

特點:
1.提供了五大工具類及其六大工具方法:
CreateModelMixin: create() 實現單增
ListModelMixin: list() 實現群查
RetrieveModelMixin:retrieve() 實現單查
UpdateModelMixin: update() 實現單改 和 perform_update() 實現局部改
DestroyModelMixin : destroy() 實現單刪

​ 2.只要調用工具類的方法,就可實現該方法的功能,內部的實現原理據說是將我們之前寫的代碼進行了一層封裝,所以我們直接調用即可

​ 3. 由於mixins里的五大工具類沒有繼承任何視圖類views,在配置url的時候沒有as_view()方法,也就是不能進行任何的增刪改查,所以寫視圖類時繼承GenericAPIView類

四、工具視圖類Mixin的用法與介紹

# 工具視圖類
from rest_framework.generics import CreateAPIView, RetrieveAPIView, ListAPIView,UpdateAPIView,DestroyAPIView
class StudentMixinAPIView(CreateAPIView,ListAPIView,RetrieveAPIView,UpdateAPIView,DestroyAPIView):
    queryset = models.Sudent.objects.all()
    serializer_class = serializers.StudentModelSerializer
    # url中單查,不一定必須提供主鍵,提供一切唯一鍵的字段名均可
    lookup_url_kwarg = 'id'

    # 有刪除需求的接口繼承DestroyAPIView,重寫destroy完成字段刪除
    def destroy(self, request, *args, **kwargs):
        pass
 

分析:

​ lookup_url_kwarg: url中單查,不一定必須提供主鍵,提供一切唯一鍵的字段名均可,url配置中也要將pk改為id

​ 優點:

​ CreateAPIView,ListAPIView,RetrieveAPIView,UpdateAPIView,DestroyAPIView這五個工具類集成了mixins與GenericAPIView裏面的類。將它們再進行一次封裝,將get,post…等方法封裝起來,我們直接繼承有該方法的類即可。
​ 缺點:

​ 單查與群查不能共存,按照繼承順序決定單查還是群查,下面介紹的視圖集就能完成共存。

五、視圖集的用法與介紹

# 視圖集
from rest_framework.viewsets import ModelViewSet
class StudentModelViewSet(ModelViewSet):
    queryset = models.Sudent.objects.all()
    serializer_class = serializers.StudentModelSerializer

    def mypost(self, request, *args, **kwargs):
        return Response('my post ok')

分析:

​ 通過使用視圖集可以實現單查與群查共存,原因從查看源代碼得知:

ModelViewSet繼承五大工具類之外還繼承了GenericViewSet

GenericViewSet繼承了ViewSet再繼承了ViewSetMixin

而在ViewSetMixin類裏面,它重寫了as_view()方法,根據繼承關係,如果路由匹配上了,先走ViewSetMixin的as_view()方法。在它的as_view()方法裏面,它通過給給as_view()方法傳參數的方式,對應的工具方法:

它的原理就是通過給傳字典,通過字典裏面的數據進行反射,得到請求想要執行的方法。

在url路由中配置,這樣我們就可以區別單查與群查了:

我們還可以自己重寫請求要執行的對應方法。來實現特殊的需求。

注:由上面的代碼可以知道:除了繼承APIView的視圖類外,其他視圖類都要在該類下設置兩個屬性:

queryset = models.Student.objects.all()  # 代表跟哪張表建立關係
serializer_class = serializers.StudentModelSerializer  # 指明用的是哪個序列化器

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

第二屆中國國際智慧網聯汽車論壇2017 – 智慧汽車網聯化資訊安全問題不容忽視!

2017年11月16-17日∣中國·上海

路協同發展創造全面感知新時代

 

隨著電子、資訊、通信、人工智慧等技術與汽車產業加速融合,汽車產品正加快向智慧化、網聯化方向發展。因此,智慧網聯汽車面臨的資訊安全挑戰也備受業界關注。

頂層設計政策體系為智慧網聯汽車的發展創建了良好的發展環境,與此相關的大資料、雲計算、人工智慧等也在持續提供著技術保障。與此同時,一個較為顯著的問題是,汽車的網聯化也極有可能徹底打開了駭客入侵智慧網聯汽車的通道。智慧網聯汽車與外部的每個介面都可能被惡意利用,每個控制單元都可能被駭客攻擊、病毒感染,智慧網聯汽車的資訊安全防護難度也因之而倍增。

第二屆中國國際智慧網聯汽車論壇將針對智慧網聯汽車資訊安全問題定向邀請包括騰訊科恩實驗室360奇虎梆梆安全中國移動中國聯通等行業內權威人士對於車聯網資訊安全問題進行更深層次的解析。此次論壇將涉及3個論壇,參觀考察及晚宴,共將有300位行業人士一起,對智慧網聯汽車發展面臨的挑戰、機遇與對策各方面進行為期兩天更深層次並具有建設和戰略性的探討。

 

會議亮點

Ø  豐富的內容:3大論壇的深度解析

Ø  參會嘉賓:300+高度滿意的企業決策者,160+業內知名企業,40+國家和地區

Ø  演講嘉賓:30+世界新能源汽車行業知名發言嘉賓

Ø  會議形式:3個論壇,2天會議,1個晚宴

 

會議結構

論壇一:智慧網聯汽車發展趨勢分析及國內外項目解析和智慧交通發展

 

論壇二:車載通訊資訊技術及車聯網未來發展

²  迎合中國製造2025,促進智慧網聯汽車發展之路

²  智慧汽車、車聯網、車載資訊服務:點、線、網、面的格局與階段

²  智慧汽車技術創新革命

²  智慧交通/汽車發展不同階段的分析

²  國際智慧交通與智慧駕駛的銜接發展

 

²  車載半導體的機遇與挑戰

²  車聯網最新技術探討

²  4G通信在車載行業的應用

²  分時租賃-建造全民共用汽車

²  移動互聯網運營與智慧汽車的融合

論壇三:智慧汽車ADAS駕駛輔助系統和智慧駕駛技術

 

考察活動:20171115

²  ADAS與智慧駕駛解決方案探討

²  ADAS駕駛輔助系統性能及匹配測試

²  駕駛輔助系統雷達與感測器的核心技術

²  高精准地圖對於智慧駕駛的重要性

²  汽車人機交互對於智慧駕駛的重要性及發展展望

 

1.參觀上海天合汽車安全系統有限公司

2.參觀上海智慧網聯汽車試點示範區-中國首家(已預訂,如無測試企業屆時即可參觀)

 

若您對峰會有更多要求,請撥打021-6093 0815與我們聯繫,謝謝理解和支持!

我們期待與貴單位一起出席於20171116-17上海舉辦的第二屆中國國際智慧網聯汽車論壇2017,以利決策!

 

欲知更多會議詳情,請登陸官方網站:http://www.ourpolaris.com/2017/icv/index_c.html

連絡人:Latika LIU(劉小姐)

電話:021-6093 0815

傳真:021-6047 5887

郵箱:

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

x86彙編分頁模式實驗 –《ORANGE’S一個操作系統的實現》中 pmtest8.asm解析

  序言(廢話) : 在看書的過程中發現一開始不是很能理解pmtest8的目的,以及書上說得很抽象..於是在自己閱讀過源代碼后,將一些自己的心得寫在這裏。

  正文 : 

  講解順序依然按照書上貼代碼的順序來。但是是幾乎逐句解釋的。可能會稍微有點啰嗦。廢話就不多說了直接貼代碼。

LABEL_DESC_FLAT_C:  Descriptor 0,        0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
LABEL_DESC_FLAT_RW: Descriptor 0,        0fffffh, DA_DRW|DA_LIMIT_4K     ; 0~4G
SelectorFlatC       equ    LABEL_DESC_FLAT_C - LABEL_GDT                
SelectorFlatRW        equ    LABEL_DESC_FLAT_RW - LABEL_GDT

  顯然,兩個分別是 FLAT_C 和  FLAT_RW 的描述符和選擇子。

  問題 : 為什麼要有這兩個東西?

  解釋 : FLAT_C是用來執行的非一致性32位代碼段,粒度為4k,也就是 limit(段限長) = (0xfffff + 1)  * 4k = 4G,FLAT_RW 是用來修改數據的,因為需要利用這個描述符的權限(可寫)來將代碼寫入到目的地(這個目的地允許在 0 – 4G區間內)。之所以要分兩個選擇符,是防止在執行的時候修改代碼(所以FLAT_C不能給寫的權限),但是又必須在執行之前進行複製,所以一定要有一個入口能提供寫入的方式,於是設置兩個描述符來進行。這樣既安全又有章法。

 

SetupPaging:
    ; 根據內存大小計算應初始化多少PDE以及多少頁表
    xor    edx, edx
    mov    eax, [dwMemSize]
    mov    ebx, 400000h    ; 400000h = 4M = 4096 * 1024, 一個頁表對應的內存大小
    div    ebx
    mov    ecx, eax    ; 此時 ecx 為頁表的個數,也即 PDE 應該的個數
    test    edx, edx
    jz    .no_remainder
    inc    ecx        ; 如果餘數不為 0 就需增加一個頁表
.no_remainder:
    mov    [PageTableNumber], ecx    ; 暫存頁表個數

    ; 為簡化處理, 所有線性地址對應相等的物理地址. 並且不考慮內存空洞.

    ; 首先初始化頁目錄
    mov    ax, SelectorFlatRW
    mov    es, ax
    mov    edi, PageDirBase0    ; 此段首地址為 PageDirBase0
    xor    eax, eax
    mov    eax, PageTblBase0 | PG_P  | PG_USU | PG_RWW
.1:    ; es:edi 初始等於 PageDirBase0 (當前頁目錄表項), eax 初始基地址等於 PageTblBase0
    stosd
    add    eax, 4096        ; 為了簡化, 所有頁表在內存中是連續的.
    loop    .1

    ; 再初始化所有頁表
    mov    eax, [PageTableNumber]    ; 頁表個數
    mov    ebx, 1024        ; 每個頁表 1024 個 PTE
    mul    ebx
    mov    ecx, eax        ; PTE個數 = 頁表個數 * 1024
    mov    edi, PageTblBase0    ; 此段首地址為 PageTblBase0
    xor    eax, eax
    mov    eax, PG_P  | PG_USU | PG_RWW
.2:    ; es:edi 初始等於 PageTblBase0 (當前頁表項), eax = 0 (線性地址 = 物理地址)
    stosd
    add    eax, 4096        ; 每一頁指向 4K 的空間
    loop    .2

    mov    eax, PageDirBase0
    mov    cr3, eax
    mov    eax, cr0
    or    eax, 80000000h
    mov    cr0, eax
    jmp    short .3
.3:
    nop

    ret

 

  這段代碼我加註了兩句註釋 分別在 .1 和 .2 這兩個標籤那行,其實這裏和之前的setPaging並沒有很大的區別,需要注意的就是 這裏的 頁目錄表 的地址是  PageDirBase0, 頁表的地址是PageTblBase0,強調這點的原因在於之後的  PSwitch 這個函數中則是 PageDirBase1 和 PageTblBase1。也就是說實際上數據中有兩個頁面管理的數據結構(頁目錄表和頁表合起來相當於一個管理頁面的數據結構)。

 1 PagingDemo:
 2     mov    ax, cs
 3     mov    ds, ax
 4     mov    ax, SelectorFlatRW        ; 設置es為基地址為0的可讀寫的段(便於複製代碼)
 5     mov    es, ax
 6     
 7     push    LenFoo
 8     push    OffsetFoo
 9     push    ProcFoo            ; 00401000h
10     call    MemCpy        
11     add    esp, 12
12 
13     push    LenBar            ; 被複制代碼段(但是以ds為段基址)的長度 
14     push    OffsetBar        ; 被複制代碼段(但是以ds為段基址)的段偏移量
15     push    ProcBar            ; 目的代碼段的物理空間地址 00501000h
16     call    MemCpy
17     add    esp, 12
18 
19     push    LenPagingDemoAll
20     push    OffsetPagingDemoProc    
21     push    ProcPagingDemo            ; [es:ProcPagingDemo] = ProcPagingDemo = 00301000h
22     call    MemCpy
23     add    esp, 12
24 
25     mov    ax, SelectorData
26     mov    ds, ax            ; 數據段選擇子
27     mov    es, ax
28 
29     call    SetupPaging        ; 啟動分頁
30     ; 當前線性地址依然等於物理地址
31     call    SelectorFlatC:ProcPagingDemo    ; 訪問的線性地址為 00301000h,物理地址也是 00301000h
32     call    PSwitch            ; 切換頁目錄,改變地址映射關係
33     call    SelectorFlatC:ProcPagingDemo    ; 訪問的線性地址為 00301000h
34 
35     ret

  在這裏首先要說明的是 MemCpy函數,這個函數有三個參數分別表示 : 

   1)被複制段(但是以ds為段基址)的 長度 
   2)被複制段(但是以ds為段基址)的 段偏移量
   3)目的地的物理空間地址(之所以說是物理空間是因為當前線性地址等於物理地址,以es為段基址,但是es的段基址為0)
功能則是 將被複制段 的數據複製 參數1)的長度字節 去目的地去(簡單說就是利用三個參數複製數據)

我們可以知道的是在上面代碼中三次調用 MemCpy 都沒有進入分頁模式,也就是說當下線性地址等於物理地址。那麼根據我上面的註釋就可以知道三個代碼分別複製到哪裡去了。
之後就是恢複數據段(之前將ds = cs,是為了複製代碼),然後啟動分頁(上面已經講了),然後啟動分頁后當前線性地址依然等於物理地址。
這個時候第一次調用 call SelectorFlatC:ProcPagingDemo,也就是訪問的線性地址為 00301000h,物理地址也是 00301000h的代碼(之前移動過去的)。
 下面這段代碼就是被移動到00301000h的代碼,這段代碼只做了一件事那就是調用 [cs:LinearAddrDemo]的代碼,但請注意,由於 call SelectorFlatC:ProcPagingDemo
所以此時的 cs = SelectorFlatC,也就是說段基址等於0,於是實際上這段代碼的功能就是訪問 物理地址為00401000h處的代碼。
PagingDemoProc:
OffsetPagingDemoProc    equ    PagingDemoProc - $$
    mov    eax, LinearAddrDemo
    call    eax        ; 未開始PSwitch前, eax = ProcFoo = 00401000h (cs 的段基址 = 0)
    retf
LenPagingDemoAll    equ    $ - PagingDemoProc

  而物理地址00401000h處就是ProcFoo的代碼(第一次調用MemCpy拷貝的代碼)。被拷貝的代碼如下

foo:
OffsetFoo        equ    foo - $$
    mov    ah, 0Ch            ; 0000: 黑底    1100: 紅字
    mov    al, 'F'
    mov    [gs:((80 * 17 + 0) * 2)], ax    ; 屏幕第 17 行, 第 0 列。
    mov    al, 'o'
    mov    [gs:((80 * 17 + 1) * 2)], ax    ; 屏幕第 17 行, 第 1 列。
    mov    [gs:((80 * 17 + 2) * 2)], ax    ; 屏幕第 17 行, 第 2 列。
    ret
LenFoo            equ    $ - foo

  功能很明顯就是現實一個字符串 Foo而已。

總結第一次分頁后的動作:

  就是拷貝三份代碼分別到ProcFoo, ProcBar, ProcPagingDemo 處(這四個都是物理內存哦,並且後面因為段基址是0(FLAT_C 段基址)於是很容易地就訪問到了物理地址)。然後開啟分頁模式(其實幾乎沒什麼影響 因為仍然和分段一樣 線性地址 = 物理地址)。然後調用 被拷貝的函數 ProcPagingDemo ,ProcPagingDemo 函數調用 ProcFoo函數,显示字符 “Foo”然後兩次返回。

第二次分頁 : call PSwitch

被調用代碼如下 :

 1 PSwitch:
 2     ; 初始化頁目錄
 3     mov    ax, SelectorFlatRW
 4     mov    es, ax
 5     mov    edi, PageDirBase1    ; 此段首地址為 PageDirBase1
 6     xor    eax, eax
 7     mov    eax, PageTblBase1 | PG_P  | PG_USU | PG_RWW
 8     mov    ecx, [PageTableNumber]
 9 .1:    ; es:edi 初始等於 PageDirBase1 (當前頁目錄表項), eax 初始基地址等於 PageTblBase1
10     stosd
11     add    eax, 4096        ; 為了簡化, 所有頁表在內存中是連續的.
12     loop    .1
13 
14     ; 再初始化所有頁表
15     mov    eax, [PageTableNumber]    ; 頁表個數
16     mov    ebx, 1024        ; 每個頁表 1024 個 PTE
17     mul    ebx
18     mov    ecx, eax        ; PTE個數 = 頁表個數 * 1024
19     mov    edi, PageTblBase1    ; 此段首地址為 PageTblBase1
20     xor    eax, eax
21     mov    eax, PG_P  | PG_USU | PG_RWW
22 .2: ; es:edi 初始等於 PageTblBase1 (當前頁表項), eax 初始基地址等於 0(線性地址等於物理地址)
23     stosd
24     add    eax, 4096        ; 每一頁指向 4K 的空間
25     loop    .2
26 
27     ; 在此假設內存是大於 8M 的
28     ; 下列代碼將LinearAddrDemo所處的頁表的相對第一個頁表的偏移地址放入ecx中
29     mov    eax, LinearAddrDemo
30     shr    eax, 22
31     mov    ebx, 4096        ; (LinearAddrDemo / 4M)表示第幾個頁表
32     mul    ebx                ; 第幾個頁表 * 4k (1024(一個頁表項的數量) * 4(一個頁表項的字節))
33     mov    ecx, eax        ; 也就是對應頁表的偏移地址
34     
35     ; 下列代碼將LinearAddrDemo所處的頁表項相對第一個頁表項的偏移地址放入eax中
36     mov    eax, LinearAddrDemo
37     shr    eax, 12            ; LinearAddrDemo / 4k,表示第幾個頁表項
38     and    eax, 03FFh    ; 1111111111b (10 bits)    ; 取低10位,也就是餘下的零散頁表項(一個頁表有2^10個頁表項)
39     mov    ebx, 4                                
40     mul    ebx                                    ; * 4 表示的是具體偏移字節數
41     add    eax, ecx                            ; eax = (((LinearAddrDemo / 2^12) & 03FFh) * 4) + (4k * (LinearAddrDemo / 2^22))
42     
43     
44     add    eax, PageTblBase1                    ; 第一個頁表的第一個頁表項
45     mov    dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
46 
47     mov    eax, PageDirBase1
48     mov    cr3, eax
49     jmp    short .3
50 .3:
51     nop
52 
53     ret

  在這裏我加了幾個比較重要的註釋分別在第 9, 22, 28,35處。

  這段代碼做了什麼?

  首先是設置頁面管理的數據結構(頁表和頁目錄表),但是需要注意的是,這裏設置頁表和頁目錄表除了不是之前的頁面管理結構之外,其實內容是差不多的,也就是說當前(第25行)這裏的狀態也是 線性地址 = 物理地址 !!!

 但是在第27行做了一個操作,就是將LinearAddrDemo對應的 頁表項的地址 換成了 ProcBar(00501000h) 的地址。(具體如何實現的請看27-45行我寫的註釋)。
  在做完這些之後就返回第二次執行 call SelectorFlatC:ProcPagingDemo 了,在這個時候 cs = SelectorFlatC (段基址等於0), eip = ProcPagingDemo = 00301000h,也就是說訪問了
線性地址 = 00301000h處,但是這裏已經被修改,除了這個頁面之外,其他頁面都是 線性地址 = 物理地址,但是這裏 線性地址 = 00301000h ,映射的物理地址是 ProcBar(00501000h)
於是便調用了 ProcBar 段的代碼,而這段的代碼是第二次調用MemCpy時候複製過去的。被複制的具體代碼是:
bar:
OffsetBar        equ    bar - $$
    mov    ah, 0Ch            ; 0000: 黑底    1100: 紅字
    mov    al, 'B'
    mov    [gs:((80 * 18 + 0) * 2)], ax    ; 屏幕第 18 行, 第 0 列。
    mov    al, 'a'
    mov    [gs:((80 * 18 + 1) * 2)], ax    ; 屏幕第 18 行, 第 1 列。
    mov    al, 'r'
    mov    [gs:((80 * 18 + 2) * 2)], ax    ; 屏幕第 18 行, 第 2 列。
    ret
LenBar            equ    $ - bar
也就是显示一個字符串 "Bar", 然後返回到PagingDemo的最後一句 ret,再次返回。於是這段代碼也就結束了。
第二次代碼是如何實現調用 ProcBar的?
  通過將線性地址 = ProcPaging(00301000h)對應的頁表項的地址值給修改成了 PaocBar(00501000h)的物理地址,於是從 00301000h 的線性地址 映射到 00501000h的物理地址上去了,
但是其實其他地方(除了這個頁之外)的線性地址 = 物理地址依然成立。也是上面這段代碼很小,一定是小於 4k(一頁的大小),於是只需要修改一個頁表項就可以了!
 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

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

SpringSecurity退出功能實現的正確方式

本文將介紹在Spring Security框架下如何實現用戶的”退出”logout的功能。其實這是一個非常簡單的功能,我見過很多的程序員在使用了Spring Security之後,仍然去自己寫controller方法實現logout功能,這種做法就好像耕地,你有机械設備你不用,你非要用牛。

一、logout最簡及最佳實踐

其實使用Spring Security進行logout非常簡單,只需要在spring Security配置類配置項上加上這樣一行代碼:http.logout()。關於spring Security配置類的其他很多實現、如:HttpBasic模式、formLogin模式、自定義登錄驗證結果、使用權限表達式、session會話管理,在本號的之前的文章已經都寫過了。本節的核心內容就是在原有配置的基礎上,加上這樣一行代碼:http.logout()。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.logout();
   }

}

加上logout配置之後,在你的“退出”按鈕上使用/logtou作為請求登出的路徑。

<a href="/logout" >退出</a>

logout功能我們就完成了。實際上的核心代碼只有兩行。

二、默認的logout做了什麼?

雖然我們簡簡單單的實現了logout功能,是不是還不足夠放心?我們下面就來看一下Spring Security默認在logout過程中幫我們做了哪些動作。

  • 當前session失效,即:logout的核心需求,session失效就是訪問權限的回收。
  • 刪除當前用戶的 remember-me“記住我”功能信息
  • clear清除當前的 SecurityContext
  • 重定向到登錄頁面,loginPage配置項指定的頁面

通常對於一個應用來講,以上動作就是logout功能所需要具備的功能了。

三、個性化配置

雖然Spring Security默認使用了/logout作為退出處理請求路徑,登錄頁面作為退出之後的跳轉頁面。這符合絕大多數的應用的開發邏輯,但有的時候我們需要一些個性化設置,如下:

 http.logout()
     .logoutUrl("/signout")
     .logoutSuccessUrl("/aftersignout.html")
     .deleteCookies("JSESSIONID")
  • 通過指定logoutUrl配置改變退出請求的默認路徑,當然html退出按鈕的請求url也要修改
  • 通過指定logoutSuccessUrl配置,來顯式指定退出之後的跳轉頁面
  • 還可以使用deleteCookies刪除指定的cookie,參數為cookie的名稱

四、LogoutSuccessHandler

如果上面的個性化配置,仍然滿足不了您的應用需求。可能您的應用需要在logout的時候,做一些特殊動作,比如登錄時長計算,清理業務相關的數據等等。你可以通過實現LogoutSuccessHandler 接口來實現你的業務邏輯。

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    
    @Override
    public void onLogoutSuccess(HttpServletRequest request, 
                                HttpServletResponse response, 
                                Authentication authentication) 
                                throws IOException, ServletException {
        //這裏書寫你自己的退出業務邏輯
        
        // 重定向到登錄頁
        response.sendRedirect("/login.html");
    }
}

然後進行配置使其生效,核心代碼就是一行logoutSuccessHandler。注意logoutSuccessUrl不要與logoutSuccessHandler一起使用,否則logoutSuccessHandler將失效。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
    
@Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
         http.logout()
             .logoutUrl("/signout")
             //.logoutSuccessUrl(``"/aftersignout.html"``)
             .deleteCookies("JSESSIONID")
              //自定義logoutSuccessHandler
             .logoutSuccessHandler(myLogoutSuccessHandler);   
   }
}

期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

【NServiceBus】什麼是Saga,Saga能做什麼

前言

          Saga單詞翻譯過來是指尤指古代挪威或冰島講述冒險經歷和英雄業績的長篇故事,對,這裏強調長篇故事。許多系統都存在長時間運行的業務流程,NServiceBus使用基於事件驅動的體繫結構將容錯性和可伸縮性融入這些業務處理過程中。
          當然一個單一接口調用則算不上一個長時間運行的業務場景,那麼如果在給定的用例中有兩個或多個調用,則應該考慮數據一致性的問題,這裡有可能第一個接口調用成功,第二次調用則可能失敗或者超時,Saga的設計以簡單而健壯的方式處理這樣的業務用例。

認識Saga

         先來通過一段代碼簡單認識一下Saga,在NServiceBus里,使用Saga的話則需要實現抽象類Saga ,SqlSaga ,這裏的T的是Saga業務實體,封裝數據,用來在長時間運行過程中封裝業務數據。

public class Saga:Saga<State>,
        IAmStartedByMessages<StartOrder>,
        IHandleMessages<CompleteOrder>
    {
        protected override void ConfigureHowToFindSaga(SagaPropertyMapper<State> mapper)
        {
            mapper.ConfigureMapping<StartOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
            mapper.ConfigureMapping<CompleteOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
        }

        public Task Handle(StartOrder message, IMessageHandlerContext context)
        {
            return Task.CompletedTask;
        }

        public Task Handle(CompleteOrder message, IMessageHandlerContext context)
        {
            MarkAsComplete();
            return Task.CompletedTask;
        }
    }

臨時狀態

     長時間運行則意味着有狀態,任何涉及多個網絡調用的進程都需要一個臨時狀態,這個臨時狀態可以存儲在內存中,序列化在磁盤中,也可以存儲在分佈式緩存中。在NServiceBus中我們定義實體,繼承抽象類ContainSagaData即可,默認情況下,所有公開訪問的屬性都會被持久化。

public class State:ContainSagaData
{
    public Guid OrderId { get; set; }
}

添加行為

      在NServiceBus里,處理消息的有兩種接口:IHandlerMessages 、IAmStartedByMessages 。

開啟一個Saga

       在前面的代碼片段里,我們看到已經實現了接口IAmStartedByMessages ,這個接口告訴NServiceBus,如果收到了StartOrder 消息,則創建一個Saga實例(Saga Instance),當然Saga長流程處理的實體至少有一個需要開啟Saga流程。

處理無序消息

       如果你的業務用例中確實存在無序消息的情況,則還需要業務流程正常輪轉,那麼則需要多個messaeg都要事先接口IAmStartedByMessages接口,也就是說多個message都可以創建Saga實例。

依賴可恢復性

      在處理無序消息和多個消息類型的時候,就存在消息丟失的可能,必須在你的Saga狀態完成以後,這個Saga實例又收到一條消息,但這時Saga狀態已經是完結狀態,這條消息則仍然需要處理,這裏則實現NServiceBus的IHandleSagaNotFound接口。

 public class SagaNotFoundHandler:IHandleSagaNotFound
 {
    public Task Handle(object message, IMessageProcessingContext context)
    {
        return context.Reply(new SagaNotFoundMessage());
    }
 }
  
 public class SagaNotFoundMessage
 {
        
 }

結束Saga

      當你的業務用例不再需要Saga實例時,則調用MarkComplete()來結束Saga實例。這個方法在前面的代碼片段中也可以看到,其實本質也就是設置Saga.Complete屬性,這是個bool值,你在業務用例中也可以用此值來判斷Saga流程是否結束。

namespace NServiceBus
{
    using System;
    using System.Threading.Tasks;
    using Extensibility;

    public abstract class Saga
    {
        /// <summary>
        /// The saga's typed data.
        /// </summary>
        public IContainSagaData Entity { get; set; }

        
        public bool Completed { get; private set; }

        internal protected abstract void ConfigureHowToFindSaga(IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration);

       
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, DateTime at) where TTimeoutMessageType : new()
        {
            return RequestTimeout(context, at, new TTimeoutMessageType());
        }

        
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, DateTime at, TTimeoutMessageType timeoutMessage)
        {
            if (at.Kind == DateTimeKind.Unspecified)
            {
                throw new InvalidOperationException("Kind property of DateTime 'at' must be specified.");
            }

            VerifySagaCanHandleTimeout(timeoutMessage);

            var options = new SendOptions();

            options.DoNotDeliverBefore(at);
            options.RouteToThisEndpoint();

            SetTimeoutHeaders(options);

            return context.Send(timeoutMessage, options);
        }

        
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within) where TTimeoutMessageType : new()
        {
            return RequestTimeout(context, within, new TTimeoutMessageType());
        }

        
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage)
        {
            VerifySagaCanHandleTimeout(timeoutMessage);

            var sendOptions = new SendOptions();

            sendOptions.DelayDeliveryWith(within);
            sendOptions.RouteToThisEndpoint();

            SetTimeoutHeaders(sendOptions);

            return context.Send(timeoutMessage, sendOptions);
        }

        
        protected Task ReplyToOriginator(IMessageHandlerContext context, object message)
        {
            if (string.IsNullOrEmpty(Entity.Originator))
            {
                throw new Exception("Entity.Originator cannot be null. Perhaps the sender is a SendOnly endpoint.");
            }

            var options = new ReplyOptions();

            options.SetDestination(Entity.Originator);
            context.Extensions.Set(new AttachCorrelationIdBehavior.State { CustomCorrelationId = Entity.OriginalMessageId });

            
            options.Context.Set(new PopulateAutoCorrelationHeadersForRepliesBehavior.State
            {
                SagaTypeToUse = null,
                SagaIdToUse = null
            });

            return context.Reply(message, options);
        }

        //這個方法結束saga流程,標記Completed屬性
        protected void MarkAsComplete()
        {
            Completed = true;
        }

        void VerifySagaCanHandleTimeout<TTimeoutMessageType>(TTimeoutMessageType timeoutMessage)
        {
            var canHandleTimeoutMessage = this is IHandleTimeouts<TTimeoutMessageType>;
            if (!canHandleTimeoutMessage)
            {
                var message = $"The type '{GetType().Name}' cannot request timeouts for '{timeoutMessage}' because it does not implement 'IHandleTimeouts<{typeof(TTimeoutMessageType).FullName}>'";
                throw new Exception(message);
            }
        }

        void SetTimeoutHeaders(ExtendableOptions options)
        {
            options.SetHeader(Headers.SagaId, Entity.Id.ToString());
            options.SetHeader(Headers.IsSagaTimeoutMessage, bool.TrueString);
            options.SetHeader(Headers.SagaType, GetType().AssemblyQualifiedName);
        }
    }
}

    

Saga持久化

      本機開發環境我們使用LearningPersistence,但是投產的話則需要使用數據庫持久化,這裏我們基於MySQL,SQL持久化需要引入NServiceBus.Persistence.Sql。SQL Persistence會生成幾種關係型數據庫的sql scripts,然後會根據你的斷言配置選擇所需數據庫,比如SQL Server、MySQL、PostgreSQL、Oracle。
     持久化Saga自動創建所需表結構,你只需手動配置即可,配置后編譯成功後項目執行目錄下會生成sql腳本,文件夾名稱是NServiceBus.Persistence.Sql,下面會有Saga子目錄。


/* TableNameVariable */

set @tableNameQuoted = concat('`', @tablePrefix, 'Saga`');
set @tableNameNonQuoted = concat(@tablePrefix, 'Saga');


/* Initialize */

drop procedure if exists sqlpersistence_raiseerror;
create procedure sqlpersistence_raiseerror(message varchar(256))
begin
signal sqlstate
    'ERROR'
set
    message_text = message,
    mysql_errno = '45000';
end;

/* CreateTable */

set @createTable = concat('
    create table if not exists ', @tableNameQuoted, '(
        Id varchar(38) not null,
        Metadata json not null,
        Data json not null,
        PersistenceVersion varchar(23) not null,
        SagaTypeVersion varchar(23) not null,
        Concurrency int not null,
        primary key (Id)
    ) default charset=ascii;
');
prepare script from @createTable;
execute script;
deallocate prepare script;

/* AddProperty OrderId */

select count(*)
into @exist
from information_schema.columns
where table_schema = database() and
      column_name = 'Correlation_OrderId' and
      table_name = @tableNameNonQuoted;

set @query = IF(
    @exist <= 0,
    concat('alter table ', @tableNameQuoted, ' add column Correlation_OrderId varchar(38) character set ascii'), 'select \'Column Exists\' status');

prepare script from @query;
execute script;
deallocate prepare script;

/* VerifyColumnType Guid */

set @column_type_OrderId = (
  select concat(column_type,' character set ', character_set_name)
  from information_schema.columns
  where
    table_schema = database() and
    table_name = @tableNameNonQuoted and
    column_name = 'Correlation_OrderId'
);

set @query = IF(
    @column_type_OrderId <> 'varchar(38) character set ascii',
    'call sqlpersistence_raiseerror(concat(\'Incorrect data type for Correlation_OrderId. Expected varchar(38) character set ascii got \', @column_type_OrderId, \'.\'));',
    'select \'Column Type OK\' status');

prepare script from @query;
execute script;
deallocate prepare script;

/* WriteCreateIndex OrderId */

select count(*)
into @exist
from information_schema.statistics
where
    table_schema = database() and
    index_name = 'Index_Correlation_OrderId' and
    table_name = @tableNameNonQuoted;

set @query = IF(
    @exist <= 0,
    concat('create unique index Index_Correlation_OrderId on ', @tableNameQuoted, '(Correlation_OrderId)'), 'select \'Index Exists\' status');

prepare script from @query;
execute script;
deallocate prepare script;

/* PurgeObsoleteIndex */

select concat('drop index ', index_name, ' on ', @tableNameQuoted, ';')
from information_schema.statistics
where
    table_schema = database() and
    table_name = @tableNameNonQuoted and
    index_name like 'Index_Correlation_%' and
    index_name <> 'Index_Correlation_OrderId' and
    table_schema = database()
into @dropIndexQuery;
select if (
    @dropIndexQuery is not null,
    @dropIndexQuery,
    'select ''no index to delete'';')
    into @dropIndexQuery;

prepare script from @dropIndexQuery;
execute script;
deallocate prepare script;

/* PurgeObsoleteProperties */

select concat('alter table ', table_name, ' drop column ', column_name, ';')
from information_schema.columns
where
    table_schema = database() and
    table_name = @tableNameNonQuoted and
    column_name like 'Correlation_%' and
    column_name <> 'Correlation_OrderId'
into @dropPropertiesQuery;

select if (
    @dropPropertiesQuery is not null,
    @dropPropertiesQuery,
    'select ''no property to delete'';')
    into @dropPropertiesQuery;

prepare script from @dropPropertiesQuery;
execute script;
deallocate prepare script;

/* CompleteSagaScript */

生成的表結構:

持久化配置

      Saga持久化需要依賴NServiceBus.Persistence.Sql。引入后需要實現SqlSaga抽象類,抽象類需要重寫ConfigureMapping,配置Saga工作流程業務主鍵。

public class Saga:SqlSaga<State>,
        IAmStartedByMessages<StartOrder>
{
   protected override void ConfigureMapping(IMessagePropertyMapper mapper)
   {
      mapper.ConfigureMapping<StartOrder>(message=>message.OrderId);
   }

   protected override string CorrelationPropertyName => nameof(StartOrder.OrderId);

   public Task Handle(StartOrder message, IMessageHandlerContext context)
   {
       Console.WriteLine($"Receive message with OrderId:{message.OrderId}");

       MarkAsComplete();
       return Task.CompletedTask;
    }
 }
    
 static async Task MainAsync()
 {
     Console.Title = "Client-UI";

     var configuration = new EndpointConfiguration("Client-UI");
     //這個方法開啟自動建表、自動創建RabbitMQ隊列
     configuration.EnableInstallers(); 
     configuration.UseSerialization<NewtonsoftSerializer>();
     configuration.UseTransport<LearningTransport>();

     string connectionString = "server=127.0.0.1;uid=root;pwd=000000;database=nservicebus;port=3306;AllowUserVariables=True;AutoEnlist=false";
     var persistence = configuration.UsePersistence<SqlPersistence>();
     persistence.SqlDialect<SqlDialect.MySql>();
     //配置mysql連接串
     persistence.ConnectionBuilder(()=>new MySqlConnection(connectionString));

     var instance = await Endpoint.Start(configuration).ConfigureAwait(false);

     var command = new StartOrder()
     {
         OrderId = Guid.NewGuid()
     };

     await instance.SendLocal(command).ConfigureAwait(false);

     Console.ReadKey();

     await instance.Stop().ConfigureAwait(false);
 }

     

Saga Timeouts

     在消息驅動類型的環境中,雖然傳遞的無連接特性可以防止在線等待過程中消耗資源,但是畢竟等待時間需要有一個上線。在NServiceBus里已經提供了Timeout方法,我們只需訂閱即可,可以在你的Handle方法中根據需要訂閱Timeout,可參考如下代碼:

public class Saga:Saga<State>,
        IAmStartedByMessages<StartOrder>,
        IHandleMessages<CompleteOrder>,
        IHandleTimeouts<TimeOutMessage>
    {
        
        public Task Handle(StartOrder message, IMessageHandlerContext context)
        {
            var model=new TimeOutMessage();
            
            //訂閱超時消息
            return RequestTimeout(context,TimeSpan.FromMinutes(10));
        }

        public Task Handle(CompleteOrder message, IMessageHandlerContext context)
        {
            MarkAsComplete();
            return Task.CompletedTask;
        }

        protected override string CorrelationPropertyName => nameof(StartOrder.OrderId);


        public Task Timeout(TimeOutMessage state, IMessageHandlerContext context)
        {
            //處理超時消息
        }

        protected override void ConfigureHowToFindSaga(SagaPropertyMapper<State> mapper)
        {
            mapper.ConfigureMapping<StartOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
            mapper.ConfigureMapping<CompleteOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
        }
    }
//從Timeout的源碼看,這個方法是通過設置SendOptions,然後再把當前這個消息發送給自己來實現 
protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage)
 {
     VerifySagaCanHandleTimeout(timeoutMessage);
     var sendOptions = new SendOptions();
     sendOptions.DelayDeliveryWith(within);
     sendOptions.RouteToThisEndpoint();
     SetTimeoutHeaders(sendOptions);

     return context.Send(timeoutMessage, sendOptions);
 }

總結

       NServiceBus因為是商業產品,對分佈式消息系統所涉及到的東西都做了實現,包括分佈式事務(Outbox)、DTC都有,還有心跳檢測,監控都有,全而大,目前我們用到的也只是NServiceBus里很小的一部分功能。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

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

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

[UWP]用Win2D實現鏤空文字

1. 前言

之前用PointLight做了一個番茄鍾,效果還不錯,具體可見這篇文章:

後來試玩了Win2D,這次就用Win2D實現文字的鏤空效果,配合PointLight做一個內斂不張揚的番茄鍾。

實現鏤空文字的核心思想是使用CanvasGeometry.CreateText從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。這篇文章介紹了具體的實現步驟。

2. 參考例子

Win2D Gallery提供了大量Win2D的Sample,這次就參考了其中的文字鏤空效果例子,地址和運行效果如下:

3. 實現步驟

Sample的代碼量雖多,其實核心並不複雜,下面講講需要用到的API:

3.1 CanvasDevice.GetSharedDevice

因為要用到Win2D,所以首先要引用 nuget包。因為我的目標不是輸出到CanvasControl上,而是想要輸出到一個SpriteVisual上,所以使用:

var canvasDevice = CanvasDevice.GetSharedDevice();

3.2 CanvasComposition.CreateCompositionGraphicsDevice

然後創建一個Compositor,並將這個Compositor和CanvasDevice關聯起來,這裏需要使用 創建 :

var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

3.3 CompositionGraphicsDevice.CreateDrawingSurface

然後使用創建一個對象,它是用來繪畫內容的表面:

var drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);

3.4 Compositor.CreateSurfaceBrush

使用創建一個CompositionSurfaceBrush,它的作用是使用像素繪製SpriteVisual,簡單來說它就是一張位圖,然後輸出到SpriteVisual上:

var maskSurfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
spriteTextVisual.Brush = maskSurfaceBrush;

3.5 CanvasComposition.CreateDrawingSession

有了CompositionDrawingSurface就可以為所欲為了,將這個DrawingSurface作為參數,調用創建,DrawingSession提供了多個函數,可以自由地在DrawingSurface上畫文字、形狀、圖片甚至SVG。

using (var session = CanvasComposition.CreateDrawingSession(drawingSurface))
{

}

3.6 CanvasTextFormat和CanvasTextLayout

要再DrawingSurface上寫字,需要,而CanvasTextLayout中的文字大小、格式等則由定義:

using (var textFormat = new CanvasTextFormat()
{
    FontSize = (float)FontSize,
    Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
    VerticalAlignment = CanvasVerticalAlignment.Center,
    HorizontalAlignment = CanvasHorizontalAlignment.Center,

})
{
    using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
    {
        Color fontColor = FontColor;
        session.DrawTextLayout(textLayout, 0, 0, fontColor);
    }
}

3.7 CanvasGeometry.CreateText

因為我的目標是鏤空的文字,所以不能直接使用DrawTextLayout。這裏需要使用從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。CanvasStrokeStyle是可選的,它控制邊框的虛線。

using (var textGeometry = CanvasGeometry.CreateText(textLayout))
{
    var dashedStroke = new CanvasStrokeStyle()
    {
        DashStyle = DashStyle
    };
    session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
}

4. 封裝為控件

將上面的代碼總結一下,封裝為一個OutlineTextControl 控件,它提供了Text、OutlineColor、FontColor等屬性,在控件SizeChanged時,或者各個屬性改變時調用DrawText重新在CompositionDrawingSurface上繪製文字。代碼大致如下:

public class OutlineTextControl : Control
{
    private CompositionDrawingSurface _drawingSurface;

    public OutlineTextControl()
    {
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
        var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, CanvasDevice.GetSharedDevice());
        var spriteTextVisual = compositor.CreateSpriteVisual();

        ElementCompositionPreview.SetElementChildVisual(this, spriteTextVisual);
        SizeChanged += (s, e) =>
        {
            _drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
            DrawText();
            var maskSurfaceBrush = compositor.CreateSurfaceBrush(_drawingSurface);
            spriteTextVisual.Brush = maskSurfaceBrush;
            spriteTextVisual.Size = e.NewSize.ToVector2();
        };
        RegisterPropertyChangedCallback(FontSizeProperty, new DependencyPropertyChangedCallback((s, e) =>
        {
            DrawText();
        }));
    }


    private void DrawText()
    {
        if (ActualHeight == 0 || ActualWidth == 0 || string.IsNullOrWhiteSpace(Text) || _drawingSurface == null)
            return;

        var width = (float)ActualWidth;
        var height = (float)ActualHeight;
        using (var session = CanvasComposition.CreateDrawingSession(_drawingSurface))
        {
            session.Clear(Colors.Transparent);
            using (var textFormat = new CanvasTextFormat()
            {
                FontSize = (float)FontSize,
                Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
                VerticalAlignment = CanvasVerticalAlignment.Center,
                HorizontalAlignment = CanvasHorizontalAlignment.Center,

            })
            {
                using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
                {
                    if (ShowNonOutlineText)
                    {
                        session.DrawTextLayout(textLayout, 0, 0, FontColor);
                    }

                    using (var textGeometry = CanvasGeometry.CreateText(textLayout))
                    {
                        var dashedStroke = new CanvasStrokeStyle()
                        {
                            DashStyle = DashStyle
                        };
                        session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
                    }
                }
            }
        }
    }

//SOME CODE AND PROPERTIES

}

5. 結語

文章開頭的那個番茄鍾源碼可以在這裏查看:

也可以安裝我的番茄鍾應用試玩一下,安裝地址:

6. 參考

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

[springboot 開發單體web shop] 8. 商品詳情&評價展示

上文回顧

我們實現了根據搜索關鍵詞查詢商品列表和根據商品分類查詢,並且使用到了mybatis-pagehelper插件,講解了如何使用插件來幫助我們快速實現分頁數據查詢。本文我們將繼續開發商品詳情頁面和商品留言功能的開發。

需求分析

關於商品詳情頁,和往常一樣,我們先來看一看jd的示例:

從上面2張圖,我們可以看出來,大體上需要展示給用戶的信息。比如:商品圖片,名稱,價格,等等。在第二張圖中,我們還可以看到有一個商品評價頁簽,這些都是我們本節要實現的內容。

商品詳情

開發梳理

我們根據上圖(權當是需求文檔,很多需求文檔寫的比這個可能還差勁很多…)分析一下,我們的開發大致都要關注哪些points:

  • 商品標題
  • 商品圖片集合
  • 商品價格(原價以及優惠價)
  • 配送地址(我們的實現不在此,我們後續直接實現在下單邏輯中)
  • 商品規格
  • 商品分類
  • 商品銷量
  • 商品詳情
  • 商品參數(生產場地,日期等等)

根據我們梳理出來的信息,接下來開始編碼就會很簡單了,大家可以根據之前課程講解的,先自行實現一波,請開始你們的表演~

編碼實現

DTO實現

因為我們在實際的數據傳輸過程中,不可能直接把我們的數據庫entity之間暴露到前端,而且我們商品相關的數據是存儲在不同的數據表中,我們必須要封裝一個ResponseDTO來對數據進行傳遞。

  • ProductDetailResponseDTO包含了商品主表信息,以及圖片列表、商品規格(不同SKU)以及商品具體參數(產地,生產日期等信息)
@Data
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductDetailResponseDTO {
    private Products products;
    private List<ProductsImg> productsImgList;
    private List<ProductsSpec> productsSpecList;
    private ProductsParam productsParam;
}

Custom Mapper實現

根據我們之前表的設計,這裏使用生成的通用mapper就可以滿足我們的需求。

Service實現

從我們封裝的要傳遞到前端的ProductDetailResponseDTO就可以看出,我們可以根據商品id分別查詢出商品的相關信息,在controller進行數據封裝就可以了,來實現我們的查詢接口。

  • 查詢商品主表信息(名稱,內容等)

    com.liferunner.service.IProductService中添加接口方法:

        /**
         * 根據商品id查詢商品
         *
         * @param pid 商品id
         * @return 商品主信息
         */
        Products findProductByPid(String pid);

    接着,在com.liferunner.service.impl.ProductServiceImpl中添加實現方法:

        @Override
        @Transactional(propagation = Propagation.SUPPORTS)
        public Products findProductByPid(String pid) {
            return this.productsMapper.selectByPrimaryKey(pid);
        }

    直接使用通用mapper根據主鍵查詢就可以了。

    同上,我們依次來實現圖片、規格、以及商品參數相關的編碼工作

  • 查詢商品圖片信息列表

        /**
         * 根據商品id查詢商品規格
         *
         * @param pid 商品id
         * @return 規格list
         */
        List<ProductsSpec> getProductSpecsByPid(String pid);
    
    ----------------------------------------------------------------
    
        @Override
        public List<ProductsSpec> getProductSpecsByPid(String pid) {
            Example example = new Example(ProductsSpec.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsSpecMapper.selectByExample(example);
        }
  • 查詢商品規格列表

        /**
         * 根據商品id查詢商品規格
         *
         * @param pid 商品id
         * @return 規格list
         */
        List<ProductsSpec> getProductSpecsByPid(String pid);
    
    ------------------------------------------------------------------
    
        @Override
        public List<ProductsSpec> getProductSpecsByPid(String pid) {
            Example example = new Example(ProductsSpec.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsSpecMapper.selectByExample(example);
        }
  • 查詢商品參數信息

        /**
         * 根據商品id查詢商品參數
         *
         * @param pid 商品id
         * @return 參數
         */
        ProductsParam findProductParamByPid(String pid);
    
    ------------------------------------------------------------------
    
        @Override
        public ProductsParam findProductParamByPid(String pid) {
            Example example = new Example(ProductsParam.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsParamMapper.selectOneByExample(example);
        }

Controller實現

在上面將我們需要的信息查詢實現之後,然後我們需要在controller對數據進行包裝,之後再返回到前端,供用戶來進行查看,在com.liferunner.api.controller.ProductController中添加對外接口/detail/{pid},實現如下:

    @GetMapping("/detail/{pid}")
    @ApiOperation(value = "根據商品id查詢詳情", notes = "根據商品id查詢詳情")
    public JsonResponse findProductDetailByPid(
        @ApiParam(name = "pid", value = "商品id", required = true)
        @PathVariable String pid) {
        if (StringUtils.isBlank(pid)) {
            return JsonResponse.errorMsg("商品id不能為空!");
        }
        val product = this.productService.findProductByPid(pid);
        val productImgList = this.productService.getProductImgsByPid(pid);
        val productSpecList = this.productService.getProductSpecsByPid(pid);
        val productParam = this.productService.findProductParamByPid(pid);
        val productDetailResponseDTO = ProductDetailResponseDTO
            .builder()
            .products(product)
            .productsImgList(productImgList)
            .productsSpecList(productSpecList)
            .productsParam(productParam)
            .build();
        log.info("============查詢到商品詳情:{}==============", productDetailResponseDTO);

        return JsonResponse.ok(productDetailResponseDTO);
    }

從上述代碼中可以看到,我們分別查詢了商品、圖片、規格以及參數信息,使用ProductDetailResponseDTO.builder().build()封裝成返回到前端的對象。

Test API

按照慣例,寫完代碼我們需要進行測試。

{
  "status": 200,
  "message": "OK",
  "data": {
    "products": {
      "id": "smoke-100021",
      "productName": "(奔跑的人生) - 中華",
      "catId": 37,
      "rootCatId": 1,
      "sellCounts": 1003,
      "onOffStatus": 1,
      "createdTime": "2019-09-09T06:45:34.000+0000",
      "updatedTime": "2019-09-09T06:45:38.000+0000",
      "content": "吸煙有害健康“
    },
    "productsImgList": [
      {
        "id": "1",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img1.png",
        "sort": 0,
        "isMain": 1,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      },
      {
        "id": "2",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img2.png",
        "sort": 1,
        "isMain": 0,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      },
      {
        "id": "3",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img3.png",
        "sort": 2,
        "isMain": 0,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      }
    ],
    "productsSpecList": [
      {
        "id": "1",
        "productId": "smoke-100021",
        "name": "中華",
        "stock": 2276,
        "discounts": 1.00,
        "priceDiscount": 7000,
        "priceNormal": 7000,
        "createdTime": "2019-07-01T06:54:20.000+0000",
        "updatedTime": "2019-07-01T06:54:28.000+0000"
      },
    ],
    "productsParam": {
      "id": "1",
      "productId": "smoke-100021",
      "producPlace": "中國",
      "footPeriod": "760天",
      "brand": "中華",
      "factoryName": "中華",
      "factoryAddress": "陝西",
      "packagingMethod": "盒裝",
      "weight": "100g",
      "storageMethod": "常溫",
      "eatMethod": "",
      "createdTime": "2019-05-01T09:38:30.000+0000",
      "updatedTime": "2019-05-01T09:38:34.000+0000"
    }
  },
  "ok": true
}

商品評價

在文章一開始我們就看過jd詳情頁面,有一個詳情頁簽,我們來看一下:

它這個實現比較複雜,我們只實現相對重要的幾個就可以了。

開發梳理

針對上圖中紅色方框圈住的內容,分別有:

  • 評價總數
  • 好評度(根據好評總數,中評總數,差評總數計算得出)
  • 評價等級
  • 以及用戶信息加密展示
  • 評價內容

我們來實現上述分析的相對必要的一些內容。

編碼實現

查詢評價

根據我們需要的信息,我們需要從用戶表、商品表以及評價表中來聯合查詢數據,很明顯單表通用mapper無法實現,因此我們先來實現自定義查詢mapper,當然數據的傳輸對象是我們需要先來定義的。

Response DTO實現

創建com.liferunner.dto.ProductCommentDTO.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductCommentDTO {
    //評價等級
    private Integer commentLevel;
    //規格名稱
    private String specName;
    //評價內容
    private String content;
    //評價時間
    private Date createdTime;
    //用戶頭像
    private String userFace;
    //用戶昵稱
    private String nickname;
}

Custom Mapper實現

com.liferunner.custom.ProductCustomMapper中添加查詢接口方法:

    /***
     * 根據商品id 和 評價等級查詢評價信息
     * <code>
     *         Map<String, Object> paramMap = new HashMap<>();
     *         paramMap.put("productId", pid);
     *         paramMap.put("commentLevel", level);
     *</code>
     * @param paramMap
     * @return java.util.List<com.liferunner.dto.ProductCommentDTO>
     * @throws
     */
    List<ProductCommentDTO> getProductCommentList(@Param("paramMap") Map<String, Object> paramMap);

mapper/custom/ProductCustomMapper.xml中實現該接口方法的SQL:

    <select id="getProductCommentList" resultType="com.liferunner.dto.ProductCommentDTO" parameterType="Map">
        SELECT
        pc.comment_level as commentLevel,
        pc.spec_name as specName,
        pc.content as content,
        pc.created_time as createdTime,
        u.face as userFace,
        u.nickname as nickname
        FROM items_comments pc
        LEFT JOIN users u
        ON pc.user_id = u.id
        WHERE pc.item_id = #{paramMap.productId}
        <if test="paramMap.commentLevel != null and paramMap.commentLevel != ''">
            AND pc.comment_level = #{paramMap.commentLevel}
        </if>
    </select>

如果沒有傳遞評價級別的話,默認查詢全部評價信息。

Service 實現

com.liferunner.service.IProductService中添加查詢接口方法:

    /**
     * 查詢商品評價
     *
     * @param pid        商品id
     * @param level      評價級別
     * @param pageNumber 當前頁碼
     * @param pageSize   每頁展示多少條數據
     * @return 通用分頁結果視圖
     */
    CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize);

com.liferunner.service.impl.ProductServiceImpl實現該方法:

    @Override
    public CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("productId", pid);
        paramMap.put("commentLevel", level);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val productCommentList = this.productCustomMapper.getProductCommentList(paramMap);
        for (ProductCommentDTO item : productCommentList) {
            item.setNickname(SecurityTools.HiddenPartString4SecurityDisplay(item.getNickname()));
        }
        // 獲取mybatis插件中獲取到信息
        PageInfo<?> pageInfo = new PageInfo<>(productCommentList);
        // 封裝為返回到前端分頁組件可識別的視圖
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(productCommentList)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

因為評價過多會使用到分頁,這裏使用通用分頁返回結果,關於分頁,可查看。

Controller實現

com.liferunner.api.controller.ProductController中添加對外查詢接口:

    @GetMapping("/comments")
    @ApiOperation(value = "查詢商品評價", notes = "根據商品id查詢商品評價")
    public JsonResponse getProductComment(
        @ApiParam(name = "pid", value = "商品id", required = true)
        @RequestParam String pid,
        @ApiParam(name = "level", value = "評價級別", required = false, example = "0")
        @RequestParam Integer level,
        @ApiParam(name = "pageNumber", value = "當前頁碼", required = false, example = "1")
        @RequestParam Integer pageNumber,
        @ApiParam(name = "pageSize", value = "每頁展示記錄數", required = false, example = "10")
        @RequestParam Integer pageSize
    ) {
        if (StringUtils.isBlank(pid)) {
            return JsonResponse.errorMsg("商品id不能為空!");
        }
        if (null == pageNumber || 0 == pageNumber) {
            pageNumber = DEFAULT_PAGE_NUMBER;
        }
        if (null == pageSize || 0 == pageSize) {
            pageSize = DEFAULT_PAGE_SIZE;
        }
        log.info("============查詢商品評價:{}==============", pid);

        val productComments = this.productService.getProductComments(pid, level, pageNumber, pageSize);
        return JsonResponse.ok(productComments);
    }

FBI WARNING:

@ApiParam(name = “level”, value = “評價級別”, required = false, example = “0”)
@RequestParam Integer level
關於ApiParam參數,如果接收參數為非字符串類型,一定要定義example為對應類型的示例值,否則Swagger在訪問過程中會報example轉換錯誤,因為example缺省為””空字符串,會轉換失敗。例如我們刪除掉level這個字段中的example=”0“,如下為錯誤信息(但是並不影響程序使用。)

2019-11-23 15:51:45 WARN  AbstractSerializableParameter:421 - Illegal DefaultValue null for parameter type integer
java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Long.parseLong(Long.java:601)
    at java.lang.Long.valueOf(Long.java:803)
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:688)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:721)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)

Test API

福利講解

添加Propagation.SUPPORTS和不加的區別

有心的小夥伴肯定又注意到了,在Service中處理查詢時,我一部分使用了@Transactional(propagation = Propagation.SUPPORTS),一部分查詢又沒有添加事務,那麼這兩種方式有什麼不一樣呢?接下來,我們來揭開神秘的面紗。

  • Propagation.SUPPORTS

      /**
       * Support a current transaction, execute non-transactionally if none exists.
       * Analogous to EJB transaction attribute of the same name.
       * <p>Note: For transaction managers with transaction synchronization,
       * {@code SUPPORTS} is slightly different from no transaction at all,
       * as it defines a transaction scope that synchronization will apply for.
       * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
       * will be shared for the entire specified scope. Note that this depends on
       * the actual synchronization configuration of the transaction manager.
       * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
       */
      SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

    主要關注Support a current transaction, execute non-transactionally if none exists.從字面意思來看,就是如果當前環境有事務,我就加入到當前事務;如果沒有事務,我就以非事務的方式執行。從這方面來看,貌似我們加不加這一行其實都沒啥差別。

    划重點:NOTE,對於一個帶有事務同步的管理器來說,這裡有一丟丟的小區別啦。(所以大家在讀註釋的時候,一定要看這個Note.往往這裏面會有好東西給我們,就相當於我們的大喇叭!)

    這個同步事務管理器定義了一個事務同步的一個範圍,如果加了這個註解,那麼就等同於我讓你來管我啦,你裏面的資源我想用就可以用(JDBC Connection, Hibernate Session).

結論1

SUPPORTS 標註的方法可以獲取和當前事務環境一致的 Connection 或 Session,不使用的話一定是一個新的連接;
再注意下面又一個NOTE,即便上面的配置加入了,但是事務管理器的實際同步配置會影響到真實的執行到底是否會用你。看它的說明:@see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization.

  /**
   * Set when this transaction manager should activate the thread-bound
   * transaction synchronization support. Default is "always".
   * <p>Note that transaction synchronization isn't supported for
   * multiple concurrent transactions by different transaction managers.
   * Only one transaction manager is allowed to activate it at any time.
   * @see #SYNCHRONIZATION_ALWAYS
   * @see #SYNCHRONIZATION_ON_ACTUAL_TRANSACTION
   * @see #SYNCHRONIZATION_NEVER
   * @see TransactionSynchronizationManager
   * @see TransactionSynchronization
   */
  public final void setTransactionSynchronization(int transactionSynchronization) {
      this.transactionSynchronization = transactionSynchronization;
  }

描述信息只是說在同一個事務管理器才能起作用,並沒有什麼實際意義,我們來看一下TransactionSynchronization具體的內容:

package org.springframework.transaction.support;

import java.io.Flushable;

public interface TransactionSynchronization extends Flushable {

  /** Completion status in case of proper commit. */
  int STATUS_COMMITTED = 0;

  /** Completion status in case of proper rollback. */
  int STATUS_ROLLED_BACK = 1;

  /** Completion status in case of heuristic mixed completion or system errors. */
  int STATUS_UNKNOWN = 2;

  /**
   * Suspend this synchronization.
   * Supposed to unbind resources from TransactionSynchronizationManager if managing any.
   * @see TransactionSynchronizationManager#unbindResource
   */
  default void suspend() {
  }

  /**
   * Resume this synchronization.
   * Supposed to rebind resources to TransactionSynchronizationManager if managing any.
   * @see TransactionSynchronizationManager#bindResource
   */
  default void resume() {
  }

  /**
   * Flush the underlying session to the datastore, if applicable:
   * for example, a Hibernate/JPA session.
   * @see org.springframework.transaction.TransactionStatus#flush()
   */
  @Override
  default void flush() {
  }

  /**
   * ...
   */
  default void beforeCommit(boolean readOnly) {
  }

  /**
   * ...
   */
  default void beforeCompletion() {
  }

  /**
   * ...
   */
  default void afterCommit() {
  }

  /**
   * ...
   */
  default void afterCompletion(int status) {
  }
}

事務管理器可以通過org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization(int)來對當前事務進行行為干預,比如將它設置為1,可以執行事務回調,設置為2,表示出錯了,但是如果沒有加入PROPAGATION.SUPPORTS註解的話,即便你在當前事務中,你也不能對我進行操作和變更。

結論2

添加PROPAGATION.SUPPORTS之後,當前查詢中可以對當前的事務進行設置回調動作,不添加就不行。

源碼下載

下節預告

下一節我們將繼續開發商品詳情展示以及商品評價業務,在過程中使用到的任何開發組件,我都會通過專門的一節來進行介紹的,兄弟們末慌!

gogogo!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

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

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

高德服務單元化方案和架構實踐

導讀:本文主要介紹了高德在服務單元化建設方面的一些實踐經驗,服務單元化建設面臨很多共性問題,如請求路由、單元封閉、數據同步,有的有成熟方案可以借鑒和使用,但不同公司的業務不盡相同,要盡可能的結合業務特點,做相應的設計和處理。

一、為什麼要做單元化

  • 單機房資源瓶頸

隨着業務體量和服務用戶群體的增長,單機房或同城雙機房無法支持服務的持續擴容。

  • 服務異地容災

異地容災已經成為核心服務的標配,有的服務雖然進行了多地多機房部署,但數據還是只在中心機房,實現真正意義上的異地多活,就需要對服務進行單元化改造。

二、高德單元化的特點

在做高德單元化項目時,我們首先要考慮的是結合高德的業務特點,看高德的單元化有什麼不一樣的訴求,這樣就清楚哪些經驗和方案是可以直接拿來用的,哪些又是需要我們去解決的。

高德業務和傳統的在線交易業務還是不太一樣,高德為用戶提供以導航為代表的出行服務,很多業務場景對服務的RT要求會很高,所以在做單元化方案時,盡可能減少對整體服務RT的影響就是我們需要重點考慮的問題,盡量做到數據離用戶近一些。轉換到單元化技術層面需要解決兩個問題:

1.用戶設備的單元接入需要盡可能的做到就近接入,用戶真實地理位置接近哪個單元就接入哪個單元,如華北用戶接入到張北,華南接入到深圳。

2.用戶的單元劃分最好能與就近接入的單元保持一致,減少單元間的跨單元路由。如用戶請求從深圳進來,用戶的單元劃分最好就在深圳單元,如果劃到張北單元就會造成跨單元路由。

另外一個區別就是高德很多業務是無須登錄的,所以我們的單元化方案除了用戶ID也要支持基於設備ID。

三、高德單元化實踐

服務的單元化架構改造需要一個至上而下的系統性設計,核心要解決請求路由、單元封閉、數據同步三方面問題。

請求路由:根據高德業務的特點,我們提供了取模路由和路由表路由兩種策略,目前上線應用使用較多的是路由表路由策略。

單元封閉:得益於集團的基礎設施建設,我們使用vipserver、hsf等服務治理能力保證服務同機房調用,從而實現單元封閉(hsf unit模式也是一種可行的方案,但個人認為同機房調用的架構和模式更簡潔且易於維護)。

數據同步:數據部分使用的是集團DB產品提供的DRC數據同步。

單元路由服務採用什麼樣的部署方案是我們另一個要面臨的問題,考慮過以下三種方案:

第一種SDK的方式因為對業務的強侵入性是首先被排除的,統一接入層進行代理和去中心化插件集成兩種方案各有利弊,但當時首批要接入單元化架構的服務很多都還沒有統一接入到gateway,所以基於現狀的考慮使用了去中心化插件集成的方式,通過在應用的nginx集成UnitRouter。

服務單元化架構

目前高德賬號,雲同步、用戶評論系統都完成了單元化改造,採用三地四機房部署,寫入量較高的雲同步服務,單元寫高峰能達到數w+QPS (存儲是mongodb集群)。

以賬號系統為例介紹下高德單元化應用的整體架構。

賬號系統服務是三地四機房部署,數據分別存儲在tair為代表的緩存和XDB里,數據存儲三地集群部署、全量同步。賬號系統服務器的Tengine上安裝UntiRouter,它請求的負責單元識別和路由,用戶單元劃分是通過記錄用戶與單元關係的路由表來控制。

PS:因歷史原因緩存使用了tair和自建的uredis(在redis基礎上添加了基於log的數據同步功能),目前已經在逐步統一到tair。數據同步依賴tair和alisql的數據同步方案,以及自建的uredis數據同步能力。

就近接入實現方案

為滿足高德業務低延時要求,就要想辦法做到數據(單元)離用戶更近,其中有兩個關鍵鏈路,一個是通過aserver接入的外網連接,另一個是服務內部路由(盡可能不產生跨單元路由)。

措施1:客戶端的外網接入通過aserver上的配置,將不同地理區域(七個大區)的設備劃分到對應近的單元,如華北用戶接入張北單元。

措施2:通過記錄用戶和單元關係的路由表來劃分用戶所屬單元,這個關係是通過系統日誌分析出來的,用戶經常從哪個單元入口進來,就會把用戶劃分到哪個單元,從而保證請求入口和單元劃分的相對一致,從而減少跨單元路由。

所以,在最終的單元路由實現上我們提供了傳統的取模路由,和為降延時而設計的基於路由表路由兩種策略。同時,為解無須登錄的業務場景問題,上述兩種策略除了支持用戶ID,我們同時也支持設備ID。

路由表設計

路由表分為兩部分,一個是用戶-分組的關係映射表,另一個是分組-單元的關係映射表。在使用時,通過路由表查對應的分組,再通過分組看用戶所屬單元。分組對應中國大陸的七個大區。

先看“用戶-(大區)分組”:

路由表是定期通過系統日誌分析出來的,看用戶最近IP屬於哪個大區就劃分進哪個分組,同時也對應上了具體單元。當一個北京的用戶長期去了深圳,因IP的變化路由表更新后將划進新大區分組,從而完成用戶從張北單元到深圳單元的遷移。

再看“分組-單元”:

分組與單元的映射有一個默認關係,這是按地理就近來配置的,比如華南對應深圳。除了默認的映射關係,還有幾個用於切流預案的關係映射。

老用戶可以通過路由表來查找單元,新用戶怎麼辦?對於新用戶的處理我們會降級成取模的策略進行單元路由,直至下次路由表的更新。所以整體上看新用戶跨單元路由比例肯定是比老用戶大的多,但因為新用戶是一個相對穩定的增量,所以整體比例在可接受範圍內。

路由計算

有了路由表,接下來就要解工程化應用的問題,性能、空間、靈活性和準確率,以及對服務穩定性的影響這幾個方面是要進行綜合考慮的,首先考慮外部存儲會增加服務的穩定性風險,後面我們在BloomFilter 、BitMap和MapDB多種方案中選擇BloomFilter,萬分之幾的誤命中率導致的跨單元路由在業務可接受範圍內。

通過日誌分析出用戶所屬大區后,我們將不同分組做成多個布隆過濾器,計算時逐層過濾。這個計算有兩種特殊情況:

1) 因為BloomFilter存在誤算率,有可能存在一種情況,華南分組的用戶被計算到華北了,這種情況比例在萬分之3 (生成BloomFilter時可調整),它對業務上沒有什麼影響,這類用戶相當於被劃分到一個非所在大區的分組裡,但這個關係是穩定的,不會影響到業務,只是存在跨單元路由,是可接受的。

2) 新用戶不在分組信息里,所以經過逐層的計算也沒有匹配到對應大區分組,此時會使用取模進行模除分組的計算。

如果業務使用的是取模路由而非路由表路由策略,則直接根據tid或uid計算對應的模除分組,原理簡單不詳表了。

單元切流

在發生單元故障進行切流時,主要分為四步驟

打開單元禁寫 (跨單元寫不敏感業務可以不配置)

檢查業務延時

切換預案

解除單元禁寫

PS:更新路由表時,也需要上述操作,只是第3步的切換預案變成切換新版本路由表;單元禁寫主要是了等待數據同步,避免數據不一致導致的業務問題。

核心指標

單元計算耗時1~2ms

跨單元路由比例底於5%

除了性能外,因就近接入的訴求,跨單元路由比例也是我們比較關心的重要指標。從線上觀察看,路由表策略單元計算基本上在1、2ms內完成,跨單元路由比例3%左右,整體底於5%。

四、後續優化

統一接入集成單元化能力

目前大部分服務都接入了統一接入網關服務,在網關集成單元化能力將大大減少服務單元化部署的成本,通過簡單的配置就可以實現單元路由,服務可將更多的精力放在業務的單元封閉和數據同步上。

分組機制的優化

按大區分組存在三個問題:

通過IP計算大區有一定的誤算率,會導致部分用戶劃分錯誤分組。

分組粒度太大,單元切流時流量不好分配。舉例,假如華東是我們用戶集中的大區,切流時把這個分組切到任意一個指定單元,都會造成單元服務壓力過大。

計算次數多,分多少個大區,理論最大計算次數是有多少次,最後採取取模策略。

針對上述幾個問題我們計劃對分組機製做如下改進

通過用戶進入單元的記錄來確認用戶所屬單元,而非根據用戶IP所在大區來判斷,解上述問題1。

每個單元劃分4個虛擬分組,支持更細粒度單元切流,解上述問題2。

用戶確實單元后,通過取模來劃分到不同的虛擬分組。每個單元只要一次計算就能完成,新用戶只需經過3次計算,解上述問題3。

熱更時的雙表計算

與取模路由策略不同,路由表策略為了把跨單元路由控制在一個較好的水平需要定期更新,目前更新時需要一個短暫的單元禁寫,這對於很多業務來說是不太能接受的。

為優化這個問題,系統將在路由表更新時做雙(路由)表計算,即將新老路由表同時加載進內存,更新時不再對業務做完全的禁寫,我們會分別計算當前用戶(或設備)在新老路由表的單元結果,如果單元一致,則說明路由表的更新沒有導致該用戶(或設備)變更單元,所以請求會被放行,相反如果計算結果是不同單元,說明發生了單元變更,該請求會被攔截,直至到達新路由表的一個完全起用時間。

優化前服務會完全禁寫比如10秒(時間取決於數據同步時間),優化後會變成觸髮禁寫的是這10秒內路由發生變更的用戶,這將大大減少對業務的影響。

服務端數據驅動的單元化場景

前面提到高德在路由策略上結合業務的特別設計,但整體單元劃分還是以用戶(或設備)為維度來進行的,但高德業務還有一個大的場景是我們未來要面對和解決的,就是以數據維度驅動的單元設計,基於終端的服務路由會變成基於數據域的服務路由。

高德很多服務是以服務數據為核心的,像地圖數據等它並非由用戶直接產生。業務的發展數據存儲也將不斷增加,包括5G和自動駕駛,對應數據的爆髮式增長單點全量存儲並不實現,以服務端數據驅動的服務單元化設計,是我們接下來要考慮的重要應用場景。

寫在最後

不同的業務場景對單元化會有不同的訴求,我們提供不同的策略和能力供業務進行選擇,對於多數據服務我們建議使用業務取模路由,簡單且易於維護;對於RT敏感的服務使用路由表的策略來盡可能的降低服務響應時長的影響。另外,要注意的是強依賴性的服務要採用相同的路由策略。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

圖文詳解基於角色的權限控制模型RBAC

我們開發一個系統,必然面臨權限控制的問題,即不同的用戶具有不同的訪問、操作、數據權限。形成理論的權限控制模型有:自主訪問控制(DAC: Discretionary Access Control)、強制訪問控制(MAC: Mandatory Access Control)、基於屬性的權限驗證(ABAC: Attribute-Based Access Control)等。最常被開發者使用也是相對易用、通用的就是RBAC權限模型(Role-Based Access Control),本文就將向大家介紹該權限模型。

一、RBAC權限模型簡介

RBAC權限模型(Role-Based Access Control)即:基於角色的權限控制。模型中有幾個關鍵的術語:

  • 用戶:系統接口及訪問的操作者
  • 權限:能夠訪問某接口或者做某操作的授權資格
  • 角色:具有一類相同操作權限的用戶的總稱

RBAC權限模型核心授權邏輯如下:

  • 某用戶是什麼角色?
  • 某角色具有什麼權限?
  • 通過角色的權限推導用戶的權限

二、RBAC的演化進程

2.1.用戶與權限直接關聯

想到權限控制,人們最先想到的一定是用戶與權限直接關聯的模式,簡單地說就是:某個用戶具有某些權限。如圖:

  • 張三具有創建用戶和刪除用戶的權限,所以他可能系統維護人員
  • 李四具有產品記錄管理和銷售記錄管理權限,所以他可能是一個業務銷售人員

這種模型能夠清晰的表達用戶與權限之間的關係,足夠簡單。但同時也存在問題:

  • 現在用戶是張三、李四,以後隨着人員增加,每一個用戶都需要重新授權
  • 或者張三、李四離職,需要針對每一個用戶進行多種權限的回收

2.2.一個用戶擁有一個角色

在實際的團體業務中,都可以將用戶分類。比如對於薪水管理系統,通常按照級別分類:經理、高級工程師、中級工程師、初級工程師。也就是按照一定的角色分類,通常具有同一角色的用戶具有相同的權限。這樣改變之後,就可以將針對用戶賦權轉換為針對角色賦權。

  • 一個用戶有一個角色
  • 一個角色有多個操作(菜單)權限
  • 一個操作權限可以屬於多個角色

我們可以用下圖中的數據庫設計模型,描述這樣的關係。

2.3 一個用戶一個或多個角色

但是在實際的應用系統中,一個用戶一個角色遠遠滿足不了需求。如果我們希望一個用戶既擔任銷售角色、又暫時擔任副總角色。該怎麼做呢?為了增加系統設計的適用性,我們通常設計:

  • 一個用戶有一個或多個角色
  • 一個角色包含多個用戶
  • 一個角色有多種權限
  • 一個權限屬於多個角色

我們可以用下圖中的數據庫設計模型,描述這樣的關係。

二、頁面訪問權限與操作權限

  • 頁面訪問權限: 所有系統都是由一個個的頁面組成,頁面再組成模塊,用戶是否能看到這個頁面的菜單、是否能進入這個頁面就稱為頁面訪問權限。
  • 操作權限: 用戶在操作系統中的任何動作、交互都需要有操作權限,如增刪改查等。比如:某個按鈕,某個超鏈接用戶是否可以點擊,是否應該看見的權限。

為了適應這種需求,我們可以把頁面資源(菜單)和操作資源(按鈕)分表存放,如上圖。也可以把二者放到一個表裡面存放,用一個字段進行標誌區分。

三、數據權限

數據權限比較好理解,就是某個用戶能夠訪問和操作哪些數據。

  • 通常來說,數據權限由用戶所屬的組織來確定。比如:生產一部只能看自己部門的生產數據,生產二部只能看自己部門的生產數據;銷售部門只能看銷售數據,不能看財務部門的數據。而公司的總經理可以看所有的數據。
  • 在實際的業務系統中,數據權限往往更加複雜。非常有可能銷售部門可以看生產部門的數據,以確定銷售策略、安排計劃等。

所以為了面對複雜的需求,數據權限的控制通常是由程序員書寫個性化的SQL來限制數據範圍的,而不是交給權限模型或者Spring Security或shiro來控制。當然也可以從權限模型或者權限框架的角度去解決這個問題,但適用性有限。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

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

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

SpringBoot Application深入學習

本節主要介紹SpringBoot Application類相關源碼的深入學習。

主要包括:

  1. SpringBoot應用自定義啟動配置
  2. SpringBoot應用生命周期,以及在生命周期各個階段自定義配置。

本節採用SpringBoot 2.1.10.RELASE,對應示例源碼在:

SpringBoot應用啟動過程:

SpringApplication application = new SpringApplication(DemoApplication.class);
application.run(args);

一、Application類自定義啟動配置

創建SpringApplication對象后,在調用run方法之前,我們可以使用SpringApplication對象來添加一些配置,比如禁用banner、設置應用類型、設置配置文件(profile)

舉例:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(DemoApplication.class);
        // 設置banner禁用
        application.setBannerMode(Banner.Mode.OFF);
        // 將application-test文件啟用為profile
        application.setAdditionalProfiles("test");
        // 設置應用類型為NONE,即啟動完成后自動關閉
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);
    }

}

​ 也可以使用SpringApplicationBuilder類來創建SpringApplication對象,builder類提供了鏈式調用的API,更方便調用,增強了可讀性。

        new SpringApplicationBuilder(YqManageCenterApplication.class)
                .bannerMode(Banner.Mode.OFF)
                .profiles("test")
                .web(WebApplicationType.NONE)
                .run(args);

二、application生命周期

SpringApplication的生命周期主要包括:

  1. 準備階段:主要包括加載配置、設置主bean源、推斷應用類型(三種)、創建和設置SpringBootInitializer、創建和設置Application監聽器、推斷主入口類
  2. 運行階段:開啟時間監聽、加載運行監聽器、創建Environment、打印banner、創建和裝載context、廣播應用已啟動、廣播應用運行中

我們先來看一下源碼的分析:

SpringBootApplication構造器:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        
        // 設置默認配置
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.addConversionService = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.isCustomEnvironment = false;
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        // 設置主bean源
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        // 推斷和設置應用類型(三種)
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        // 創建和設置SpringBootInitializer
  this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 創建和設置SpringBoot監聽器
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        // 推斷和設置主入口類
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

SpringApplication.run方法源碼:

public ConfigurableApplicationContext run(String... args) {
        // 開啟時間監聽
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
    
        // 加載Spring應用運行監聽器(SpringApplicationRunListenter)
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

        Collection exceptionReporters;
        try {
            // 創建environment(包括PropertySources和Profiles)
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            
            // 打印banner
            Banner printedBanner = this.printBanner(environment);
            
            // 創建context(不同的應用類型對應不同的上下文)
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
            // 裝載context(其中還初始化了IOC容器)
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            // 調用applicationContext.refresh
            this.refreshContext(context);
            // 空方法
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop(); // 關閉時間監聽;這樣可以計算出完整的啟動時間
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            // 廣播SpringBoot應用已啟動,會調用所有SpringBootApplicationRunListener里的started方法
            listeners.started(context);
            
            // 遍歷所有ApplicationRunner和CommadnLineRunner的實現類,執行其run方法
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            // 廣播SpringBoot應用運行中,會調用所有SpringBootApplicationRunListener里的running方法
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            // run出現異常時,處理異常;會調用報錯的listener里的failed方法,廣播應用啟動失敗,將異常擴散出去
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

三、application生命周期自定義配置

在SpringApplication的生命周期中,我們還可以添加一些自定義的配置。

下面的配置,主要是通過實現Spring提供的接口,然後在resources下新建META-INF/spring.factories文件,在裏面添加這個類而實現引入的。

準備階段,可以添加如下自定義配置:

3.1 自定義ApplicationContextInitializer的實現類

@Order(100)
public class MyInitializer implements ApplicationContextInitializer {

@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
    System.out.println("自定義的應用上下文初始化器:" + configurableApplicationContext.toString());
}
}

再定義一個My2Initializer,設置@Order(101)

然後在spring.factories文件里如下配置:

# initializers
org.springframework.context.ApplicationContextInitializer=\
  com.example.applicationdemo.MyInitializer,\
  com.example.applicationdemo.My2Initializer

啟動項目:

3.2 自定義ApplicationListener的實現類

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}![file](https://img2018.cnblogs.com/blog/1860493/201911/1860493-20191125130012982-1676057906.png)

即監聽ApplicationEvents類的ApplicationListener接口的實現類。

首先查看有多少種ApplicationEvents:

裏面還可以進行拆分。

我們這裏設置兩個ApplicationListener,都用於監聽ApplicationEnvironmentPreparedEvent

@Order(200)
public class MyApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
        System.out.println("MyApplicationListener: 應用環境準備完畢" + applicationEnvironmentPreparedEvent.toString());
    }
}

在spring.factories中加入applicationListener的配置:

# application-listeners
org.springframework.context.ApplicationListener=\
  com.example.applicationdemo.MyApplicationListener,\
  com.example.applicationdemo.MyApplicationListener2

啟動階段,可以添加如下自定義配置:

3.3 自定義SpringBootRunListener的實現類

監聽整個SpringBoot應用生命周期

public interface SpringApplicationRunListener {
    // 應用啟動
    void starting();

    // 應用ConfigurableEnvironment準備完畢,此刻可以將其調整
    void environmentPrepared(ConfigurableEnvironment environment);

    // 上下文準備完畢
    void contextPrepared(ConfigurableApplicationContext context);

    // 上下文裝載完畢
    void contextLoaded(ConfigurableApplicationContext context);

    // 啟動完成(Beans已經加載到容器中)
    void started(ConfigurableApplicationContext context);

    // 應用運行中
    void running(ConfigurableApplicationContext context);

    // 應用運行失敗
    void failed(ConfigurableApplicationContext context, Throwable exception);
}

我們可以自定義SpringApplicationRunListener的實現類,通過重寫以上方法來定義自己的listener。

比如:

public class MyRunListener implements SpringApplicationRunListener {

    // 注意要加上這個構造器,兩個參數都不能少,否則啟動會報錯,報錯的詳情可以看這個類的最下面
    public MyRunListener(SpringApplication springApplication, String[] args) {

    }

    @Override
    public void starting() {
        System.out.println("MyRunListener: 程序開始啟動");
    }

    // 其他方法省略,不做修改
}

然後在spring.factories文件中添加這個類:

org.springframework.boot.SpringApplicationRunListener=\
  com.example.applicationdemo.MyRunListener

啟動:

3.4 自定義ApplicationRunner或CommandLineRunner

application的run方法中,有這樣一行:

this.callRunners(context, applicationArguments);

仔細分析源碼,發現這一句的作用是:SpringBoot應用啟動過程中,會遍歷所有的ApplicationRunner和CommandLineRunner,執行其run方法。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        Iterator var4 = (new LinkedHashSet(runners)).iterator();

        while(var4.hasNext()) {
            Object runner = var4.next();
            if (runner instanceof ApplicationRunner) {
                this.callRunner((ApplicationRunner)runner, args);
            }

            if (runner instanceof CommandLineRunner) {
                this.callRunner((CommandLineRunner)runner, args);
            }
        }

    }
@FunctionalInterface
public interface CommandLineRunner {
    void run(String... args) throws Exception;
}
@FunctionalInterface
public interface ApplicationRunner {
    void run(ApplicationArguments args) throws Exception;
}

分別定義一個實現類,添加@Component,這兩個實現類不需要在spring.factories中配置

好了,關於這些自定義配置的具體使用,後續會繼續進行介紹,請持續關注!感謝!

具體示例代碼請去查看。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!