2021年了居然還不會用springcloud,源碼帶你一步步搭建(小白教程)

公共模塊封裝

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

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 i
Date 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()i
return 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) i
return Base64.encodeBase64String( bytes);
}
public static byte[] base64Decode(String base64Code) throws Exception {
return Base64.decodeBase64(base64Code);
}
public static byte[] aesEncryptToBytes(String content,String encryptKey) throws
Exception {
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)throws
Exception {
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) throws
Exception i
return 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) throws
NoSuchAlgorithmException,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)f
if(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文件,按照操作步驟即可完成安裝。

安裝完成後,點擊Win+R鍵,在打開的運行窗口中輸人命令services.msc並按下Enter鍵,可以打開服務列表,如圖6-1所示。


可以看到,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;
@SpringBootConfiguration
public class Rabbitconfiguration {
@Bean
public Queue queue(){
return new Queue( "someQueue");
}
}

前面已經講過,Spring Boot可以利用@SpringBootConfiguration註解對應用程序進行配置。我們集成RabbitMQ依賴後,也需要對其進行基本配置。在上述代碼中,我們定義了一個 Bean,該Bean的作用是自動創建消息隊列名。如果不通過代碼創建隊列,那麼每次都需要手動去RabbitMQ的Web管理界面添加隊列,否則會報錯,如圖6-4所示。


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

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

創建一個名爲MyBean的類,用於發送和接收消息隊列:

@Component
public class MyBean {
private final AmqpAdmin amqpAdmin;
private final AmqpTemplate amqpTemplate;
@Autowired
public 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
@Documented
public @interface ApiVersion {
int value();
}

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

(2)自定義RequestMappingHandler:

public class CustomRequestMappingHandlerMapping extends
RequestMappingHandlerMapping i
@override
protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?>
handlerType) {
ApiVersion apiVersion = Annotationutils.findAnnotation(handlerType,
Apiversion.class);
return createCondition( apiversion);
}
@override
protected RequestCondition<ApiVersionConditionz getCustomMethodCondition(Nethod method){
ApiVersion apiversion = AnnotationUtils.findAnnotation(method,ApiVersion.class);
return createCondition(apiversion) ;
}
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion)f
return 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方法,如:

@Override
public 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註解的類中,將每個屬性加入校驗規則註解,如:

@Data
public class TestRequest {
@NotEmpty
private String name;
}

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

  • @NotNull:不能爲空。
  • @NotEmpty:不能爲空或空字符串。
  • @Max:最大值。
  • @Min:最小值。
  • @Pattern:正則匹配。
  • @Length:最大長度和最小長度。

異常的統一處理
異常,在產品開發中是較爲常見的,譬如程序運行或數據庫連接等,這些過程中都可能會拋出異常,如果不進行任何處理,客戶端就會接收到如圖6-5所示的內容。


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

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

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

@ExceptionHandler
public 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方法:

@override
public 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 的常規操作,其代碼如下:

@Component
public class Redis i
@Autowired
private 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) i
return template.opsForValue().get(key);
}
public void delete(String key) {
template.delete(key);
}
}

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

小結
本篇主要封裝了博客網站的公共模塊,即每個模塊都可能用到的方法和類庫,保證代碼的複用性。讀者也可以根據自己的理解和具體的項目要求去封裝一些方法,提供給各個模塊調用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章