SpringBoot + Vue + ElementUI 实现后台管理系统模板 — 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(1) 相关博文地址:

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y-h/p/12930895.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(二):引入 element-ui 定义基本页面显示:https://www.cnblogs.com/l-y-h/p/12935300.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(三):引入 js-cookie、axios、mock 封装请求处理以及返回结果:https://www.cnblogs.com/l-y-h/p/12955001.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(四):引入 vuex 进行状态管理、引入 vue-i18n 进行国际化管理:https://www.cnblogs.com/l-y-h/p/12963576.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(五):引入 vue-router 进行路由管理、模块化封装 axios 请求、使用 iframe 标签嵌套页面:https://www.cnblogs.com/l-y-h/p/12973364.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(六):使用 vue-router 进行动态加载菜单:https://www.cnblogs.com/l-y-h/p/13052196.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(一): 搭建基本环境、整合 Swagger、MyBatisPlus、JSR303 以及国际化操作:https://www.cnblogs.com/l-y-h/p/13083375.html

(2)代码地址:

https://github.com/lyh-man/admin-vue-template.git

一、SpringBoot 整合 Redis

1、简单回顾一下 Redis

(1)特点:
基于 key-value 存储。
支持多种数据结构:string(字符串)、list(列表)、set(集合)、zset(sorted set 有序集合)、hash(哈希)。
通过内存存储、操作数据,支持持久化将数据存储在硬盘中。
支持过期时间、事务。
参考:
https://www.cnblogs.com/l-y-h/p/12715723.html

(2)什么时候使用?
一般把经常查询、不经常修改 且 不是特别重要的数据放到 redis 中作为缓存(比如网站首页的一些图片、视频等)。

(3)Redis 与 Memcache 区别?
二者都是将数据缓存在内存中。
Memcache 只能将数据缓存在内存中,无法将数据写入硬盘,即机器断电、重启使内存清空后,数据会丢失。适用于缓存无需持久化的数据。
Redis 可以周期性的将数据存入磁盘(RDB)或者 将写操作以追加的方式写入文件(AOF)的方式对数据进行持久化。

2、SpringBoot 整合 Redis 缓存

(1)使用缓存目的:
对于一些经常被查询的数据,若每次都从数据库中获取,则会极大地消耗系统性能,将这些数据放在缓存中,并设置过期时间,查询时若缓存中有值,则从缓存中获取,否则从数据库中获取,并将新值存入缓存。这样可以减轻数据库的访问频率,从而提高系统性能。

(2)安装 Redis

【docker 安装 Redis 参考:】
    https://www.cnblogs.com/l-y-h/p/12622730.html#_label6

【linux(CnetOs7) 安装 Redis 参考:】
    https://www.cnblogs.com/l-y-h/p/12656614.html#_label0_1

(3)添加依赖信息

org.springframework.boot
    spring-boot-starter-data-redis

    org.apache.commons
    commons-pool2
    2.6.0

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(4)配置 Redis
SpringBoot 2.x 采用 lettuce 客户端,而非 Jedis。
如下:host 为 redis 存在的 ip 地址(需修改)。

【yml:】
spring:
  # Redis 配置
  redis:
    # Redis 服务器地址
    host: 121.26.184.41
    # 连接端口号
    port: 6379
    # 数据库索引(0 - 15)
    database: 0
    # 连接超时时间(毫秒)
    timeout: 10000
    # lettuce 参数
    lettuce:
      pool:
        # 最大连接数(使用负值表示没有限制) 默认为 8
        max-active: 10
        # 最大阻塞等待时间(使用负值表示没有限制) 默认为 -1 ms
        max-wait: -1
        # 最大空闲连接 默认为 8
        max-idle: 5
        # 最小空闲连接 默认为 0
        min-idle: 0

【properties:】
Redis 服务器地址
spring.redis.host=121.26.184.41
连接端口号
spring.redis.port=6379
数据库(0 - 15)
spring.redis.database= 0
超时时间(毫秒)
spring.redis.timeout=600000

lettuce 参数
最大连接数(使用负值表示没有限制) 默认为 8
spring.redis.lettuce.pool.max-active=20
最大阻塞等待时间(使用负值表示没有限制) 默认为 -1
spring.redis.lettuce.pool.max-wait=-1
最大空闲连接 默认为 8
spring.redis.lettuce.pool.max-idle=5
最小空闲连接 默认为 0
spring.redis.lettuce.pool.min-idle=0

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(5)新增一个 Redis 配置类 RedisConfig.java
用于自定义配置、使用 Redis。
当然可以直接注入并使用 RedisTemplate,源码中给出的是 RedisTemplate

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

自定义 RedisConfig.java 配置类如下:
@EnableCaching 表示开启缓存功能。

package com.lyh.admin_template.back.common.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 配置 redisTemplate
     */
    @Bean(name = "redisTemplate")
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        // 实例化一个 redisTemplate
        RedisTemplate redisTemplate = new RedisTemplate<>();
        // 配置连接工厂
        redisTemplate.setConnectionFactory(factory);
        // 设置 key 序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置 value 序列化方式
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 设置 hash key 序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // 设置 hash value 序列化方式
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }

    /**
     * 配置缓存
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 定义缓存 key 前缀,默认为 ::
        final String keyPrefix = ":";
        // 缓存对象配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 设置缓存默认超时时间:600 秒
                .entryTtl(Duration.ofSeconds(600))
                // 设置 key 序列化器
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置 value 序列化器
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                // 如果该值为 null,则不缓存
                .computePrefixWith(cacheName -> cacheName + keyPrefix)
                .disableCachingNullValues();
        // 根据 redis 缓存配置生成 redis 缓存管理器
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
        return redisCacheManager;
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(6)使用注解标注即可。
常用注解:

【@Cacheable】
    标注在方法上(一般标注在查询操作的方法上),将方法的返回结果进行缓存,下次请求时,
    若缓存存在,则直接读取缓存数据。
    若缓存不存在,则执行方法,并将返回结果再次保存在缓存中。
常用属性:
    value、cacheNames   表示缓存命名空间,二选一,不能为空。
    keyGenerator        表示 key 生成规则。
    key                 表示 key 名。

【@CachePut】
    标注在方法上(一般标注在新增操作的方法上),将方法的返回结果进行缓存。
注:
    其每次都会执行,不管缓存中是否有值。
常用属性:
    value、cacheNames   表示缓存命名空间,二选一,不能为空。
    keyGenerator        表示 key 生成规则。
    key                 表示 key 名。

【@CacheEvict】
    标注在方法上(一般用于更新操作、删除操作的方法上),清空指定的缓存。
常用属性:
    value、cacheNames   表示缓存命名空间,二选一,不能为空。
    keyGenerator        表示 key 生成规则。
    allEntries          默认为 false,若为 true 表示方法执行后清空缓存。
    beforeInvocation    默认为 false,若为 true 表示方法执行前清空缓存。
    key                 表示 key 名。

在 TestController.java 中编写测试代码用于测试上面三个注解的用法:

package com.lyh.admin_template.back.controller;

import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用于测试环境搭建各个功能是否成功
 */
