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

公共模塊封裝

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

common 工程常用類庫的封裝
common工程是整個(gè)應(yīng)用的公共模塊,因此,它里面應(yīng)該包含常用類庫,比如日期時(shí)間的處理、字符串的處理、加密/解密封裝、消息隊(duì)列的封裝等。

日期時(shí)間的處理

在一個(gè)應(yīng)用程序中,對日期時(shí)間的處理是使用較廣泛的操作之一,比如博客發(fā)布時(shí)間和評論時(shí)間等。而時(shí)間是以時(shí)間戳的形式存儲(chǔ)到數(shù)據(jù)庫中的,這就需要我們經(jīng)過一系列處理才能返回給客戶端。

因此,我們可以在common工程下創(chuàng)建日期時(shí)間處理工具類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日 +"小時(shí)前";}else if (interval< fiveDays) {
return interval / 1000 / 3600/ 24+"天前";}else i
Date date = new Date(time);
return parseDate2String(date,"MM-dd");
}
}
}

在處理日期格式時(shí),我們可以調(diào)用上述代碼提供的方法,如判斷日期是否合法的方法isLegalDate。我們在做日期轉(zhuǎn)換時(shí),可以調(diào)用以 parse開頭的這些方法,通過方法名大致能知道其含義,如parseCalendar2String表示將calendar類型的對象轉(zhuǎn)化為String類型,parseDate2String 表示將Date類型的對象轉(zhuǎn)化為string類型,parseString2Date表示將String類型轉(zhuǎn)化為Date類型。

當(dāng)然,上述代碼無法囊括所有對日期的處理。如果你在開發(fā)過程中有新的處理需求時(shí),可以在DateUtils 中新增方法。

另外,我們在做項(xiàng)目開發(fā)時(shí)應(yīng)遵循“不重復(fù)造輪子”的原則,即盡可能引入成熟的第三方類庫。目前,市面上對日期處理較為成熟的框架是 Joda-Time,其引入方法也比較簡單,只需要在pom.xml加入其依賴即可,如:

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

使用Joda-Time 也比較簡單,只需構(gòu)建DateTime對象,通過DateTime對象進(jìn)行日期時(shí)間的操作即可。如取得當(dāng)前日期后90天的日期,可以編寫如下代碼:

DateTime dateTime = new DateTime();

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

Joda-Time是一個(gè)高效的日期處理工具,它作為JDK原生日期時(shí)間類的替代方案,被越來越多的人使用。在進(jìn)行日期時(shí)間處理時(shí),你可優(yōu)先考慮它。

字符串的處理

在應(yīng)用程序開發(fā)中,字符串可以說是最常見的數(shù)據(jù)類型,對它的處理也是最普遍的,比如需要判斷字符串的非空性、隨機(jī)字符串的生成等。接下來,我們就來看一下字符串處理工具類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();
}
}

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

上述代碼封裝了字符串的常用操作,如 isEmpty 和 isBlank均用于判斷是否為空,區(qū)別在于:isEmpty單純比較字符串長度,長度為0則返回true,否則返回false,如“”(此處表示空格)將返回false;而isBlank判斷是否真的有內(nèi)容,如“”(此處表示空格)返回true。同理,isNotEmpty和isNotBlank均判斷是否不為空,區(qū)別同上。randomString表示隨機(jī)生成6個(gè)數(shù)字的字符串,常用于短信驗(yàn)證碼的生成。uuid用于生成唯一標(biāo)識(shí),常用于數(shù)據(jù)庫主鍵、文件名的生成。

加密/解密封裝

對于一些敏感數(shù)據(jù),比如支付數(shù)據(jù)、訂單數(shù)據(jù)和密碼,在HTTP傳輸過程或數(shù)據(jù)存儲(chǔ)中,我們往往需要對其進(jìn)行加密,以保證數(shù)據(jù)的相對安全,這時(shí)就需要用到加密和解密算法。

目前常用的加密算法分為對稱加密算法、非對稱加密算法和信息摘要算法。

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

本書的實(shí)戰(zhàn)項(xiàng)目用到了AES、RSA、MD5和 SHA-1算法,故在common 工程下對它們分別進(jìn)行了封裝。

