首頁>技術>

公共模組封裝

從本篇開始,我們將學習框架的搭建。由於程式碼量巨大,本書不可能全部貼出,所以只展示一些核心程式碼。全部原始碼可以從本書配套原始碼中檢視。

經過前幾章的學習,讀者應該對本專案有了大致的瞭解,也已搭建好了各個基本模組。為了保證應用程式的複用性和可擴充套件性,我們需要將一些常用的基本方法封裝起來,以便各個模組呼叫。

在一個完整的微服務架構體系中,字串和日期的處理往往是最多的。在一些安全應用場景下,還會用到加密演算法。為了提升應用的擴充套件性,我們還應對介面進行版本控制。因此,我們需要對這些場景進行一定的封裝,方便開發人員使用。本章中,我們優先從公共模組入手搭建一套完整的微服務架構。

common 工程常用類庫的封裝

common工程是整個應用的公共模組,因此,它裡面應該包含常用類庫,比如日期時間的處理、字串的處理、加密/解密封裝、訊息佇列的封裝等。

日期時間的處理

因此,我們可以在common工程下建立日期時間處理工具類Dateutils,其程式碼如下:

import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.calendar;import java.util.Date;public final class DateUtils {public static boolean isLegalDate(String str, String pattern){try {SimpleDateFormat format = new SimpleDateFormat(pattern);format.parse(str);return true;} catch (Exception e){return false;}}public static Date parseString2Date(String str,String pattern){try {SimpleDateFormat format = new SimpleDateFormat(pattern);return format.parse( str);}catch (ParseException e){e.printstackTrace();return null;}}public static calendar parseString2calendar(String str,String pattern){return parseDate2Calendar(parsestring2Date(str, pattern));}public static String parseLong2DateString(long date,String pattern){SimpleDateFormat sdf = new SimpleDateFormat(pattern);String sd = sdf.format(new Date(date));return sd;}public static Calendar parseDate2Calendar(Date date){Calendar calendar = Calendar.getInstance();calendar.setTime(date);return calendar;}public static Date parseCalendar2Date(calendar calendar){return calendar.getTime();}public static String parseCalendar2String(calendar calendar,String pattern){return parseDate2String(parsecalendar2Date(calendar), pattern);}public static String parseDate2String(Date date,String pattern) {SimpleDateFormat format = new SimpleDateFormat(pattern);return format.format(date);}public static String formatTime( long time){long nowTime = System.currentTimeMillis();long interval = nowTime - time;long hours = 3600 * 1000;long days = hours * 24;long fiveDays = days *5;if (interval < hours){long minute = interval / 1008/ 60;if (minute == 0) {return“剛剛";}return minute +"分鐘前";}else if (interval < days){return interval / 1000/ 360日 +"小時前";}else if (interval< fiveDays) {return interval / 1000 / 3600/ 24+"天前";}else iDate date = new Date(time);return parseDate2String(date,"MM-dd");}}}

在處理日期格式時,我們可以呼叫上述程式碼提供的方法,如判斷日期是否合法的方法isLegalDate。我們在做日期轉換時,可以呼叫以 parse開頭的這些方法,透過方法名大致能知道其含義,如parseCalendar2String表示將calendar型別的物件轉化為String型別,parseDate2String 表示將Date型別的物件轉化為string型別,parseString2Date表示將String型別轉化為Date型別。

當然,上述程式碼無法囊括所有對日期的處理。如果你在開發過程中有新的處理需求時,可以在DateUtils 中新增方法。

另外,我們在做專案開發時應遵循“不重複造輪子”的原則,即儘可能引入成熟的第三方類庫。目前,市面上對日期處理較為成熟的框架是 Joda-Time,其引入方法也比較簡單,只需要在pom.xml加入其依賴即可,如:

<dependency><groupId>joda-time</groupId><artifactId>joda-time</ artifactId><version>2.10.1</version></dependency>

使用Joda-Time 也比較簡單,只需構建DateTime物件,透過DateTime物件進行日期時間的操作即可。如取得當前日期後90天的日期,可以編寫如下程式碼:

DateTime dateTime = new DateTime();

System.out.println(dateTime.plusDays(90).toString("yyyy-MM-dd HH:mm:ss"));

Joda-Time是一個高效的日期處理工具,它作為JDK原生日期時間類的替代方案,被越來越多的人使用。在進行日期時間處理時,你可優先考慮它。

字串的處理

在應用程式開發中,字串可以說是最常見的資料型別,對它的處理也是最普遍的,比如需要判斷字串的非空性、隨機字串的生成等。接下來,我們就來看一下字串處理工具類stringUtils:

public final class StringUtils{private static final char[] CHARS ={ '0','1','2','3', '4', '5','6', '7',' 8','9'};private static int char_length =CHARS.length;public static boolean isEmpty( string str){return null == str ll str.length()== 0;}public static boolean isNotEmpty(string str){return !isEmpty(str);}public static boolean isBlank(String str){int strLen;if (null == str ll(strLen = str.length())== 0){return true;}for (int i= e; i< strLen; i++){if ( !Character.iswhitespace(str.charAt(i))){return false;}}return true;}public static boolean isNotBlank(String str){return !isBlank(str);}public static String randomString(int length){StringBuilder builder = new StringBuilder(length);Random random = new Random();for (int i = 0; i< length; i++){builder.append(random.nextInt(char_length));}return builder.toString();}public static string uuid()ireturn UUID.randomUUID().toString().replace("-","");}private StringUtils(){throw new AssertionError();}}

字串亦被稱作萬能型別,任何基本型別(如整型、浮點型、布林型等)都可以用字串代替,因此我們有必要進行字串基本操作的封裝。

上述程式碼封裝了字串的常用操作,如 isEmpty 和 isBlank均用於判斷是否為空,區別在於:isEmpty單純比較字串長度,長度為0則返回true,否則返回false,如“”(此處表示空格)將返回false;而isBlank判斷是否真的有內容,如“”(此處表示空格)返回true。同理,isNotEmpty和isNotBlank均判斷是否不為空,區別同上。randomString表示隨機生成6個數字的字串,常用於簡訊驗證碼的生成。uuid用於生成唯一標識,常用於資料庫主鍵、檔名的生成。

加密/解密封裝

對於一些敏感資料,比如支付資料、訂單資料和密碼,在HTTP傳輸過程或資料儲存中,我們往往需要對其進行加密,以保證資料的相對安全,這時就需要用到加密和解密演算法。

目前常用的加密演算法分為對稱加密演算法、非對稱加密演算法和資訊摘要演算法。

對稱加密演算法:加密和解密都使用同一個金鑰的加密演算法,常見的有AES、DES和XXTEA。非對稱加密演算法:分別生成一對公鑰和私鑰,使用公鑰加密,私鑰解密,常見的有RSA。資訊摘要演算法:一種不可逆的加密演算法。顧名思義,它只能加密而無法解密,常見的有MD5.SHA-1和 SHA-256。

本書的實戰專案用到了AES、RSA、MD5和 SHA-1演算法,故在common 工程下對它們分別進行了封裝。

(1)在pom.xml 中下新增依賴:

<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId></dependency><dependency><groupId>commons-io</groupid><artifactId>commons-io</ artifactId><version>2.6</version></dependency>

在上述依賴中,commons-codec是 Apache基金會提供的用於資訊摘要和 Base64編碼解碼的包。在常見的對稱和非對稱加密演算法中,都會對密文進行 Base64編碼。而 commons-io是 Apache基金會提供的用於操作輸入輸出流的包。在對RSA 的加密/解密演算法中,需要用到位元組流的操作,因此需要新增此依賴包。

(2)編寫AES 演算法:

import javax.crypto.spec. SecretKeySpec;public class AesEncryptUtils {private static final String ALGORITHMSTR = "AES/ECB/PKCSSPadding";public static String base64Encode(byte[] bytes) ireturn Base64.encodeBase64String( bytes);}public static byte[] base64Decode(String base64Code) throws Exception {return Base64.decodeBase64(base64Code);}public static byte[] aesEncryptToBytes(String content,String encryptKey) throwsException {KeyGenerator kgen = KeyGenerator.getInstance("AES");kgen.init(128);Cipher cipher = Cipher.getInstance(ALGORITHMSTR);cipher.init(Cipher.ENCRYPT_MODE,new SecretKeySpec(encryptKey.getBytes(),"AES"));return cipher.doFinal(content.getBytes("utf-8"));}public static String aesEncrypt(String content, String encryptKey) throwS Exception {return base64Encode(aesEncryptToBytes(content,encryptKey));}public static string aesDecryptByBytes(byte[] encryptBytes, String decryptKey)throwsException {KeyGenerator kgen = KeyGenerator.getInstance("AES");kgen.init(128);Cipher cipher = Cipher.getInstance(ALGORITHMSTR);cipher.init(Cipher.DECRYPT_MODE,new SecretKeySpec(decryptKey.getBytes(),"AES"));byte[] decryptBytes = cipher.doFinal(encryptBytes);return new String(decryptBytes);}public static String aesDecrypt(String encryptStr, String decryptKey) throwsException ireturn aesDecryptByBytes(base64Decode(encryptStr),decryptKey);}}

上述程式碼是通用的AES加密演算法,加密和解密需要統一金鑰,金鑰是自定義的任意字串,長度為16位、24位或32位。這裡呼叫aesEncrypt方法進行加密,其中第一個引數為明文,第二個引數為金鑰;呼叫aesDecrypt進行解密,其中第一個引數為密文,第二個引數為金鑰。

我們注意到,程式碼中定義了一個字串常量 ALGORITHMSTR,其內容為AES/ECB/PKCS5Padding,它定義了對稱加密演算法的具體加解密實現,其中 AES表示該演算法為AES演算法,ECB為加密模式,PKCS5Padding為具體的填充方式,常用的填充方式還有 PKCS7Padding和 NoPadding等。使用不同的方式對同一個字串加密,結果都是不一樣的。因此,我們在設定加密演算法時需要和客戶端統一,否則客戶端無法正確解密服務端返回的密文。

(3)編寫RSA演算法:

public class RSAUtils {public static final String CHARSET ="UTF-8";public static final String RSA_ALGORITHM="RSA";public static Map<String,String>createKeys(int keySize){KeyPairGenerator kpg;try{kpg =KeyPairGenerator.getInstance(RSA_ALGORITHM);Security.addProvider(new com.sun.crypto.provider. SunJCE());}catch(NoSuchAlgorithmException e){throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM +"]");}kpg.initialize(keySize);KeyPair keyPair = kpg.generateKeyPair();Key publicKey = keyPair.getPublic();string publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());Key privateKey = keyPair.getPrivate();String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());Map<String,String> keyPairMap = new HashMap<>(2);keyPairMap.put("publicKey", publicKeyStr);keyPairMap.put( "privateKey", privateKeyStr);return keyPairMap;}public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException,InvalidKeySpecException {KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);x509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey)) ;RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic( x509KeySpec);return key;}public static RSAPrivateKey getPrivateKey(String privateKey) throwsNoSuchAlgorithmException,InvalidKeySpecException {KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);return key;}public static String publicEncrypt(String data,RSAPublicKey publicKey){try{Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");cipher.init(Cipher. ENCRYPT_MODE,publicKey);return Base64.encodeBase64String(rsaSplitCodec(cipher,Cipher. ENCRYPT_MODE,data.getBytes(CHARSET),publicKey.getModulus().bitLength()));}catch(Exception e){throw new RuntimeException("加密字串["+data +"]時遇到異常",e);}}public static String privateDecrypt(String data,RSAPrivateKey privateKey){try{Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");cipher.init(Cipher. DECRYPT_MODE, privateKey);return new String(rsaSplitCodec(cipher,Cipher. DECRYPT_MODE,Base64.decodeBase64(data),privateKey.getModulus().bitLength()),CHARSET);}catch(Exception e){e.printStackTrace();throw new RuntimeException("解密字串["+data+"]時遇到異常",e);}}private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas,int keySize){int maxBlock = 0;if(opmode == Cipher. DECRYPT_MODE){maxBlock = keysize / 8;}else{maxBlock =keysize / 8 -11;}ByteArrayOutputStream out = new ByteArrayoutputStream();int offSet = 0;byte[] buff;int i = 0;try{while(datas. length > offSet)fif(datas.length-offSet > maxBlock){buff = cipher.doFinal(datas,offSet,maxBlock);}else{buff = cipher.doFinal(datas,offSet, datas.length-offSet);}out.write(buff, 0,buff.length);i++;offSet = i * maxBlock;}}catch(Exception e){e.printStackTrace();throw new RuntimeException("加解密閾值為["+maxBlock+"]的資料時發生異常",e);}byte[] resultDatas = out.toByteArray();IOUtils.closeQuietly(out);return resultDatas;}}