@RestController
@RequestMapping("/test")
@Api(tags = "测试页面")
public class TestController {

    @Autowired
    private UserService userService;

    @Cacheable(value = "list", key = "'userList'")
    @ApiOperation(value = "测试 Redis 缓存注解 @Cacheable")
    @GetMapping("/testRedis/cacheable")
    public Result testRedisCacheable() {
        return Result.ok().data("item", userService.list());
    }

    @CachePut(value = "list", key = "'userList2'")
    @ApiOperation(value = "测试 Redis 缓存注解 @CachePut")
    @GetMapping("/testRedis/cachePut")
    public Result testRedisCachePut() {
        return Result.ok().data("item", userService.list());
    }

    @CacheEvict(value = "list", key = "'userList'")
    @ApiOperation(value = "测试 Redis 缓存注解 @CacheEvict")
    @GetMapping("/testRedis/cacheEvict")
    public Result testRedisCacheEvict() {
        return Result.ok().data("item", userService.list());
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

使用 swagger.ui 简单测试一下:
首先,执行 testRedisCacheable、testRedisCachePut 方法,两者获取数据都是一样的,且都会存于缓存中。
直接修改数据库数据,再次访问,testRedisCacheable 不会获取到数据库最新的值(返回缓存中的值),但是 testRedisCachePut 会取到最新数据库的值。
然后访问 testRedisCacheEvict 方法,可以查看到 testRedisCacheable 的缓存被删除,重新访问 testRedisCacheable 可以获取到最新数据库的数据。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

3、整一个 Redis 常用工具类 RedisUtil.java

(1)基本使用

RedisTemplate 中操作 Redis 如下:
    HashOperations           通过 redisTemplate.opsForHash() 获取,用于操作 hash
    ValueOperations          通过 redisTemplate.opsForValue() 获取,用于操作 string
    ListOperations           通过 redisTemplate.opsForList() 获取,用于操作 list
    SetOperations            通过 redisTemplate.opsForSet() 获取,用于操作 set
    ZSetOperations           通过 redisTemplate.opsForZSet() 获取,用于操作 zset

可以直接通过 方法获取,也可以通过配置文件并使用 @Bean 标注方式交给 Spring 管理。
如下,在配置文件中通过 @Bean 标注。

package com.lyh.admin_template.back.common.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 配置 redisTemplate
     */
    @Bean(name = "redisTemplate")
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        // 实例化一个 redisTemplate
        RedisTemplate redisTemplate = new RedisTemplate<>();
        // 配置连接工厂
        redisTemplate.setConnectionFactory(factory);
        // 设置 key 序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置 value 序列化方式
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 设置 hash key 序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // 设置 hash value 序列化方式
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }

    /**
     * 配置缓存
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 定义缓存 key 前缀,默认为 ::
        final String keyPrefix = ":";
        // 缓存对象配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 设置缓存默认超时时间:600 秒
                .entryTtl(Duration.ofSeconds(600))
                // 设置 key 序列化器
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置 value 序列化器
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                // 如果该值为 null,则不缓存
                .computePrefixWith(cacheName -> cacheName + keyPrefix)
                .disableCachingNullValues();
        // 根据 redis 缓存配置生成 redis 缓存管理器
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
        return redisCacheManager;
    }

    /**
     * 用于操作 hash
     */
    @Bean
    public HashOperations hashOperations(RedisTemplate redisTemplate) {
        return redisTemplate.opsForHash();
    }

    /**
     * 用于操作 list
     */
    @Bean
    public ListOperations listOperations(RedisTemplate redisTemplate) {
        return redisTemplate.opsForList();
    }

    /**
     * 用于操作 set
     */
    @Bean
    public SetOperations setOperations(RedisTemplate redisTemplate) {
        return redisTemplate.opsForSet();
    }

    /**
     * 用于操作 zset
     */
    @Bean
    public ZSetOperations zSetOperations(RedisTemplate redisTemplate) {
        return redisTemplate.opsForZSet();
    }