(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基金會(huì)提供的用于信息摘要和 Base64編碼解碼的包。在常見的對稱和非對稱加密算法中,都會(huì)對密文進(jìn)行 Base64編碼。而 commons-io是 Apache基金會(huì)提供的用于操作輸入輸出流的包。在對RSA 的加密/解密算法中,需要用到字節(jié)流的操作,因此需要添加此依賴包。

(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加密算法,加密和解密需要統(tǒng)一密鑰,密鑰是自定義的任意字符串,長度為16位、24位或32位。這里調(diào)用aesEncrypt方法進(jìn)行加密,其中第一個(gè)參數(shù)為明文,第二個(gè)參數(shù)為密鑰;調(diào)用aesDecrypt進(jìn)行解密,其中第一個(gè)參數(shù)為密文,第二個(gè)參數(shù)為密鑰。

我們注意到,代碼中定義了一個(gè)字符串常量 ALGORITHMSTR,其內(nèi)容為AES/ECB/PKCS5Padding,它定義了對稱加密算法的具體加解密實(shí)現(xiàn),其中 AES表示該算法為AES算法,ECB為加密模式,PKCS5Padding為具體的填充方式,常用的填充方式還有 PKCS7Padding和 NoPadding等。使用不同的方式對同一個(gè)字符串加密,結(jié)果都是不一樣的。因此,我們在設(shè)置加密算法時(shí)需要和客戶端統(tǒng)一,否則客戶端無法正確解密服務(wù)端返回的密文。

(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 +"]時(shí)遇到異常",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+"]時(shí)遇到異常",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+"]的數(shù)據(jù)時(shí)發(fā)生異常",e);
}
byte[] resultDatas = out.toByteArray();IOUtils.closeQuietly(out);
return resultDatas;
}
}

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

通常情況下,非對稱加密算法在客戶端使用公鑰加密,傳到服務(wù)端后,服務(wù)端利用私鑰進(jìn)行解密。例如,上述代碼提供了加解密方法,分別是publicEncrypt和 privateDecrypt方法,但是這兩個(gè)方法不能直接傳公私鑰字符串,而是通過getPublicKey和getPrivateKey方法返回RSAPublicKey和RSAPrivateKey對象后再傳給加解密方法。

公鑰和私鑰的生成方式有很多種,如OpenSSL 工具、第三方在線工具和編碼實(shí)現(xiàn)等。由于非對稱加密算法分別維護(hù)了公鑰和私鑰,其算法效率比對稱加密算法低,但安全級別比對稱加密算法高,讀者在選用加密算法時(shí)應(yīng)綜合考慮,采取適合項(xiàng)目的加密算法。

(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自帶信息摘要算法,但返回的是字節(jié)數(shù)組類型,在實(shí)際中需要將字節(jié)數(shù)組轉(zhuǎn)化成十六進(jìn)制字符串,因此上述代碼對信息摘要算法做了簡要的封裝。通過調(diào)用MessageDigestutils.encrypt方法即可返回加密后的字符串密文,其中第一個(gè)參數(shù)為明文,第二個(gè)參數(shù)為具體的信息摘要算法,可選值有MD5、SHA1和SHA256等。

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

消息隊(duì)列的封裝

消息隊(duì)列一般用于異步處理、高并發(fā)的消息處理以及延時(shí)處理等情形,它在當(dāng)前互聯(lián)網(wǎng)環(huán)境下也被廣泛應(yīng)用,因此同樣對它進(jìn)行了封裝,以便后續(xù)消息隊(duì)列使用。

在本例中,使用RabbitMQ來演示消息隊(duì)列。首先,在Windows系統(tǒng)下安裝RabbitMQ。由于RabbitMQ依賴Erlang,應(yīng)先安裝Erlang,下載地址為http:/www.rabbitmq.com/which-erlang.html,雙擊下載的文件即可完成安裝。然后安裝RabbitMQ,下載地址為 http:/www.rabbitmq.com/install-windows.html,雙擊下載的exe文件,按照操作步驟即可完成安裝。

安裝完成后,點(diǎn)擊Win+R鍵,在打開的運(yùn)行窗口中輸人命令services.msc并按下Enter鍵,可以打開服務(wù)列表,如圖6-1所示。


在這里插入圖片描述

可以看到,RabbitMQ已啟動(dòng)。在默認(rèn)情況下,RabbitMQ安裝后只開啟5672端口,我們只能通過命令的方式查看和管理RabbitMQ。為了方便,我們可以通過安裝插件來開啟RabbitMQ的 Web管理功能。打開cmd命令控制臺(tái),進(jìn)入 RabbitMQ安裝目錄的 sbin目錄,輸入

 rabbitmq-plugins enablerabbitmq_management

即可,如圖6-2所示。


在這里插入圖片描述

Web管理界面的默認(rèn)啟動(dòng)端口為15672。在瀏覽器中輸人localhost:15672,默認(rèn)的賬號(hào)和密碼都是guest,填寫后可以進(jìn)入Web管理主界面,如圖6-3所示。

在這里插入圖片描述

接下來,我們就封裝消息隊(duì)列。(1)添加 RabbitMQ依賴:

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

消息隊(duì)列都是通過Spring Cloud組件Spring Cloud Bus集成的,通過添加依賴spring-cloud-starter-bus-amqp,就可以很方便地使用RabbitMQ。

(2)創(chuàng)建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");
}
}

