美文网首页JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统
2021年了居然还不会用springcloud,源码带你一步步搭

2021年了居然还不会用springcloud,源码带你一步步搭

作者: java架构师联盟 | 来源:发表于2021-04-10 17:44 被阅读0次

    公共模块封装

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

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

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

    相关文章

      网友评论

        本文标题:2021年了居然还不会用springcloud,源码带你一步步搭

        本文链接:https://www.haomeiwen.com/subject/uioekltx.html