4. 彤哥說netty系列之Java NIO實現群聊(自己跟自己聊上癮了)

你好,我是彤哥,本篇是netty系列的第四篇。

歡迎來我的公從號彤哥讀源碼系統地學習源碼&架構的知識。

簡介

上一章我們一起學習了Java中的BIO/NIO/AIO的故事,本章將帶着大家一起使用純純的NIO實現一個越聊越上癮的“群聊系統”。

業務邏輯分析

首先,我們先來分析一下群聊的功能點:

(1)加入群聊,並通知其他人;

(2)發言,並通知其他人;

(3)退出群聊,並通知其他人;

一個簡單的群聊系統差不多這三個功能足夠了,為了方便記錄用戶信息,當用戶加入群聊的時候自動給他分配一個用戶ID。

業務實現

上代碼:

// 這是一個內部類
private static class ChatHolder {
    // 我們只用了一個線程,用普通的HashMap也可以
    static final Map<SocketChannel, String> USER_MAP = new ConcurrentHashMap<>();

    /**
     * 加入群聊
     * @param socketChannel
     */
    static void join(SocketChannel socketChannel) {
        // 有人加入就給他分配一個id,本文來源於公從號“彤哥讀源碼”
        String userId = "用戶"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
        send(socketChannel, "您的id為:" + userId + "\n\r");

        for (SocketChannel channel : USER_MAP.keySet()) {
            send(channel, userId + " 加入了群聊" + "\n\r");
        }

        // 將當前用戶加入到map中
        USER_MAP.put(socketChannel, userId);
    }

    /**
     * 退出群聊
     * @param socketChannel
     */
    static void quit(SocketChannel socketChannel) {
        String userId = USER_MAP.get(socketChannel);
        send(socketChannel, "您退出了群聊" + "\n\r");
        USER_MAP.remove(socketChannel);

        for (SocketChannel channel : USER_MAP.keySet()) {
            if (channel != socketChannel) {
                send(channel, userId + " 退出了群聊" + "\n\r");
            }
        }
    }

    /**
     * 擴散說話的內容
     * @param socketChannel
     * @param content
     */
    public static void propagate(SocketChannel socketChannel, String content) {
        String userId = USER_MAP.get(socketChannel);
        for (SocketChannel channel : USER_MAP.keySet()) {
            if (channel != socketChannel) {
                send(channel, userId + ": " + content + "\n\r");
            }
        }
    }

    /**
     * 發送消息
     * @param socketChannel
     * @param msg
     */
    static void send(SocketChannel socketChannel, String msg) {
        try {
            ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
            writeBuffer.put(msg.getBytes());
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服務端代碼

服務端代碼直接使用上一章NIO的實現,只不過這裏要把上面實現的業務邏輯適時地插入到相應的事件中。

(1)accept事件,即連接建立的時候,說明加入了群聊;

(2)read事件,即讀取數據的時候,說明有人說話了;

(3)連接斷開的時候,說明退出了群聊;

OK,直接上代碼,為了與上一章的代碼作區分,彤哥特意加入了一些標記:

public class ChatServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 將accept事件綁定到selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // 阻塞在select上
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍歷selectKeys
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 如果是accept事件
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = ssc.accept();
                    System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    // 加入群聊,本文來源於公從號“彤哥讀源碼”
                    ChatHolder.join(socketChannel);
                } else if (selectionKey.isReadable()) {
                    // 如果是讀取事件
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 將數據讀入到buffer中
                    int length = socketChannel.read(buffer);
                    if (length > 0) {
                        buffer.flip();
                        byte[] bytes = new byte[buffer.remaining()];
                        // 將數據讀入到byte數組中
                        buffer.get(bytes);

                        // 換行符會跟着消息一起傳過來
                        String content = new String(bytes, "UTF-8").replace("\r\n", "");
                        if (content.equalsIgnoreCase("quit")) {
                            // 退出群聊,本文來源於公從號“彤哥讀源碼”
                            ChatHolder.quit(socketChannel);
                            selectionKey.cancel();
                            socketChannel.close();
                        } else {
                            // 擴散,本文來源於公從號“彤哥讀源碼”
                            ChatHolder.propagate(socketChannel, content);
                        }
                    }
                }
                iterator.remove();
            }
        }
    }
}

測試

打開四個XSHELL客戶端,分別連接telnet 127.0.0.1 8080,然後就可以開始群聊了。

彤哥發現,自己跟自己聊天也是會上癮的,完全停不下來,不行了,我再去自聊一會兒^^

總結

本文彤哥跟着大家一起實現了“群聊系統”,去掉註釋也就100行左右的代碼,是不是非常簡單?這就是NIO網絡編程的魅力,我發現寫網絡編程也上癮了^^

問題

這兩章我們都沒有用NIO實現客戶端,你知道怎麼實現嗎?

提示:服務端需要監聽accept事件,所以需要有一個ServerSocketChannel,而客戶端是直接去連服務器了,所以直接用SocketChannel就可以了,一個SocketChannel就相當於一個Connection。

最後,也歡迎來我的公從號彤哥讀源碼系統地學習源碼&架構的知識。

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

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

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

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

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

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

Algorithm: GCD、EXGCD、Inverse Element

數論基礎

數論是純數學的一個研究分支,主要研究整數的性質。初等數論包括整除理論、同余理論、連分數理論。這一篇主要記錄的是同余相關的基礎知識。

取模

取模是一種運算,本質就是帶余除法,運算結果就是餘數。取模運算結果的符號由被模數(被除數)決定。
\[ 7\%4=3;\space7\%(-4)=3;\\ (-7)\%4=-3;\space(-7)\%(-4)=-3 \]

取模運算的性質

\[ 設a>b>0,有:\\ (a+b)\%c=(a\%c+b\%c)\%c\\ (a-b)\%c=(a\%c-b\%c+c)\%c \\ (a\times b)\%c=(a\%c\times b\%c)\%c \]

例題1

\[ 給定兩個非負整數a,b和正整數n,計算f(a^b)\%n。\\ 其中,f(0)=f(1)=1,且對於\forall i>0,f(i+2)=f(i+1)+f(i)\\ (0\le a,b\le 2^{64},1\le n\le 1000) \]

找規律,對於mod n來說,最多n²項就會出現重複,找出重複項即可得到循環周期,然後找對應的項即可。

例題2:hdu 1212

題意是給一個位數不超過1,000的正整數A和一個大小不超過100,000的正整數B,求A%B。

這裏只需要用到上面的兩條性質,把A截斷,從高到低模擬做除法即可。

#include <iostream>
string s; int mod, ans;
int main() {
    while(std::cin >> str >> mod) {
        ans = 0;
        for(int i = 0; s[i]; i++) ans = (ans * 10 + (s[i] - '0')) % mod;
        std::cout << ans << std::endl;
    }
    return 0;
}

GCD

GCD即Greatest Common Divisor,最大公約數。最大公約數即能同時整除給定兩個整數的最大正整數。特殊地:gcd(a,0)=a。求解最大公約數通常使用輾轉相除法。下面是輾轉相除法的代碼實現:

typedef long long LL;
LL gcd(LL a, LL b) {
    return b ? gcd(b, a % b) : a;
}

輾轉相除法得到的餘數序列增長速度比斐波那契數列更快,已知斐波那契的增長是指數級別,則輾轉相除法的複雜度是對數級別。

輾轉相除法的證明:

1.設兩個數a、b(a>b),他們的最大公約數為gcd(a,b),r = a % b,k = (a – a % b)/ b,那麼證明輾轉相除法,即是要證明:gcd(a,b)=gcd(b,r)。首先我們令c = gcd(a,b),那麼肯定存在互質的整數m,n,使得a = mc,b = nc。那麼有r = a – kb = mc – knc = (m – kn)c,根據這條式子,c也是r的因數。回過頭看,如果能證明m-kn和n互質,那麼就可以說明c=gcd((m-kn)c,nc)=gcd(b,r),所以把問題再次轉化為:求證m-kn和n互質。

2.反證法證明m-kn和n互質:假設m-kn和n不互質,用數學語言描述為:假設存在整數x,y,d,其中d>1,使得m-kn=xd,n=yd。那麼有m=kn+xd=kyd+xd=(ky+x)d,從而a=mc=(ky+x)cd,b=nc=ycd,則gcd(a,b)=cd≠c,與前設矛盾。故m-kn和n互質得證,也就證明了gcd(a,b)=gcd(b,r)。

唯一分解定理

對於任意大於2的正整數X,它總能寫成如下形式:
\[ X=p_1^{a_1}\times p_2^{a_2} \times ……\times p_k^{a_k},其中p是各不相同的質數 \]
即質因數分解。且這個分解唯一。

LCM

LCM即Least Common Multiple,最小公倍數。

\[ \begin{align} &根據唯一分解定理,對於給定的a,b:\\ &a=p_1^{a_1}\times p_2^{a_2} \times……\times p_k^{a_k}\\ &b=p_1^{b_1}\times p_2^{b_2} \times……\times p_k^{b_k}\\ &那麼:\\ &gcd(a,b)=p_1^{min(a_1,b_1)}\times p_2^{min(a_2,b_2)}\times ……p_1^{min(a_k,b_k)}\\ &lcm(a,b)=p_1^{max(a_1,b_1)}\times p_2^{max(a_2,b_2)}\times ……p_1^{max(a_k,b_k)}\\ &因此,gcd(a,b)\times lcm(a,b)=a\times b \end{align} \]