前面已經(jīng)講過,Spring Boot可以利用@SpringBootConfiguration注解對應(yīng)用程序進(jìn)行配置。我們集成RabbitMQ依賴后,也需要對其進(jìn)行基本配置。在上述代碼中,我們定義了一個(gè) Bean,該Bean的作用是自動(dòng)創(chuàng)建消息隊(duì)列名。如果不通過代碼創(chuàng)建隊(duì)列,那么每次都需要手動(dòng)去RabbitMQ的Web管理界面添加隊(duì)列,否則會(huì)報(bào)錯(cuò),如圖6-4所示。


在這里插入圖片描述

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

(3) RabbitMQ是異步請求,即客戶端發(fā)送消息,RabbitMQ服務(wù)端收到消息后會(huì)回發(fā)給客戶端。發(fā)送消息的稱為生產(chǎn)者,接收消息的稱為消費(fèi)者,因此還需要封裝消息的發(fā)送和接收。

創(chuàng)建一個(gè)名為MyBean的類,用于發(fā)送和接收消息隊(duì)列:

@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){
//消息隊(duì)列消費(fèi)者
system.out.println( content);
}
public void send(string content){
//消息隊(duì)列生產(chǎn)者
amqpTemplate.convertAndSend("someQueue", content);
}
}

其中,send為消息生產(chǎn)者,負(fù)責(zé)發(fā)送隊(duì)列名為someQueue 的消息,processNessage為消息消費(fèi)者,在其方法上定義了@RabbitHandler和@RabbitListener注解,表示該方法為消息消費(fèi)者,并且指定了消費(fèi)哪種隊(duì)列。

接口版本管理
一般在第一版產(chǎn)品發(fā)布并上線后,往往會(huì)不斷地進(jìn)行迭代和優(yōu)化,我們無法保證在后續(xù)升級過程中不會(huì)對原有接口進(jìn)行改動(dòng),而且有些改動(dòng)可能會(huì)影響線上業(yè)務(wù)。因此,想要對接口進(jìn)行改造卻不能影響線上業(yè)務(wù),就需要引人版本的概念。顧名思義,在請求接口時(shí)加上版本號(hào),后端根據(jù)版本號(hào)執(zhí)行不同版本時(shí)期的業(yè)務(wù)邏輯。那么,即便我們升級改造接口,也不會(huì)對原有的線上接口造成影響,從而保證系統(tǒng)正常運(yùn)行。

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

通過請求頭帶人版本號(hào),如 header( "version" , "1.0");URL地址后面帶人版本號(hào),如 api?version=1.0;RESTful風(fēng)格的版本號(hào)定義,如/ api/v1。

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

@RequestMapping("/v1/index")

這種方式的壞處就是擴(kuò)展性不好,而且一旦傳入其他版本號(hào),接口就會(huì)報(bào)404錯(cuò)誤。比如,客戶端接口地址的請求為/v2/index,而我們的項(xiàng)目只定義了v1,則無法請求index接口。

我們希望的效果是,如果傳入的版本號(hào)在項(xiàng)目中無法找到,則自動(dòng)找最高版本的接口,怎么做呢?請參照以下代碼實(shí)現(xiàn)。

(1)定義注解類:

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

在上面的代碼中,首先定義了一個(gè)注解,用于指定控制器的版本號(hào),比如@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 來定義請求路徑,因此如果我們要自動(dòng)通過v1這樣的地址來請求指定的控制器,就應(yīng)該繼承RequestMappingHandlerMapping類來重寫其方法。