前面提到了RSA是一種非對稱加密演算法,所謂非對稱,即加密和解密所採用的金鑰是不一樣的。RSA 的基本思想是透過一定的規則生成一對金鑰,分別是公鑰和私鑰,公鑰是提供給客戶端使用的,即任何人都可以得到,而私鑰存放到服務端,任何人都不能透過正常渠道拿到。

通常情況下,非對稱加密演算法在客戶端使用公鑰加密,傳到服務端後,服務端利用私鑰進行解密。例如,上述程式碼提供了加解密方法,分別是publicEncrypt和 privateDecrypt方法,但是這兩個方法不能直接傳公私鑰字串,而是透過getPublicKey和getPrivateKey方法返回RSAPublicKey和RSAPrivateKey物件後再傳給加解密方法。

公鑰和私鑰的生成方式有很多種,如OpenSSL 工具、第三方線上工具和編碼實現等。由於非對稱加密演算法分別維護了公鑰和私鑰,其演算法效率比對稱加密演算法低,但安全級別比對稱加密演算法高,讀者在選用加密演算法時應綜合考慮,採取適合專案的加密演算法。

(4)編寫資訊摘要演算法:

import java.security.MessageDigest;public class MessageDigestutils {public static string encrypt(String password,string algorithm){try {MessageDigest md =MessageDigest.getInstance(algorithm);byte[] b = md.digest(password.getBytes("UTF-8"));return ByteUtils.byte2HexString(b);}catch (Exception e){e.printStackTrace();return null;}}}