LL lcm(LL a, LL b) {
    return a / gcd(a, b) * b;
}

例題:hdu1788

題意是求最小的正整數N,滿足除以每一個給定的數Mi,其結果均為Mi-a。作如下轉化:
\[ N\%M_i\equiv(M_i-a)\%M_i\Rightarrow (N+a)\%M_i=0(a<M_i<100) \]
即N是Mi的倍數,那麼題意也就是求所有M的最小公倍數,注意数字範圍即可。

同余

對於兩個不同的數a,b,如果有a % p = b % p(p>1),那我們就稱a和b模p同余,記作:
\[ a\equiv b(mod\space p) \]

同余的性質

\[ \begin{aligned} &1.自反性:a\equiv a(mod\space m)\\ &2.對稱性:a\equiv b(mod\space m),則b\equiv a(mod\space m)\\ &3.傳遞性:若a\equiv b(mod\space m),b\equiv c(mod\space m),則a\equiv c(mod\space m)\\ &4.線性運算:若a\equiv b(mod\space m),c\equiv d(mod\space m)\\ &\space\space\space則:a\pm c\equiv b\pm d(mod\space m);\space a\times c\equiv b\times d(mod\space m)\\ &5.冪運算:若a\equiv b(mod\space m),那麼a^n\equiv b^n(mod\space m)\\ &6.若ac\equiv bc(mod\space m),且c≠0,那麼a\equiv b(mod\space {m\over gcd(c,m)})\\ &\space\space\space特殊地,若gcd(c,m)=1,則a\equiv b(mod\space m) \end{aligned} \]

EXGCD

貝祖定理(Bezouts Identity):若設a,b是不全為0的整數,則存在整數x,y,使得ax+by=gcd(a,b),(a,b)代表最大公因數,則設a,b是不全為零的整數,則肯定存在整數x,y,使得ax+by=(a,b)。這個式子稱為貝祖等式。

EXtend GCD 即擴展歐幾里得算法,它可以用於解關於x,y的貝祖等式。

EXGCD的可行性

當a=0時,ax+by=gcd(a,b)=gcd(0,b)=b,這時可以解出y=1而x為任意。同理可得b=0時,解得x=1而y為任意。當a,b都不為0時:
\[ \begin{aligned} &設存在一組解為x_1,y_1,即ax_1+by_1=gcd(a,b)\\ &由歐幾里得定理:gcd(a,b)=gcd(b,a\%b)得:\\ &ax_1+by_1=gcd(a,b)=gcd(b,a\%b)\\ &那麼,bx_2+(a\%b)y_2=gcd(b,a\%b)=ax_1+by_1\\ &又a\%b=a-a/b\times b\\ &\begin {align} 那麼,&ax_1+by_1\\ =&bx_2+(a-a/b\times b)y_2\\ =&bx_2+ay_2-a/b*b*y_2\\ =&ay_2+b(x_2-a/b*y_2) \end{align}\\ &故可以得到:x_1=y_2,\space y_1=x_2-a/b*y_2 \end{aligned} \]
a或b為0即迭代求解的出口,迭代過程用代碼錶示如下:

typedef long long LL;
// ver 1
// 返回值為gcd(a,b),x和y即求出的特解
LL exgcd(LL a, LL b, LL &x, LL &y) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    LL ans = exgcd(b, a % b, x, y);
    //這裏通過迭代求出了一組解x,y,這組解對應上面推導過程中的x2和y2。
    LL temp = x;
    x = y;
    y = temp - a / b * y;
    return ans;
}

// ver 2
// 在熟悉了推導過程之後,我們可以利用引用的性質得到更為簡潔的ver2寫法
LL exgcd(LL a, LL b, LL &x, LL &y) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    LL ans = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return ans;
}

EXGCD的應用

求關於x,y的方程ax+by=c的一組解

這裏只需要對貝祖等式稍作變換即可。假設EXGCD求出的方程ax+by=gcd(a,b)的一組解為x1和y1。在方程兩邊同時乘上一個數m,使得c=m*gcd(a,b),這裏也就要求c是gcd(a,b)的倍數,即c%gcd(a,b)=0。這也是方程有解的條件。此時方程為:
\[ \begin{align} &ax_1\times m+by_1\times m=gcd(a,b)\times m=c \end{align} \]
對於ax+by=gcd(a,b),其參數解為:
\[ \begin{align} &x=x_1+{b\over gcd(a,b)}\times t,y=y_1-{a\over gcd(a,b)}\times t,t為參數\\ &這裏選用{b\over gcd(a,b)}和{a\over gcd(a,b)}是為了保證結果均為整數 \end{align} \]
那麼對比方程ax+by=c,可以得到其特解為:
\[ \begin{align} &x_0=x_1\times m=x_1\times {c\over gcd(a,b)}\\ &y_0=y_1\times m=y_1\times {c\over gcd(a,b)} \end{align} \]
那麼其通解為:
\[ \begin{align} &X=x_0+{b\over gcd(a,b)}\times t,Y=y_0-{a\over gcd(a,b)}\times t,t為參數\\ &這裏選用{b\over gcd(a,b)}和{a\over gcd(a,b)}作為係數是為了確保得到均為整數解。 \end{align} \]
對於任意一個確定的t,都有一組確定的解與之對應,只需要根據需要找出對應的解即可。

例如求滿足ax+by=c的X的最小非負整數解,那麼在得到X的通解之後只需調整t,調整過程用偽代碼錶示為:

S = b / gcd(a, b);
X = (x0 % S + S) % S;
// 由於x0可能是負數,故在模之後還要加上S在取一次模。
// 同理可得Y的最小非負整數解
T = a / gcd(a, b);
Y = (y0 % T + T) % T;

完整的exgcd求解方程ax+by=c的X和Y的最小非負整數解得代碼如下:

LL exgcd(LL a, LL b, LL c, LL& x, LL& y) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    LL ans = exgcd(a, b, c, y, x);
    y -= a / b * x;
    LL S = b / ans, T = a / ans;
    x = (x % S + S) % S;
    y = (y % T + T) % T;
    return ans;
}

求關於x的方程ax≡b(mod m)的最小非負整數解

ax≡b(mod m)即ax%m=b%m,即求ax+my=b%m(取模其實就相當於減去(加上)了若干個模數)。但只有a和m互質時有唯一解,否則無解。轉化為ax+my=b(假設b<m)后,套用exgcd即可。

求a關於模數p的乘法逆元

設x是a關於p的乘法逆元,那麼有ax≡1(mod p),這是第二個應用的特殊情況,顯然也可以通過類似方法求得。

逆元

Inverse Element,逆元,推廣了加法中的加法逆元和乘法中的倒數。直觀地說,它是一個可以取消另一給定元素運算的元素。a關於模p的逆元存在的條件是gcd(a,p)=1。
\[ \begin{align} &在模p意義下,設A的逆元為A^{-1},那麼有A\times A^{-1}\equiv 1(mod\space p) \end{align} \]
為什麼需要逆元呢?
\[ \begin{align} &在模意義下,{A\over B}\%p = {A\%p\over B\%p}\%p並不成立,\\ &那麼設B在模p意義下的逆元表示為B^{-1},根據逆元的定義,有{A\over B}=A\times B^{-1}(mod \space p)。 \end{align} \]
這裏,我們把除法轉化為乘法,就可以運用取模運算的性質:(a * b) % c = (a % c * b % c) % c,優化算法。

逆元的求法

EXGCD求法

給定模數m,求a的逆元相當於求解關於x的方程ax≡1(mod m)。這個方程可以轉化為ax-my=1 。用EXGCD可以求得一組x,y和gcd。檢查gcd是否為1(因為EXGCD是把問題轉化為求解方程ax-my=gcd(a,m),顯然只有gcd(a,m)=1時才能轉化),gcd不為1則說明逆元不存在。在能夠成功求解的情況下,把解調整x到0~m-1的範圍中即可。

費馬小定理

在模p為質數的情況下,有費馬小定理:
\[ a^{p-1}\equiv 1(mod\space p) \]
那麼:
\[ a\times a^{p-2}\equiv 1(mod\space p)\\ 故a的逆元a^{-1}=a^{p-2} \]
然後用快速冪求出逆元即可。

拓展:快速冪

// a是底數,b是指數
LL pow(LL a, LL b, LL mod) {
    LL ans = 1;
    while(b) {
        if(b&1) ans = ans * a % mod;
        b >>= 1, a = a * a % mod;
    }
    return ans;
}

拓展:慢速乘,防止快速冪過程中乘法運算爆long long,只需把快速冪中的乘法替換為mul()即可,一般情況用不上。

// a為因數1,b為因數2
LL mul(LL a, LL b, LL mod) {
    LL ans = 0;
    while(b) {
        if(b&1) ans = (ans + a) % mod;
        b >>= 1, a = (a + a) % mod;
    }
    return ans;
}

歐拉定理

費馬小定理其實是歐拉定理的特殊情況。在模數p不為質數時,有:
\[ a^{\phi(p)}\equiv 1(其中\phi()是歐拉函數) \]
同理可以得到:
\[ a^{-1}\equiv a^{\phi(p)-1} \]