Spring MVC在啟動(dòng)應(yīng)用后會(huì)自動(dòng)映射所有控制器類,并將標(biāo)有@RequestMapping注解的方法加載到內(nèi)存中。由于我們繼承了RequestMappingHandlerMapping 類,所以在映射時(shí)會(huì)執(zhí)行重寫的getCustomTypeCondition和getCustomMethodCondition方法,由方法體的內(nèi)容可以知道,我們創(chuàng)建了自定義的RequestCondition,并將版本信息傳給Requestcondition。

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

在public 工程中創(chuàng)建webConfig 類,并繼承 webNvcConfigurationSupport類,然后重寫requestMappingHandlerMapping方法,如:

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

在上述代碼中,我們重寫了requestMappingHandlerMapping方法并實(shí)例化了RequestMapping-HandlerMapping對象,返回的是前面自定義的CustomRequestMappingHandlerMapping類。

(4)在控制器類中加入注解@ApiVersion(1)實(shí)現(xiàn)版本控制,其中數(shù)字1表示版本號(hào)v1。在請求接口時(shí),輸入類似/api/v1/index的地址即可,代碼如下:

@RequestMapping("{version}")
@RestController
@ApiVersion(1)
public class TestV1controller{
@GetMapping("index ")
public String index(){
return "";
}
}

輸入?yún)?shù)的合法性校驗(yàn)
我們在定義接口時(shí),需要對輸入?yún)?shù)進(jìn)行校驗(yàn),防止非法參數(shù)的侵入。比如在實(shí)現(xiàn)登錄接口時(shí),手機(jī)號(hào)和密碼不能為空,手機(jī)號(hào)必須是11位數(shù)字等。雖然客戶端也會(huì)進(jìn)行校驗(yàn),但它只針對正常的用戶請求,如果用戶繞過客戶端,直接請求接口,就可能會(huì)傳入一些異常字符。因此,后端同時(shí)對輸人參數(shù)進(jìn)行合法性校驗(yàn)是必要的。

進(jìn)行合法性校驗(yàn)最簡單的方式是在每個(gè)接口內(nèi)做if-else判斷,但這種方式不夠優(yōu)雅。Spring 提供了校驗(yàn)類validator,我們可以對其做文章。

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

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

Validator的校驗(yàn)結(jié)果會(huì)存放到BindingResult類中,因此上述方法傳入了BindingResult類。在上面的代碼中,程序通過 hasFieldErrors判斷是否存在校驗(yàn)不通過的情況,如果存在,則通過getFieldErrors方法取出所有錯(cuò)誤信息并循環(huán)該錯(cuò)誤列表,一旦發(fā)現(xiàn)錯(cuò)誤,就用Assert 斷言方法拋出異常,6.4節(jié)將介紹異常的處理,統(tǒng)一返回校驗(yàn)失敗的提示信息。

我們使用斷言的好處在于它拋出的是運(yùn)行時(shí)異常,即我們不需要用顯式在方法后面加 throwsException,也能夠保證擴(kuò)展性較好,同時(shí)簡化了代碼量。

然后在控制器接口的參數(shù)中添加@valid注解,后面緊跟 BindingResult類,在方法體中調(diào)用validate(result)方法即可,如:

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

要實(shí)現(xiàn)接口校驗(yàn),需要在定義了@valid注解的類中,將每個(gè)屬性加入校驗(yàn)規(guī)則注解,如:

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

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

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

異常的統(tǒng)一處理
異常,在產(chǎn)品開發(fā)中是較為常見的,譬如程序運(yùn)行或數(shù)據(jù)庫連接等,這些過程中都可能會(huì)拋出異常,如果不進(jìn)行任何處理,客戶端就會(huì)接收到如圖6-5所示的內(nèi)容。


在這里插入圖片描述

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

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

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

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

在doError方法上加入@ExceptionHandler注解表示發(fā)生異常時(shí),則執(zhí)行該注解標(biāo)注的方法,該方法接收Exception類。我們知道,Exception類是所有異常類的父類,因此在發(fā)生異常時(shí),SpringMVC會(huì)找到標(biāo)有@ExceptionHandler注解的方法,調(diào)用它并傳人具體的異常對象。