JDK自帶資訊摘要演算法,但返回的是位元組陣列型別,在實際中需要將位元組陣列轉化成十六進位制字串,因此上述程式碼對資訊摘要演算法做了簡要的封裝。透過呼叫MessageDigestutils.encrypt方法即可返回加密後的字串密文,其中第一個引數為明文,第二個引數為具體的資訊摘要演算法,可選值有MD5、SHA1和SHA256等。

資訊摘要加密是一種不可逆演算法,即只能加密,無法解密。在技術高度發達的今天,資訊摘要演算法雖然無法直接解密,但是可以透過碰撞算法曲線破解。我國著名數學家、密碼學專家王小云女士早已透過碰撞演算法破解了MD5和SHA1演算法。因此,為了提高加密技術的安全性,我們一般使用“多重加密+salt”的方式加密,如ND5(MD5(明文+salt)),讀者可以將salt理解為金鑰,只是無法透過salt解密。

訊息佇列的封裝

訊息佇列一般用於非同步處理、高併發的訊息處理以及延時處理等情形,它在當前網際網路環境下也被廣泛應用,因此同樣對它進行了封裝,以便後續訊息佇列使用。

在本例中,使用RabbitMQ來演示訊息佇列。首先,在Windows系統下安裝RabbitMQ。由於RabbitMQ依賴Erlang,應先安裝Erlang,下載地址為http:/www.rabbitmq.com/which-erlang.html,雙擊下載的檔案即可完成安裝。然後安裝RabbitMQ,下載地址為 http:/www.rabbitmq.com/install-windows.html,雙擊下載的exe檔案,按照操作步驟即可完成安裝。

