<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          通透!一口氣搞懂注解到底怎么用!

          共 15315字,需瀏覽 31分鐘

           ·

          2020-09-24 18:29

          在開始本文之前簡單說幾句,有讀者反應(yīng)在面試阿里時被問了網(wǎng)關(guān)相關(guān)的內(nèi)容,這位讀者剛好看到本號之前兩篇網(wǎng)關(guān)的文章,幫上了忙,再次推薦給大家,相信大家看完肯定有收獲

          高性能接入層網(wǎng)關(guān)設(shè)計實(shí)踐

          高性能 Java 應(yīng)用層網(wǎng)關(guān)設(shè)計實(shí)踐

          前言

          日志作為排查問題的重要手段,可以說是應(yīng)用集成中必不可少的一環(huán),但在日志中,又不宜暴露像電話,身份證,地址等個人敏感信息,去年 Q4 我司就開展了對 ELK 日志脫敏的全面要求。那么怎樣快速又有效地實(shí)現(xiàn)日志脫敏呢。相信讀者看完標(biāo)題已經(jīng)猜到了,沒錯,用注解!那么用注解該怎么實(shí)現(xiàn)日志脫敏呢,除了日志脫敏,注解還能用在哪些場景呢,注解的實(shí)現(xiàn)原理又是怎樣的呢。本文將會為你詳細(xì)介紹。

          本文將會從以下幾個方面來介紹注解。

          • 日志脫敏場景簡介
          • 巧用注解解決這兩類問題
            1. 注解的定義與實(shí)現(xiàn)原理
            2. 使用注解解決日志脫敏
          • 注解高級用法-解決銀行中參數(shù)傳遞順序要求

          相信大家看了肯定有收獲!

          日志脫敏場景簡介

          在日志里我們的日志一般打印的是 model 的 Json string,比如有以下 model 類

          public?class?Request?{
          ????/**
          ?????*??用戶姓名
          ?????*/

          ????private?String?name;
          ????/**
          ?????*??身份證?
          ?????*/

          ????private?String?idcard;
          ????/**
          ?????*??手機(jī)號
          ?????*/

          ????private?String?phone;

          ????/**
          ?????*??圖片的?base64
          ?????*/

          ????private?String?imgBase64;
          }

          有以下類實(shí)例

          Request?request?=?new?Request();
          request.setName("愛新覺羅");
          request.setIdcard("450111112222");
          request.setPhone("18611111767");
          request.setImgBase64("xxx");

          我們一般使用 fastJson 來打印此 Request 的 json string:

          log.info(JSON.toJSONString(request));

          這樣就能把 Request 的所有屬性值給打印出來,日志如下:

          {"idcard":"450111112222","imgBase64":"xxx","name":"張三","phone":"17120227942"}

          這里的日志有兩個問題

          1. 安全性: name,phone, idcard 這些個人信息極其敏感,不應(yīng)以明文的形式打印出來,我們希望這些敏感信息是以脫敏的形式輸出的
          2. 字段冗余:imgBase64 是圖片的 base64,是一串非常長的字符串,在生產(chǎn)上,圖片 base64 數(shù)據(jù)對排查問題幫助不大,反而會增大存儲成本,而且這個字段是身份證正反面的 base64,也屬于敏感信息,所以這個字段在日志中需要把它去掉。我們希望經(jīng)過脫敏和瘦身(移除 imgBase64 字段)后的日志如下:
          {"idcard":"450******222","name":"愛**羅","phone":"186****1767","imgBase64":""}

          可以看到各個字段最后都脫敏了,不過需要注意的這幾個字段的脫敏規(guī)則是不一樣的

          • 身份證(idcard),保留前三位,后三位,其余打碼
          • 姓名(name)保留前后兩位,其余打碼
          • 電話號碼(phone)保持前三位,后四位,其余打碼
          • 圖片的 base64(imgBase64)直接展示空字符串

          該怎么實(shí)現(xiàn)呢,首先我們需要知道一個知識點(diǎn),即 JSON.toJSONString 方法指定了一個參數(shù) ValueFilter,可以定制要轉(zhuǎn)化的屬性。我們可以利用此 Filter 讓最終的 JSON string 不展示或展示脫敏后的 value。大概邏輯如下

          public?class?Util?{
          ????public?static?String?toJSONString(Object?object)?{
          ????????try?{
          ????????????return?JSON.toJSONString(object,?getValueFilter());
          ????????}?catch?(Exception?e)?{
          ????????????return?ToStringBuilder.reflectionToString(object);
          ????????}
          ????}
          ????
          private?static?ValueFilter?getValueFilter()?{
          ????????return?(obj,?key,?value)?->?{
          ????????????//?obj-對象?key-字段名?value-字段值
          ????????????return??格式化后的value
          ????????};
          }

          如上圖示,我們只要在 getValueFilter 方法中對 value 作相關(guān)的脫敏操作,即可在最終的日志中展示脫敏后的日志。現(xiàn)在問題來了,該怎么處理字段的脫敏問題,我們知道有些字段需要脫敏,有些字段不需要脫敏,所以有人可能會根據(jù) key 的名稱來判斷是否脫敏,代碼如下:

          private?static?ValueFilter?getValueFilter()?{
          ????????return?(obj,?key,?value)?->?{
          ????????????//?obj-對象?key-字段名?value-字段值
          ????????????if?(Objects.equal(key,?"phone"))?{
          ????????????????return?脫敏后的phone
          ????????????}
          ????????????if?(Objects.equal(key,?"idcard"))?{
          ????????????????return?脫敏后的idcard
          ????????????}
          ????????????if?(Objects.equal(key,?"name"))?{
          ????????????????return?脫敏后的name
          ????????????}
          ????????????//?其余不需要脫敏的按原值返回
          ????????????return??value
          ????????};
          }

          這樣看起來確實(shí)實(shí)現(xiàn)了需求,但僅僅實(shí)現(xiàn)了需求就夠了嗎,這樣的實(shí)現(xiàn)有個比較嚴(yán)重的問題:

          脫敏規(guī)則與具體的屬性名緊藕合,需要在 valueFilter 里寫大量的 if else 判斷邏輯,可擴(kuò)展性不高,通用性不強(qiáng),舉個簡單的例子,由于業(yè)務(wù)原因,在我們的工程中電話有些字段名叫 phone, 有些叫 tel,有些叫 telephone,它們的脫敏規(guī)則是一樣的,但你不得不在上面的方法中寫出如下丑陋的代碼。

          private?static?ValueFilter?getValueFilter()?{
          ????????return?(obj,?key,?value)?->?{
          ????????????//?obj-對象?key-字段名?value-字段值
          ????????????if?(Objects.equal(key,?"phone")?||?Objects.equal(key,?"tel")?||?Objects.equal(key,?"telephone")?||?)?{
          ????????????????return?脫敏后的phone
          ????????????}

          ????????????//?其余不需要脫敏的按原值返回
          ????????????return??value
          ????????};
          }

          那么能否用一種通用的,可擴(kuò)展性好的方法來解決呢,相信你看到文章的標(biāo)題已經(jīng)心中有數(shù)了,沒錯,就是用的注解,接下來我們來看看什么是注解以及如何自定義注解

          注解的定義與實(shí)現(xiàn)原理

          注解(Annotation)又稱 ?Java 標(biāo)注,是 JDK 5.0 引入的一種注釋機(jī)制,如果說代碼的注釋是給程序員看的,那么注解就是給程序看的,程序看到注解后就可以在運(yùn)行時拿到注解,根據(jù)注解來增強(qiáng)運(yùn)行時的能力,常見的應(yīng)用在代碼中的注解有如下三個

          • @Override 檢查該方法是否重寫了父類方法,如果發(fā)現(xiàn)父類或?qū)崿F(xiàn)的接口中沒有此方法,則報編譯錯誤
          • @Deprecated 標(biāo)記過時的類,方法,屬性等
          • @SuppressWarnings - 指示編譯器去忽略注解中聲明的警告。

          那這些注解是怎么實(shí)現(xiàn)的呢,我們打開 @Override 這個注解看看

          @Documented
          @Retention(RetentionPolicy.RUNTIME)
          @Target(value={CONSTRUCTOR,?FIELD,?LOCAL_VARIABLE,?METHOD,?PACKAGE,?PARAMETER,?TYPE})
          public?@interface?Deprecated?{
          }

          可以看到 Deprecated 注解上又有 @Documented, @Retention, @Target 這些注解,這些注解又叫元注解,即注解 Deprecated 或其他自定義注解的注解,其他注解的行為由這些注解來規(guī)范和定義,這些元注解的類型及作用如下

          • @Documented 表明它會被 javadoc 之類的工具處理, 這樣最終注解類型信息也會被包括在生成的文檔中

          • @Retention ?注解的保存策略,主要有以下三種

            • RetentionPolicy.SOURCE 源代碼級別的注解,表示指定的注解只在編譯期可見,并不會寫入字節(jié)碼文件,Override, SuppressWarnings 就屬于此類型,這類注解對于程序員來說主要起到在編譯時提醒的作用,在運(yùn)行保存意義并不大,所以最終并不會被編譯入字節(jié)碼文件中
            • RetentionPolicy.RUNTIME ?表示注解會被編譯入最終的字符碼文件中,JVM 啟動后也會讀入注解,這樣我們在運(yùn)行時就可以通過反射來獲取這些注解,根據(jù)這些注解來做相關(guān)的操作,這是多數(shù)自定義注解使用的保存策略,這里可能大家有個疑問,為啥 Deprecated 被標(biāo)為 RUNTIME 呢,對于程序員來說,理論上來說只關(guān)心調(diào)用的類,方法等是否 Deprecated 就夠了,運(yùn)行時獲取有啥意義呢,考慮這樣一種場景,假設(shè)你想在生產(chǎn)上統(tǒng)計過時的方法被調(diào)用的頻率以評估你工程的壞味道或作為重構(gòu)參考,此時這個注解是不是派上用場了。
            • RetentionPolicy.CLASS 注解會被編譯入最終的字符碼文件,但并不會載入 JVM 中(在類加載的時候注解會被丟棄),這種保存策略不常用,主要用在字節(jié)碼文件的處理中。
          • @Target 表示該注解可以用在什么地方,默認(rèn)情況下可以用在任何地方,該注解的作用域主要通過 value 來指定,這里列舉幾個比較常見的類型:

            • FIELD 作用于屬性
            • METHOD 作用于方法
            • ElementType.TYPE: 作用于類、接口(包括注解類型) 或 enum 聲明
          • @Inherited - 標(biāo)記這個注解是繼承于哪個注解類(默認(rèn) 注解并沒有繼承于任何子類)

          再來看 @interface, 這個是干啥用的,其實(shí)如果你反編譯之后就會發(fā)現(xiàn)在字節(jié)碼中編譯器將其編碼成了如下內(nèi)容。

          public?interface?Override?extends?Annotation?{???
          }

          Annotation 是啥


          我們可以看出注解的本質(zhì)其實(shí)是繼承了 Annotation 這個接口的接口,并且輔以 Retention,Target 這些規(guī)范注解運(yùn)行時行為,作用域等的元注解。

          Deprecated 注解中沒有定義屬性,其實(shí)如果需要注解是可以定義屬性的,比如 Deprecated 注解可以定義一個 value 的屬性,在聲明注解的時候可以指定此注解的 value 值

          @Documented
          @Retention(RetentionPolicy.RUNTIME)
          @Target(value={CONSTRUCTOR,?FIELD,?LOCAL_VARIABLE,?METHOD,?PACKAGE,?PARAMETER,?TYPE})
          public?@interface?Deprecated?{
          ????String?value()?default?"";
          }

          這樣我將此注解應(yīng)用于屬性等地方時,可以指定此 value ?值,如下所示

          public?class?Person?{
          ????@Deprecated(value?=?"xxx")
          ????private?String?tail;
          }

          如果注解的保存策略為 RetentionPolicy.RUNTIME,我們就可以用如下方式在運(yùn)行時獲取注解,進(jìn)而獲取注解的屬性值等

          field.getAnnotation(Deprecated.class);

          巧用注解解決日志脫敏問題

          上文簡述了注解的原理與寫法,接下來我們來看看如何用注解來實(shí)現(xiàn)我們的日志脫敏。

          首先我們要定義一下脫敏的注解,由于此注解需要在運(yùn)行時被取到,所以保存策略要為 RetentionPolicy.RUNTIME,另外此注解要應(yīng)用于 phone,idcard 這些字段,所以@Target 的值為 ElementType.FIELD,另外我們注意到,像電話號碼,身份證這些字段雖然都要脫敏,但是它們的脫敏策略不一樣,所以我們需要為此注解定義一個屬性,這樣可以指定它的屬性屬于哪種脫敏類型,我們定義的脫敏注解如下:

          //?敏感信息類型
          public?enum?SensitiveType?{
          ????ID_CARD,?PHONE,?NAME,?IMG_BASE64
          }

          @Target({?ElementType.FIELD?})
          @Retention(RetentionPolicy.RUNTIME)
          public?@interface?SensitiveInfo?{
          ????SensitiveType?type();
          }

          定義好了注解,現(xiàn)在就可以為我們的敏感字段指定注解及其敏感信息類型了,如下

          public?class?Request?{
          ????@SensitiveInfo(type?=?SensitiveType.NAME)
          ????private?String?name;
          ????@SensitiveInfo(type?=?SensitiveType.ID_CARD)
          ????private?String?idcard;
          ????@SensitiveInfo(type?=?SensitiveType.PHONE)
          ????private?String?phone;
          ????@SensitiveInfo(type?=?SensitiveType.IMG_BASE64)
          ????private?String?imgBase64;
          }

          為屬性指定好了注解,該怎么根據(jù)注解來實(shí)現(xiàn)相應(yīng)敏感字段類型的脫敏呢,可以用反射,先用反射獲取類的每一個 Field,再判定 Field 上是否有相應(yīng)的注解,若有,再判斷此注解是針對哪種敏感類型的注解,再針對相應(yīng)字段做相應(yīng)的脫敏操作,直接上代碼,注釋寫得很清楚了,相信大家應(yīng)該能看懂

          private?static?ValueFilter?getValueFilter()?{
          ????????return?(obj,?key,?value)?->?{
          ????????????//?obj-對象?key-字段名?value-字段值
          ????????????try?{
          ????????????????//?通過反射獲取獲取每個類的屬性
          ????????????????Field[]?fields?=?obj.getClass().getDeclaredFields();
          ????????????????for?(Field?field?:?fields)?{
          ????????????????????if?(!field.getName().equals(key))?{
          ????????????????????????continue;
          ????????????????????}
          ????????????????????//?判定屬性是否有相應(yīng)的?SensitiveInfo?注解
          ????????????????????SensitiveInfo?annotation?=?field.getAnnotation(SensitiveInfo.class);
          ????????????????????//?若有,則執(zhí)行相應(yīng)字段的脫敏方法
          ????????????????????if?(null?!=?annotation)?{
          ????????????????????????switch?(annotation.type())?{
          ????????????????????????????case?PHONE:
          ????????????????????????????????return?電話脫敏;
          ????????????????????????????case?ID_CARD:
          ????????????????????????????????return?身份證脫敏;
          ????????????????????????????case?NAME:
          ????????????????????????????????return?姓名脫敏;
          ????????????????????????????case?IMG_BASE64:
          ????????????????????????????????return?"";?//?圖片的?base?64?不展示,直接返回空
          ????????????????????????????default:
          ????????????????????????????????//?這里可以拋異常
          ????????????????????????}
          ????????????????????}
          ????????????????????}
          ????????????????}
          ????????????}?catch?(Exception?e)?{
          ????????????????log.error("To?JSON?String?fail",?e);
          ????????????}
          ????????????return?value;
          ????????};
          ????}

          有人可能會說了,使用注解的方式來實(shí)現(xiàn)脫敏代碼量翻了一倍不止,看起來好像不是很值得,其實(shí)不然,之前的方式,脫敏規(guī)則與某個字段名強(qiáng)藕合,可維護(hù)性不好,而用注解的方式,就像工程中出現(xiàn)的 phone, tel,telephone 這些都屬于電話脫敏類型的,只要統(tǒng)一標(biāo)上 ?**@SensitiveInfo(type = SensitiveType.PHONE) ** 這樣的注解即可,而且后續(xù)如有新的脫敏類型,只要重新加一個 SensitiveType 的類型即可,可維護(hù)性與擴(kuò)展性大大增強(qiáng)。所以在這類場景中,使用注解是強(qiáng)烈推薦的。

          注解的高級應(yīng)用-利用注解消除重復(fù)代碼

          在與銀行對接的過程中,銀行提供了一些 API 接口,對參數(shù)的序列化有點(diǎn)特殊,不使用 JSON,而是需要我們把參數(shù)依次拼在一起構(gòu)成一個大字符串。

          • 按照銀行提供的 API 文檔的順序,把所有參數(shù)構(gòu)成定長的數(shù)據(jù),然后拼接在一起作為整個字符串。
          • 因?yàn)槊恳环N參數(shù)都有固定長度,未達(dá)到長度時需要做填充處理:
            • 字符串類型的參數(shù)不滿長度部分需要以下劃線右填充,也就是字符串內(nèi)容靠左;
            • 數(shù)字類型的參數(shù)不滿長度部分以 0 左填充,也就是實(shí)際數(shù)字靠右;
            • 貨幣類型的表示需要把金額向下舍入 2 位到分,以分為單位,作為數(shù)字類型同樣進(jìn)行左填充。
          • 對所有參數(shù)做 MD5 操作作為簽名(為了方便理解,Demo 中不涉及加鹽處理)。簡單看兩個銀行的接口定義

          1、創(chuàng)建用戶

          在這里插入圖片描述

          2、支付接口

          常規(guī)的做法是為每個接口都根據(jù)之前的規(guī)則填充參數(shù),拼接,驗(yàn)簽,以以上兩個接口為例,先看看常規(guī)做法

          創(chuàng)建用戶與支付的請求如下:

          //?創(chuàng)建用戶?POJO
          @Data
          public?class?CreateUserRequest?{?
          ????private?String?name;?
          ????private?String?identity;?
          ????private?String?mobile;
          ????private?int?age;
          }

          //?支付?POJO
          @Data
          public?class?PayRequest?{?
          ????private?long?userId;?
          ????private?BigDecimal?amount;
          }

          public?class?BankService?{

          ????//創(chuàng)建用戶方法
          ????public?static?String?createUser(CreateUserRequest?request)?throws?IOException?{
          ????????StringBuilder?stringBuilder?=?new?StringBuilder();
          ????????//字符串靠左,多余的地方填充_
          ????????stringBuilder.append(String.format("%-10s",?request.getName()).replace('?',?'_'));
          ????????//字符串靠左,多余的地方填充_
          ????????stringBuilder.append(String.format("%-18s",?request.getIdentity()).replace('?',?'_'));
          ????????//數(shù)字靠右,多余的地方用0填充
          ????????stringBuilder.append(String.format("%05d",?age));
          ????????//字符串靠左,多余的地方用_填充
          ????????stringBuilder.append(String.format("%-11s",?mobile).replace('?',?'_'));
          ????????//最后加上MD5作為簽名
          ????????stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
          ????????return?Request.Post("http://baseurl/createUser")
          ????????????????.bodyString(stringBuilder.toString(),?ContentType.APPLICATION_JSON)
          ????????????????.execute().returnContent().asString();
          ????}
          ????
          ????//支付方法
          ????public?static?String?pay(PayRequest?request)?throws?IOException?{
          ????????StringBuilder?stringBuilder?=?new?StringBuilder();
          ????????//數(shù)字靠右,多余的地方用0填充
          ????????stringBuilder.append(String.format("%020d",?request.getUserId()));
          ????????//金額向下舍入2位到分,以分為單位,作為數(shù)字靠右,多余的地方用0填充
          ?stringBuilder.append(String.format("%010d",request.getAmount().setScale(2,RoundingMode.DOWN).multiply(new??????????????????????????????????????????????????????????????????????????????????BigDecimal("100")).longValue()));?
          ????????//最后加上MD5作為簽名
          ????????stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
          ????????return?Request.Post("http://baseurl//pay")
          ????????????????.bodyString(stringBuilder.toString(),?ContentType.APPLICATION_JSON)
          ????????????????.execute().returnContent().asString();
          ????}
          }

          可以看到光寫這兩個請求,邏輯就有很多重復(fù)的地方:

          1、 字符串,貨幣,數(shù)字三種類型的格式化邏輯大量重復(fù),以處理字符串為例


          可以看到,格式化字符串的的處理只是每個字段的長度不同,其余格式化規(guī)則完全一樣,但在上文中我們卻為每一個字符串都整了一套相同的處理邏輯,這套拼接規(guī)則完全可以抽出來(因?yàn)橹皇情L度不一樣,拼接規(guī)則是一樣的)

          2、 處理流程中字符串拼接、加簽和發(fā)請求的邏輯,在所有方法重復(fù)。

          3、 由于每個字段參與拼接的順序不一樣,這些需要我們?nèi)巳庥簿幋a保證這些字段的順序,維護(hù)成本極大,而且很容易出錯,想象一下如果參數(shù)達(dá)到幾十上百個,這些參數(shù)都需要按一定順序來拼接,如果要人肉來保證,很難保證正確性,而且重復(fù)工作太多,得不償失

          接下來我們來看看如何用注解來極大簡化我們的代碼。

          1、 首先對于每一個調(diào)用接口來說,它們底層都是需要請求網(wǎng)絡(luò)的,只是請求方法不一樣,針對這一點(diǎn) ,我們可以搞一個如下針對接口的注解

          @Retention(RetentionPolicy.RUNTIME)
          @Target(ElementType.TYPE)
          @Documented
          @Inherited
          public?@interface?BankAPI?{?
          ????String?url()?default?"";
          ????String?desc()?default?"";?
          }

          這樣在網(wǎng)絡(luò)請求層即可統(tǒng)一通過注解獲取相應(yīng)接口的方法名

          2、 針對每個請求接口的 POJO,我們注意到每個屬性都有 類型(字符串/數(shù)字/貨幣)長度順序這三個屬性,所以可以定義一個注解,包含這三個屬性,如下

          @Retention(RetentionPolicy.RUNTIME)
          @Target(ElementType.FIELD)
          @Documented
          @Inherited
          public?@interface?BankAPIField?{
          ????int?order()?default?-1;
          ????int?length()?default?-1;
          ????String?type()?default?"";?//?M代表貨幣,S代表字符串,N代表數(shù)字
          }

          接下來我們將上文中定義的注解應(yīng)用到上文中的請求 POJO 中

          對于創(chuàng)建用戶請求

          @BankAPI(url?=?"/createUser",?desc?=?"創(chuàng)建用戶接口")
          @Data
          public?class?CreateUserAPI?extends?AbstractAPI?{
          ????@BankAPIField(order?=?1,?type?=?"S",?length?=?10)
          ????private?String?name;
          ????@BankAPIField(order?=?2,?type?=?"S",?length?=?18)
          ????private?String?identity;
          ????@BankAPIField(order?=?4,?type?=?"S",?length?=?11)?//注意這里的order需要按照API表格中的順序
          ????private?String?mobile;
          ????@BankAPIField(order?=?3,?type?=?"N",?length?=?5)
          ????private?int?age;
          }

          對于支付接口

          @BankAPI(url?=?"/bank/pay",?desc?=?"支付接口")
          @Data
          public?class?PayAPI?extends?AbstractAPI?{
          ????@BankAPIField(order?=?1,?type?=?"N",?length?=?20)
          ????private?long?userId;
          ????@BankAPIField(order?=?2,?type?=?"M",?length?=?10)
          ????private?BigDecimal?amount;
          }

          接下來利用注解來調(diào)用的流程如下

          1. 根據(jù)反射獲取類的 Field 數(shù)組,然后再根據(jù) Field 的 BankAPIField 注解中的 order 值對 Field 進(jìn)行排序
          2. 對排序后的 Field 依次進(jìn)行遍歷,首先判斷其類型,然后根據(jù)類型再對其值格式化,如判斷為"S",則按接口要求字符串的格式對其值進(jìn)行格式化,將這些格式化后的 Field 值依次拼接起來并進(jìn)行簽名
          3. 拼接后就是發(fā)請求了,此時再拿到 POJO 類的注解,獲取注解 BankAPI ?的 url 值,將其與 baseUrl 組合起來即可構(gòu)成一個完整的的 url,再加上第 2 步中拼接字符串即可構(gòu)造一個完全的請求

          代碼如下:


          private?static?String?remoteCall(AbstractAPI?api)?throws?IOException?{
          ????//從BankAPI注解獲取請求地址
          ????BankAPI?bankAPI?=?api.getClass().getAnnotation(BankAPI.class);
          ????bankAPI.url();
          ????StringBuilder?stringBuilder?=?new?StringBuilder();
          ????Arrays.stream(api.getClass().getDeclaredFields())?//獲得所有字段
          ????????????.filter(field?->?field.isAnnotationPresent(BankAPIField.class))?//查找標(biāo)記了注解的字段
          ????????????.sorted(Comparator.comparingInt(a?->?a.getAnnotation(BankAPIField.class).order()))?//根據(jù)注解中的order對字段排序
          ????????????.peek(field?->?field.setAccessible(true))?//設(shè)置可以訪問私有字段
          ????????????.forEach(field?->?{
          ????????????????//獲得注解
          ????????????????BankAPIField?bankAPIField?=?field.getAnnotation(BankAPIField.class);
          ????????????????Object?value?=?"";
          ????????????????try?{
          ????????????????????//反射獲取字段值
          ????????????????????value?=?field.get(api);
          ????????????????}?catch?(IllegalAccessException?e)?{
          ????????????????????e.printStackTrace();
          ????????????????}
          ????????????????//根據(jù)字段類型以正確的填充方式格式化字符串
          ????????????????switch?(bankAPIField.type())?{
          ????????????????????case?"S":?{
          ????????????????????????stringBuilder.append(String.format("%-"?+?bankAPIField.length()?+?"s",?value.toString()).replace('?',?'_'));
          ????????????????????????break;
          ????????????????????}
          ????????????????????case?"N":?{
          ????????????????????????stringBuilder.append(String.format("%"?+?bankAPIField.length()?+?"s",?value.toString()).replace('?',?'0'));
          ????????????????????????break;
          ????????????????????}
          ????????????????????case?"M":?{
          ????????????????????????if?(!(value?instanceof?BigDecimal))
          ????????????????????????????throw?new?RuntimeException(String.format("{}?的?{}?必須是BigDecimal",?api,?field));
          ????????????????????????stringBuilder.append(String.format("%0"?+?bankAPIField.length()?+?"d",?((BigDecimal)?value).setScale(2,?RoundingMode.DOWN).multiply(new?BigDecimal("100")).longValue()));
          ????????????????????????break;
          ????????????????????}
          ????????????????????default:
          ????????????????????????break;
          ????????????????}
          ????????????});
          ????//簽名邏輯
          ??? stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
          ????String?param?=?stringBuilder.toString();
          ????long?begin?=?System.currentTimeMillis();
          ????//發(fā)請求
          ????String?result?=?Request.Post("http://localhost:45678/reflection"?+?bankAPI.url())
          ????????????.bodyString(param,?ContentType.APPLICATION_JSON)
          ????????????.execute().returnContent().asString();
          ????log.info("調(diào)用銀行API?{}?url:{}?參數(shù):{}?耗時:{}ms",?bankAPI.desc(),?bankAPI.url(),?param,?System.currentTimeMillis()?-?begin);
          ????return?result;
          }

          現(xiàn)在再來看一下創(chuàng)建用戶和付款的邏輯


          //創(chuàng)建用戶方法
          public?static?String?createUser(CreateUserAPI?request)?throws?IOException?{
          ????return?remoteCall(request);
          }
          //支付方法
          public?static?String?pay(PayAPI?request)?throws?IOException?{
          ????return?remoteCall(request);
          }

          可以看到所有的請求現(xiàn)在都只要統(tǒng)一調(diào)用 ?remoteCall 這個方法即可,remoteCall 這個方法統(tǒng)一了所有請求的邏輯,省略了巨量無關(guān)的代碼,讓代碼的可維護(hù)性大大增強(qiáng)!使用注解和反射讓我們可以對這類結(jié)構(gòu)性的問題進(jìn)行通用化處理,確實(shí) Cool!

          總結(jié)

          如果說反射給了我們在不知曉類結(jié)構(gòu)的情況下按照固定邏輯處理類成員的能力的話,注解則是擴(kuò)展補(bǔ)充了這些成員的元數(shù)據(jù)的能力,使用得我們在利用反射實(shí)現(xiàn)通用邏輯的時候,可以從外部獲取更多我們關(guān)心的數(shù)據(jù),進(jìn)而對這些數(shù)據(jù)進(jìn)行通用的處理,巧用反射,確實(shí)能讓我們達(dá)到事半功倍的效果,能極大的減少重復(fù)代碼,有效解藕,使擴(kuò)展性大大提升。

          額外提一句,上文中注解的高級應(yīng)用的例子來自文末的參考鏈接中的一個例子,如果大家沒有訂閱教程,可以加我好友(geekoftaste),邀請你試讀此文

          巨人的肩膀

          https://time.geekbang.org/column/article/228964


          瀏覽 41
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文板真人性操逼 | 美女高潮喷水影院 | 俺来也我也去www官网 | 影音先锋av资源网站 | 大地电影资源第五页在线观看 |