一、starter的概念
starter 的理念:
starter 会把所有用到的依赖都给包含进来,避免了开发者自己去引入依赖所带来的麻烦。 需要注意的是
不同的 starter 是为了解决不同的依赖,所以它们内部的实现可能会有很大的差异,
例如 jpa 的 starter 和 Redis 的 starter 可能实现就不一样,这是因为 starter 的本质在 synthesize , 这是一层在逻辑层面的抽象,也许这种理念有点类似于Docker ,因为它们都是在做一个 “ 包装 ” 的操作。
starter 的实现:
虽然不同的 starter 实现起来各有差异, 但是他们基本上都会使用到两个相同的内容:
ConfigurationProperties 和 AutoConfiguration 。 因为 SpringBoot坚信 “ 约定大于配置 ” 这一理念,所以我们使用 ConfigurationProperties 来保存我们的配置, 并且这 些配置都可以有一个默认值,即在我们没有主动覆写原始配置的情况下,默认值就会生效,这在很多情
况下是非常有用的。
除此之外, starter 的 ConfigurationProperties 还使得所有的配置属性被聚集到一个文件中 (一般在 resources目录下的 application.properties ),这样我们就告别了 Spring 项目中 XML 地狱。
原理图
二、邮件的使用
1.pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
2.yml文件
可以决定是否加载JavaMailSender
spring: application: name: springBoot6 redis: database: 0 #数据库索引 host: 127.0.0.1 #主机位置 port: 6379 #端口 password: #密码 jedis: pool: max-active: 8 #最大连接数 max-wait: -1 #最大阻塞等待时间(负数表示没限制) max-idle: 8 #最大空闲 min-idle: 0 #最小空闲 timeout: 10000 #连接超时时间 email: host: smtp.qq.com username: 2103349206@qq.com password: vnmjnmokyudgefgh enable: true
password不是QQ密码,是邮箱的授权码
测试类中进行测试
package com.sjy.demo2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@SpringBootTest
class Demo2ApplicationTests {
@Autowired
JavaMailSender mailSender;
@Test
void contextLoads() {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("2490527049@qq.com");
message.setTo("sunjiayi0513@qq.com");
message.setSubject("主题:简单邮件");
message.setText("测试邮件内容");
mailSender.send(message);
}
}
三、自定义启动器
为了更加方便,我们将验证码功能变成启动器,用时直接导入就可以了
由于启动器本身是一个spring项目,这时我们新建一个模块,不需要web
1.导入邮件的pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
2.建立类EmailProperties,属性为yml文件的参数
报错原因是没有被spring所管理,spring没有填充属性
3.yml文件
email: host: smtp.qq.com username: 2103349206@qq.com password: vnmjnmokyudgefgh enable: true
4.创建接口
发送行为的方法,参数是接收方的邮箱
5.建立实现类
@EnableConfigurationProperties(value = EmailProperties.class) 表示开启加载配置的属性
相当于在 EmailProperties上加上compoent@Configuration 将实现类变成一个组件,立马被加载
package com.yzm.yzm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
/**
* @author 小宝的宝
*/
@Configuration
@EnableConfigurationProperties(value = EmailProperties.class)
public class EmailSendImpl implements EmailSend{
private EmailProperties emailProperties;
JavaMailSenderImpl mailSender;
//
@Autowired
public EmailSendImpl(EmailProperties emailProperties){
this.emailProperties=emailProperties;
mailSender =new JavaMailSenderImpl();
mailSender.setHost(emailProperties.getHost());
mailSender.setUsername(emailProperties.getUsername());
mailSender.setPassword(emailProperties.getPassword());
mailSender.setDefaultEncoding("Utf-8");
Properties p = new Properties();
p.setProperty("mail.smtp.auth", "true");
p.setProperty("mail.smtp.starttls.enable", "true");
p.setProperty("mail.smtp.starttls.required", "true");
mailSender.setJavaMailProperties(p);
}
@Override
public String sendtext(String receiver) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(emailProperties.getUsername());
message.setTo(receiver);
message.setSubject("验证码");
message.setText("1234");
mailSender.send(message);
return "aabb";
}
}
测试类放入一个接收邮箱
package com.lj.springboot6; import com.yzm.yzm.EmailSender; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; @SpringBootTest class SpringBoot6ApplicationTests { @Autowired private EmailSender emailSender; @Test void contextLoads() { emailSender.sendText("maliuan@foxmail.com"); } }
结果
为了控制是否启用,可以增加一个属性 enable
在实现类上添加注解
@ConditionalOnProperty(prefix = "email",name= "enable",havingValue = "true")
表示只有enable属性为true才被spring管理,如果为false会直接报错
5.引入其他依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
将上面的plugin替换掉之前的pom里面最后一个长的plugin
为了命名规范,在pom文件中进行修改name和artifactId
然后下载相应的插件
此时在maven仓库中可看到文件
自定义启动器就完成了,下面我们来测试以下
在spring项目的pom中引入自定义的依赖
<dependency> <groupId>com.yzm</groupId> <artifactId>yzm-spring-boot-statrer</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
然后再配置yml文件
spring: application: name: springBoot6 redis: database: 0 #数据库索引 host: 127.0.0.1 #主机位置 port: 6379 #端口 password: #密码 jedis: pool: max-active: 8 #最大连接数 max-wait: -1 #最大阻塞等待时间(负数表示没限制) max-idle: 8 #最大空闲 min-idle: 0 #最小空闲 timeout: 10000 #连接超时时间 email: host: smtp.qq.com username: 2103349206@qq.com password: vnmjnmokyudgefgh enable: true
测试
package com.lj.springboot6; import com.yzm.yzm.EmailSender; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; @SpringBootTest class SpringBoot6ApplicationTests { @Autowired private EmailSender emailSender; @Test void contextLoads() { emailSender.sendText("maliuan@foxmail.com"); } }
结果
四、实用场景
创建用户时发送验证码功能
1.创建用户实体类
邮箱名字
package com.lj.springboot6.pojo; import lombok.Data; @Data public class User { private String account; }
2.配置yml文件(redis用于将验证码存入缓存并保存60秒)
spring: application: name: springBoot6 redis: database: 0 #数据库索引 host: 127.0.0.1 #主机位置 port: 6379 #端口 password: #密码 jedis: pool: max-active: 8 #最大连接数 max-wait: -1 #最大阻塞等待时间(负数表示没限制) max-idle: 8 #最大空闲 min-idle: 0 #最小空闲 timeout: 10000 #连接超时时间 email: host: smtp.qq.com username: 2103349206@qq.com password: vnmjnmokyudgefgh enable: true
用来操作各种redis类型
package com.lj.springboot6.util; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.jedis.JedisConnection; import org.springframework.data.redis.connection.lettuce.LettuceConnection; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.data.redis.core.types.Expiration; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; /** * Redis工具类 * <p> * 声明: 此工具只简单包装了redisTemplate的大部分常用的api,没有包装redisTemplate所有的api * 如果对此工具类中的功能不太满意,或对StringRedisTemplate提供的api不太满意, * 那么可自行实现相应的{@link StringRedisTemplate}类中的对应execute方法,以达 * 到自己想要的效果; 至于如何实现,则可参考源码或{@link LockOps}中的方法 * <p> * 注: 此工具类依赖spring-boot-starter-data-redis类库 * 注: 更多javadoc细节,可详见{@link RedisOperations} * <p> * 统一说明一: 方法中的key、 value都不能为null * 统一说明二: 不能跨数据类型进行操作,否者会操作失败/操作报错 * 如: 向一个String类型的做Hash操作,会失败/报错......等等 */ @Slf4j @Component @SuppressWarnings("unused") public class RedisUtil implements ApplicationContextAware { /** * 使用StringRedisTemplate(,其是RedisTemplate的定制化升级) */ private static StringRedisTemplate redisTemplate; private static final ObjectMapper mapper = new ObjectMapper(); @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { RedisUtil.redisTemplate = applicationContext.getBean(StringRedisTemplate.class); } /** * key相关操作 */ public static class KeyOps { /** * 根据key,删除redis中的对应key-value * <p> * 注: 若删除失败,则返回false * <p> * 若redis中,不存在该key,那么返回的也是false * 所以,不能因为返回了false,就认为redis中一定还存 * 在该key对应的key-value * * @param key 要删除的key * @return 删除是否成功 */ public static Boolean delete(String key) { log.info("delete(...) => key -> {}", key); // 返回值只可能为true/false,不可能为null Boolean result = redisTemplate.delete(key); log.info("delete(...) => result -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 根据keys,批量删除key-value * <p> * 注: 若redis中,不存在对应的key,那么计数不会加1,即: * redis中存在的key-value里,有名为a1、a2的key, * 删除时,传的集合是a1、a2、a3,那么返回结果为2 * * @param keys 要删除的key集合 * @return 删除了的key-value个数 */ public static long delete(Collection<String> keys) { log.info("delete(...) => keys -> {}", keys); Long count = redisTemplate.delete(keys); log.info("delete(...) => count -> {}", count); if (count == null) { throw new RedisOpsResultIsNullException(); } return count; } /** * 将key对应的value值进行序列化,并返回序列化后的value值 * <p> * 注: 若不存在对应的key,则返回null * 注: dump时,并不会删除redis中的对应key-value * 注: dump功能与restore相反 * * @param key 要序列化的value的key * @return 序列化后的value值 */ public static byte[] dump(String key) { log.info("dump(...) =>key -> {}", key); byte[] result = redisTemplate.dump(key); log.info("dump(...) => result -> {}", result); return result; } /** * 将给定的value值,反序列化到redis中,形成新的key-value * * @param key value对应的key * @param value 要反序列的value值 * 注: 这个值可以由{@link this#dump(String)}获得 * @param timeToLive 反序列化后的key-value的存活时长 * @param unit timeToLive的单位 * @throws RedisSystemException 如果redis中已存在同样的key时,抛出此异常 */ public static void restore(String key, byte[] value, long timeToLive, TimeUnit unit) { restore(key, value, timeToLive, unit, false); } /** * 将给定的value值,反序列化到redis中,形成新的key-value * * @param key value对应的key * @param value 要反序列的value值 * 注: 这个值可以由{@link this#dump(String)}获得 * @param timeout 反序列化后的key-value的存活时长 * @param unit timeout的单位 * @param replace 若redis中已经存在了相同的key,是否替代原来的key-value * @throws RedisSystemException 如果redis中已存在同样的key,且replace为false时,抛出此异常 */ public static void restore(String key, byte[] value, long timeout, TimeUnit unit, boolean replace) { log.info("restore(...) => key -> {},value -> {},timeout -> {},unit -> {},replace -> {}", key, value, timeout, unit, replace); redisTemplate.restore(key, value, timeout, unit, replace); } /** * redis中是否存在,指定key的key-value * * @param key 指定的key * @return 是否存在对应的key-value */ public static boolean hasKey(String key) { log.info("hasKey(...) => key -> {}", key); Boolean result = redisTemplate.hasKey(key); log.info("hasKey(...) => result -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 给指定的key对应的key-value设置: 多久过时 * <p> * 注:过时后,redis会自动删除对应的key-value * 注:若key不存在,那么也会返回false * * @param key 指定的key * @param timeout 过时时间 * @param unit timeout的单位 * @return 操作是否成功 */ public static boolean expire(String key, long timeout, TimeUnit unit) { log.info("expire(...) => key -> {},timeout -> {},unit -> {}", key, timeout, unit); Boolean result = redisTemplate.expire(key, timeout, unit); log.info("expire(...) => result is -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 给指定的key对应的key-value设置: 什么时候过时 * <p> * 注:过时后,redis会自动删除对应的key-value * 注:若key不存在,那么也会返回false * * @param key 指定的key * @param date 啥时候过时 * @return 操作是否成功 */ public static boolean expireAt(String key, Date date) { log.info("expireAt(...) => key -> {},date -> {}", key, date); Boolean result = redisTemplate.expireAt(key, date); log.info("expireAt(...) => result is -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 找到所有匹配pattern的key,并返回该key的结合. * <p> * 提示:若redis中键值对较多,此方法耗时相对较长,慎用!慎用!慎用! * * @param pattern 匹配模板 * 注: 常用的通配符有: * ? 有且只有一个; * * >=0个; * @return 匹配pattern的key的集合 可能为null */ public static Set<String> keys(String pattern) { log.info("keys(...) => pattern -> {}", pattern); Set<String> keys = redisTemplate.keys(pattern); log.info("keys(...) => keys -> {}", keys); return keys; } /** * 将当前数据库中的key对应的key-value,移动到对应位置的数据库中 * <p> * 注:单机版的redis,默认将存储分为16个db,index为0 到 15 * 注:同一个db下,key唯一; 但是在不同db中,key可以相同 * 注:若目标db下,已存在相同的key,那么move会失败,返回false * * @param key 定位要移动的key-value的key * @param dbIndex 要移动到哪个db * @return 移动是否成功 * 注: 若目标db下,已存在相同的key,那么move会失败,返回false */ public static boolean move(String key, int dbIndex) { log.info("move(...) => key -> {},dbIndex -> {}", key, dbIndex); Boolean result = redisTemplate.move(key, dbIndex); log.info("move(...) =>result -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 移除key对应的key-value的过期时间,使该key-value一直存在 * <p> * 注: 若key对应的key-value,本身就是一直存在(无过期时间的),那么persist方法会返回false; * 若没有key对应的key-value存在,本那么persist方法会返回false; * * @param key 定位key-value的key * @return 操作是否成功 */ public static boolean persist(String key) { log.info("persist(...) => key -> {}", key); Boolean result = redisTemplate.persist(key); log.info("persist(...) => result -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 获取key对应的key-value的过期时间 * <p> * 注: 若key-value永不过期,那么返回的为-1 * 注: 若不存在key对应的key-value,那么返回的为-2 * 注:若存在零碎时间不足1 SECONDS,则(大体上)四舍五入到SECONDS级别 * * @param key 定位key-value的key * @return 过期时间(单位s) */ public static long getExpire(String key) { return getExpire(key, TimeUnit.SECONDS); } /** * 获取key对应的key-value的过期时间 * <p> * 注: 若key-value永不过期,那么返回的为-1 * 注: 若不存在key对应的key-value,那么返回的为-2 * 注:若存在零碎时间不足1 unit,则(大体上)四舍五入到unit别 * * @param key 定位key-value的key * @return 过期时间(单位unit) */ public static long getExpire(String key, TimeUnit unit) { log.info("getExpire(...) =>key -> {},unit is -> {}", key, unit); Long result = redisTemplate.getExpire(key, unit); log.info("getExpire(...) => result -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 从redis的所有key中,随机获取一个key * <p> * 注: 若redis中不存在任何key-value,那么这里返回null * * @return 随机获取到的一个key */ public static String randomKey() { String result = redisTemplate.randomKey(); log.info("randomKey(...) => result is -> {}", result); return result; } /** * 重命名对应的oldKey为新的newKey * <p> * 注: 若oldKey不存在,则会抛出异常. * 注: 若redis中已存在与newKey一样的key, * 那么原key-value会被丢弃, * 只留下新的key,以及原来的value * 示例说明: 假设redis中已有 (keyAlpha,valueAlpha) 和 (keyBeta,valueBeat), * 在使用rename(keyAlpha,keyBeta)替换后,redis中只会剩下(keyBeta,valueAlpha) * * @param oldKey 旧的key * @param newKey 新的key * @throws RedisSystemException 若oldKey不存在时,抛出此异常 */ public static void rename(String oldKey, String newKey) { log.info("rename(...) => oldKey -> {},newKey -> {}", oldKey, newKey); redisTemplate.rename(oldKey, newKey); } /** * 当redis中不存在newKey时,重命名对应的oldKey为新的newKey * 否者不进行重命名操作 * <p> * 注: 若oldKey不存在,则会抛出异常. * * @param oldKey 旧的key * @param newKey 新的key * @throws RedisSystemException 若oldKey不存在时,抛出此异常 */ public static boolean renameIfAbsent(String oldKey, String newKey) { log.info("renameIfAbsent(...) => oldKey -> {},newKey -> {}", oldKey, newKey); Boolean result = redisTemplate.renameIfAbsent(oldKey, newKey); log.info("renameIfAbsent(...) => result -> {}", result); if (result == null) { throw new RedisOpsResultIsNullException(); } return result; } /** * 获取key对应的value的数据类型 * <p> * 注: 若redis中不存在该key对应的key-value,那么这里返回NONE * * @param key 用于定位的key * @return key对应的value的数据类型 */ public static DataType type(String key) { log.info("type(...) => key -> {}", key); DataType result = redisTemplate.type(key); log.info("type(...) => result -> {}", result); return result; } } /** * string相关操作 */ public static class StringOps { /** * 设置key-value * <p> * 注: 若已存在相同的key,那么原来的key-value会被丢弃 * * @param key key * @param value key对应的value */ public static void set(String key, String value) { log.info("set(...) => key -> {},value -> {}", key, value); redisTemplate.opsForValue().set(key, value); } /** * 处理redis中key对应的value值,将第offset位的值,设置为1或0 * <p> * 说明: 在redis中,存储的字符串都是以二级制的进行存在的; 如存储的key-value里,值为abc,实际上, * 在redis里面存储的是01