我們要返回上述JSON格式,只需要返回SingleResult對象即可。注意,SingleResult是自定義的數(shù)據(jù)結(jié)果類,它繼承自Result類,表示返回單個(gè)數(shù)據(jù)對象;與之相對應(yīng)的是MultiResult類,用于返回多個(gè)結(jié)果集,所有接口都應(yīng)返回Result。關(guān)于該類,讀者可以參考本書配套源碼,在common工程的 com.lynn.blog.common.result包下。

更換JSON轉(zhuǎn)換器
Spring MVC默認(rèn)采用Jackson框架作為數(shù)據(jù)輸出的JSON格式的轉(zhuǎn)換引擎,但目前市面上涌現(xiàn)出了很多JSON解析框架,如 FastJson、Gson等,Jackson作為老牌框架已經(jīng)無法和這些框架媲美。

Spring 的強(qiáng)大之處也在于其擴(kuò)展性,它提供了大量的接口,方便開發(fā)者可以更換其默認(rèn)引擎,JSON轉(zhuǎn)換亦不例外。下面我們就來看看如何將Jackson更換為FastJson。

(1)添加FastJson依賴:

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

FastJson是阿里巴巴出品的用于生成和解析JSON 數(shù)據(jù)的類庫,其執(zhí)行效率也是同類框架中出類拔萃的,因此本書采用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);
}

當(dāng)程序啟動(dòng)時(shí),會(huì)執(zhí)行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) {}

這時(shí), Spring MVC將Jackson作為其默認(rèn)的JSON解析引擎,所以我們一旦重寫configureMessage-Converters方法,它將覆蓋Jackson,把我們自定義的JSON解析器作為JSON解析引擎。

得益于Spring的擴(kuò)展性設(shè)計(jì),我們可以將JSON解析引擎替換為FastJson,它提供了AbstractHttp-MessageConverter 抽象類和GenericHttpMessageConverter接口。通過實(shí)現(xiàn)它們的方法,就可以自定義JSON解析方式。

在上述代碼中,F(xiàn)astJsonHttpMessageConverter就是FastJson為了集成Spring而實(shí)現(xiàn)的一個(gè)轉(zhuǎn)換器。因此,我們在重寫configureMessageConverters方法時(shí),首先要實(shí)例化FastJsonHttpMessage-Converter對象,并進(jìn)行Fast]sonConfig基本配置。PrettyFormat表示返回的結(jié)果是否是格式化的;而MediaType 設(shè)置了編碼為UTF-8的規(guī)則。最后,將Fast3sonHttpMessageConverter對象添加到conterters列表中。

這樣我們在請求接口返回?cái)?shù)據(jù)時(shí),Spring MVC 就會(huì)使用FastJson轉(zhuǎn)換數(shù)據(jù)。

Redis的封裝
Redis 作為內(nèi)存數(shù)據(jù)庫,使用非常廣泛,我們可以將一些數(shù)據(jù)緩存,提高應(yīng)用的查詢性能,如保存登錄數(shù)據(jù)(驗(yàn)證碼和 token等)、實(shí)現(xiàn)分布式鎖等。

本文實(shí)戰(zhàn)項(xiàng)目也用到了Redis,且 Spring Boot操作Redis非常方便。SpringBoot集成了Redis并實(shí)現(xiàn)了大量方法,有些方法可以共用,我們可以根據(jù)項(xiàng)目需求封裝一套自己的Redis操作代碼。

(1)添加 Redis 的依賴:

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

spring-boot-starter-data包含了與數(shù)據(jù)相關(guān)的包,比如jpa、mongodb和elasticsearch等。因此,Redis也放到了spring-boot-starter-data 下。

(2)創(chuàng)建Redis類,該類包含了Redis 的常規(guī)操作,其代碼如下:

@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類。代碼中只實(shí)現(xiàn)了Redis的基本操作,包括鍵值保存、讀取和刪除操作。set方法重載了兩個(gè)方法,可以接收數(shù)據(jù)保存的有效期,TimeUnit.SECONDS 指定了該有效期單位為秒。讀者如果在項(xiàng)目開發(fā)過程中發(fā)現(xiàn)這些操作不能滿足要求時(shí),可以在這個(gè)類中添加方法滿足需求。

小結(jié)
本篇主要封裝了博客網(wǎng)站的公共模塊,即每個(gè)模塊都可能用到的方法和類庫,保證代碼的復(fù)用性。讀者也可以根據(jù)自己的理解和具體的項(xiàng)目要求去封裝一些方法,提供給各個(gè)模塊調(diào)用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容