    /**
     * 用于操作 String
     */
    @Bean
    public ValueOperations valueOperations(RedisTemplate redisTemplate) {
        return redisTemplate.opsForValue();
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

注:
上面对于 value 的处理,采用了 GenericJackson2JsonRedisSerializer 进行序列化、反序列化。
其余序列化方式可以参考链接(https://www.freesion.com/article/966738549/)。

解决一个坑:
GenericJackson2JsonRedisSerializer 反序列化失败时,首先检查是否存在默认无参构造器(自定义构造器时,必须得带上无参构造器(=_=) )。

(2)编写工具类 RedisUtil.java
如下代码,基本覆盖了 Redis 各类型常用的方法,其余方法可以根据需要自行封装。

package com.lyh.admin_template.back.common.utils;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Redis 工具类
 */
@Component
public class RedisUtil {

    /**
     * 用于操作 redis
     */
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 用于操作 String
     */
    @Autowired
    private ValueOperations valueOperations;
    /**
     * 用于操作 hash
     */
    @Autowired
    private HashOperations hashOperations;
    /**
     * 用于操作 list
     */
    @Autowired
    private ListOperations listOperations;
    /**
     * 用于操作 set
     */
    @Autowired
    private SetOperations setOperations;
    /**
     * 用于操作 zset
     */
    @Autowired
    private ZSetOperations zSetOperations;

    /* ================================ common ================================== */
    /**
     * 设置默认过期时间(一天)
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
    /**
     * 不设置过期时间
     */
    public final static long NOT_EXPIRE = -1;

    /**
     * 设置 key 的过期时间(单位为 秒)
     */
    public void expire(String key, long expire) {
        redisTemplate.expire(key, expire, TimeUnit.SECONDS);
    }

    /**
     * 根据 key 获取过期时间
     */
    public Long getExpire(String key) {
        return StringUtils.isNotEmpty(key) ? redisTemplate.getExpire(key, TimeUnit.SECONDS) : null;
    }

    /**
     * 根据 pattern 返回匹配的所有 key
     */
    public Set keys(String pattern) {
        return StringUtils.isNotEmpty(pattern) ? redisTemplate.keys(pattern) : null;
    }

    /**
     * 删除 一个 或 多个 key
     */
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * 判断 key 是否存在
     */
    public Boolean hasKey(String key) {
        return StringUtils.isNotEmpty(key) ? redisTemplate.hasKey(key) : null;
    }
    /* ================================ common ================================== */

    /* ================================ string ================================== */
    /**
     * string set 操作,设置 key-value
     */
    public void set(String key, String value) {
        set(key, value, DEFAULT_EXPIRE);
    }

    /**
     * string set 操作,设置过期时间
     */
    public void set(String key, String value, long expire) {
        valueOperations.set(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * string get 操作,获取 value
     */
    public String get(String key) {
        return StringUtils.isNotEmpty(key) ? valueOperations.get(key) : null;
    }

    /**
     * string incr 操作,加法
     */
    public Long incr(String key, long data) {
        if (data < 0) {
            throw new RuntimeException("请输入正整数");
        }
        return valueOperations.increment(key, data);
    }

    /**
     * string decr 操作,减法
     */
    public Long decr(String key, long data) {
        if (data < 0) {
            throw new RuntimeException("请输入正整数");
        }
        return valueOperations.decrement(key, data);
    }
    /* ================================ string ================================== */

    /* ================================ hash ================================== */
    /**
     * hash set 操作,设置 一个 field - value
     */
    public void hset(String key, String field, Object value) {
        hset(key, field, value, DEFAULT_EXPIRE);
    }

    /**
     * hash set 操作,设置过期时间
     */
    public void hset(String key, String field, Object value, long expire) {
        hashOperations.put(key, field, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * hash set 操作,设置 多个 field - value
     */
    public void hmset(String key, Map map) {
        hmset(key, map, DEFAULT_EXPIRE);
    }

    /**
     * hash set 操作,设置过期时间
     * 此处过期时间是对 key 设置,而非 field(即 key 过期,所有的 field 均失效)。
     */
    public void hmset(String key, Map map, long expire) {
        hashOperations.putAll(key, map);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * hash keys 操作,获取所有的 field
     */
    public Set hkeys(String key) {
        return StringUtils.isNotEmpty(key) ? hashOperations.keys(key) : null;
    }

    /**
     * hash exists 操作,判断 keys 中是否存在某个 field
     */
    public Boolean hHasKey(String key, String field) {
        if (StringUtils.isNotEmpty(key) && StringUtils.isNotEmpty(field)) {
            return hashOperations.hasKey(key, field);
        }
        return null;
    }

    /**
     * hash del 操作,删除一个 或 多个 field
     */
    public void hdel(String key, String... field) {
        if (StringUtils.isNotEmpty(key) && field.length > 0) {
            hashOperations.delete(key, field);
        }
    }

    /**
     * hash get 操作,获取一个 field 对应的 value 值
     */
    public Object hget(String key, String field) {
        if (StringUtils.isNotEmpty(key) && StringUtils.isNotEmpty(field)) {
            return hashOperations.get(key, field);
        }
        return null;
    }

    /**
     * hash get 操作,获取所有的 value 值
     */
    public List hmget(String key) {
        return StringUtils.isNotEmpty(key) ? hashOperations.values(key) : null;
    }

    /**
     * hash get 操作,获取所有的 field-value
     */
    public Map hgetAll(String key) {
        return StringUtils.isNotEmpty(key) ? hashOperations.entries(key) : null;
    }
    /* ================================ hash ================================== */

    /* ================================ list ================================== */
    /**
     * list left push 操作,设置一个 value
     */
    public void lpush(String key, Object value) {
        lpush(key, value, DEFAULT_EXPIRE);
    }

    /**
     * list left push 操作,设置过期时间
     */
    public void lpush(String key, Object value, long expire) {
        listOperations.leftPush(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * list left push 操作,设置多个 value
     */
    public void lpush(String key, List value) {
        lpush(key, value, DEFAULT_EXPIRE);
    }

    /**
     * list left push 操作,设置过期时间
     */
    public void lpush(String key, List value, long expire) {
        listOperations.leftPushAll(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * list right push 操作,设置一个 value
     */
    public void rpush(String key, Object value) {
        rpush(key, value, DEFAULT_EXPIRE);
    }

    /**
     * list right push 操作,设置过期时间
     */
    public void rpush(String key, Object value, long expire) {
        listOperations.rightPush(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * list right push 操作,设置多个 value
     */
    public void rpush(String key, List value) {
        rpush(key, value, DEFAULT_EXPIRE);
    }

    /**
     * list right push 操作,设置过期时间
     */
    public void rpush(String key, List value, long expire) {
        listOperations.rightPushAll(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * list set 操作,根据 index 下标设置 value
     */
    public void lsetIndex(String key, long index, Object value) {
        lsetIndex(key, index, value, DEFAULT_EXPIRE);
    }

    /**
     * list set 操作,设置过期时间
     */
    public void lsetIndex(String key, long index, Object value, long expire) {
        listOperations.set(key, index, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * list range get 操作,获取指定范围的 value
     */
    public List lrange(String key, long start, long end) {
        return StringUtils.isNotEmpty(key) ? listOperations.range(key, start, end) : null;
    }

    /**
     * list index get 操作,根据指定下标返回 value
     */
    public Object lgetIndex(String key, long index) {
        return StringUtils.isNotEmpty(key) ? listOperations.index(key, index) : null;
    }

    /**
     * list len 操作, 获取 list 长度
     */
    public Long llen(String key) {
        return StringUtils.isNotEmpty(key) ? listOperations.size(key) : null;
    }

    /**
     * list remove 操作,移除 list 中 指定数量的 value
     */
    public Long lremove(String key, long count, Object value) {
        return StringUtils.isNotEmpty(key) ? listOperations.remove(key, count, value) : null;
    }

    /**
     * list trim 操作,截取指定范围的 value,并作为新的 list
     */
    public void ltrim(String key, long start, long end) {
        listOperations.trim(key, start, end);
    }

    /**
     * list left pop 操作,返回头部第一个元素
     */
    public Object lpop(String key) {
        return StringUtils.isNotEmpty(key) ? listOperations.leftPop(key) : null;
    }

    /**
     * list right pop 操作,返回尾部第一个元素
     */
    public Object rpop(String key) {
        return StringUtils.isNotEmpty(key) ? listOperations.rightPop(key) : null;
    }
    /* ================================ list ================================== */

    /* ================================ set ================================== */

    /**
     * set set 操作,设置一个 或 多个 value
     */
    public void sset(String key, Object... value) {
        sset(key, DEFAULT_EXPIRE, value);
    }

    /**
     * set set 操作,设置过期时间
     */
    public void sset(String key, long expire, Object... value) {
        setOperations.add(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * set smembers 操作,返回所有 value
     */
    public Set smembers(String key) {
        return StringUtils.isNotEmpty(key) ? setOperations.members(key) : null;
    }

    /**
     * set sisMember 操作,判断是否存在 某个 value 值
     */
    public Boolean sisMember(String key, Object value) {
        return StringUtils.isNotEmpty(key) ? setOperations.isMember(key, value) : null;
    }

    /**
     * set scard 操作,返回当前 value 个数。
     */
    public Long slen(String key) {
        return StringUtils.isNotEmpty(key) ? setOperations.size(key) : null;
    }

    /**
     * set srem 操作,根据 value 值移除元素
     */
    public Long sremove(String key, Object... value) {
        if (StringUtils.isNotEmpty(key) && value.length > 0) {
            return setOperations.remove(key, value);
        }
        return null;
    }

    /**
     * set spop 操作,随机移除一个元素
     */
    public Object spop(String key) {
        return StringUtils.isNotEmpty(key) ? setOperations.pop(key) : null;
    }

    /**
     * set spop 操作,随机移除 指定个数的元素
     */
    public List spop(String key, long count) {
        return StringUtils.isNotEmpty(key) ? setOperations.pop(key, count) : null;
    }

    /**
     * set srandmember 操作,随机返回一个 元素(非移除)
     */
    public Object srandomMember(String key) {
        return StringUtils.isNotEmpty(key) ? setOperations.randomMember(key) : null;
    }

    /**
     * set srandmember 操作,随机返回指定个数的 元素(非移除)
     */
    public List srandomMember(String key, long count) {
        return StringUtils.isNotEmpty(key) ? setOperations.randomMembers(key, count) : null;
    }
    /* ================================ set ================================== */

    /* ================================ zset ================================== */

    /**
     * zset set 操作,设置一个 value
     */
    public void zset(String key, Object value, double score) {
        zset(key, value, score, DEFAULT_EXPIRE);
    }

    /**
     * zset set 操作,设置 过期时间
     */
    public void zset(String key, Object value, double score, long expire) {
        zSetOperations.add(key, value, score);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * zset set 操作,设置多个 value
     */
    public void zset(String key, Set> value) {
        zset(key, value, DEFAULT_EXPIRE);
    }

    /**
     * zset set 操作,设置 过期时间
     */
    public void zset(String key, Set> value, long expire) {
        zSetOperations.add(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    /**
     * zset zrange 操作,返回指定下标范围的 value(升序)
     */
    public Set zrange(String key, long start, long end) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.range(key, start, end) : null;
    }

    /**
     * zset zrange 操作,返回指定下标范围的 value - score (升序)
     */
    public Set> zrangeWithScores(String key, long start, long end) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.rangeWithScores(key, start, end) : null;
    }

    /**
     * zset zrange 操作,返回指定 score 范围的 value(升序)
     */
    public Set zrangeByScore(String key, double min, double max) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.rangeByScore(key, min, max) : null;
    }

    /**
     * zset zrevrange 操作,返回指定下标范围的 value(降序)
     */
    public Set zreverseRange(String key, long start, long end) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.reverseRange(key, start, end) : null;
    }

    /**
     * zset zrevrange 操作,返回指定 score 范围的 value(降序)
     */
    public Set zreverseRangeByScore(String key, double min, double max) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.reverseRangeByScore(key, min, max) : null;
    }

    /**
     * zset zrevrange 操作,返回指定下标范围的 value - score (升序)
     */
    public Set> zreverseRangeWithScores (String key, long start, long end) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.reverseRangeWithScores(key, start, end) : null;
    }

    /**
     * zset zcard 操作,返回 value 个数
     */
    public Long zlen(String key) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.size(key) : null;
    }

    /**
     * zset zcount 操作,返回指定 score 范围内的 value 个数。
     */
    public Long zlenByScore(String key, long min, long max) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.count(key, min, max) : null;
    }

    /**
     * zset zscore 操作,返回指定 value 的 score 值
     */
    public Double zscore(String key, Object value) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.score(key, value) : null;
    }

    /**
     * zset zrem 操作,根据 value 移除一个 或 多个 value
     */
    public Long zremove(String key, Object... value) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.remove(key, value) : null;
    }

    /**
     * zset zremrangebyscore 操作,按照指定 score 范围移除 value
     */
    public Long zremoveByScore(String key, long min, long max) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.removeRangeByScore(key, min, max) : null;
    }

    /**
     * zset zremrangebyrank 操作,按照排序下标范围 移除 value。
     */
    public Long zremoveRange(String key, long start, long end) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.removeRange(key, start, end) : null;
    }

    /**
     * zset zrank 操作,返回升序序列中 value 的排名
     */
    public Long zrank(String key, Object value) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.rank(key, value) : null;
    }

    /**
     * zset zrevrank 操作,返回降序序列中 value 的排名
     */
    public Long zreverseRank(String key, Object value) {
        return StringUtils.isNotEmpty(key) ? zSetOperations.reverseRank(key, value) : null;
    }
    /* ================================ zset ================================== */
},>,>,>,>,>,>,>,>,>

(3)简单测试一下:
编写一个测试类,简单测试一下。

package com.lyh.admin_template.back;

import com.lyh.admin_template.back.common.utils.RedisUtil;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.util.CollectionUtils;

import java.util.*;

@SpringBootTest
public class TestRedis {

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 测试 Redis common 操作
     */
    @Test
    void testCommon() {
        redisUtil.set("hello", "10", 100);
        System.out.println("所有的 key: " + redisUtil.keys("*"));
        System.out.println("key 中是否存在 hello: " + redisUtil.hasKey("hello"));
        System.out.println("获取 hello 的过期时间: " + redisUtil.getExpire("hello"));
        redisUtil.del("hello");
        System.out.println("获取 hello 的过期时间: " + redisUtil.getExpire("hello"));
    }

    /**
     * 测试 Redis string 操作
     */
    @Test
    void testString() {
        System.out.println("所有的 key: " + redisUtil.keys("*"));
        redisUtil.set("hello", "10", 100);
        System.out.println(redisUtil.get("hello"));
        System.out.println(redisUtil.incr("hello",100));
        System.out.println(redisUtil.get("hello"));
    }

    /**
     * 测试 Redis hash 操作
     */
    @Test
    void testHash() {
        List lists = new ArrayList<>();
        lists.add(new Student("tom"));
        lists.add(new Student("jarry"));
        Teacher teacher = new Teacher();
        teacher.setName("teacher");
        teacher.setStudents(lists);

        Map map = new HashMap<>();
        map.put("tom", 12);
        map.put("jarry", 24);

        redisUtil.hset("hash", "hello", teacher);
        redisUtil.hmset("hash", map, 100);

        System.out.println(redisUtil.hkeys("hash"));
        System.out.println(redisUtil.hHasKey("hash", "hello"));
        System.out.println(redisUtil.hget("hash", "hello"));
        System.out.println(redisUtil.hgetAll("hash"));
        System.out.println(redisUtil.hmget("hash"));
        System.out.println(redisUtil.getExpire("hash"));

        redisUtil.del("hash", "hello", "hello2");
        System.out.println(redisUtil.hkeys("hash"));
    }

    /**
     * 测试 Redis list 操作
     */
    @Test
    void testList() {
        redisUtil.lpush("list", 1);
        redisUtil.rpush("list", 2);
        redisUtil.lpush("list", CollectionUtils.arrayToList(new int[]{3, 4, 5}));
        redisUtil.rpush("list", CollectionUtils.arrayToList(new int[]{6, 7, 8}));

        System.out.println(redisUtil.llen("list"));
        System.out.println(redisUtil.lrange("list", 0, -1));

        redisUtil.lsetIndex("list", 2, 10);
        System.out.println(redisUtil.lgetIndex("list", 2));

        System.out.println(redisUtil.lrange("list", 0, -1));
        System.out.println(redisUtil.lremove("list", 10, 2));
        System.out.println(redisUtil.lrange("list", 0, -1));

        redisUtil.ltrim("list", 0, 3);
        System.out.println(redisUtil.lrange("list", 0, -1));

        System.out.println(redisUtil.lpop("list"));
        System.out.println(redisUtil.rpop("list"));
        System.out.println(redisUtil.lrange("list", 0, -1));

        redisUtil.del("list");
    }

    /**
     * 测试 Redis set 操作
     */
    @Test
    void testSet() {
        redisUtil.sset("set", "1");
        redisUtil.sset("set", "1", "2", "3");
        System.out.println(redisUtil.smembers("set"));
        System.out.println(redisUtil.sisMember("set", "2"));
        System.out.println(redisUtil.slen("set"));

        System.out.println(redisUtil.srandomMember("set", 10));
        System.out.println(redisUtil.smembers("set"));

        System.out.println(redisUtil.spop("set"));
        System.out.println(redisUtil.smembers("set"));

        redisUtil.del("set");
    }

    /**
     * 测试 Redis zset 操作
     */
    @Test
    void testZset() {
        redisUtil.zset("zset", "20", 1);
        redisUtil.zset("zset", "10", 1);
        redisUtil.zset("zset", "30", 2);
        System.out.println(redisUtil.zrange("zset", 0, -1));
        System.out.println(redisUtil.zrangeByScore("zset", 0, 1));

        System.out.println(redisUtil.zreverseRange("zset", 0, -1));
        System.out.println(redisUtil.zreverseRangeByScore("zset", 0, 1));

        System.out.println(redisUtil.zlen("zset"));
        System.out.println(redisUtil.zlenByScore("zset", 0, 1));

        System.out.println(redisUtil.zscore("zset", "20"));

//        System.out.println(redisUtil.zremove("zset", "20", "30", "40"));
//        System.out.println(redisUtil.zremoveRange("zset", 1, 4));
        System.out.println(redisUtil.zremoveByScore("zset", 2, 4));
        System.out.println(redisUtil.zrange("zset", 0, -1));

        redisUtil.zset("zset", new Student("tom"), 10);
        redisUtil.zset("zset", new Student("jarry"), 20);
        System.out.println(redisUtil.zrange("zset", 0, -1));
        System.out.println(redisUtil.zrank("zset", new Student("tom")));
        System.out.println(redisUtil.zreverseRange("zset", 0, -1));
        System.out.println(redisUtil.zreverseRank("zset", new Student("tom")));

        Set> set = new HashSet<>();
        set.add(new DefaultTypedTuple("20", 1.0));
        set.add(new DefaultTypedTuple("10", 1.0));
        set.add(new DefaultTypedTuple("30", 2.0));
        redisUtil.zset("zset", set);
        redisUtil.zrangeWithScores("zset", 0, -1).forEach((item) -> {
            System.out.println(item.getValue() + "===" + item.getScore());
        });

        redisUtil.zreverseRangeWithScores("zset", 0, -1).forEach((item) -> {
            System.out.println(item.getValue() + "===" + item.getScore());
        });

        redisUtil.del("zset");
    }
}
@Data
class Teacher {
    private List students;
    private String name;
}

@Data
class Student {
    private String name;
    Student() {

    }
    Student(String name) {
        this.name = name;
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

简单测试一下 hash 操作。
主要看看序列化、反序列化问题。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

二、SpringBoot 发送邮件

1、简单了解一下基本概念

(1)常见协议:
想要在计算机之间发送邮件,需要各种协议来支撑,比如:SMTP、IMAP、POP3 等。

【来源于网络:】
【SMTP:】
    为 Simple Mail Transfer Protocol 简写,即简单邮件传输协议。
    是一组从源地址到目的地地址传输邮件的规范。
    需要使用 账号名、密码 才能登陆 SMTP 服务器,从而减少用户收到垃圾邮件的机会。
    主要用于邮件客户端 与 邮件服务器之间邮件的发送、接收。

【IMAP:】
    为 Internet Mail Access Protocol 简写,即互联网邮件访问协议。
    主要用于邮件客户端 从 邮件服务器上获取邮件信息、下载邮件。

【POP 3:】
    为 Post Office Protocol 3 简写,即邮局协议 3。
    是一个离线协议标准。即 邮件发送到服务器后,当邮件客户端访问邮件服务器时,会下载所有的未读的邮件。
    主要也用于 下载邮件。

【IMAP 与 POP 3 的区别:】
    IMAP 在邮件客户端的操作 会 影响到邮件服务器。
    POP 3 在邮件客户端的操作 不会 影响到邮件服务器。

(2)开启邮件服务(此处使用 网易邮箱)。
Step1:
登录 网易 163 邮箱(注册、登录一个邮箱作为服务器)。

【网址:】
    https://email.163.com/

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

Step2:
选择设置,并点击 POP3/SMTP/IMAP。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

Step3:
点击开启 IMAP/SMTP服务、POP3/SMTP服务。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

授权码是用于登录第三方邮件客户端的专用密码。

【IMAP/SMTP服务 授权码:】
    KFYRVDQAUXLNBSUO

(3)简单了解一下 JavaMailSender 和 JavaMailSenderImpl
JavaMailSender 是 Spring 官方提供的集成邮件服务的一个接口。
JavaMailSenderImpl 是 JavaMailSender 的一个实现类。
通过 @Autowired 注入并使用 JavaMailSenderImpl 的 send 方法即可发送邮件。
查看 MailSenderPropertiesConfiguration.java 配置类截图如下:

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

对于简单的邮件,可以使用 SimpleMailMessage 封装相关信息。
对于复杂的邮件(比如存在附件),可以使用 MimeMessage 封装相关信息(通过 MimeMessageHelper 进一步操作相关信息)。

2、SpringBoot 发送邮件

(1)添加依赖
由于可能涉及到 json 与 string 之间的相互转换,可以引入 Gson 依赖。

com.google.code.gson
    gson
    2.8.6

    org.springframework.boot
    spring-boot-starter-mail

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(2)配置邮箱信息

spring:
  # mail 配置
  mail:
    # SMTP 服务器地址
    host: smtp.163.com
    # 邮件服务器账号
    username: m_17730125031@163.com
    # 授权码
    password: KFYRVDQAUXLNBSUO
    # 配置端口号(默认使用 25,若项目发布到云服务器,需要开放相应端口 465,需要配置相关 ssl 协议)
    port: 465
    # 编码字符集采用 UTF-8
    default-encoding: UTF-8
    # 配置 ssl 协议(端口为 25 时,可以不用配置)
    properties:
      mail:
        smtp:
          ssl:
            enable: true
          socketFactory:
            port: 465
            class: javax.net.ssl.SSLSocketFactory
  # 文件上传大小配置(由于附件的存在,可以视项目情况修改)
  servlet:
    multipart:
      # 限制单个文件大小
      max-file-size: 10MB
      # 限制单次请求总文件大小
      max-request-size: 50MB

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(3)编写一个 vo 对象(MailVo.java),用于保存邮件相关信息。

package com.lyh.admin_template.back.vo;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

@Data
public class MailVo {

    /**
     * 邮件发送人
     */
    private String from;
    /**
     * 邮件接收人
     */
    private String[] to;
    /**
     * 邮件抄送
     */
    private String[] cc;
    /**
     * 邮件密送
     */
    private String[] bcc;
    /**
     * 邮件主题
     */
    private String subject;
    /**
     * 邮件内容
     */
    private String text;
    /**
     * 邮件附件
     */
    private MultipartFile[] files;
    /**
     * 邮件附件在服务器存储的地址
     */
    private String[] fileUrls;
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(4)编写一个邮件发送工具类(MailUtil.java),每次发邮件调用即可。
当然,对邮件数据还是需要做必要的检查。比如使用 正则表达式检查邮箱格式。
正则表达式在线生成器:https://www.sojson.com/regex/generate

package com.lyh.admin_template.back.common.utils;

import com.lyh.admin_template.back.vo.MailVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.mail.MessagingException;
import java.util.Date;

/**
 * 邮件发送 工具类
 */
@Component
public class MailUtil {
    /**
     * 用于操作邮件
     */
    @Autowired
    private JavaMailSender javaMailSender;

    /**
     * 发送邮件
     */
    public void sendMail(MailVo mailVo) {
        // 检查邮件地址是否正确
        if (checkMail(mailVo)) {
            // 发送邮件
            sendMailReal(mailVo);
        } else {
            throw new RuntimeException("邮件地址异常");
        }
    }

    /**
     * 邮件必须项检查
     */
    private boolean checkMail(MailVo mailVo) {
        return checkAddress(mailVo.getFrom()) && checkAddress(mailVo.getTo())
                && checkAddress(mailVo.getCc()) && checkAddress(mailVo.getBcc());
    }

    /**
     * 检查邮箱地址是否正确
     */
    private boolean checkAddress(String... address) {
        if (address.length == 0 || address == null) {
            return true;
        }
        String regex = "\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}";
        for (String item : address) {
            if (!item.matches(regex)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 发送邮件真实操作
     */
    private void sendMailReal(MailVo mailVo) {
        // 构造一个邮件助手
        MimeMessageHelper mimeMessageHelper = null;
        try {
            // 传入 MimeMessage,并对其进行一系列操作,true 表示支持复杂邮件(支持附件等)
            mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(), true);
            // 设置邮件发送人
            mimeMessageHelper.setFrom(mailVo.getFrom());
            // 设置邮件接收人
            mimeMessageHelper.setTo(mailVo.getTo());
            // 设置邮件抄送人
            mimeMessageHelper.setCc(mailVo.getCc());
            // 设置邮件密送人
            mimeMessageHelper.setBcc(mailVo.getBcc());
            // 设置邮件主题
            mimeMessageHelper.setSubject(mailVo.getSubject());
            // 设置邮件内容
            mimeMessageHelper.setText(mailVo.getText());
            // 设置邮件日期
            mimeMessageHelper.setSentDate(new Date());
            // 设置附件
            for (MultipartFile file : mailVo.getFiles()) {
                mimeMessageHelper.addAttachment(file.getOriginalFilename(), file);
            }

            // 发送邮件
            javaMailSender.send(mimeMessageHelper.getMimeMessage());
        } catch (MessagingException e) {
            throw new RuntimeException("邮件发送失败");
        }
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(5)编写测试类 TestMailController.java,对邮件发送功能进行测试。
碰到的问题:
由于存在附件,后台接口中可以使用 MultipartFile 类型去接收文件。但是若想同时接收其他参数 ,不能直接使用 @RequestBody 接收参数转为相应实体类。其属于两种流信息,@RequestBody 属于 application/json,MultipartFile 属于 multipart/form-data,两者同时使用,解析时就会出现问题(有能完美解决这个问题的,还望不吝赐教 (+_+))。

我的解决方案:
第一种形式,参数使用 HttpServletRequest 接收,然后自己手动去解析。
第二种方式,使用 @RequestParam 注解去接收,若参数过多,可以使用 String 去保存 前端发送的 JSON 数据,然后在后台 手动将 String 数据转为相应的实体类去处理(可使用 Gson)。
第三种方式,将附件单独处理(做一个文件上传接口,返回附件在服务器的地址),发送邮件时,根据服务器地址获取附件信息,然后发送。这样做就可以使用 @RequestBody 注解了,因为传递的是附件 URL 地址(String)而非文件。

【参数接收异常:】
public Result testFiles(@RequestBody MailVo test, MultipartFile[] multipartFiles)

【参数接收正常:(方式一)】
@PostMapping("/send")
public Result send(HttpServletRequest request) {
    MultipartHttpServletRequest params = (MultipartHttpServletRequest) request;
    List files = params.getFiles("files");
    MailVo mailVoReal = GsonUtil.fromJson(params.getParameter("mailVo"), MailVo.class);
    mailVoReal.setFiles(files.toArray(new MultipartFile[]{}));
    mailUtil.sendMail(mailVoReal);
    return Result.ok();
}

【参数接收正常:(方式二)】
@PostMapping("/send2")
public Result send2(@RequestParam String mailVo, MultipartFile[] files) {
    MailVo mailVoReal = GsonUtil.fromJson(mailVo, MailVo.class);
    mailVoReal.setFiles(files);
    mailUtil.sendMail(mailVoReal);
    return Result.ok();
}

涉及到 Gson 对数据进行转换,为了方便使用,将其抽成一个工具类 GsonUtil.java。

【依赖:】

    com.google.code.gson
    gson
    2.8.6

【工具类:】
package com.lyh.admin_template.back.common.utils;

import com.google.gson.Gson;

/**
 * Gson 工具类,用于 Object 与 Json 字符串形式互转
 */
public class GsonUtil {
    private final static Gson GSON = new Gson();

    /**
     * Object 转 String 数据(JSON 字符串)
     */
    public static String toJson(Object object) {
        if (object instanceof Integer || object instanceof Short || object instanceof Byte
            || object instanceof Long || object instanceof Character || object instanceof Boolean
            || object instanceof Double || object instanceof String || object instanceof Float) {
            return String.valueOf(object);
        }
        return GSON.toJson(object);
    }

    /**
     * string(Json 字符串) 转 Object。
     */
    public static  T fromJson(String json, Class tClass) {
        return GSON.fromJson(json, tClass);
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

完整 TestMailController.java 如下:

package com.lyh.admin_template.back.controller.test;

import com.lyh.admin_template.back.common.utils.GsonUtil;
import com.lyh.admin_template.back.common.utils.MailUtil;
import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.vo.MailVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * 测试邮件发送功能
 */
@RestController
@RequestMapping("/test/mail")
public class TestMailController {

    @Autowired
    private MailUtil mailUtil;

    /**
     * 获取邮件方式一,使用 HttpServletRequest 获取,并手动解析数据
     */
    @PostMapping("/send")
    public Result send(HttpServletRequest request) {
        MultipartHttpServletRequest params = (MultipartHttpServletRequest) request;
        List files = params.getFiles("files");
        MailVo mailVoReal = GsonUtil.fromJson(params.getParameter("mailVo"), MailVo.class);
        mailVoReal.setFiles(files.toArray(new MultipartFile[]{}));
        mailUtil.sendMail(mailVoReal);
        return Result.ok();
    }

    /**
     * 获取邮件方式二,使用 @RequestParam 获取 json 字符串(使用 Gson 手动转换为 对象)
     */
    @PostMapping("/send2")
    public Result send2(@RequestParam String mailVo, MultipartFile[] files) {
        MailVo mailVoReal = GsonUtil.fromJson(mailVo, MailVo.class);
        mailVoReal.setFiles(files);
        mailUtil.sendMail(mailVoReal);
        return Result.ok();
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

(6)测试
由于 Swagger 传递多个文件时,得到的值为 null(不知道怎么解决)。
所以此处使用 postman 进行测试。
测试需要的 json 数据如下:

【用于测试方式一:】
{
    "bcc": [],
    "cc": [],
    "from": "m_17730125031@163.com",
    "subject": "测试 1",
    "text": "测试邮件发送 11111111111111",
    "to": [
        "13865561381@163.com"
    ]
}

【用于测试方式二:】
{
    "bcc": [
        "13865561381@163.com"
    ],
    "cc": [
        "13865561381@163.com",
        m_17730125031@163.com
    ],
    "from": "m_17730125031@163.com",
    "subject": "测试 2",
    "text": "测试邮件发送 22222222222222222",
    "to": [
        "13865561381@163.com",
        m_17730125031@163.com
    ]
}

邮件发送的过程有点慢(暂时不知道怎么解决 (=_=)),但是确实是可以发送的。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

Original: https://www.cnblogs.com/l-y-h/p/13163653.html
Author: 累成一条狗
Title: SpringBoot + Vue + ElementUI 实现后台管理系统模板 — 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/598444/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 文件的压缩与打包

    文件的压缩与打包 常用文件拓展名 *.tar.gz tar程序打包的文件,并且经过gzip的压缩 *.tar.bz2 tar程序打包的文件,并且经过bzip2的压缩 tar 命令,…

    Linux 2023年6月11日
    083
  • docker安装rabbitmq

    安装镜像 docker pull rabbitmq:3.9-management-alpine 实例化容器 docker run -id –hostname rabbitmq -…

    Linux 2023年6月7日
    096
  • Java Web登录界面

    非常激动的开通了我的第一个博客,在这里希望大家能多多指点,相互学习。 一个简单的登录界面 首先我们先把这个登录分为三块: 一、数据库 数据库我用的是MYSQL; 二、前端 三、后台…

    Linux 2023年6月13日
    0111
  • redis中setbit的用法

    原文地址:http://www.zhihu.com/question/27672245 在redis中,存储的字符串都是以二级制的进行存在的。举例:设置一个 key-value ,…

    Linux 2023年5月28日
    084
  • fabric2.2.网络部署

    在执行测试项目时,多次使用并修改此文件,部分地方没有及时更新.如果问题请联系 487008159 更正. 项目: fabric-samples 工作目录 : ~/go/src/gi…

    Linux 2023年6月13日
    0107
  • vue组件传值和路由——day04

    <script><br> var vm = new Vue({<br> el: ‘#app’,<br> data: {<br&…

    Linux 2023年6月7日
    091
  • WEB自动化-05-Cypress-元素交互

    5 元素交互 元素识别和操作是UI自动化测试的基础,下面一起来学习一下在Cypress中的元素交互操作吧。 5.1 元素定位器选择 每一个测试用例都包含对元素的定位识别和操作等。因…

    Linux 2023年6月7日
    098
  • Redis实现延迟队列方法介绍

    延迟队列,顾名思义它是一种带有延迟功能的消息队列。那么,是在什么场景下我才需要这样的队列呢? 背景 我们先看看以下业务场景: 当订单一直处于未支付状态时,如何及时的关闭订单 如何定…

    Linux 2023年5月28日
    069
  • sed用法

    基础sed命令 sed OPTIONS… [SCRIPT] [INPUTFILE…] 常用的选项: -n,–quiet: 不输出模式空间中的内容 -i: 直…

    Linux 2023年6月6日
    0125
  • 实验1:SDN拓扑实践

    实验1:SDN拓扑实践 基础要求 a) mininet运行结果图 b)2的执行结果截图 2.a)3台交换机,每个交换机连接1台主机,3台交换机连接成一条线。 2.b)3台主机,每个…

    Linux 2023年6月7日
    099
  • Web前端基础精品入门(HTML+CSS+JavaScript+JS)[爱前端]听课笔记2:导航条的制作——css学习仿作马蜂窝

    马蜂窝的首页是非常正能量,青春的网页,首页非常大气 logo在上一篇我们已经制作好,现在我们开始制作导航条 这个导航条字数不等,宽窄不一致,就是所有的li不一样宽,字多就宽,字少就…

    Linux 2023年6月14日
    072
  • 思科CISCO ASA 5521 防火墙 Ipsec 配置详解

    版本信息: Cisco Adaptive Security Appliance Software Version 9.9(2) Firepower Extensible Opera…

    Linux 2023年6月6日
    093
  • Visual studio prebuild/postbuild 设置条件不生效

    这两天有一个需求就是,在编译完成后,对生成的dll进行混淆加密处理,并且自动上传到nuget。混淆加密和自动上传已经写成了cmd命令,但是又不想在Debug模式下调用这个命令,毕竟…

    Linux 2023年6月13日
    086
  • 【原创】Linux虚拟化KVM-Qemu分析(六)之中断虚拟化

    背景 Read the fucking source code! –By 鲁迅 A picture is worth a thousand words. –…

    Linux 2023年6月8日
    0107
  • Unit 1 Computer hardware【石家庄铁道大学-专业英语课 】

    Unit 1 Computer hardware 1、Introduction of computer A computer is a machine that can be in…

    Linux 2023年6月14日
    094
  • 【转】我是一个CPU:这个世界慢!死!了!

    简介 我经常听到人们说磁盘慢,网络很慢,这是从人类感知的角度来表达的。比如,把一个文件拷贝到硬盘上需要几分钟到几十分钟,足够我吃一顿饭;而从网上下载一部电影,有时需要几个小时,我可…

    Linux 2023年5月27日
    090
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球