公共模塊封裝
在一個(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í)間戳的形式存儲到數(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ù)據(jù)庫主鍵、文件名的生成发笔。
加密/解密封裝
對于一些敏感數(shù)據(jù)盟萨,比如支付數(shù)據(jù)、訂單數(shù)據(jù)和密碼了讨,在HTTP傳輸過程或數(shù)據(jù)存儲中捻激,我們往往需要對其進(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命令控制臺,進(jìn)入 RabbitMQ安裝目錄的 sbin目錄色乾,輸入
rabbitmq-plugins enablerabbitmq_management
即可誊册,如圖6-2所示。
Web管理界面的默認(rèn)啟動(dòng)端口為15672暖璧。在瀏覽器中輸人localhost:15672案怯,默認(rèn)的賬號和密碼都是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í)加上版本號盆驹,后端根據(jù)版本號執(zhí)行不同版本時(shí)期的業(yè)務(wù)邏輯。那么滩愁,即便我們升級改造接口躯喇,也不會(huì)對原有的線上接口造成影響,從而保證系統(tǒng)正常運(yùn)行硝枉。
版本定義的思路有很多廉丽,比如:
通過請求頭帶人版本號,如 header( "version" , "1.0");URL地址后面帶人版本號妻味,如 api?version=1.0;RESTful風(fēng)格的版本號定義正压,如/ api/v1。
本節(jié)將介紹第三種版本號的定義思路责球,最簡單的方式就是直接在RequestMapping 中寫入固定的版本號焦履,如:
@RequestMapping("/v1/index")
這種方式的壞處就是擴(kuò)展性不好,而且一旦傳入其他版本號雏逾,接口就會(huì)報(bào)404錯(cuò)誤嘉裤。比如,客戶端接口地址的請求為/v2/index栖博,而我們的項(xiàng)目只定義了v1屑宠,則無法請求index接口。
我們希望的效果是,如果傳入的版本號在項(xiàng)目中無法找到,則自動(dòng)找最高版本的接口,怎么做呢?請參照以下代碼實(shí)現(xiàn)仇让。
(1)定義注解類:
@Target(ElementType. TYPE)
@Retention(RetentionPolicy.RUNTIME)@Mapping
@Documented
public @interface ApiVersion {
int value();
}
在上面的代碼中典奉,首先定義了一個(gè)注解躺翻,用于指定控制器的版本號,比如@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表示版本號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ī)號和密碼不能為空,手機(jī)號必須是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)用凡资。