可以看到,RabbitMQ已啟動。在預設情況下,RabbitMQ安裝後只開啟5672埠,我們只能透過命令的方式檢視和管理RabbitMQ。為了方便,我們可以透過安裝外掛來開啟RabbitMQ的 Web管理功能。開啟cmd命令控制檯,進入 RabbitMQ安裝目錄的 sbin目錄,輸入 rabbitmq-plugins enablerabbitmq_management即可,如圖6-2所示。

Web管理介面的預設啟動埠為15672。在瀏覽器中輸人localhost:15672,預設的賬號和密碼都是guest,填寫後可以進入Web管理主介面,如圖6-3所示。

接下來,我們就封裝訊息佇列。(1)新增 RabbitMQ依賴:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bus-amqp</ artifactId></dependency>

訊息佇列都是透過Spring Cloud元件Spring Cloud Bus整合的,透過新增依賴spring-cloud-starter-bus-amqp,就可以很方便地使用RabbitMQ。

(2)建立RabbitMQ配置類RabbitConfiguration,用於定義RabbitMQ基本屬性:

import org.springframework.amqp.core.Queue;import org.springframework.boot.SpringBootConfiguration;import org.springframework.context.annotation. Bean;@SpringBootConfigurationpublic class Rabbitconfiguration {@Beanpublic Queue queue(){return new Queue( "someQueue");}}