線性逆元表

有時我們需要快速得出1~p-1的所有逆元,這個時候我們需要一種O(n)的方法,這就是線性逆元表。

首先,1關於任意模p的逆元的逆元都是1。設p = k * i + r(r < i ,1 < i < p),那麼有:
\[ k\times i + r \equiv0(mod\space p) \]
利用逆元性質,兩邊同時乘上i的逆元以及r的逆元,得到:
\[ k\times r^{-1}+i^{-1}\equiv 0(mod\space p) \]
移項得:
\[ i^{-1}\equiv-k \times r^{-1}\equiv -⌊{p\over i}⌋\times (p\%i)^{-1}(mod\space p) \]
顯然p%i是小於i的,那麼我們就可以利用之前的結果在O(1)時間算出單獨的逆元。

用代碼錶示即:

// inv[i]對應i的逆元
LL inv[maxn];
void CalInv() {
    // 0沒有逆元,故不初始化
    inv[1] = 1;
    for (int i = 2; i < maxn; i++)
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}

利用
\[ (a\times c)\% mod = (a\%mod\times c\%mod)\%mod;\\ 由a\times a^{-1}\equiv1(mod \space p)得,\\ inv_{fac(i)}\equiv inv_{fac(i)}\times (i+1)^{-1}\times (i+1)\equiv inv_{fac(i+1)}\times (i+1)(mod\space p) \]
我們還可以在線性時間求出1~min(n,p)的階乘的逆元,代碼如下:

LL fac[maxn], inv[maxn];
void CalFacInv() {
    // 0的階乘是1
    fac[0] = fac[1] = 1;
    for (int i = 2; i < maxn; i++)
        fac[i] = fac[i - 1] * i % mod;
    inv[maxn - 1] = QPow(fac[maxn - 1], mod - 2);
    // 注意邊界是≥
    for (int i = maxn - 2; i >= 0; i--)
        inv[i] = inv[i + 1] * (i + 1) % mod;
}

例題:hdu1576

題意即把除法轉化為逆元乘法。3種方法的代碼:

#include<iostream>
#include<algorithm>
using namespace std;

typedef long long LL;
LL n, B;
const LL mod = 9973;

LL exgcd(LL a, LL b, LL &x, LL &y) {
    if (b == 0) {
        x = 1, y = 0;
        return a;
    }
    LL ans = exgcd(b, a % b, y, x);
    y -= a/b*x;
    return ans;
}

LL QPow(LL x, LL n)
{
    LL res = 1;
    while(n)
    {
        if(n & 1) res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}

LL inv[10000];
void CalInv() {
    inv[1] = 1;
    for (LL i = 2; i < 10000; i++) {
        inv[i] = (mod * mod - mod / i * inv[mod % i]) % mod;
    }
}


int main() {
    CalInv();
    int t;
    cin >> t;
    while (t--) {
        cin >> n >> B;
        // exgcd
        // LL x, y;
        // exgcd(B, mod, x, y);
        // cout << (x + mod) * n % mod << endl;
        
        // QPow
        // cout << QPow(B, mod - 2) * n % mod << endl;
        
        // 逆元打表
        cout << inv[B % mod] * n % mod << endl;
    }
    return 0;
}

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

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

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

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

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

CSS複合選擇器是什麼?複合選擇器是如何工作

複合選擇器介紹

  • 複合選擇器其實很好理解,說白了就跟我們生活中的有血緣關係家庭成員一樣,通過標籤或者class屬性或id屬性,去找對應的有血緣關係的某個選擇器,具體的大家往下看哦。
  • 如果是初學者對基本的選擇器不是很了解的可以看筆者之前寫過的基本選擇器文章,,在這就不過多的介紹基本選擇器的使用了。

複合選擇器說明表

選擇器 描述 舉例
選擇器1 選擇器2{屬性:值;} 多元素選擇器。同時匹配選擇器1和選擇器2,多個選擇器之間用逗號隔開即可。 h1,h2,h3{color: red;}
E F {屬性:值;} 後代元素選擇器,匹配所有屬於E元素後代的F元素,E和F之間用空格隔開即可。 .box h1{color: red;}
E>F{屬性:值;} 子元素選擇器,匹配所有E元素的子元素為F div >h1{color:red;}
E+F{屬性:值;} 相鄰元素選擇器,匹配所有緊跟隨着E元素之後的同級元素F div+div{color:red;}

多元素選擇器

  • 多元素選擇器在工作當中經常會用到,主要用於設置多個元素使用同一種CSS樣式。
  • 讓我們進入多元素選擇器實踐,實踐內容如:將HTML頁面中的div標籤、h1標籤、p標籤、中的文本顏色設置為紅色。
  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>多元素選擇器</title>
</head>
    <style>
        div,h1,p{
           color: red;
        }
    </style>
<body>
    <div>成功不是打敗別人,而是改變自己。</div>
    <h1>成功不是打敗別人,而是改變自己。</h1>
    <p>成功不是打敗別人,而是改變自己。</p>
</body>

</html>
  • 結果圖

  • 注意:作用給列表中的所有的選擇器設置樣式,class類選擇器或id選擇器也是一樣,在這裏就拿class類選擇器屬性值為.box為例,其餘的大家可以自己嘗試。

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>多元素選擇器</title>
</head>
    <style>
        .box,h1,p{
           color: red;
        }
    </style>
<body>
    <div class="box" >成功不是打敗別人,而是改變自己。</div>
    <h1>成功不是打敗別人,而是改變自己。</h1>
    <p>成功不是打敗別人,而是改變自己。</p>
</body>

</html>
  • 結果圖

後代元素選擇器

  • 讓我們進入後代元素選擇器實踐,實踐內容如:將class屬性值為.box的後代元素文本顏色設置為紅色,給大家介紹下結構:class屬性值為.box中一共有三個子元素,第一個h1標籤、第二個h1標籤、第三個div標籤、但是第三個子元素為div標籤裏面還有一個子元素為h1標籤或者我們可以理解為class屬性值為.box的孫子輩元素。
  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>後代元素選擇器</title>
</head>
    <style>
        .box h1{
           color: red;
        }
    </style>
<body>
    <div class="box" >
        <h1>成功不是打敗別人,而是改變自己。</h1>
        <h1>微笑是最初的信仰</h1>
        <div>
            <h1>成功不是打敗別人,而是改變自己。</h1>
        </div>
    </div>
    
</body>

</html>
  • 結果圖

  • 注意:後代選擇器只能包含在class屬性值為.box裏面的所有屬性。

子元素選擇器

  • 讓我們進入子元素選擇器實踐,實踐內容如:將class屬性值為.box的子元素文本顏色設置為紅色,給大家介紹下結構:class屬性值為.box中一共有三個子元素,第一個h1標籤、第二個h1標籤、div標籤、但是第三個子元素為div標籤裏面還有一個子元素為h1標籤或者我們可以理解為class屬性值為.box的孫子輩元素。

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>子元素選擇器</title>
</head>
    <style>
        .box > h1{
           color: red;
        }
    </style>
<body>
    <div class="box" >
        <h1>成功不是打敗別人,而是改變自己。</h1>
        <h1>微笑是最初的信仰</h1>
        <div>
            <h1>成功不是打敗別人,而是改變自己。</h1>
        </div>
    </div>

</body>

</html>
  • 結果圖

  • 注意:子元素選擇器和後代元素選擇器是不樣的,子元素選擇器是將class屬性值為.box中的子元素設置CSS樣式,後代元素選擇器是將class屬性值為.box中的所有元素設置CSS樣式,現在大家應該明白了,為什麼div標籤中的h1標籤文本顏色沒有被渲染的原因了吧,因為div標籤中的h1標籤是孫子輩。

相鄰元素選擇器

  • 相鄰元素選擇器必須滿足以下條件才會匹配。
  • E元素與F元素必須是兄弟關係,意思就是平輩關係。
  • E元素與F元素必須要緊挨着,就是之間不能有任何元素阻擋。
  • 要求F元素一定是在E元素的下面。
  • 讓我們進入相鄰元素選擇器實踐,實踐內容如:將HTML頁面中的class屬性值為.box的相鄰元素文本顏色設置為紅色。
  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>相鄰元素選擇器</title>
</head>
    <style>
        .box + h1{
           color: red;
        }
    </style>
<body>
    <h1>我在上面學習</h1>
    <div class="box" >
        <h1>成功不是打敗別人,而是改變自己。</h1>
    </div>
    <h1>我在下面學習</h1>
</body>
</html>
  • 結果圖

  • 注意:大家一定要注意以上的2個必須和1個一定的規則,否則CSS樣式不會被渲染。

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

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?

Spring Boot Actuator監控使用詳解

在企業級應用中,學習了如何進行SpringBoot應用的功能開發,以及如何寫單元測試、集成測試等還是不夠的。在實際的軟件開發中還需要:應用程序的監控和管理。SpringBoot的Actuator模塊實現了應用的監控與管理。

Actuator簡介

生產系統中,往往需要對系統實際運行的情況(例如cpu、io、disk、db、業務功能等指標)進行監控運維。在SpringBoot項目中Actuator模塊提供了眾多HTTP接口端點(Endpoint),來提供應用程序運行時的內部狀態信息。

Actuator模塊提供了一個監控和管理生產環境的模塊,可以使用http、jmx、ssh、telnet等來管理和監控應用。包括應用的審計(Auditing)、健康(health)狀態信息、數據採集(metrics gathering)統計等監控運維的功能。同時,提供了可以擴展 Actuator端點(Endpoint)自定義監控指標。這些指標都是以JSON接口數據的方式呈現。

Actuator的使用

使用Spring Boot Actuator需要加入如下依賴:

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

actuator並沒有默認集成在自動配置中,而在作為獨立的項目來呈現的。當引入了上面的依賴,默認會引入actuator相關的兩個項目:

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

其中spring-boot-actuator為功能實現,spring-boot-actuator-autoconfigure為自動配置。

需要注意:因SpringBoot Actuator會暴露服務的詳細信息,為了保障安全性,建議添加安全控制的相關依賴spring-boot-starter-security,這樣在訪問應用監控端點時,都需要輸入驗證信息。所需依賴如下:

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

關於security的使用我們在此不進行展開,可在application文件中配置相應的訪問密碼:

spring:
  security:
    user:
      name: admin
      password: admin

在下面的內容中為了方便,我們暫時不引入security。

經過以上步驟的操作,啟動SpringBoot項目,actuator便自動集成配置了,可通過:http://localhost:8080/actuator 訪問,結果如下:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/actuator",
            "templated": false
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",
            "templated": false
        },
        "health-component": {
            "href": "http://localhost:8080/actuator/health/{component}",
            "templated": true
        },
        "health-component-instance": {
            "href": "http://localhost:8080/actuator/health/{component}/{instance}",
            "templated": true
        },
        "info": {
            "href": "http://localhost:8080/actuator/info",
            "templated": false
        }
    }
}

