dtcms 模板下载 中国移动 acm dictionary chartjs webkit angular视频 在线考试系统代码 jquery绑定事件的方法 不用u盘装双系统 css鼠标悬浮样式 oracle查看所有数据库 java高级特性 python报错 python的数据类型 python参数 java正则表达式 javastring类型 java手册 java数组删除 java的方法 java怎么配置 java终止线程 java的输入 键盘模拟器 js删除节点 winhex使用教程 易语言多线程 comsol软件下载 msn格式 dnf武极刷图加点 视频加文字用什么软件 苹果放大镜 正则表达式数字 古特里克的杀生刀 ipad怎么清理内存垃圾 dnf风神加点 产品修图 devenv 繁简体转换
当前位置: 首页 > 学习教程  > 编程语言

SpringCloud实战:从公共模块搭建一套完整微服务架构

2021/1/28 22:36:51 文章标签:

公共模块封装 从本篇开始,我们将学习框架的搭建。由于代码量巨大,本书不可能全部贴出,所以只展示一些核心代码。全部源码可以从本书配套源码中查看。 经过前几章的学习,读者应该对本项目有了大致的了解,也已搭建好了…

公共模块封装

从本篇开始,我们将学习框架的搭建。由于代码量巨大,本书不可能全部贴出,所以只展示一些核心代码。全部源码可以从本书配套源码中查看。

经过前几章的学习,读者应该对本项目有了大致的了解,也已搭建好了各个基本模块。为了保证应用程序的复用性和可扩展性,我们需要将一些常用的基本方法封装起来,以便各个模块调用。

在一个完整的微服务架构体系中,字符串和日期的处理往往是最多的。在一些安全应用场景下,还会用到加密算法。为了提升应用的扩展性,我们还应对接口进行版本控制。因此,我们需要对这些场景进行一定的封装,方便开发人员使用。本章中,我们优先从公共模块入手搭建一套完整的微服务架构。

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所示。

SpringCloud实战:从公共模块搭建一套完整微服务架构

 

可以看到,RabbitMQ已启动。在默认情况下,RabbitMQ安装后只开启5672端口,我们只能通过命令的方式查看和管理RabbitMQ。为了方便,我们可以通过安装插件来开启RabbitMQ的 Web管理功能。打开cmd命令控制台,进入 RabbitMQ安装目录的 sbin目录,输入 rabbitmq-plugins enablerabbitmq_management即可,如图6-2所示。

SpringCloud实战:从公共模块搭建一套完整微服务架构

 

Web管理界面的默认启动端口为15672。在浏览器中输人localhost:15672,默认的账号和密码都是guest,填写后可以进入Web管理主界面,如图6-3所示。

SpringCloud实战:从公共模块搭建一套完整微服务架构

 

接下来,我们就封装消息队列。(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所示。

SpringCloud实战:从公共模块搭建一套完整微服务架构

 

但是每次都通过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所示的内容。

SpringCloud实战:从公共模块搭建一套完整微服务架构

 

可以看出,直接在界面上返回了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 指定了该有效期单位为秒。读者如果在项目开发过程中发现这些操作不能满足要求时,可以在这个类中添加方法满足需求。

小结

本篇主要封装了博客网站的公共模块,即每个模块都可能用到的方法和类库,保证代码的复用性。读者也可以根据自己的理解和具体的项目要求去封装一些方法,提供给各个模块调用。

本文给大家讲解的内容是springcloud实战:从公共模块入手搭建一套完整的微服务架构

  1. 下篇文章给大家讲解的是springcloud实战:注册中心SpringCloudNetflixEureka;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

本文链接: http://www.dtmao.cc/news_show_650035.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?