前面已經講過,Spring Boot可以利用@SpringBootConfiguration註解對應用程式進行配置。我們整合RabbitMQ依賴後,也需要對其進行基本配置。在上述程式碼中,我們定義了一個 Bean,該Bean的作用是自動建立訊息佇列名。如果不透過程式碼建立佇列,那麼每次都需要手動去RabbitMQ的Web管理介面新增佇列,否則會報錯,如圖6-4所示。

但是每次都透過Web管理介面手動建立佇列顯然不可取,因此,我們可以在上述配置類中事先定義好佇列。

(3) RabbitMQ是非同步請求,即客戶端傳送訊息,RabbitMQ服務端收到訊息後會回發給客戶端。傳送訊息的稱為生產者,接收訊息的稱為消費者,因此還需要封裝訊息的傳送和接收。

建立一個名為MyBean的類,用於傳送和接收訊息佇列:

@Componentpublic class MyBean {private final AmqpAdmin amqpAdmin;private final AmqpTemplate amqpTemplate;@Autowiredpublic MyBean(AmqpAdmin amqpAdmin,AmqpTemplate amqpTemplate){this.amqpAdmin = amqpAdmin;this.amqpTemplate = amqpTemplate;}@RabbitHandler@RabbitListener(queues = "someQueue")public void processMessage(String content){//訊息佇列消費者system.out.println( content);}public void send(string content){//訊息佇列生產者amqpTemplate.convertAndSend("someQueue", content);}}

其中,send為訊息生產者,負責傳送佇列名為someQueue 的訊息,processNessage為訊息消費者,在其方法上定義了@RabbitHandler和@RabbitListener註解,表示該方法為訊息消費者,並且指定了消費哪種佇列。

介面版本管理

一般在第一版產品釋出並上線後,往往會不斷地進行迭代和最佳化,我們無法保證在後續升級過程中不會對原有介面進行改動,而且有些改動可能會影響線上業務。因此,想要對介面進行改造卻不能影響線上業務,就需要引人版本的概念。顧名思義,在請求介面時加上版本號,後端根據版本號執行不同版本時期的業務邏輯。那麼,即便我們升級改造介面,也不會對原有的線上介面造成影響,從而保證系統正常執行。

版本定義的思路有很多,比如:

透過請求頭帶人版本號,如 header( "version" , "1.0");URL地址後面帶人版本號,如 api?version=1.0;RESTful風格的版本號定義,如/ api/v1。

本節將介紹第三種版本號的定義思路,最簡單的方式就是直接在RequestMapping 中寫入固定的版本號,如:

@RequestMapping("/v1/index")

這種方式的壞處就是擴充套件性不好,而且一旦傳入其他版本號,介面就會報404錯誤。比如,客戶端介面地址的請求為/v2/index,而我們的專案只定義了v1,則無法請求index介面。

我們希望的效果是,如果傳入的版本號在專案中無法找到,則自動找最高版本的介面,怎麼做呢?請參照以下程式碼實現。

(1)定義註解類:

@Target(ElementType. TYPE)@Retention(RetentionPolicy.RUNTIME)@Mapping@Documentedpublic @interface ApiVersion {int value();}

在上面的程式碼中,首先定義了一個註解,用於指定控制器的版本號,比如@ApiVersion(1),則透過地址v1/**就可以訪問該控制器定義的方法。

(2)自定義RequestMappingHandler:

public class CustomRequestMappingHandlerMapping extendsRequestMappingHandlerMapping i@overrideprotected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?>handlerType) {ApiVersion apiVersion = Annotationutils.findAnnotation(handlerType,Apiversion.class);return createCondition( apiversion);}@overrideprotected RequestCondition<ApiVersionConditionz getCustomMethodCondition(Nethod method){ApiVersion apiversion = AnnotationUtils.findAnnotation(method,ApiVersion.class);return createCondition(apiversion) ;}private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion)freturn apiversion == null ? null : new ApiVersionCondition(apiVersion.value());}}

Spring MVC透過RequestMapping 來定義請求路徑,因此如果我們要自動透過v1這樣的地址來請求指定的控制器,就應該繼承RequestMappingHandlerMapping類來重寫其方法。

Spring MVC在啟動應用後會自動對映所有控制器類,並將標有@RequestMapping註解的方法載入到記憶體中。由於我們繼承了RequestMappingHandlerMapping 類,所以在對映時會執行重寫的getCustomTypeCondition和getCustomMethodCondition方法,由方法體的內容可以知道,我們建立了自定義的RequestCondition,並將版本資訊傳給Requestcondition。

(3) CustomRequestMappingHandlerMapping類只繼承了RequestMappingHandlerMapping類,Spring Boot並不知曉,因此還需要在配置類中定義它,以便使Spring Boot 在啟動時執行自定義的RequestMappingHandlerMapping 類。

在public 工程中建立webConfig 類,並繼承 webNvcConfigurationSupport類,然後重寫requestMappingHandlerMapping方法,如:

@Overridepublic RequestMappingHandlerMapping requestMappingHandlerMapping(){RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();handlerMapping.set0rder(0);return handlerMapping;}

在上述程式碼中,我們重寫了requestMappingHandlerMapping方法並例項化了RequestMapping-HandlerMapping物件,返回的是前面自定義的CustomRequestMappingHandlerMapping類。

(4)在控制器類中加入註解@ApiVersion(1)實現版本控制,其中數字1表示版本號v1。在請求介面時,輸入類似/api/v1/index的地址即可,程式碼如下:

@RequestMapping("{version}")@RestController@ApiVersion(1)public class TestV1controller{@GetMapping("index ")public String index(){return "";}}
輸入引數的合法性校驗

我們在定義介面時,需要對輸入引數進行校驗,防止非法引數的侵入。比如在實現登入介面時,手機號和密碼不能為空,手機號必須是11位數字等。雖然客戶端也會進行校驗,但它只針對正常的使用者請求,如果使用者繞過客戶端,直接請求介面,就可能會傳入一些異常字元。因此,後端同時對輸人參數進行合法性校驗是必要的。

進行合法性校驗最簡單的方式是在每個介面內做if-else判斷,但這種方式不夠優雅。Spring 提供了校驗類validator,我們可以對其做文章。

在公共的控制器類中新增以下方法即可:

protected void validate(BindingResult result){if(result.hasFieldErrors()){List<FieldError> errorList = result.getFieldErrors();errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage()));}}

Validator的校驗結果會存放到BindingResult類中,因此上述方法傳入了BindingResult類。在上面的程式碼中,程式透過 hasFieldErrors判斷是否存在校驗不透過的情況,如果存在,則透過getFieldErrors方法取出所有錯誤資訊並迴圈該錯誤列表,一旦發現錯誤,就用Assert 斷言方法丟擲異常,6.4節將介紹異常的處理,統一返回校驗失敗的提示資訊。

我們使用斷言的好處在於它丟擲的是執行時異常,即我們不需要用顯式在方法後面加 throwsException,也能夠保證擴充套件性較好,同時簡化了程式碼量。

然後在控制器介面的引數中新增@valid註解,後面緊跟 BindingResult類,在方法體中呼叫validate(result)方法即可,如:

@GetMapping( "index")public String index(@valid TestRequest request, BindingResult result){validate(result);return "Hello " +request.getName();}

要實現介面校驗,需要在定義了@valid註解的類中,將每個屬性加入校驗規則註解,如:

@Datapublic class TestRequest {@NotEmptyprivate String name;}

下面列出常用註解,供讀者參考。

@NotNull:不能為空。@NotEmpty:不能為空或空字串。@Max:最大值。@Min:最小值。@Pattern:正則匹配。@Length:最大長度和最小長度。異常的統一處理

異常,在產品開發中是較為常見的,譬如程式執行或資料庫連線等,這些過程中都可能會丟擲異常,如果不進行任何處理,客戶端就會接收到如圖6-5所示的內容。

可以看出,直接在介面上返回了500,這不是我們期望的。正常情況下,即便出錯,也應返回統一的JSON格式,如:

{"code" :0,"message" :"不能為空" ,"data" :null}

其實很簡單,它利用了Spring的AOP特性,在公共控制器中新增以下方法即可:

@ExceptionHandlerpublic SingleResult doError(Exception exception){if(Stringutils.isBlank(exception.getMessage())){return SingleResult.buildFailure();}return SingleResult.buildFailure(exception.getMessage());}

在doError方法上加入@ExceptionHandler註解表示發生異常時,則執行該註解標註的方法,該方法接收Exception類。我們知道,Exception類是所有異常類的父類,因此在發生異常時,SpringMVC會找到標有@ExceptionHandler註解的方法,呼叫它並傳人具體的異常物件。

我們要返回上述JSON格式,只需要返回SingleResult物件即可。注意,SingleResult是自定義的資料結果類,它繼承自Result類,表示返回單個數據物件;與之相對應的是MultiResult類,用於返回多個結果集,所有介面都應返回Result。關於該類,讀者可以參考本書配套原始碼,在common工程的 com.lynn.blog.common.result包下。

更換JSON轉換器

Spring MVC預設採用Jackson框架作為資料輸出的JSON格式的轉換引擎,但目前市面上湧現出了很多JSON解析框架,如 FastJson、Gson等,Jackson作為老牌框架已經無法和這些框架媲美。

Spring 的強大之處也在於其擴充套件性,它提供了大量的介面,方便開發者可以更換其預設引擎,JSON轉換亦不例外。下面我們就來看看如何將Jackson更換為FastJson。

(1)新增FastJson依賴:

<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></ dependency>

FastJson是阿里巴巴出品的用於生成和解析JSON 資料的類庫,其執行效率也是同類框架中出類拔萃的,因此本書採用FastJson作為JSON的解析引擎。

(2)在webConfig 類中重寫configureMessageConverters方法:

@overridepublic void configureMessageConverters(List<HttpMessageConverter< ?>> converters){super.configureMessageConverters(converters);FastJsonHttpMessageConverter fastConverter=new Fast]sonHttpMessageConverter();FastJsonConfig fastJsonconfig=new FastsonConfig();fastJsonconfig.setSerializerFeatures(SerializerFeature.PrettyFormat);List<MediaType> mediaTypeList = new ArrayList<>();mediaTypeList.add(MediaType.APPLICATION_JSON_UTF8);fastConverter.setSupportedMediaTypes(mediaTypeList);fastConverter.setFastsonConfig(fastsonConfig);converters.add(fastConverter);}

當程式啟動時,會執行configureMessageConverters方法,如果不重寫該方法,那麼該方法體是空的,我們檢視原始碼即可得知。程式碼如下:

/*** Override this method to add custom {@link HttpMessageConverter}s to use* with the {@link RequestMappingHandlerAdapter} and the* {@link ExceptionHandlerExceptionResolver}. Adding converters to the* list turns off the default converters that would otherwise be registered* by default. Also see {@link #addDefaultHttpNessageConverters(List)} that* can be used to add default message converters.* @param converters a list to add message converters to;* initially an empty list.*/protected void configureMessageConverters(List<HttpNessageConverter<?>> converters) {}

這時, Spring MVC將Jackson作為其預設的JSON解析引擎,所以我們一旦重寫configureMessage-Converters方法,它將覆蓋Jackson,把我們自定義的JSON解析器作為JSON解析引擎。

得益於Spring的擴充套件性設計,我們可以將JSON解析引擎替換為FastJson,它提供了AbstractHttp-MessageConverter 抽象類和GenericHttpMessageConverter介面。透過實現它們的方法,就可以自定義JSON解析方式。

在上述程式碼中,FastJsonHttpMessageConverter就是FastJson為了整合Spring而實現的一個轉換器。因此,我們在重寫configureMessageConverters方法時,首先要例項化FastJsonHttpMessage-Converter物件,並進行Fast]sonConfig基本配置。PrettyFormat表示返回的結果是否是格式化的;而MediaType 設定了編碼為UTF-8的規則。最後,將Fast3sonHttpMessageConverter物件新增到conterters列表中。

這樣我們在請求介面返回資料時,Spring MVC 就會使用FastJson轉換資料。

Redis的封裝

Redis 作為記憶體資料庫,使用非常廣泛,我們可以將一些資料快取,提高應用的查詢效能,如儲存登入資料(驗證碼和 token等)、實現分散式鎖等。

本文實戰專案也用到了Redis,且 Spring Boot操作Redis非常方便。SpringBoot集成了Redis並實現了大量方法,有些方法可以共用,我們可以根據專案需求封裝一套自己的Redis操作程式碼。

(1)新增 Redis 的依賴:

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

spring-boot-starter-data包含了與資料相關的包,比如jpa、mongodb和elasticsearch等。因此,Redis也放到了spring-boot-starter-data 下。

(2)建立Redis類,該類包含了Redis 的常規操作,其程式碼如下:

@Componentpublic class Redis i@Autowiredprivate StringRedisTemplate template;public void set(String key, String value,long expire){template.opsForValue().set(key, value,expire,TimeUnit.SECONDS);}public void set(String key,string value){template.opsForValue().set(key, value);}public Object get(String key) ireturn template.opsForValue().get(key);}public void delete(String key) {template.delete(key);}}

在上述程式碼中,我們先注入StringRedisTemplate類,該類是Spring Boot 提供的Redis操作模板類,透過它的名稱可以知道該類專門用於字串的存取操作,它繼承自RedisTemplate類。程式碼中只實現了Redis的基本操作,包括鍵值儲存、讀取和刪除操作。set方法過載了兩個方法,可以接收資料儲存的有效期,TimeUnit.SECONDS 指定了該有效期單位為秒。讀者如果在專案開發過程中發現這些操作不能滿足要求時,可以在這個類中新增方法滿足需求。

小結

本篇主要封裝了部落格網站的公共模組,即每個模組都可能用到的方法和類庫,保證程式碼的複用性。讀者也可以根據自己的理解和具體的專案要求去封裝一些方法,提供給各個模組呼叫。

11
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Java操作word工具的選擇