默認支持的鏈接有:

/actuator
/actuator/health
/health/{component}/{instance}
/health/{component}
/actuator/info

可以在application配置文件中配置開啟更多的監控信息:

management:
  endpoints:
    web:
      exposure:
        include: '*'
#      base-path: /monitor
  endpoint:
    health:
      show-details: always
    shutdown:
      enabled: true
  • management.endpoints.web.exposure.include=’*’,代表開啟全部監控,也可僅配置需要開啟的監控,如: management.endpoints.web.exposure.include=beans,trace。
  • management.endpoint.health.show-details=always,health endpoint開啟显示全部細節。默認情況下/actuator/health是公開的,但不显示細節。
  • management.endpoints.web.base-path=/monitor,啟用指定的url地址訪問根路徑,默認路徑為/actuator/*,開啟則訪問路徑變為/monitor/*。
  • management.endpoint.shutdown.enabled=true,啟用接口關閉SpringBoot。

監控信息如果需要跨越調用,可通過CORS配置來支持,默認處於禁用狀態。設置management.endpoints.web.cors.allowed-origins屬性后開啟。

比如允許來自https://www.choupangxia.com 域的GET和POST調用:

management:
  endpoints:
    web:
      cors:
        allowed-origins: https://www.choupangxia.com
        allowed-methods: GET,POST

REST接口

Spring Boot Actuator提供了非常豐富的監控接口,可以通過這些接口了解應用程序運行時的內部狀況。Actuator也支持用戶自定義添加端點,可以根據實際應用,定義一些比較關心的指標,在運行期進行監控。

HTTP方法 路徑 描述
GET /auditevents 显示當前應用程序的審計事件信息
GET /beans 显示一個應用中所有Spring Beans的完整列表
GET /conditions 显示配置類和自動配置類(configuration and auto-configuration classes)的狀態及它們被應用或未被應用的原因。
GET /configprops 显示一個所有@ConfigurationProperties的集合列表
GET /env 显示來自Spring的ConfigurableEnvironment的屬性。
GET /flyway 显示數據庫遷移路徑,如果有的話。
GET /health 显示應用的健康信息(當使用一個未認證連接訪問時显示一個簡單的’status’,使用認證連接訪問則显示全部信息詳情)
GET /info 显示任意的應用信息
GET /liquibase 展示任何Liquibase數據庫遷移路徑,如果有的話
GET /metrics 展示當前應用的metrics信息
GET /mappings 显示一個所有@RequestMapping路徑的集合列表
GET /scheduledtasks 显示應用程序中的計劃任務
GET /sessions 允許從Spring會話支持的會話存儲中檢索和刪除(retrieval and deletion)用戶會話。使用Spring Session對反應性Web應用程序的支持時不可用。
POST /shutdown 允許應用以優雅的方式關閉(默認情況下不啟用)
GET /threaddump 執行一個線程dump

如果使用web應用(Spring MVC, Spring WebFlux, 或者 Jersey),還可以使用以下接口:

HTTP方法 路徑 描述
GET /heapdump 返回一個GZip壓縮的hprof堆dump文件
GET /jolokia 通過HTTP暴露JMX beans(當Jolokia在類路徑上時,WebFlux不可用)
GET /logfile 返回日誌文件內容(如果設置了logging.file或logging.path屬性的話),支持使用HTTP Range頭接收日誌文件內容的部分信息
GET /prometheus 以可以被Prometheus服務器抓取的格式显示metrics信息

接口詳解

health主要用來檢查應用的運行狀態,這是使用頻次最高的監控點。通常使用此接口显示應用實例的運行狀態,以及應用不“健康”的原因,比如數據庫連接、磁盤空間不夠等。

默認情況下health的狀態是開放的,訪問:http://localhost:8080/actuator/health 即可看到應用的狀態。

{
    "status" : "UP"
}

設置狀態碼順序:setStatusOrder(Status.DOWN,Status.OUT_OF_SERVICE, Status.UP, Status.UNKNOWN)。
過濾掉不能識別的狀態碼。如果無任何狀態碼,整個SpringBoot應用的狀態是UNKNOWN。將所有收集到的狀態碼排序。返回有序狀態碼序列中的第一個狀態碼,作為整個SpringBoot應用的狀態。

Health通過合併幾個健康指數檢查應用的健康情況。SpringBoot Actuator會自動配置以下內容:

名稱 描述
CassandraHealthIndicator 檢查Cassandra數據庫是否已啟動。
CouchbaseHealthIndicator 檢查Couchbase群集是否已啟動。
DiskSpaceHealthIndicator 檢查磁盤空間不足。
DataSourceHealthIndicator 檢查是否可以建立連接DataSource。
ElasticsearchHealthIndicator 檢查Elasticsearch集群是否已啟動。
InfluxDbHealthIndicator 檢查InfluxDB服務器是否已啟動。
JmsHealthIndicator 檢查JMS代理是否啟動。
MailHealthIndicator 檢查郵件服務器是否已啟動。
MongoHealthIndicator 檢查Mongo數據庫是否已啟動。
Neo4jHealthIndicator 檢查Neo4j服務器是否已啟動。
RabbitHealthIndicator 檢查Rabbit服務器是否已啟動。
RedisHealthIndicator 檢查Redis服務器是否啟動。
SolrHealthIndicator 檢查Solr服務器是否已啟動。

可以通過設置 management.health.defaults.enabled屬性來全部禁用。

原生端點

原生端點分為三大類:

  • 應用配置類:獲取應用程序中加載的應用配置、環境變量、自動化配置報告等與Spring Boot應用密切相關的配置類信息。
  • 度量指標類:獲取應用程序運行過程中用於監控的度量指標,比如:內存信息、線程池信息、HTTP請求統計等。
  • 操作控制類:提供了對應用的關閉等操作類功能。

應用配置類

/conditions:該端點用來獲取應用的自動化配置報告,其中包括所有自動化配置的候選項。同時還列出了每個候選項自動化配置的各個先決條件是否滿足。該端點可以幫助我們方便的找到一些自動化配置為什麼沒有生效的具體原因。

該報告內容將自動化配置內容分為兩部分:positiveMatches中返回的是條件匹配成功的自動化配置和negativeMatches中返回的是條件匹配不成功的自動化配置。

部分代碼如下:

"contexts": {
        "application": {
            "positiveMatches": {
                "MsgAutoConfiguration": [
                    {
                        "condition": "OnClassCondition",
                        "message": "@ConditionalOnClass found required class 'com.secbro2.msg.MsgService'"
                    }
                ],
                "MsgAutoConfiguration#msgService": [
                    {
                        "condition": "OnPropertyCondition",
                        "message": "@ConditionalOnProperty (msg.enabled=true) matched"
                    },
                    {
                        "condition": "OnBeanCondition",
                        "message": "@ConditionalOnMissingBean (types: com.secbro2.msg.MsgService; SearchStrategy: all) did not find any beans"
                    }
                ],

/info:就是在配置文件中配置的以info開頭的信息,如配置為:

info:
  app:
    name: spring-boot-actuator
    version: 1.0.0

返回結果:

{
  "app":{
    "name":"spring-boot-actuator",
    "version":"1.0.0"
  }
}

info中配置的參數也可以通過符號*@*包圍的屬性值來自pom.xml文件中的元素節點。如下:

info:
    build:
        artifact: @project.artifactId@
        name: @project.name@
        description: @project.description@
        ersion: @project.version@

返回結果:

{
    "build": {
        "artifact": "spring-learn",
        "name": "spring-learn",
        "description": "Demo project for Spring Boot",
        "ersion": "0.0.1-SNAPSHOT"
    }
}

/beans:該端點用來獲取應用上下文中創建的所有Bean。

{
    "contexts": {
        "application": {
            "beans": {
                "endpointCachingOperationInvokerAdvisor": {
                    "aliases": [],
                    "scope": "singleton",
                    "type": "org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor",
                    "resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class]",
                    "dependencies": ["environment"]
                },
                "defaultServletHandlerMapping": {
                    "aliases": [],
                    "scope": "singleton",
                    "type": "org.springframework.web.servlet.HandlerMapping",
                    "resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",
                    "dependencies": []
                },
            },
            "parentId": null
        }
    }
}

接口展現了bean的別名、類型、是否單例、類的地址、依賴等信息。

/configprops:該端點用來獲取應用中配置的屬性信息報告。

{
    "spring.transaction-org.springframework.boot.autoconfigure.transaction.TransactionProperties": {
        "prefix": "spring.transaction",
        "properties": {}
    }
}

上面展示了TransactionProperties屬性的配置信息。

/mappings:該端點用來返回所有SpringMVC的控制器映射關係報告。

{
  "handler": "Actuator web endpoint 'beans'",
  "predicate": "{GET /actuator/beans, produces [application/vnd.spring-boot.actuator.v2+json || application/json]}",
  "details": {
    "handlerMethod": {
      "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler",
      "name": "handle",
      "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;"
    },
    "requestMappingConditions": {
      "consumes": [],
      "headers": [],
      "methods": ["GET"],
      "params": [],
      "patterns": ["/actuator/beans"],
      "produces": [{
        "mediaType": "application/vnd.spring-boot.actuator.v2+json",
        "negated": false
      }, {
        "mediaType": "application/json",
        "negated": false
      }]
    }
  }
}

/env:該端點與/configprops不同,它用來獲取應用所有可用的環境屬性報告。包括:環境變量、JVM屬性、應用的配置配置、命令行中的參數。

度量指標類

應用配置類提供的指標為靜態報告,而度量指標類端點提供的報告內容則是動態變化的,提供了應用程序在運行過程中的一些快照信息,比如:內存使用情況、HTTP請求統計、外部資源指標等。這些端點對於構建微服務架構中的監控系統非常有幫助。

/metrics:該端點用來返回當前應用的各類重要度量指標,比如:內存信息、線程信息、垃圾回收信息等。

{
    "names": [
        "jvm.memory.max",
        "jvm.threads.states",
        "http.server.requests",
        "process.files.max",
        "jvm.gc.memory.promoted",
        "system.load.average.1m",
        "jvm.memory.used",
        "jvm.gc.max.data.size",
        "jvm.gc.pause",
        "jvm.memory.committed",
        "system.cpu.count",
        "logback.events",
        "tomcat.global.sent",
        "jvm.buffer.memory.used",
        "tomcat.sessions.created",
        "jvm.threads.daemon",
        "system.cpu.usage",
        "jvm.gc.memory.allocated",
        "tomcat.global.request.max",
        "tomcat.global.request",
        "tomcat.sessions.expired",
        "jvm.threads.live",
        "jvm.threads.peak",
        "tomcat.global.received",
        "process.uptime",
        "tomcat.sessions.rejected",
        "process.cpu.usage",
        "tomcat.threads.config.max",
        "jvm.classes.loaded",
        "jvm.classes.unloaded",
        "tomcat.global.error",
        "tomcat.sessions.active.current",
        "tomcat.sessions.alive.max",
        "jvm.gc.live.data.size",
        "tomcat.threads.current",
        "process.files.open",
        "jvm.buffer.count",
        "jvm.buffer.total.capacity",
        "tomcat.sessions.active.max",
        "tomcat.threads.busy",
        "process.start.time"
    ]
}

從上面的示例中有這些重要的度量值:

  • 系統信息:包括處理器數量processors、運行時間uptime和instance.uptime、系統平均負載systemload.average。
  • mem.*:內存概要信息,包括分配給應用的總內存數量以及當前空閑的內存數量。這些信息來自java.lang.Runtime。
  • heap.*:堆內存使用情況。這些信息來自java.lang.management.MemoryMXBean接口中getHeapMemoryUsage方法獲取的java.lang.management.MemoryUsage。
  • nonheap.*:非堆內存使用情況。這些信息來自java.lang.management.MemoryMXBean接口中getNonHeapMemoryUsage方法獲取的java.lang.management.MemoryUsage。
  • threads.*:線程使用情況,包括線程數、守護線程數(daemon)、線程峰值(peak)等,這些數據均來自java.lang.management.ThreadMXBean。
  • classes.*:應用加載和卸載的類統計。這些數據均來自java.lang.management.ClassLoadingMXBean。
  • gc.*:垃圾收集器的詳細信息,包括垃圾回收次數gc.ps_scavenge.count、垃圾回收消耗時間gc.ps_scavenge.time、標記-清除算法的次數gc.ps_marksweep.count、標記-清除算法的消耗時間gc.ps_marksweep.time。這些數據均來自java.lang.management.GarbageCollectorMXBean。
  • httpsessions.*:Tomcat容器的會話使用情況。包括最大會話數httpsessions.max和活躍會話數httpsessions.active。該度量指標信息僅在引入了嵌入式Tomcat作為應用容器的時候才會提供。
  • gauge.*:HTTP請求的性能指標之一,它主要用來反映一個絕對數值。比如上面示例中的gauge.response.hello: 5,它表示上一次hello請求的延遲時間為5毫秒。
  • counter.*:HTTP請求的性能指標之一,它主要作為計數器來使用,記錄了增加量和減少量。如上示例中counter.status.200.hello: 11,它代表了hello請求返回200狀態的次數為11。

/threaddump:會生成當前線程活動的快照。方便我們在日常定位問題的時候查看線程的情況。主要展示了線程名、線程ID、線程的狀態、是否等待鎖資源等信息。

{
    "threads": [{
        "threadName": "Reference Handler",
        "threadId": 2,
        "blockedTime": -1,
        "blockedCount": 2,
        "waitedTime": -1,
        "waitedCount": 0,
        "lockName": null,
        "lockOwnerId": -1,
        "lockOwnerName": null,
        "daemon": true,
        "inNative": false,
        "suspended": false,
        "threadState": "RUNNABLE",
        "priority": 10,
        "stackTrace": [{
            "classLoaderName": null,
            "moduleName": "java.base",
            "moduleVersion": "11.0.4",
            "methodName": "waitForReferencePendingList",
            "fileName": "Reference.java",
            "lineNumber": -2,
            "className": "java.lang.ref.Reference",
            "nativeMethod": true
        }
  ...
  "lockedMonitors": [],
        "lockedSynchronizers": [{
            "className": "java.util.concurrent.locks.ReentrantLock$NonfairSync",
            "identityHashCode": 2060076420
        }],
        "lockInfo": null
  ...
  {
        "threadName": "DestroyJavaVM",
        "threadId": 42,
        "blockedTime": -1,
        "blockedCount": 0,
        "waitedTime": -1,
        "waitedCount": 0,
        "lockName": null,
        "lockOwnerId": -1,
        "lockOwnerName": null,
        "daemon": false,
        "inNative": false,
        "suspended": false,
        "threadState": "RUNNABLE",
        "priority": 5,
        "stackTrace": [],
        "lockedMonitors": [],
        "lockedSynchronizers": [],
        "lockInfo": null
    }]
}

/trace:該端點用來返回基本的HTTP跟蹤信息。默認情況下,跟蹤信息的存儲採用。

操作控制類

/shutdown:配置文件中配置開啟此功能:

management.endpoint.shutdown.enabled=true

使用 curl 模擬 post 請求此接口:

curl -X POST "http://localhost:8080/actuator/shutdown"

显示結果為:

{
    "message": "Shutting down, bye..."
}

小結

本篇文章介紹了SpringBoot Actuator監控的基本功能和詳解,下篇文章將會帶大家了解一下該功能在Spring Boot中的實現原理。歡迎關注公眾號“程序新視界”。

原文鏈接:《》

程序新視界:精彩和成長都不容錯過

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

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

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

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

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

小三通物流營運型態?

※快速運回,大陸空運推薦?

SpringSecurity系列之自定義登錄驗證成功與失敗的結果處理

一、需要自定義登錄結果的場景

在我之前的文章中,做過登錄驗證流程的源碼解析。其中比較重要的就是

  • 當我們登錄成功的時候,是由AuthenticationSuccessHandler進行登錄結果處理,默認跳轉到defaultSuccessUrl配置的路徑對應的資源頁面(一般是首頁index.html)。
  • 當我們登錄失敗的時候,是由AuthenticationfailureHandler進行登錄結果處理,默認跳轉到failureUrl配置的路徑對應的資源頁面(一般是登錄頁login.html)。

但是在web應用開發過程中需求是千變萬化的,有時需要我們針對登錄結果做個性化處理,比如:

  • 我們希望不同的人登陸之後,看到不同的首頁
  • 我們應用是前後端分離的,驗證響應結果是JSON格式數據,而不是頁面跳轉

以上的這些情況,使用Spring Security作為安全框架的時候,都需要我們使用本節學到的知識進行自定義的登錄驗證結果處理。

二、自定義登陸成功的結果處理

為了滿足上面的需求,我們該如何去做呢?下面一小節我們來說明一下。AuthenticationSuccessHandler接口是Security提供的認證成功處理器接口,我們只需要去實現它即可。但是通常來說,我們不會直接去實現AuthenticationSuccessHandler接口,而是繼承SavedRequestAwareAuthenticationSuccessHandler 類,這個類會記住用戶上一次請求的資源路徑,比如:用戶請求books.html,沒有登陸所以被攔截到了登錄頁,當你萬成登陸之後會自動跳轉到books.html,而不是主頁面。

@Component
public class MyAuthenticationSuccessHandler 
                        extends SavedRequestAwareAuthenticationSuccessHandler {

    //在application配置文件中配置登陸的類型是JSON數據響應還是做頁面響應
    @Value("${spring.security.logintype}")
    private String loginType;

    private  static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        Authentication authentication) 
                                        throws ServletException, IOException {

        if (loginType.equalsIgnoreCase("JSON")) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(AjaxResponse.success()));
        } else {
            // 會幫我們跳轉到上一次請求的頁面上
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}
  • 在上面的自定義登陸成功處理中,既適應JSON前後端分離的應用登錄結果處理,也適用於模板頁面跳轉應用的登錄結果處理
  • ObjectMapper 是Spring Boot默認集成的JSON數據處理類庫Jackson中的類。
  • AjaxResponse是一個自定義的通用的JSON數據接口響應類。

三、自定義登錄失敗的結果處理

這裏我們同樣沒有直接實現AuthenticationFailureHandler接口,而是繼承SimpleUrlAuthenticationFailureHandler 類。該類中默認實現了登錄驗證失敗的跳轉邏輯,即登陸失敗之後回到登錄頁面。我們可以利用這一點簡化我們的代碼。

@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    //在application配置文件中配置登陸的類型是JSON數據響應還是做頁面響應
    @Value("${spring.security.logintype}")
    private String loginType;

    private  static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response, 
                                        AuthenticationException exception) 
                                        throws IOException, ServletException {

        if (loginType.equalsIgnoreCase("JSON")) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(
                    objectMapper.writeValueAsString(
                            AjaxResponse.error(
                                    new CustomException(
                                        CustomExceptionType.USER_INPUT_ERROR,
                                        "用戶名或密碼存在錯誤,請檢查后再次登錄"))));
        } else {
            response.setContentType("text/html;charset=UTF-8");
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}
  • 在上面的自定義登陸失敗處理中,既適應JSON前後端分離的應用登錄失敗結果處理,也適用於模板頁面跳轉應用的登錄失敗結果處理
  • 登陸失敗之後,將默認跳轉到默認的failureUrl,即登錄界面。

四、配置SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.csrf().disable() //禁用跨站csrf攻擊防禦,後面的章節會專門講解
           .formLogin()
           .successHandler(myAuthenticationSuccessHandler)
           .failureHandler(myAuthenticationFailureHandler)
           .defaultSuccessUrl("/index")//登錄認證成功后默認轉跳的路徑
           .failureUrl("/login.html") //登錄認證是被跳轉頁面
}
  • 將自定義的AuthenticationSuccessHandler和AuthenticationFailureHandler注入到Spring Security配置類中
  • 使用fromlogin模式,配置successHandler和failureHandler。
  • 並且配置defaultSuccessUrl和failureUrl

    期待您的關注

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

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

併發編程-深入淺出AQS

AQS是併發編程中非常重要的概念,它是juc包下的許多併發工具類,如CountdownLatch,CyclicBarrier,Semaphore 和鎖, 如ReentrantLock, ReaderWriterLock的實現基礎,提供了一個基於int狀態碼和隊列來實現的併發框架。本文將對AQS框架的幾個重要組成進行簡要介紹,讀完本文你將get到以下幾個點:

  1. AQS進行併發控制的機制是什麼

  2. AQS獨佔和共享模式是如何實現的

  3. 同步隊列和條件等待隊列的區別,和數據出入隊原則

一,AQS基本概念

AQS(AbstractQueuedSynchronizer)是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量來表示狀態,通過內置的FIFO(first in,first out)隊列來完成資源獲取線程的排隊工作。

隊列可分為兩種,一種是同步隊列,是程序執行入口出處的等待隊列;而另一種則是條件等待隊列,隊列中的元素是在程序執行時在某個條件上發生等待。

1.1 獨佔or共享模式

AQS支持兩種獲取同步狀態的模式既獨佔式和共享式。顧名思義,獨佔式模式同一時刻只允許一個線程獲取同步狀態,而共享模式則允許多個線程同時獲取。

1.2 同步隊列

當一個線程嘗試獲取同步狀態失敗時,同步器會將這個線程以及等待狀態等信息構造成一個節點加入到等待隊列中,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試重複獲取同步隊列。

1.3 條件隊列

AQS內部類ConditionObject來實現的條件隊列,當一個線程獲取到同步狀態,但是卻通過Condition調用了await相關的方法時,會將該線程封裝成Node節點並加入到條件隊列中,它的結構和同步隊列相同。

二,獨佔or共享模式

AQS框架中,通過維護一個int類型的狀態,來進行併發控制,線程通常通過修改此狀態信息來表明當前線程持有此同步狀態。AQS則是通過保存修改狀態線程的引用來實現獨佔和共享模式的。

/**
 * 獲取同步狀態
 */
public final void acquire(int arg) {
    //嘗試獲取同步狀態, 如果嘗試獲取到同步狀態失敗,則加入到同步隊列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
/**
 * 嘗試獲取同步狀態【子類中實現】,因為aqs基於模板模式,僅提供基於狀態和同步隊列的實 
 * 現思路,具體的實現由子類決定
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 如果當前狀態值為0,並且等待隊列中沒有元素,執行修改狀態值操作
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 修改狀態值成功,記錄當前持有同步狀態的線程信息
            setExclusiveOwnerThread(current);
            return true;
        }
        // 如果當前線程已經持有同步狀態,繼續修改同步狀態【重入鎖實現原理】
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

/**
 * 根據傳入的模式以及當前線程信息創建一個隊列的節點並加入到同步隊列尾部
 */
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
/**
 * 同步隊列中節點,嘗試獲取同步狀態
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋(死循環)
        for (;;) {
            // 只有當前節點的前驅節點是頭節點時才會嘗試執行獲取同步狀態操作
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

獨佔式是如何控製得?

當修改狀態信息成功后,如果執行的是獨佔式操作,AQS的具體實現類中會保存當前線程的信息來聲明同步狀態已被當前線程佔用,此時其他線程再嘗試獲取同步狀態會返回false。

三,同步隊列

3.1 隊列中保存那些信息?

同步隊列節點中主要保存着線程的信息以及模式(共享or獨佔)。

3.2 何時執行入隊操作?

/**
 * 獲取同步狀態
 */
public final void acquire(int arg) {
    //嘗試獲取同步狀態, 如果嘗試獲取到同步狀態失敗,則加入到同步隊列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

復用上文中的代碼,不難看出再獲取同步狀態失敗后,會執行入隊操作。

3.3 何時執行出隊操作?

當線程獲取同步狀態失敗時,會被封裝成Node節點加入到等待隊列中,此時所有節點都回進入自旋過程,首先判斷自己prev是否時頭節點,如果是則嘗試獲取同步狀態。
被阻塞線程的喚醒主要以靠前驅節點的出隊或阻塞線程被中斷來實現。

/**
 * 同步隊列中節點,嘗試獲取同步狀態
 * 
 * 1. 當一個線程獲取到同步狀態時,會將當前線程構造程Node並設置為頭節點
 * 2. 並將原始的head節點設置為null,以便於垃圾回收
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

四,條件等待隊列

條件變量(ConidtionObject)是AQS中的一個內部類,用來實現同步隊列機制。同步隊列復用了等待隊列中Node節點,所以同步隊列到等待隊列中不需要進行額外的轉換。

4.1 什麼時候執行入隊操作?

當線程獲取到同步狀態,但是在臨界區中調用了await()方法,此時該線程會被加入到對應的條件隊列匯總。
ps: 臨界區,加鎖和釋放鎖之間的代碼區域

/**
 * ConditionObject中的await方法,調用后使得當前執行線程加入條件等待隊列
 */
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    // -----省略代碼------
}
/**
 * 添加等待線程
 */
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // -----省略代碼------
    // 將當前線程構造程條件隊列節點,並加入到隊列中
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

4.2 什麼時候執行出隊操作?

當對應的Conditioni調用signial/signalAll()方法時回選擇從條件隊列中出隊列,同步隊列是通過自旋的方式獲取同步狀態,而條件隊列中的節點則通過通知的方式出隊。條件隊列中的節點被喚醒後會加入到入口等待隊列中。

/**
 * 喚醒當前條件等到隊列中的所有等待線程
 */
public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}
/**
 * 遍歷隊列,將元素從條件隊列 加入到 同步隊列
 */
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
final boolean transferForSignal(Node node) {
    // -----省略代碼------
    // 執行入隊操作,將node添加到同步隊列中
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

五,總結

  1. 使用Node實現的FIFO隊列,可以用於構建鎖或者其他同步裝置的基礎框架
  2. 利用一個int類型的屬性表示狀態
  3. 使用模板方法模式,子類可以通過繼承它來管理狀態實現各種併發工具
  4. 可以同時實現獨佔和共享模式

本文對AQS的基本原理進行的簡要的描述,對於子類的公平性和非公平行實現,中斷,隊列中節點的等待狀態,cas等操作沒有進行探討,感興趣的小夥伴可以進行源碼閱讀或者查閱相關資料。

六,Q&A

Question1: 在java中通常使用synchronized來實現方法同步,AQS中通過CAS保證了修改同步狀態的一致性問題,那麼對比synchronized,cas有什麼優勢不同與優勢呢?你還知道其他無鎖併發的策略嗎?

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

【其他文章推薦】

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

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

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

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

你不知道的JS系列【1】- 什麼是作用域

  幾乎所有的編程語言都能夠儲存變量,並且能在之後對這個變量值進行訪問或修改,正是儲存和訪問變量的能力將狀態帶給了程序,那麼,這些變量儲存在哪裡呢?程序需要時又是如何找到他們?這些問題說明需要一套設計良好的規則來儲存變量,並且之後可以方便的找到這些變量,這套規則被稱為作用域

1、了解編譯原理

  儘管將JS歸類為“動態”或“解釋執行”腳本語言,但事實上它是一門編譯語言。但是與傳統編譯語言不同的是,它不是提前編譯的,編譯結果也不能在分佈式系統中進行移植。JS引擎進行編譯的步驟與傳統的語言非常相似,程序中一段源代碼在執行之前會經歷三個步驟,統稱為“編譯”。

  • 分詞/詞法分析

這個過程會將由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被稱為詞法單元。例如,考慮程序 var a = 2;。這段程序通常會被分解成 為下面這些詞法單元:var、a、=、2 、;

  • 解析/語法分析

這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的代表了程序語法結構的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。var a = 2;的抽象語法樹中可能會有一個叫作VariableDeclaration的頂級節點,接下來是一個叫作 Identifier(它的值是 a)的子節點,以及一個叫作 AssignmentExpression 的子節點。AssignmentExpression 節點有一個叫作 NumericLiteral(它的值是 2)的子節點。

  • 代碼生成

AST轉換為可執行代碼的過程稱被稱為代碼生成,簡單來說就是有某種方法可以將 var a = 2;的AST轉化為一組機器指 令,用來創建一個叫作a的變量(包括分配內存等),並將一個值儲存在a中。

編譯流程如下圖所示:

JS引擎比傳統的編譯語言編譯器複雜很多,在語法分析和代碼生成階段有特定的步驟來對性能進行優化,大部分情況下編譯發生在代碼之前的前幾微秒,在討論作用域背後,js引擎用了各種辦法來保證性能最佳。

Tips:我們平時在寫JS代碼的時候,一個語句結尾要加分號(;),便於JS編譯器編譯。

2、理解作用域

  我們先了解JS編譯過程中幾個名詞,JS引擎,編譯器,作用域。

2.1.名詞介紹

  • JS引擎:從頭到尾負責整個JS程序編譯過程。
  • 編譯器:負責語法分析及代碼生成等。
  • JS引擎:負責收集並維護由所有聲明的標識符(變量)組成的一系列查 詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。

2.2.變量賦值

對於var a=2;這段代碼,我們認為這就是申明一個為變量a且初始值為2,實際上,JS引擎認為這裡有兩個完全不同的申明,一個由編譯器在編譯時處理,另一個則由引擎在運行時處理。

處理過程分為兩步:

1.遇到var a,編譯器會詢問作用域是否已經有一個該名稱的變量存在於同一個作用域的集合中。如果是,編譯器會忽略該聲明,繼續進行編譯;否則它會要求作用域在當前作用域的集合中聲明一個新的變量,並命名為a。

2.接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理a = 2這個賦值操作。引擎運行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫作a的變量。如果是,引擎就會使用這個變量;如果否,引擎會繼續查找該變量。

如果引擎最終找到a變量,就會將2賦值給它。否則就拋出異常。

Tips:聲明提前(hoist)-JS引擎在創建變量時,會將該變量提升到當前作用域的最前面。

總結:變量的賦值操作會執行兩個動作,首先編譯器會在當前作用域中聲明一個變量(如果之前沒有聲明過),然後在運行時引擎會在作用域中查找該變量,如果能夠找到就會對它賦值。

2.3.LHS查詢&RHS查詢

編譯器在編譯過程中的第二步生成了代碼,引擎在執行時,會通過查找變量a來判斷它是否已經聲明過。當變量出現在賦值操作的左側時進行LHS查詢,當變量出現在右側時進行RHS查詢。

console.log(a); //對a的引用時RHS引用,這裏沒有對a賦予任何值,需要查找a的值。

a=2; //對a的引用是LHS引用,因為這裏不關心a的值等於多少,只想為 =2 這個賦值操作找到一個目標(變量a);

LHS和RHS的含義是“賦值操作的左側或右側”並不一定意味着就是“= 賦值操作符的左側或右側”。賦值操作還有其他幾種形式,因此在概念上最好將其理解為“賦值操作的目標是誰(LHS)”以及“去找到XX變量的值,誰是賦值操作的源頭(RHS)”。

3、作用域嵌套

  作用域是根據名稱查找變量的一套規則。實際情況中,通常需要同時顧及幾個作用域。 當一個塊或函數嵌套在另一個塊或函數中時,就發生了作用域的嵌套。因此,在當前作用 域中無法找到某個變量時,引擎就會在外層嵌套的作用域中繼續查找,直到找到該變量, 或抵達最外層的作用域(也就是全局作用域)為止。

參考以下代碼:

var name='peer';
function sayHello(){
  alert('hello '+ name)
}
sayHello();
// 對name的RHS引用無法在函數sayHello完成,但是可以在上一級作用域中完成。

把作用域比喻成一個建築如下圖所示:

LHS和RHS引用都會在當前樓層進行查找,如果沒有找到,就會坐電梯前往上一層樓,如果還是沒有找到就繼續向上,以此類推。一旦抵達頂層(全局作用域),可能找到了你所需的變量,也可能沒找到,但無論如何查找過程都將停止。

4、總結

  作用域是一套規則,用於確定在何處以及如何查找變量(標識符)。LHS和RHS查詢都會在當前執行作用域中開始,如果有需要就會向上級作用域繼續查找目標標識符,這樣每次上升一級作用域,最後抵達全局作用域,無論找到或沒找到都將停止。不成功的RHS 引用會導致拋出ReferenceError異常。不成功的LHS引用會導致自動隱式地創建一個全局變量(非嚴格模式下),掌握這些基本作用域知識能使我們更深入理解JS引擎的編譯過程來編寫更高性能的代碼。

參考資料:
《你不知道的JavaScript》

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

彰化搬家費用,南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

馮提莫配上了小白鞋,整體造型就比較協調一點

隨着網絡的發展,網路主播這個行業也吸引了很多人的加入,其中比較出名的就是馮提莫了,她以為甜美的歌聲和外表獲得大家的喜歡。馮提莫的顏值在素人中確實是很不錯的,看上去很可愛,是男生都會喜歡的類型吧?不僅如此,如果你有觀察她的穿搭的話,還會發現她是一個很時尚的人,平時私底下穿的衣服款式都是很潮的,而且還懂得如何優化自己的缺點,比如她的個子比較嬌小,但是她會選擇這種款式的褲子,所以看上去並不會覺得她矮。

對自己身高不滿意的女生都該跟馮提莫取取經了,她這身打扮特別顯高,首先可以看到她上半身穿了一件襯衫,裏面穿的是短款的背心,秀出了腰,這樣做可以拉高腰線,讓下邊的比例變長。

自從她學會這樣穿褲子后,就沒有人說她矮了,看馮提莫穿的褲子是高腰款式的,都穿到腰上去了,再者看這褲子也是很長的,要是沒有鞋子擋着的話就拖地了,有着兩種設計,就能拉長腿的比例了。

褲子下邊馮提莫配上了小白鞋,整體造型就比較協調一點了,穿上這條褲子的馮提莫看着像有1米7,腿實在是太長了,所以說個子嬌小沒有關係,懂得穿衣服的話看上去也能有雙大長腿,你學會了嗎?

雖然比較矮一點,但其實馮提莫的身材是很好的,看她穿上這條弔帶裙的時候,手臂非常纖細,這裙子很特別,上面有很多桃花和桃枝的圖案,還是立體的,看上去就像是真的一樣。

馮提莫這身打扮也好看,她上半身穿的毛衣顏色很鮮艷,是有綠色,白色,黃色和藍色四種顏色組成的,穿上身會凸顯出活力感,顯得很年輕的樣子,下邊配上牛仔長褲,整體造型很好看。

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

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

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

電子煙有爭議?真相解密

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

十款男士秋冬潮品

TOP.1

超長圍巾

冬日里最百搭的莫過於一條溫暖的圍巾。

恰恰今年流行的超長款圍巾,這種自己圍着很時髦,約會時還能借給女孩一起圍,兩人一塊甜蜜保暖的脫單神器,是首推的必備的單品!

在男士密密麻麻深顏色衣服堆里,一條亮眼的圍巾,可以說是點亮整體搭配的一個非常含蓄的選擇。

圍巾並不只是作為一種時尚標籤或保暖的服飾。圍巾有着悠久而豐富的歷史,曾經有着各種各樣的含義。

圍巾的起源;

現代圍巾起源於古埃及,在古埃及,第一條有歷史記錄的圍巾是在公元前1350年女王奈費爾提蒂使用的。

中國也是早期使用圍巾的地方,它最初是軍裝的一部分。大約公元前230年,是在秦朝統治下服役的士兵制服的一部分,這些圍巾有助於表示軍銜。

到了公元1600年,克羅地亞士兵也按軍銜戴着圍巾。下級士兵佩戴棉製的圍巾,而軍官的圍巾則是用細綢做的。

在公元10年左右的古羅馬,從事運動或劇烈體力勞動的人通常會在身邊放一塊亞麻布,以保持清潔和擦汗,圍巾稱之為 “汗布”。男人們經常戴着它,它就成了一種配飾——系在脖子上,披在肩膀上,或者系在腰上。

直到19世紀,圍巾才成為世界流行的時尚配飾。

這是當歐洲的時裝屋將印度等地區的特殊面料和設計正式引入了歐洲市場,並進行了龐大的資本化運營。

1837年,愛馬仕設計了第一條印花絲巾。

同年,在維多利亞女王坐上王位,用令人驚嘆的圖案印花推廣華麗的絲綢領帶之後,歐美的時尚界擁抱了圍巾。所用的圖案和織物從本來的表示階級,都用來了表示時尚感。現代圍巾從此誕生。從那時起,圍巾就一直是時尚和功能的主要配飾。

NO.2

可愛毛衣

今年排名第二的脫單單品就是一件俏皮的可愛毛衣

上個季度非常火爆的各種印花T恤,到了秋冬,有着各種可愛圖案的大毛衣就成上個季度的延續。這些顏色鮮艷,具有藝術感的毛衣款式,能讓人感覺更親切活潑,非常適合平常有點酷酷的男生去嘗試。

NO.3

牛仔套裝

在周末約會的時刻,想要打扮成陽光型男的感覺,今年流行的牛仔套裝可以幫助你。

從今年春夏到秋冬都大熱的工裝style,成熟和活力兼具,在裏面搭配一件圓領寬鬆的針織衫和直筒休閑褲,就能既保暖又時尚。

NO.4

復古皮衣

男士衣櫥中最有男人味的莫過於皮衣了。

今年如果要買一件皮衣,不要再買機車款的了,其實機車款式一般人很難hold住,與其走壞男孩路線,還不如走成熟暖男路線,或許還更能虜獲暖萌女孩的心。

今年大熱的復古皮衣,沒有金屬元素,帶有小啞光的質感,能更顯穩重,滿滿的雅緻男人味。

NO.5

Oversize風衣

今年風衣的熱度比往年更加劇烈,但今年風衣的關鍵在於oversize,超大號廓形的風衣是今年秋冬必備的單品。

特別是身形高大的男士們非常推薦嘗試,裏面簡單搭配毛衣和休閑直筒褲就能輕鬆出門。

NO.6

長款羽絨

羽絨絕對是每個人冬天的真愛。

特別是今年,加上工裝style的潮流,實用又滿滿工業風的直身長款羽絨服大行其道,不僅非常保暖,還非常潮流。“暖”男必備!

NO.7

西裝

男士想要展現氣質和風度,永遠少不了一套西裝

每個男人必備的黑西裝,今年強勢回歸。

如果身形比較窄的完全可以選擇修身版的,如果比較厚壯的還可以選擇oversize一點的。裏面搭配一件淺色的薄針織衫,一天到晚各個場合都可以讓你帥氣過人。

NO.8

雙排扣西裝

自帶銀行家氣質的雙排扣西裝,今年非常流行。

因為它帶有復古感,選擇比較oversize的,帶有點灰色調的,或是今年無論男女都流行的復古紋理的,質感比較柔軟的,穿起來感覺比較輕鬆自在,不像從前印象中雙排扣西裝那麼正式刻板,很好的平衡正式和休閑,可謂是今年秋冬的約會良品。

NO.9

厚底短靴

今年秋冬如果要買一雙鞋,那麼一雙厚底的短靴絕對要在你的top list里。

無論是經典的馬丁靴,或是厚底設計的切爾斯靴都能既具有暗增高的美好功能,還能輕鬆搭配任何日常穿搭。

NO.10

掛脖包

很多男士其實日常都沒有帶包包的習慣,但今年流行的掛脖包絕對可以嘗試一下。

不要把掛脖包當做包包用,你可以把它當做裝飾或是小玩具,增加造型的層次感,成為一種時髦好玩的點綴。掛脖包自帶的萌感和時尚感,或許還能讓女生加深印象!

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

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

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

電子煙有爭議?真相解密

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

學會8 款牛仔褲穿搭技巧

女生們的牛仔褲穿搭,看似基本淺顯,但是蘊含無限變化!對於崇尚經典百搭的女生來說,這裏尤為推薦如下8款: 深藍牛仔褲,淺藍牛仔褲,高腰牛仔褲以及低腰牛仔褲等。每個牛仔褲款式,各自擁有不同的風格和氣質,也有不同的穿搭技巧,只要多加運用牛仔褲穿搭法,便可穿出經典的時尚造型。

深藍牛仔褲穿搭:優雅之最

深藍牛仔褲穿搭總是讓人覺得有顯瘦的功效,除此之外,深藍牛仔褲還更加附帶着獨一無二的優雅氣質,它是上班族休閑周五的重要一員,隨手搭配白色T恤或者換上一雙紅色鞋子,又能散發出一種法式的利落感。總而言之,深藍色牛仔褲的多變,就是出乎大家的意料之外!

淺藍色牛仔褲穿搭:減齡街頭風

淺藍牛仔褲穿搭能讓穿者看上去更加年輕活力,也散發著無可取代的輕盈感,即使是最常見的10-13盎司牛仔褲,換上一身淺藍,也立即變得薄多了!但就淺藍牛仔褲穿搭而言,千萬藥學避重就輕,將容易被無限放大的臀部,大腿,以上衣稍微長點的上衣遮掩,又或者以加大碼的上衣,營造下身較為均勻的比例。再者,boot cut 和略為修腳的設計也有修飾腳形的魅力!

黑色牛仔褲穿搭:皇室寵幸

減掉了牛仔褲藍,黑色牛仔褲的休閑感瞬間被拿掉,給人以相對正式的感覺,因此深受需要時刻保持形象的英國皇室成員的喜愛。出席戶外場合時,王妃們便常以黑色牛仔褲穿搭登場, Kate Middleton 和 Meghan Markle 分別西裝褲搭配黑色牛仔褲,一深一淺,前者千鳥格帶馬術風,後者白色條紋帶海軍風,搭配短靴和sling back 高跟效果鞋迥然不同,一年四季皆可穿搭出各式造型。

高腰牛仔褲穿搭:復古時尚

高腰牛仔褲在多個高級品牌的助攻下宣告回歸!可是若對高腰牛仔褲穿搭掉以輕心的話,很有可能將潮流翻轉成老土。想為高腰牛仔褲穿搭注入現代感,參考模特兒,時裝網紅,博主選以入時的Statement Tee、cropped Tee,又或加大碼外套,西裝褲搭配,全是個簡單得人人都可以駕馭的入門基礎穿搭法

低腰牛仔褲穿搭:休閑中性風

低腰牛仔褲是另外一款回歸的90年代的產物,不過大家要留意,那些年流行的低腰喇叭褲款,今期流行的是街頭感滿分的松身裁剪,城中最佳的低腰牛仔褲穿搭示範有碧咸嫂 Victoria Beckham、名模 Gigi Hadid,她們以波鞋,涼鞋搭配低腰牛仔褲,穿出自然不造作的休閑中性風。

直筒牛仔褲穿搭:衣櫃必備

直筒牛仔褲穿搭容易駕馭,隨便一件Statement Tee、襯衫就能營造出顯瘦而具有不同風格的造型,但首要條件是找到一條跟自己身型完美匹配的直筒牛仔褲。因為不同品牌廠商的版型,各家出品的牛仔褲略有不同,腰圍,臀圍,以及褲管這3個部位的闊窄尤其重要,要不多不少,也可成為百搭顯瘦之冠。

緊身牛仔褲:曲線展現

緊身牛仔褲穿搭能突顯曲線,首選以輕薄物料,裝飾波浪花邊的女性化設計。當然你也可以寬鬆上衣作為搭配,營造強烈的對比效果。特別一提,過分貼身的緊身牛仔褲,顯瘦度不增反減,千萬別誤會,否則長時間穿着只會為難自己。

喇叭牛仔褲搭配:高難度挑戰

喇叭牛仔褲穿搭是一場身心的考驗。穿者既要高挑,勻稱的身材去撐起喇叭牛仔褲外,還要有備受注目的膽識。喇叭牛仔褲穿搭豐儉由人,搭配基本單品如襯衫,高領上衣,弔帶背心也可以好看得很,當然你也可以選擇向高難度挑戰,配以亮色,設計複雜的上身衣服,不過千萬記住造型的繁簡度,褲管大小,與喇叭牛仔褲的駕馭難度有着密不可分的關係。

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

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

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

電子煙有爭議?真相解密

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