开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

redis分布式锁(基于springboot实现)

发表于 2020-11-25

在公司的项目中用到了分布式锁,但只会用却不明白其中的规则

所以写一篇文章来记录

使用场景:交易服务,使用redis分布式锁,防止重复提交订单,出现超卖问题

分布式锁应该具备哪些条件

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
  • 具备锁失效机制,防止死锁
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁的实现方式

  1. 基于数据库乐观锁/悲观锁
  2. Redis分布式锁(本文):利用setnx命令。此命令是原子性操作,只有key不存在的情况下,才能set,就意味着线程获取到了锁
  3. Zookeeper分布式锁:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的
  4. Memcached:利用add命令。此命令是原子性操作,只有key不存在的情况下,才能add,也就意味着线程获取到了锁

redis是如何实现加锁的?

在redis中,有一条命令,实现锁

1
bash复制代码SETNX key value

该命令的作用是将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。设置成功,返回 1 ;设置失败,返回 0

使用 redis 来实现锁的逻辑就是这样的

1
2
3
4
5
6
7
bash复制代码线程 1 获取锁  -- > setnx lockKey lockvalue
-- > 1 获取锁成功
线程 2 获取锁 -- > setnx lockKey lockvalue
-- > 0 获取锁失败 (继续等待,或者其他逻辑)
线程 1 释放锁 -- >
线程 2 获取锁 -- > setnx lockKey lockvalue
-- > 1 获取成功

接下来我们将基于springboot实现redis分布式锁

1. 引入redis、springmvc、lombok依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.miao.redis</groupId>
<artifactId>springboot-caffeine-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-redis-lock-demo</name>
<description>Demo project for Redis Distribute Lock</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>

<!--springMvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

2. 新建RedisDistributedLock.java并书写加锁解锁逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
java复制代码import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;

import java.nio.charset.StandardCharsets;

/**
* @author miao
* redis 加锁工具类
*/
@Slf4j
public class RedisDistributedLock {

/**
* 超时时间
*/
private static final long TIMEOUT_MILLIS = 15000;

/**
* 重试次数
*/
private static final int RETRY_TIMES = 10;

/***
* 睡眠时间
*/
private static final long SLEEP_MILLIS = 500;

/**
* 用来加锁的lua脚本
* 因为新版的redis加锁操作已经为原子性操作
* 所以放弃使用lua脚本
*/
private static final String LOCK_LUA =
"if redis.call(\"setnx\",KEYS[1],ARGV[1]) == 1 " +
"then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";

/**
* 用来释放分布式锁的lua脚本
* 如果redis.get(KEYS[1]) == ARGV[1],则redis delete KEYS[1]
* 否则返回0
* KEYS[1] , ARGV[1] 是参数,我们只调用的时候 传递这两个参数就可以了
* KEYS[1] 主要用來传递在redis 中用作key值的参数
* ARGV[1] 主要用来传递在redis中用做 value值的参数
*/
private static final String UNLOCK_LUA =
"if redis.call(\"get\",KEYS[1]) == ARGV[1] "
+ "then "
+ " return redis.call(\"del\",KEYS[1]) "
+ "else "
+ " return 0 "
+ "end ";

/**
* 检查 redisKey 是否上锁
*
* @param redisKey redisKey
* @param template template
* @return Boolean
*/
public static Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template) {

return lock(redisKey, value, template, RETRY_TIMES);
}

private static Boolean lock(String redisKey,
String value,
RedisTemplate<Object, Object> template,
int retryTimes) {

boolean result = lockKey(redisKey, value, template);

while (!(result) && retryTimes-- > 0) {
try {

log.debug("lock failed, retrying...{}", retryTimes);
Thread.sleep(RedisDistributedLock.SLEEP_MILLIS);
} catch (InterruptedException e) {

return false;
}
result = lockKey(redisKey, value, template);
}

return result;
}


private static Boolean lockKey(final String key,
final String value,
RedisTemplate<Object, Object> template) {
try {

RedisCallback<Boolean> callback = (connection) -> connection.set(
key.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8),
Expiration.milliseconds(RedisDistributedLock.TIMEOUT_MILLIS),
RedisStringCommands.SetOption.SET_IF_ABSENT
);

return template.execute(callback);
} catch (Exception e) {

log.info("lock key fail because of ", e);
}

return false;
}


/**
* 释放分布式锁资源
*
* @param redisKey key
* @param value value
* @param template redis
* @return Boolean
*/
public static Boolean releaseLock(String redisKey,
String value,
RedisTemplate<Object, Object> template) {
try {
RedisCallback<Boolean> callback = (connection) -> connection.eval(
UNLOCK_LUA.getBytes(),
ReturnType.BOOLEAN,
1,
redisKey.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8)
);

return template.execute(callback);
} catch (Exception e) {

log.info("release lock fail because of ", e);
}

return false;
}

}

补充:

1. spring-data-redis 有StringRedisTempla和RedisTemplate两种,但是我选择了RedisTemplate,因为他比较万能。他们的区别是:当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可, 但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是 更好的选择。
2. 选择lua脚本是因为,脚本运行是原子性的,在脚本运行期间没有客户端可以操作,所以在释放锁的时候用了lua脚本,
而redis最新版加锁时保证了Redis值和自动过期时间的原子性,所用没用lua脚本

3. 创建测试类 TestController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
* @author miao
*/
@RestController
@Slf4j
public class TestController {

@Resource
private RedisTemplate<Object, Object> redisTemplate;

@PostMapping("/order")
public String createOrder() throws InterruptedException {

log.info("开始创建订单");

Boolean isLock = RedisDistributedLock.isLock("testLock", "456789", redisTemplate);

if (!isLock) {

log.info("锁已经被占用");
return "fail";
} else {
//.....处理逻辑
}

Thread.sleep(10000);
//一定要记得释放锁,否则会出现问题
RedisDistributedLock.releaseLock("testLock", "456789", redisTemplate);

return "success";
}
}

4. 使用postman进行测试

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5. redis分布式锁的缺点

上面我们说的是redis,是单点的情况。如果是在redis sentinel集群中情况就有所不同了。在redis sentinel集群中,我们具有多台redis,他们之间有着主从的关系,例如一主二从。我们的set命令对应的数据写到主库,然后同步到从库。当我们申请一个锁的时候,对应就是一条命令 setnx mykey myvalue ,在redis sentinel集群中,这条命令先是落到了主库。假设这时主库down了,而这条数据还没来得及同步到从库,sentinel将从库中的一台选举为主库了。这时,我们的新主库中并没有mykey这条数据,若此时另外一个client执行 setnx mykey hisvalue , 也会成功,即也能得到锁。这就意味着,此时有两个client获得了锁。这不是我们希望看到的,虽然这个情况发生的记录很小,只会在主从failover的时候才会发生,大多数情况下、大多数系统都可以容忍,但是不是所有的系统都能容忍这种瑕疵。

6.redis分布式锁的优化

为了解决故障转移情况下的缺陷,Antirez 发明了 Redlock 算法,使用redlock算法,需要多个redis实例,加锁的时候,它会想多半节点发送 setex mykey myvalue 命令,只要过半节点成功了,那么就算加锁成功了。释放锁的时候需要想所有节点发送del命令。这是一种基于【大多数都同意】的一种机制。感兴趣的可以查询相关资料。在实际工作中使用的时候,我们可以选择已有的开源实现,python有redlock-py,java 中有Redisson redlock。

redlock确实解决了上面所说的“不靠谱的情况”。但是,它解决问题的同时,也带来了代价。你需要多个redis实例,你需要引入新的库 代码也得调整,性能上也会有损。所以,果然是不存在“完美的解决方案”,我们更需要的是能够根据实际的情况和条件把问题解决了就好。

至此,我大致讲清楚了redis分布式锁方面的问题(日后如果有新的领悟就继续更新)。


** 分布式锁对比**

数据库分布式锁实现

缺点:

  1. db操作性能较差,并且有锁表的风险
  2. 非阻塞操作失败后,需要轮询,占用cpu资源
  3. 长时间不commit或者长时间轮询,可能会占用较多连接资源

Redis分布式锁实现

缺点:

  1. 锁删除失败 过期时间不好控制
  2. 非阻塞,操作失败后,需要轮询,占用cpu资源

ZK分布式锁实现

缺点:

  1. 性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。

总结

从理解的难易程度角度(从低到高)数据库 > Redis >Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= Redis > 数据库
从性能角度(从高到低)缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)Zookeeper > Redis > 数据库

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

68篇干货,手把手教你通关 Spring Security!

发表于 2020-11-25

Spring Security 系列前前后后整了 68 篇文章了,是时候告一个段落了。

这两天松哥抽空把该系列的文章整理了一下,做成了一个索引,方便小伙伴们查找。

教程地址如下:

  • www.javaboy.org/springsecur…
  • www.itboyhub.com/springsecur…

对应的 Demo 地址如下:

  • github.com/lenve/sprin…
  • github.com/lenve/oauth…

前面 javaboy.org 是国外服务器,如果响应慢小伙伴只需要 xxx 即可,不需要我多说了吧。

后面的 itboyhub.com 是国内服务器,虽然松哥已经买了 CDN 加速服务了,但是响应速度好像还是一般般,所以文末松哥也给出了微信公众号的文章索引。

小伙伴们按照松哥已经排列好的顺序,一篇一篇练级通关吧!

文章索引:

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登录
  4. Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互
  5. Spring Security 中的授权操作原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登录功能
  9. Spring Boot 自动登录,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. Spring Security 中如何快速查看登录用户 IP 地址等信息?
  13. Spring Security 自动踢掉前一个登录用户,一个配置搞定!
  14. Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?
  15. Spring Security 自带防火墙!你都不知道自己的系统有多安全!
  16. 什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击?
  17. 集群化部署,Spring Security 要如何处理 session 共享?
  18. 松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!
  19. 要学就学透彻!Spring Security 中 CSRF 防御源码解析
  20. Spring Boot 中密码加密的两种姿势!
  21. Spring Security 要怎么学?为什么一定要成体系的学习?
  22. Spring Security 两种资源放行策略,千万别用错了!
  23. 松哥手把手教你入门 Spring Boot + CAS 单点登录
  24. Spring Boot 实现单点登录的第三种方案!
  25. Spring Boot+CAS 单点登录,如何对接数据库?
  26. Spring Boot+CAS 默认登录页面太丑了,怎么办?
  27. 用 Swagger 测试接口,怎么在请求头中携带 Token?
  28. Spring Boot 中三种跨域场景总结
  29. Spring Boot 中如何实现 HTTP 认证?
  30. Spring Security 中的四种权限控制方式
  31. Spring Security 多种加密方案共存,老破旧系统整合利器!
  32. 神奇!自己 new 出来的对象一样也可以被 Spring 容器管理!
  33. Spring Security 配置中的 and 到底该怎么理解?
  34. 一文搞定 Spring Security 异常处理机制!
  35. 写了这么多年代码,这样的登录方式还是头一回见!
  36. Spring Security 竟然可以同时存在多个过滤器链?
  37. Spring Security 可以同时对接多个用户表?
  38. 在 Spring Security 中,我就想从子线程获取用户登录信息,怎么办?
  39. 深入理解 FilterChainProxy【源码篇】
  40. 深入理解 SecurityConfigurer 【源码篇】
  41. 深入理解 HttpSecurity【源码篇】
  42. 深入理解 AuthenticationManagerBuilder 【源码篇】
  43. 花式玩 Spring Security ,这样的用户定义方式你可能没见过!
  44. 深入理解 WebSecurityConfigurerAdapter【源码篇】
  45. 盘点 Spring Security 框架中的八大经典设计模式
  46. Spring Security 初始化流程梳理
  47. 为什么你使用的 Spring Security OAuth 过期了?松哥来和大家捋一捋!
  48. 一个诡异的登录问题
  49. 什么是计时攻击?Spring Boot 中该如何防御?
  50. Spring Security 中如何让上级拥有下级的所有权限?
  51. Spring Security 权限管理的投票器与表决机制
  52. Spring Security 中的 hasRole 和 hasAuthority 有区别吗?
  53. Spring Security 中如何细化权限粒度?
  54. 一个案例演示 Spring Security 中粒度超细的权限控制!
  55. Spring Security 中最流行的权限管理模型!
  56. 我又发现 Spring Security 中一个小秘密!
  57. 聊一个 GitHub 上开源的 RBAC 权限管理系统,很6!
  58. RBAC 案例解读【2】

下面是 OAuth2 相关的技能点:

  1. 做微服务绕不过的 OAuth2,松哥也来和大家扯一扯
  2. 这个案例写出来,还怕跟面试官扯不明白 OAuth2 登录流程?
  3. 死磕 OAuth2,教练我要学全套的!
  4. OAuth2 令牌还能存入 Redis ?越玩越溜!
  5. 想让 OAuth2 和 JWT 在一起愉快玩耍?请看松哥的表演
  6. 最近在做 Spring Cloud 项目,松哥和大家分享一点微服务架构中的安全管理思路
  7. Spring Boot+OAuth2,一个注解搞定单点登录!
  8. 分分钟让自己的网站接入 GitHub 第三方登录功能
  9. Spring Boot+OAuth2,如何自定义返回的 Token 信息?
  10. OAuth2,想说懂你不容易

从三月份到十月份,大半年的业余时间都耗在这个上面了,现在整理索引这个过程真的很享受!感觉这一年的业余时间没浪费。

最后,松哥还搜集了 50+ 个项目需求文档,想做个项目练练手的小伙伴不妨看看哦~



需求文档地址:github.com/lenve/javad…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

肝了一周总结的SpringBoot实战教程,太实用了!

发表于 2020-11-25

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

天天在用SpringBoot,但有些SpringBoot的实用知识点却不是很清楚!最近又对SpringBoot中的实用知识点做了个总结,相信对从Spring过渡到SpringBoot的朋友会很有帮助!

前言

首先我们来了解下为什么要有SpringBoot?

Spring作为J2EE的轻量级代替品,让我们无需开发重量级的Enterprise JavaBean(EJB),通过依赖注入和面向切面编程,使用简单的Java对象(POJO)即可实现EJB的功能。

虽然Spring的组件代码是轻量级的,但它的配置却是重量级的。即使后来Spring引入了基于注解的组件扫描和基于Java的配置,让它看上去简洁不少,但Spring还是需要不少配置。除此之外,项目的依赖管理也很麻烦,我们无法确保各个版本的依赖都能兼容。

为了简化Spring中的配置和统一各种依赖的版本,SpringBoot诞生了!

简介

SpringBoot从本质上来说就是Spring,它通过了一些自己的特性帮助我们简化了Spring应用程序的开发。主要有以下三个核心特性:

  • 自动配置:对于很多Spring应用程序常见的应用功能,SpringBoot能自动提供相关配置,集成功能开发者仅需很少的配置。
  • 起步依赖:告诉SpringBoot需要什么功能,它就能引入对应的库,无需考虑该功能依赖库的版本问题。
  • Actuator:可以深入了解SpringBoot应用程序内部情况,比如创建了哪些Bean、自动配置的决策、应用程序的状态信息等。

开始使用

创建应用

创建SpringBoot应用的方式有很多种,这里使用最流行的开发工具IDEA来创建应用。

  • 首先通过File->New Project来创建一个项目;

  • 然后选择通过Spring Initializr来创建一个SpringBoot应用;

  • 填写好Maven项目的groupId和artifactId及选择好Java版本;

  • 选择好起步依赖,这里选择的是开启Web功能的起步依赖;

  • 选择好项目的存放位置即可顺利创建一个SpringBoot应用。

查看应用

项目结构

一个新创建的SpringBoot应用基本结构如下。

1
2
3
4
5
6
7
8
9
10
11
bash复制代码mall-tiny-boot
├─pom.xml # Maven构建文件
└─src
├─main
│ ├─java
│ │ └─MallTinyApplication.java # 应用程序启动类
│ └─resources
│ └─application.yml # SpringBoot配置文件
└─test
└─java
└─MallTinyApplicationTests.java # 基本的集成测试类

应用启动类

MallTinyApplication在SpringBoot应用中有配置和引导的作用,通过@SpringBootApplication注解开启组件扫描和自动配置,通过SpringApplication.run()引导应用程序启动;

1
2
3
4
5
6
7
8
9
10
java复制代码//开启组件扫描和应用装配
@SpringBootApplication
public class MallTinyApplication {

public static void main(String[] args) {
//负责引导应用程序启动
SpringApplication.run(MallTinyApplication.class, args);
}

}

@SpringBootApplication注解是三个注解的结合体,拥有以下三个注解的功能:

  • @Configuration:用于声明Spring中的Java配置;
  • @ComponentScan:启用组件扫描,当我们声明组件时,会自动发现并注册为Spring应用上下文中的Bean;
  • @EnableAutoConfiguration:开启SpringBoot自动配置功能,简化配置编写。

测试应用

可以使用@RunWith和@SpringBootTest来创建Spring应用上下文,通过@Test注解来声明一个测试方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class MallTinyApplicationTests {
@Autowired
private PmsBrandService pmsBrandService;

@Test
public void contextLoads() {
}

@Test
public void testMethod() {
List<PmsBrand> brandList = pmsBrandService.listAllBrand();
log.info("testMethod:{}", brandList);
}

}

编写应用配置

当我们需要微调自动配置的参数时,可以在application.yml文件中进行配置,比如微调下端口号。

1
2
yaml复制代码server:
port: 8088

项目构建过程

SpringBoot项目可以使用Maven进行构建,首先我们需要继承spring-boot-starter-parent这个父依赖,父依赖可以控制所有SpringBoot官方起步依赖的版本,接下来当我们使用官方起步依赖时,就不用指定版本号了。我们还需要使用SpringBoot的插件,该插件主要用于将应用打包为可执行Jar。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.macro.mall</groupId>
<artifactId>mall-tiny-boot</artifactId>
<version>1.0-SNAPSHOT</version>
<name>mall-tiny-boot</name>
<description>Demo project for Spring Boot</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<skipTests>true</skipTests>
</properties>

<!--继承SpringBoot父项目,控制所有依赖版本-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--SpringBoot起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<!--SpringBoot插件,可以把应用打包为可执行Jar-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

使用起步依赖

使用起步依赖的好处

在使用起步依赖之前,我们先来了解下使用起步依赖的好处,当我们使用SpringBoot需要整合Web相关功能时,只需在pom.xml中添加一个起步依赖即可。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

如果是Spring项目的话,我们需要添加很多依赖,还需要考虑各个依赖版本的兼容性问题,是个相当麻烦的事情。

指定基于功能的依赖

当我们需要开发一个Web应用,需要使用MySQL数据库进行存储,使用Swagger生成API文档,添加如下起步依赖即可。需要注意的是只有官方的起步依赖不需要指定版本号,其他的还是需要自行指定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
xml复制代码<dependencies>
<!--SpringBoot Web功能起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--MyBatis分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
<!--集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!--springfox swagger官方Starter-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>

覆盖起步依赖中的库

其实起步依赖和你平时使用的依赖没什么区别,你可以使用Maven的方式来排除不想要的依赖。比如你不想使用tomcat容器,想使用undertow容器,可以在Web功能依赖中排除掉tomcat。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<dependencies>
<!--SpringBoot Web功能起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--排除tomcat依赖-->
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!--undertow容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
</dependencies>

使用自动配置

SpringBoot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定Spring配置应该用哪个,不该用哪个。

举个例子,当我们使用Spring整合MyBatis的时候,需要完成如下几个步骤:

  • 根据数据库连接配置,配置一个dataSource对象;
  • 根据dataSource对象和SqlMapConfig.xml文件(其中包含mapper.xml文件路径和mapper接口路径配置),配置一个sqlSessionFactory对象。

当我们使用SpringBoot整合MyBatis的时候,会自动创建dataSource和sqlSessionFactory对象,只需我们在application.yml和Java配置中添加一些自定义配置即可。

在application.yml中配置好数据库连接信息及mapper.xml文件路径。

1
2
3
4
5
6
7
8
9
10
yaml复制代码spring:
datasource:
url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root

mybatis:
mapper-locations:
- classpath:mapper/*.xml
- classpath*:com/**/mapper/*.xml

使用Java配置,配置好mapper接口路径。

1
2
3
4
5
6
7
8
java复制代码/**
* MyBatis配置类
* Created by macro on 2019/4/8.
*/
@Configuration
@MapperScan("com.macro.mall.tiny.mbg.mapper")
public class MyBatisConfig {
}

使用自动配置以后,我们整合其他功能的配置大大减少了,可以更加专注程序功能的开发了。

自定义配置

自定义Bean覆盖自动配置

虽然自动配置很好用,但有时候自动配置的Bean并不能满足你的需要,我们可以自己定义相同的Bean来覆盖自动配置中的Bean。

例如当我们使用Spring Security来保护应用安全时,由于自动配置并不能满足我们的需求,我们需要自定义基于WebSecurityConfigurerAdapter的配置。这里我们自定义了很多配置,比如将基于Session的认证改为使用JWT令牌、配置了一些路径的无授权访问,自定义了登录接口路径,禁用了csrf功能等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
java复制代码/**
* SpringSecurity的配置
* Created by macro on 2018/4/26.
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UmsAdminService adminService;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
List<String> urls = ignoreUrlsConfig.getUrls();
String[] urlArray = ArrayUtil.toArray(urls, String.class);
httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
.disable()
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET,urlArray) // 允许对于网站静态资源的无授权访问
.permitAll()
.antMatchers("/admin/login")// 对登录注册要允许匿名访问
.permitAll()
.antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
.permitAll()
.anyRequest()// 除上面外的所有请求全部需要鉴权认证
.authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> {
AdminUserDetails admin = adminService.getAdminByUsername(username);
if (admin != null) {
return admin;
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}

@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

}

自动配置微调

有时候我们只需要微调下自动配置就能满足需求,并不需要覆盖自动配置的Bean,此时我们可以在application.yml属性文件中进行配置。

比如微调下应用运行的端口。

1
2
yaml复制代码server:
port: 8088

比如修改下数据库连接信息。

1
2
3
4
5
yaml复制代码spring:
datasource:
url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root

读取配置文件的自定义属性

有时候我们会在属性文件中自定义一些属性,然后在程序中使用。此时可以将这些自定义属性映射到一个属性类里来使用。

比如说我们想给Spring Security配置一个白名单,访问这些路径无需授权,我们可以先在application.yml中添添加如下配置。

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码secure:
ignored:
urls:
- /
- /swagger-ui/
- /*.html
- /favicon.ico
- /**/*.html
- /**/*.css
- /**/*.js
- /swagger-resources/**
- /v2/api-docs/**

之后创建一个属性类,使用@ConfigurationProperties注解配置好这些属性的前缀,再定义一个urls属性与属性文件相对应即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 用于配置白名单资源路径
* Created by macro on 2018/11/5.
*/
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {

private List<String> urls = new ArrayList<>();

}

Actuator

SpringBoot Actuator的关键特性是在应用程序里提供众多Web端点,通过它们了解应用程序运行时的内部状况。

端点概览

Actuator提供了大概20个端点,常用端点路径及描述如下:

路径 请求方式 描述
/beans GET 描述应用程序上下文里全部的Bean,以及它们之间关系
/conditions GET 描述自动配置报告,记录哪些自动配置生效了,哪些没生效
/env GET 获取全部环境属性
/env/{name} GET 根据名称获取特定的环境属性
/mappings GET 描述全部的URI路径和控制器或过滤器的映射关系
/configprops GET 描述配置属性(包含默认值)如何注入Bean
/metrics GET 获取应用程序度量指标,比如JVM和进程信息
/metrics/{name} GET 获取指定名称的应用程序度量值
loggers GET 查看应用程序中的日志级别
/threaddump GET 获取线程活动的快照
/health GET 报告应用程序的健康指标,这些值由HealthIndicator的实现类提供
/shutdown POST 关闭应用程序
/info GET 获取应用程序的定制信息,这些信息由info打头的属性提供

查看配置明细

  • 直接访问根端点,可以获取到所有端点访问路径,根端点访问地址:http://localhost:8088/actuator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
json复制代码{
"_links": {
"self": {
"href": "http://localhost:8088/actuator",
"templated": false
},
"beans": {
"href": "http://localhost:8088/actuator/beans",
"templated": false
},
"caches-cache": {
"href": "http://localhost:8088/actuator/caches/{cache}",
"templated": true
},
"caches": {
"href": "http://localhost:8088/actuator/caches",
"templated": false
},
"health": {
"href": "http://localhost:8088/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8088/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8088/actuator/info",
"templated": false
},
"conditions": {
"href": "http://localhost:8088/actuator/conditions",
"templated": false
},
"configprops": {
"href": "http://localhost:8088/actuator/configprops",
"templated": false
},
"env": {
"href": "http://localhost:8088/actuator/env",
"templated": false
},
"env-toMatch": {
"href": "http://localhost:8088/actuator/env/{toMatch}",
"templated": true
},
"loggers": {
"href": "http://localhost:8088/actuator/loggers",
"templated": false
},
"loggers-name": {
"href": "http://localhost:8088/actuator/loggers/{name}",
"templated": true
},
"heapdump": {
"href": "http://localhost:8088/actuator/heapdump",
"templated": false
},
"threaddump": {
"href": "http://localhost:8088/actuator/threaddump",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://localhost:8088/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:8088/actuator/metrics",
"templated": false
},
"scheduledtasks": {
"href": "http://localhost:8088/actuator/scheduledtasks",
"templated": false
},
"mappings": {
"href": "http://localhost:8088/actuator/mappings",
"templated": false
}
}
}
  • 通过/beans端点,可以获取到Spring应用上下文中的Bean的信息,比如Bean的类型和依赖属性等,访问地址:http://localhost:8088/actuator/beans
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
json复制代码{
"contexts": {
"application": {
"beans": {
"sqlSessionFactory": {
"aliases": [],
"scope": "singleton",
"type": "org.apache.ibatis.session.defaults.DefaultSqlSessionFactory",
"resource": "class path resource [org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.class]",
"dependencies": [
"dataSource"
]
},
"jdbcTemplate": {
"aliases": [],
"scope": "singleton",
"type": "org.springframework.jdbc.core.JdbcTemplate",
"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/JdbcTemplateConfiguration.class]",
"dependencies": [
"dataSource",
"spring.jdbc-org.springframework.boot.autoconfigure.jdbc.JdbcProperties"
]
}
}
}
}
}
  • 通过/conditions端点,可以获取到当前应用的自动配置报告,positiveMatches表示生效的自动配置,negativeMatches表示没有生效的自动配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码{
"contexts": {
"application": {
"positiveMatches": {
"DruidDataSourceAutoConfigure": [{
"condition": "OnClassCondition",
"message": "@ConditionalOnClass found required class 'com.alibaba.druid.pool.DruidDataSource'"
}]
},
"negativeMatches": {
"RabbitAutoConfiguration": {
"notMatched": [{
"condition": "OnClassCondition",
"message": "@ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel'"
}],
"matched": []
}
}
}
}
}
  • 通过/env端点,可以获取全部配置属性,包括环境变量、JVM属性、命令行参数和application.yml中的属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
json复制代码{
"activeProfiles": [],
"propertySources": [{
"name": "systemProperties",
"properties": {
"java.runtime.name": {
"value": "Java(TM) SE Runtime Environment"
},
"java.vm.name": {
"value": "Java HotSpot(TM) 64-Bit Server VM"
},
"java.runtime.version": {
"value": "1.8.0_91-b14"
}
}
},
{
"name": "applicationConfig: [classpath:/application.yml]",
"properties": {
"server.port": {
"value": 8088,
"origin": "class path resource [application.yml]:2:9"
},
"spring.datasource.url": {
"value": "jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai",
"origin": "class path resource [application.yml]:6:10"
},
"spring.datasource.username": {
"value": "root",
"origin": "class path resource [application.yml]:7:15"
},
"spring.datasource.password": {
"value": "******",
"origin": "class path resource [application.yml]:8:15"
}
}
}
]
}
  • 通过/mappings端点,可以查看全部的URI路径和控制器或过滤器的映射关系,这里可以看到我们自己定义的PmsBrandController和JwtAuthenticationTokenFilter的映射关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
json复制代码{
"contexts": {
"application": {
"mappings": {
"dispatcherServlets": {
"dispatcherServlet": [{
"handler": "com.macro.mall.tiny.controller.PmsBrandController#createBrand(PmsBrand)",
"predicate": "{POST /brand/create}",
"details": {
"handlerMethod": {
"className": "com.macro.mall.tiny.controller.PmsBrandController",
"name": "createBrand",
"descriptor": "(Lcom/macro/mall/tiny/mbg/model/PmsBrand;)Lcom/macro/mall/tiny/common/api/CommonResult;"
},
"requestMappingConditions": {
"consumes": [],
"headers": [],
"methods": [
"POST"
],
"params": [],
"patterns": [
"/brand/create"
],
"produces": []
}
}
}]
}
},
"servletFilters": [{
"servletNameMappings": [],
"urlPatternMappings": [
"/*",
"/*",
"/*",
"/*",
"/*"
],
"name": "jwtAuthenticationTokenFilter",
"className": "com.macro.mall.tiny.component.JwtAuthenticationTokenFilter"
}]
}
}
}

查看运行时度量

  • 通过/metrics端点,可以获取应用程序度量指标,不过只能获取度量的名称;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
json复制代码{
"names": [
"http.server.requests",
"jvm.buffer.count",
"jvm.buffer.memory.used",
"jvm.buffer.total.capacity",
"jvm.classes.loaded",
"jvm.classes.unloaded",
"jvm.gc.live.data.size",
"jvm.gc.max.data.size",
"jvm.gc.memory.allocated",
"jvm.gc.memory.promoted",
"jvm.gc.pause",
"jvm.memory.committed",
"jvm.memory.max",
"jvm.memory.used",
"jvm.threads.daemon",
"jvm.threads.live",
"jvm.threads.peak",
"jvm.threads.states",
"logback.events",
"process.cpu.usage",
"process.start.time",
"process.uptime",
"system.cpu.count",
"system.cpu.usage"
]
}
  • 需要添加指标名称才能获取对应的值,比如获取当前JVM使用的内存信息,访问地址:http://localhost:8088/actuator/metrics/jvm.memory.used
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
json复制代码{
"name": "jvm.memory.used",
"description": "The amount of used memory",
"baseUnit": "bytes",
"measurements": [
{
"statistic": "VALUE",
"value": 3.45983088E8
}
],
"availableTags": [
{
"tag": "area",
"values": [
"heap",
"nonheap"
]
},
{
"tag": "id",
"values": [
"Compressed Class Space",
"PS Survivor Space",
"PS Old Gen",
"Metaspace",
"PS Eden Space",
"Code Cache"
]
}
]
}
  • 通过loggers端点,可以查看应用程序中的日志级别信息,可以看出我们把ROOT范围日志设置为了INFO,而com.macro.mall.tiny包范围的设置为了DEBUG。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
json复制代码{
"levels": [
"OFF",
"ERROR",
"WARN",
"INFO",
"DEBUG",
"TRACE"
],
"loggers": {
"ROOT": {
"configuredLevel": "INFO",
"effectiveLevel": "INFO"
},
"com.macro.mall.tiny": {
"configuredLevel": "DEBUG",
"effectiveLevel": "DEBUG"
}
}
}
  • 通过/health端点,可以查看应用的健康指标。
1
2
3
json复制代码{
"status": "UP"
}

关闭应用

通过POST请求/shutdown端点可以直接关闭应用,但是需要将endpoints.shutdown.enabled属性设置为true才可以使用。

1
2
3
json复制代码{
"message": "Shutting down, bye..."
}

定制Actuator

有的时候,我们需要自定义一下Actuator的端点才能满足我们的需求。

  • 比如说Actuator有些端点默认是关闭的,我们想要开启所有端点,可以这样设置;
1
2
3
4
5
yaml复制代码management:
endpoints:
web:
exposure:
include: '*'
  • 比如说我们想自定义Actuator端点的基础路径,比如改为/monitor,这样我们我们访问地址就变成了这个:http://localhost:8088/monitor
1
2
3
4
yaml复制代码management:
endpoints:
web:
base-path: /monitor

常用起步依赖

起步依赖不仅能让构建应用的依赖配置更简单,还能根据提供给应用程序的功能将它们组织到一起,这里整理了一些常用的起步依赖。

官方依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
xml复制代码<dependencies>
<!--SpringBoot整合Web功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringBoot整合Actuator功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot整合AOP功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--SpringBoot整合测试功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringBoot整合注解处理功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--SpringBoot整合Spring Security安全功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--SpringBoot整合Redis数据存储功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--SpringBoot整合Elasticsearch数据存储功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!--SpringBoot整合MongoDB数据存储功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!--SpringBoot整合AMQP消息队列功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--SpringBoot整合Quartz定时任务功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--SpringBoot整合JPA数据存储功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--SpringBoot整合邮件发送功能依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>

第三方依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
xml复制代码<dependencies>
<!--SpringBoot整合MyBatis数据存储功能依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-version.version}</version>
</dependency>
<!--SpringBoot整合PageHelper分页功能依赖-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper-starter.version}</version>
</dependency>
<!--SpringBoot整合Druid数据库连接池功能依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--SpringBoot整合Springfox的Swagger API文档功能依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox-version}</version>
</dependency>
<!--SpringBoot整合MyBatis-Plus数据存储功能依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-version}</version>
</dependency>
<!--SpringBoot整合Knife4j API文档功能依赖-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j-version}</version>
</dependency>
</dependencies>

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android Jetpack 开发套件

发表于 2020-11-25

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

Android Jetpack 开发套件是 Google 推出的 Android 应用开发编程范式,为开发者提供了解决应用开发场景中通用的模式化问题的最佳实践,让开发者可将时间精力集中于真正重要的业务编码工作上。

这篇文章是 Android Jetpack 系列文章的第 9 篇文章,完整目录可以移步至文章末尾~

前言

大家好,我是小彭。

2020 年 10 月 28 日,JetPack | App Startup 1.0.0 终于迎来正式发布,正好最近在总结组件化架构专题,所以也专门学习下 App Startup 的工作原理。在这篇文章里,我将带你总结 App Startup 的使用方法 & 实现原理 & 源码分析。有用请点赞给 Star,给小彭一点创作的动力,谢谢。

记录:2022 年 9 月 4 日修订:优化文章结构


学习路线图:


  1. 认识 AppStartup

1.1 App Startup 解决了什么问题?

App Startup 是 Google 提供的 Android 轻量级初始化框架:

  • 优点:使用 App Startup 框架,可以简化启动序列并显式设置初始化依赖顺序,在简单、高效这方面,App Startup 基本满足需求。
  • 不足:App Startup 框架的不足也是因为它太简单了,提供的特性太过简单,往往并不能完美契合商业化需求。例如以下特性 App Startup 就无法满足:
    • 缺乏异步等待: 同步等待指的是在当前线程先初始化所依赖的组件,再初始化当前组件,App Startup 是支持的,但是异步等待就不支持了。举个例子,所依赖的组件需要执行一个耗时的异步任务才能完成初始化,那么 App Startup 就无法等待异步任务返回;
    • 缺乏依赖回调: 当前组件所依赖的组件初始化完成后,未发出回调。

1.2 App Startup 如何实现自动初始化?

App Startup 利用了 ContentProvider 在应用启动的时候初始化的特性,提供了一个自定义 ContentProvider 来实现自动初始化。很多库都利用了 ContentProvider 的启动机制,来实现无侵入初始化,例如 LeakCanary 等

使用 AppStartup 还能够合并所有用于初始化的 ContentProvider ,减少创建 ContentProvider,并提供全局管理。

App Startup 示意图

详细的源码分析下文内容。


  1. App Startup 使用方法

这一节,我们来总结 App Startup 的使用步骤。

2.1 基本用法

  • 1、添加依赖

在模块级 build.gradle 添加依赖:

模块级 build.gradle

1
groovy复制代码implementation "androidx.startup:startup-runtime:1.0.0"
  • 2、实现 Initializer 接口

Initializer 接口是 App Startup 定义组件接口,用于指定组件的初始化逻辑和初始化顺序(也就是依赖关系),接口定义如下:

  • 1、create(…) 初始化操作: 返回的初始化结果将被缓存,其中 context 参数就是当前进程的 Application 对象;
  • 2、dependencies() 依赖关系: 返回值是一个依赖组件的列表,如果不需要依赖于其它组件,返回一个空列表。App Startup 在初始化当前组件时,会保证所依赖的组件已经完成初始化。

Initializer.java

1
2
3
4
5
6
7
8
9
10
java复制代码public interface Initializer<T> {

// 1、初始化操作,返回值将被缓存??
@NonNull
T create(@NonNull Context context);

// 2、依赖关系,返回值是一个依赖组件的列表
@NonNull
List<Class<? extends Initializer<?>>> dependencies();
}

示例程序

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// LeakCanary 2.9.1
internal class AppWatcherStartupInitializer : Initializer<AppWatcherStartupInitializer> {
override fun create(context: Context) = apply {
// 实现初始化操作
val application = context.applicationContext as Application
AppWatcher.manualInstall(application)
}

override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}
  • 3、配置

在 Manifest 文件中将 Initializer 实现类配置到 androidx.startup.InitializationProvider 的 <meta-data> 中。

示例程序

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!-- LeakCanary 2.9.1 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">

<meta-data
android:name="leakcanary.internal.AppWatcherStartupInitializer"
android:value="androidx.startup"/>
</provider>

要点如下:

  • 1、组件名必须是 androidx.startup.InitializationProvider;
  • 2、需要声明 android:exported="false",以限制其他应用访问此组件;
  • 3、要求 android:authorities 要求在设备中全局唯一,通常使用 ${applicationId} 作为前缀;
  • 4、需要声明 tools:node="merge",确保 manifest merger tool 能够正确解析冲突的节点;
  • 5、meta-data android:name 为组件的 Initializer 实现类的全限定类名,android:value 固定为 androidx.startup。

提示: 为什么要将 androidx.startup 设置为 value,而不是 name?因为在键值对中,name 是唯一的,而 value 是允许重复的,将 androidx.startup 放到 value 的话才能允许同时配置多个相同语义的 <meta-data>。

至此,App Startup 基本的使用与配置完成,在应用启动时,App Startup 会自动收集各个模块配置的 Initializer 实现类,并按照依赖顺序依次执行。

2.2 进阶用法

  • 1、手动初始化

当你的组件需要进行手动初始化,而不是自动初始化时(例如存在耗时任务),可以进行手动初始化,而且手动初始化是可以在子线程调用的,而自动初始化均是在主线程执行的。

  • App Startup 中会缓存初始化后的结果,重复调用 initializeComponent() 也不会导致重复初始化;
  • 要手动初始化的 Initializer 实现类不能在声明到 AndroidManifest 中,也不能被其它组件依赖,否则它依然会自动初始化。

调用以下方即可进行手动初始化:

示例程序

1
kotlin复制代码AppInitializer.getInstance(context).initializeComponent(ExampleLoggerInitializer::class.java)
  • 2、取消自动初始化

假如有些库已经配置了自动初始化,而我们又希望进行懒加载时,就需要利用 manifest merger tool 的合并规则来移除这个库对应的 Initializer。具体如下:

示例程序

1
2
3
4
5
6
7
8
9
ini复制代码<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.ExampleLoggerInitializer"
tools:node="remove" />
</provider>
  • 3、禁用 App Startup

假如需要完全禁用 App Startup 自动初始化,同样也可以利用到 manifest merger tool 的合并规则:

示例程序

1
2
3
4
xml复制代码<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />

  1. App Startup 原理分析

3.1 App Startup 如何实现自动初始化?

App Startup 利用了 ContentProvider 的启动机制实现自动初始化。ContentProvider 通常的用法是为当前进程 / 远程进程提供内容服务,它们会在应用启动的时候初始化。利用这个特性,App Startup 的方案就是自定义一个 ContentProvider 的实现类 InitializationProvider,在 onCreate(…) 方法中执行初始化逻辑。

InitializationProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码已简化

public final class InitializationProvider extends ContentProvider {

@Override
public boolean onCreate() {
Context context = getContext();
if (context != null) {
// 初始化
AppInitializer.getInstance(context).discoverAndInitialize();
} else {
throw new StartupException("Context cannot be null");
}
return true;
}

@Override
public Cursor query(...) {
throw new IllegalStateException("Not allowed.");
}

@Override
public String getType(...) {
throw new IllegalStateException("Not allowed.");
}

@Nullable
@Override
public Uri insert(...) {
throw new IllegalStateException("Not allowed.");
}

@Override
public int delete(...) {
throw new IllegalStateException("Not allowed.");
}

@Override
public int update(...) {
throw new IllegalStateException("Not allowed.");
}
}

由于 ContentProvider 的其他方法是没有意义的,所以都抛出了 IllegalStateException。

3.2 说一下 App Startup 的初始化过程

从上一节可以看到,App Startup 在 InitializationProvider 中调用了AppInitializer#discoverAndInitialize()执行自动初始化。AppInitializer是 App StartUp 框架的核心类,整个 App Startup 框架的代码其实非常少,其中很大部分核心代码都在 AppInitializer 类中。

我将整个自动初始化过程概括为 3 个阶段:

  • 步骤 1 - 获取 数据: 扫描 Manifest 中定义在 InitializationProvider 里面的 数据,从中筛选出 Initializer 的配置信息;
  • 步骤 2 - 递归执行初始化器: 通过 Initializer#create() 执行每个初始化器的逻辑,并且会通过 Initializer#dependencies() 优先保证依赖项已经初始化;
  • 步骤 3 - 缓存初始化结果: 将初始化后的结果缓存到映射表中,避免重复初始化。

源码摘要如下:

AppInitializer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
java复制代码private static final Object sLock = new Object(); // 后面会提到

// 记录扫描 <meta-data> 得到的初始化器(可用于判断组件是否已经自动启动)
final Set<Class<? extends Initializer<?>>> mDiscovered;

// 缓存每个组件的初始化结果
final Map<Class<?>, Object> mInitialized;

void discoverAndInitialize() {
// 1、获取 androidx.startup.InitializationProvider 组件信息
ComponentName provider = new ComponentName(mContext.getPackageName(), InitializationProvider.class.getName());
ProviderInfo providerInfo = mContext.getPackageManager().getProviderInfo(provider, GET_META_DATA);

// 2、androidx.startup 字符串
String startup = mContext.getString(R.string.androidx_startup);

// 3、获取组件信息中的 meta-data 数据
Bundle metadata = providerInfo.metaData;

// 4、遍历所有 meta-data 数据
if (metadata != null) {
Set<Class<?>> initializing = new HashSet<>();
Set<String> keys = metadata.keySet();
for (String key : keys) {
String value = metadata.getString(key, null);

// 4.1 筛选 value 为 androidx.startup 的 meta-data 数据中
if (startup.equals(value)) {
Class<?> clazz = Class.forName(key);

// 4.2 检查指定的类是 Initializer 接口的实现类
if (Initializer.class.isAssignableFrom(clazz)) {
Class<? extends Initializer<?>> component = (Class<? extends Initializer<?>>) clazz;

// 4.3 将 Class 添加到 mDiscovered Set 中
mDiscovered.add(component);

// 4.4 初始化此组件
doInitialize(component, initializing);
}
}
}
}
}

// -> 4.3 mDiscovered 用于判断组件是否已经自动启动
public boolean isEagerlyInitialized(@NonNull Class<? extends Initializer<?>> component) {
return mDiscovered.contains(component);
}

// -> 4.4 初始化此组件(已简化)
<T> T doInitialize(Class<? extends Initializer<?>> component, Set<Class<?>> initializing) {
// 1、对 sLock 加锁,我后文再说。

Object result;

// 2、判断 initializing 中存在当前组件,说明存在循环依赖
if (initializing.contains(component)) {
String message = String.format("Cannot initialize %s. Cycle detected.", component.getName());
throw new IllegalStateException(message);
}

// 3、检查当前组件是否已初始化
if (!mInitialized.containsKey(component)) {
// 3.1 当前组件未初始化

// 3.1.1 记录正在初始化
initializing.add(component);

// 3.1.2 通过反射实例化 Initializer 接口实现类
Object instance = component.getDeclaredConstructor().newInstance();
Initializer<?> initializer = (Initializer<?>) instance;

// 3.1.3 遍历所依赖的组件(关键:优先处理依赖的组件)
List<Class<? extends Initializer<?>>> dependencies = initializer.dependencies();
if (!dependencies.isEmpty()) {
for (Class<? extends Initializer<?>> clazz : dependencies) {

// 递归:如果所依赖的组件未初始化,执行初始化
if (!mInitialized.containsKey(clazz)) {
// 注意:这里将 initializing 作为参数传入,用于判断循环依赖
doInitialize(clazz, initializing);
}
}
}

// 3.1.4 (到这里,所依赖的组件已经初始化完成)初始化当前组件
result = initializer.create(mContext);

// 3.1.5 移除正在初始化记录
initializing.remove(component);

// 3.1.6 缓存初始化结果
mInitialized.put(component, result);
} else {
// 3.2 当前组件已经初始化,直接返回
result = mInitialized.get(component);
}
return (T) result;
}

3.3 手动初始化的执行过程

前面我们提到使用 initializeComponent() 方法可以手动初始化,我们来看手动初始化(懒加载)的源码:

AppInitializer.java

1
2
3
4
java复制代码public <T> T initializeComponent(@NonNull Class<? extends Initializer<T>> component) {
// 调用 doInitialize(...) 方法:
return doInitialize(component, new HashSet<Class<?>>());
}

其实非常简单,就是调用上一节的 doInitialize(...) 执行初始化。需要注意的是,这个方法是允许在子线程调用的,换句话说,自动初始化与手动初始化是存在线程同步问题的,那么 App Startup 是如何解决的呢?还记得我们前面有一个 sLock 没有说吗?其实它就是用来保证线程同步的锁:

AppInitializer.java

1
2
3
4
5
6
java复制代码<T> T doInitialize(Class<? extends Initializer<?>> component, Set<Class<?>> initializing) {
// 1、对 sLock 加锁
synchronized (sLock) {
...
}
}

  1. 总结

到这里,App Startup 的内容就讲完了。可以看到 App Startup 只是一个轻量级的初始化框架,能做的事情有限。市面上有开发者开源了基于 DAU 有向无环图的初始化框架,这个我们下次再说。关注我,带你了解更多。

参考资料

  • App Startup —— Android Developers
  • 合并多个清单文件 —— Android Developers
  • AndroidX: App Startup —— Husayn Hakeem 著
  • Jetpack新成员,App Startup 一篇就懂 —— 郭霖 著
  • 我为何弃用 Jetpack 的 App Startup? —— 午后一小憩 著
  • 更快!这才是我想要的 Android Startup 库! —— idisfkj 著
  • 组件化:代码隔离也难不倒组件的按序初始化 —— leobert-lan 著
  • 从源码看 Jetpack(5)Startup 源码详解 —— 叶志陈 著

推荐阅读

Android Jetpack 系列文章目录如下(2023/07/08 更新):

  • #1 Lifecycle:生命周期感知型组件的基础
  • #2 为什么 LiveData 会重放数据,怎么解决?
  • #3 为什么 Activity 都重建了 ViewModel 还存在?
  • #4 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
  • #5 Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI
  • #6 ViewBinding 与 Kotlin 委托双剑合璧
  • #7 AndroidX Fragment 核心原理分析
  • #8 OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势
  • #9 食之无味!App Startup 可能比你想象中要简单
  • #10 从 Dagger2 到 Hilt 玩转依赖注入(一)

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HIVE用户自义定函数 正则匹配所有子串

发表于 2020-11-24

函数: 正则匹配返回所有子串,并返回array

regexp_extract_all(字段: string, 正则: string, group: int),返回array: string,可用于一行转多行

代码地址:https://github.com/leeshuaichao/hive_functions

创建mvn项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.moxi.hive</groupId>
<artifactId>hive_udf</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>HiveUDFs</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
<version>3.1.2</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

编写UDTF类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
java复制代码package com.moxi.hive.udf.regexp;

import com.moxi.hive.udf.utils.RegexpUtils;
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.exec.UDFArgumentLengthException;
import org.apache.hadoop.hive.ql.exec.UDFArgumentTypeException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorUtils;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.io.IntWritable;

/**
* 正则匹配,返回匹配到的所有子串(返回regexp_extract的全部结果)
* regexp_extract_all(字段, 正则, 返回第几个括号内的内容:0是全部)
* @author lishuaichao@xi-ai.com
* 2020/11/23 下午2:33
**/
public class UdtfRegexpExtractAll extends GenericUDF {
@Override
public ObjectInspector initialize(ObjectInspector[] objectInspectors) throws UDFArgumentException {
// Check if two arguments were passed
if (objectInspectors.length != 2 && objectInspectors.length != 3) {
throw new UDFArgumentLengthException(
"The function regexp_extract_all takes exactly 2 or 3 arguments.");
}

for (int i = 0; i < 2; i++) {
if (!ObjectInspectorUtils.compareTypes(PrimitiveObjectInspectorFactory.javaStringObjectInspector, objectInspectors[i])) {
throw new UDFArgumentTypeException(i,
"\"" + PrimitiveObjectInspectorFactory.javaStringObjectInspector.getTypeName() + "\" "
+ "expected at function regexp_extract_all, but "
+ "\"" + objectInspectors[i].getTypeName() + "\" "
+ "is found");
}
}

if (objectInspectors.length == 3) {
if (!ObjectInspectorUtils.compareTypes(PrimitiveObjectInspectorFactory.javaIntObjectInspector, objectInspectors[2])) {
throw new UDFArgumentTypeException(2,
"\"" + PrimitiveObjectInspectorFactory.javaLongObjectInspector.getTypeName() + "\" "
+ "expected at function regexp_extract_all, but "
+ "\"" + objectInspectors[2].getTypeName() + "\" "
+ "is found");
}
}

ObjectInspector expect = PrimitiveObjectInspectorFactory.javaStringObjectInspector;

return ObjectInspectorFactory.getStandardListObjectInspector(expect);
}

@Override
public Object evaluate(DeferredObject[] deferredObjects) throws HiveException {
String source = deferredObjects[0].get().toString();
String pattern = deferredObjects[1].get().toString();
Integer groupIndex = 0;
if (deferredObjects.length == 3) {
groupIndex = ((IntWritable) deferredObjects[2].get()).get();
}

if (source == null) {
return null;
}

return RegexpUtils.findAll(pattern, source, groupIndex);
}

@Override
public String getDisplayString(String[] strings) {
assert (strings.length == 2 || strings.length == 3);
if (strings.length == 2) {
return "regexp_extract_all(" + strings[0] + ", "
+ strings[1] + ")";
} else {
return "regexp_extract_all(" + strings[0] + ", "
+ strings[1] + ", " + strings[2] + ")";
}
}

}

正则工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码package com.moxi.hive.udf.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* 正则工具类
* lishuaichao@xi-ai.com
* 2020/11/23 下午2:45
**/
public class RegexpUtils {
/**
* 查询所有子串并返回list
* @param regex 正则表达式
* @param content 被识别字符串
* @param group 获取第几个括号的内容,0为整个正则
* @return
*/
public static List<String> findAll(String regex, CharSequence content, int group) {
List<String> collection = new ArrayList<>();
Pattern pattern = Pattern.compile(regex);
if (null != content) {
Matcher matcher = pattern.matcher(content);
while(matcher.find()) {
collection.add(matcher.group(group));
}
}
return collection;
}

}

打包并上传到服务器测试

遇到的坑:

很久没有用maven打普通jar包了,maven把依赖打进来需要特殊处理,因此去掉了使用hutool工具类

创建临时函数

1
2
3
4
bash复制代码# 添加jar包到当前窗口
add jar /home/hive/apache-hive-3.1.2/lib/hive_udf-1.0-SNAPSHOT.jar;
# 创建临时函数
create temporary function regexp_extract_all AS 'com.moxi.hive.udf.regexp.UdtfRegexpExtractAll';

测试临时函数

1
2
3
hiveql复制代码select voice_num from (
select regexp_extract_all(ret.abc, "@#(.*?)#@", 1) as vn from (select "@#命中5#@我要承@#命中1#@@#命中2#@诺还款, 你@#命中3#@说我应该怎么办呢诺兰@#命中4#@" as abc) ret) test
LATERAL VIEW explode(test.vn) r as voice_num;

删除临时函数

1
2
hiveql复制代码drop temporary function regexp_extract_all;
delete jar /home/hive/apache-hive-3.1.2/lib/hive_udf-1.0-SNAPSHOT.jar;

生成永久函数

把jar包上传到hdfs

1
2
3
4
5
6
bash复制代码# 创建hdfs目录
hadoop fs -mkdir /lib
# jar添加到hdfs
hadoop fs -put /home/hive/apache-hive-3.1.2/lib/hive_udf-1.0-SNAPSHOT.jar /lib/
# 查看是否添加成功
hadoop fs -lsr /lib

创建永久函数

1
2
hiveql复制代码create function data_mart.regexp_extract_all AS 'com.moxi.hive.udf.regexp.UdtfRegexpExtractAll' using jar 'hdfs:/lib/hive_udf-1.0-SNAPSHOT.jar';
create function data_center.regexp_extract_all AS 'com.moxi.hive.udf.regexp.UdtfRegexpExtractAll' using jar 'hdfs:/lib/hive_udf-1.0-SNAPSHOT.jar';

测试

1
2
3
hiveql复制代码select voice_num from (
select regexp_extract_all(ret.abc, "@#(.*?)#@", 1) as vn from (select "@#命中5#@我要承@#命中1#@@#命中2#@诺还款, 你@#命中3#@说我应该怎么办呢诺兰@#命中4#@" as abc) ret) test
LATERAL VIEW explode(test.vn) r as voice_num;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NestJS JWT实现用户认证 初始化项目 User Mo

发表于 2020-11-24

功能说明:

  1. client端使用用户名和用户密码登录,登录成功后server端发送JWT;
  2. client在header中携带JWT访问server,server端对client端携带的JWT进行认证;

初始化项目

创建项目并初始化User,Auth模块,terminal执行:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码 # 新建nestjs工程: auth-jwt
nest new auth-jwt
# 初始化UserModule,AuthModule
cd auth-jwt
nest g module auth
nest g service auth
nest g controller auth

nest g module user
nest g service user
nest g controller user

工程结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码├── nest-cli.json
├── package.json
├── package-lock.json
├── README.md
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── auth
│ │ ├── auth.controller.spec.ts
│ │ ├── auth.controller.ts
│ │ ├── auth.module.ts
│ │ └── auth.service.ts
│ ├── main.ts
│ └── user
│ ├── user.controller.ts
│ ├── user.module.ts
│ └── user.service.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

User Module

  1. 通过用户名获取用户;
  2. 获取用户列表;
  3. 导出 User Service以便Auth Service引用;

user.entity.ts:

1
2
3
4
5
typescript复制代码export interface UserEntity {
id: number;
username: string;
password?: string;
}

user.service.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typescripy复制代码import { Injectable } from '@nestjs/common';
import { UserEntity } from './user.entity';

@Injectable()
export class UserService {
private readonly users: Array<UserEntity>;

constructor() {
this.users = [
{ id: 1, username: 'admin', password: 'admin' },
{ id: 2, username: 'tester', password: 'tester' },
];
}

/**
* find user by username
* @param username
*/
async find(username: string): Promise<UserEntity> {
const user = this.users.find((user) => user.username === username);
if (user) return user;
else return null;
}

/**
* list all users
*/
async listAll(): Promise<Array<UserEntity>> {
return this.users.map((user) => {
const { password, ...info } = user;
return info;
});
}
}

user.controller.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';
import { UserEntity } from './user.entity';

@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {
this.userService = userService;
}

@Get()
async list(): Promise<Array<UserEntity>> {
return this.userService.listAll();
}
}

User Module中导出UserService,user.module.ts

1
2
3
4
5
6
7
8
9
10
typescript复制代码import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
providers: [UserService],
controllers: [UserController],
exports: [UserService],
})
export class UserModule {}

Auth Module,实现用户登录

安装登录认证所需依赖passport, passport-local, @nestjs/passport, @types/passport-local, terminal中执行:

1
2
bash复制代码npm i passport passport-local @nestjs/passport
npm i @types/passport-local -D

账户、密码认证策略实现

AuthService实现用户身份认证

auth.service.ts,用户名密码验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typescript复制代码import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { UserEntity } from '../user/user.entity';

@Injectable()
export class AuthService {
constructor(private readonly userService: UserService) {
this.userService = userService;
}

/**
* validate user name and password
* @param username
* @param password
*/
async validate(username: string, password: string): Promise<UserEntity> {
const user = await this.userService.find(username);
// 注:实际中的密码处理应通过加密措施
if (user && user.password === password) {
const { password, ...userInfo } = user;
return userInfo;
} else {
return null;
}
}
}

LocalStrategy,实现账户、密码认证策略。

auth目录下新建local.strategy.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserEntity } from '../user/user.entity';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
this.authService = authService;
}

async validate(username: string, password: string): Promise<UserEntity> {
const user = await this.authService.validate(username, password);
if (user) return user;
else throw new UnauthorizedException('incorrect username or password');
}
}

validate方法为默认的用户身份认证的实现,passport-local守卫将自动调用。

用户登录API

AuthController中实现login API,使用passport-local守卫。
auth.controller.ts:

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { UserEntity } from '../user/user.entity';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth')
export class AuthController {
@UseGuards(AuthGuard('local'))
@Post('/login')
async login(@Request() request): Promise<UserEntity> {
return request.user;
}
}

@UseGuards(AuthGuard('local'))守卫将从body中提取username、password,然后调用LocalStrategy中的validate方法,若认证通过,则将User信息赋值给request.user。

auth.module.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { LocalStrategy } from './local.strategy';

@Module({
imports: [UserModule, PassportModule],
providers: [AuthService, LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {}

npm run start启动服务,使用POST请求访问http://127.0.0.1:3000/auth/login,若用户名、密码正确,则获得用户信息,否则response code为401。curl命令如下:

1
2
3
bash复制代码curl -X POST http://127.0.0.1:3000/auth/login -d '{"username": "admin", "password": "123456"}' -H "Content-Type: application/json"

{"statusCode":401,"message":"incorrect username or password","error":"Unauthorized"}

JWT Strategy实现

至此,使用Local Strategy用户认证守卫完成了用户登录功能.现在我们来实现:

  1. 当用户登录成功后下发access_token;
  2. User List服务使用jwt认证。

安装依赖

terminal执行:

npm i passport-jwt @nestjs/jwt
npm i @types/passport-jwt -D

重写login,登录成功下发access_token

auth.service.ts添加login方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
typescript复制代码import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { UserEntity } from '../user/user.entity';
import { JwtService } from '@nestjs/jwt';
import { TokenEntity } from './token.entity';

@Injectable()
export class AuthService {
private readonly userService: UserService;
private readonly jwtService: JwtService;
constructor(userService: UserService, jwtService: JwtService) {
this.userService = userService;
this.jwtService = jwtService;
}

/**
* validate user name and password
* @param username
* @param password
*/
async validate(username: string, password: string): Promise<UserEntity> {
const user = await this.userService.find(username);
// 注:实际中的密码处理应通过加密措施
if (user && user.password === password) {
const { password, ...userInfo } = user;
return userInfo;
} else {
return null;
}
}

/**
* user login
* @param user
*/
async login(user: UserEntity): Promise<TokenEntity> {
const { id, username } = user;
return {
token: this.jwtService.sign({ username, sub: id }),
};
}
}

auth.controller.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { TokenEntity } from './token.entity';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {
this.authService = authService;
}
@UseGuards(AuthGuard('local'))
@Post('/login')
async login(@Request() request): Promise<TokenEntity> {
return this.authService.login(request.user);
}
}

AuthModule 中注册 JwtModule

auth目录下新建jwt.contants.ts:

1
2
3
typescript复制代码export const jwtContants = {
secret: 'json_web_token_secret_key',
};

注册JwtModule,auth.module.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '@nestjs/jwt';
import { jwtContants } from './jwt.contants';

@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: jwtContants.secret,
}),
],
providers: [AuthService, LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {}

启动服务,访问登录服务,认证通过后将返回token:

1
2
erlang复制代码curl -X POST http://127.0.0.1:3000/auth/login -d '{"username": "admin", "password": "admin"}' -H "Content-Type: application/json"
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoxLCJpYXQiOjE2MDYxNDQwMjF9.IIOMnGgjMmaqVB4RhNGBxS_rEKuSLsr40yG_ooTuFVU"}

JWT access_token认证

auth下新建jwt.strategy.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtContants } from './jwt.contants';
import { UserEntity } from '../user/user.entity';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
// 获取请求header token值
jwtFromRequest: ExtractJwt.fromHeader('token'),
secretOrKey: jwtContants.secret,
});
}

async validate(payload: any): Promise<UserEntity> {
//payload:jwt-passport认证jwt通过后解码的结果
return { username: payload.username, id: payload.sub };
}
}

auth.module.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '@nestjs/jwt';
import { jwtContants } from './jwt.contants';
import { JwtStrategy } from './jwt.strategy';

@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: jwtContants.secret,
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}

user.controller.ts 添加jwt认证守卫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码import { Controller, Get, UseGuards } from '@nestjs/common';
import { UserService } from './user.service';
import { UserEntity } from './user.entity';
import { AuthGuard } from '@nestjs/passport';

@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {
this.userService = userService;
}

@UseGuards(AuthGuard('jwt'))
@Get()
async list(): Promise<Array<UserEntity>> {
return this.userService.listAll();
}
}

启动服务,访问http://127.0.0.1:3000/user,token认证通过将获得用户列表:

1
2
3
bash复制代码curl http://127.0.0.1:3000/user -H "token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoxLCJpYXQiOjE2MDYxODA4MjR9.y9xt_rn6nORS5MEU18MeNB0brnGHvZLxe7sAYNkz0KY"
## 返回
[{"id":1,"username":"admin"},{"id":2,"username":"tester"}]
  1. demo工程源码 github.com/louie-001/n…
  2. NestJS 官方nestjs.com/。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【算法与数据结构 03】什么是DFS和BFS?

发表于 2020-11-23

这两周陷入了测试和作业的漩涡中,今天才有时间坐在电脑前码字了。好吧,我承认我是在找借口了QAQ

  • 引入
  • 介绍
    • DFS
    • BFS
  • 应用
    • DFS应用一:二叉树中的遍历
    • DFS应用二:岛屿问题
    • BFS应用一:二叉树的层序遍历
    • BFS应用二:图的BFS
    • BFS应用三:岛屿问题

引入

DFS(Depth First Search) 即深度优先搜索,而提到DFS就得说起BFS(Breadth First Search) 广度优先搜索 了

在我的上一篇文章 二叉树的引入 当中,我有提到 二叉树的前序、中序、后序遍历本质和DFS相同,而层序遍历本质和BFS相同,那么,DFS和BFS又是什么呢,怎么去实现它们呢~

来看看我在百科上看到的图,是和这次谈到的内容有关的哦~

自然界中的宽搜

自然界中的宽搜

介绍

DFS

假如去游玩一个市里面的景点,从景点0开始:

DFS的介绍

DFS的介绍

要去玩这些景点有很多种方式,那怎样才算是DFS呢~

这里先给出DFS的概念

深度优先搜索 简单来说就是 先深入探索,走到头再回退来寻找其他路径继续,以此反复 的遍历方式

也就是说,它可以是这样去走的:

DFS的介绍

DFS的介绍

拿二叉树举例来说可以是这样走的(二叉树的前序遍历):

二叉树的DFS

二叉树的DFS

那如果不是二叉树呢,而是在一个图中呢,就比如一个二维数组中:

二维数组

二维数组

又怎么走完这个图中的灰色部分呢?

首先依照遍历二维数组的方式,总可以找到第一个灰色部分,然后从这个灰色部分开始 深入探索

因为是要走完所有的灰色部分,所以很显然要先探索与之相邻的四个方框(当然,不符合就直接return啦~):

图的BFS

图的BFS

再然后像之前走完所有符合的路径就像下面这个样子了:

图的BFS

图的BFS

细心的读者可能会发现,因为是要探索与之相邻的四个方框,那么到下一相邻方框时岂不是要返回重新来过一遍,这里先将这个疑问放着,等介绍算法实现部分再来解释吧~

BFS

而BFS简单来说就是 一层一层由内而外 的遍历方式

那么,又怎么通过BFS的方式来走完上面的景点呢,它可以是这样走的:

BFS的介绍

BFS的介绍

对于二叉树来说差不多是这样走的(二叉树的层序遍历):

二叉树的层序遍历

二叉树的层序遍历

那如果是在一个二维数组中呢,与DFS又有什么不同呢?

它也是从一个方框依次向周围探索,与DFS区别不是很大,直接上图:

二维数组的BFS

二维数组的BFS

相信到这里朋友们对于DFS和BFS是怎样走的应该了解了,那么接下来该说说它们的应用及算法实现了

应用

DFS应用一:二叉树中的遍历

DFS在二叉树中的遍历概括起来就是使用递归:

1
2
3
4
5
6
7
复制代码void dfs(TreeNode node) {
    if (node == null) {
        return;
    }
    dfs(node.left);
    dfs(node.right);
}

然后根据遍历父结点的顺序分为了前序、中序、后序遍历(这里不深入探讨了)

DFS应用二:岛屿问题

由 m*n 个小方块组成的网格上(每一个小方块都与周围四个相邻,可以参考上文),在这样的网格上进行搜索

空头说感觉不太好说,不如直接拿道题出来说说吧~

比如在这道题中:

463. 岛屿的周长

1表示陆地,0表示水域(目的是遍历完陆地的部分):

岛屿的周长

岛屿的周长

在上文也提到 从一个小方格要去探索周围四个方格,以此来走完所有陆地的部分

首先要注意的是边界的问题:

1
2
3
4
5
6
复制代码void dfs(int[][] grid, int r, int c){
    // 如果坐标不合法,直接返回
    if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) {
        return;
    }
}

另外要注意的就是上面留下的疑问了:遍历过的网格如何确定它遍历过没有,这样就不至于卡在死循环里

题目中是用数值来表示陆地和水域,那么可以改变遍历过的网格 的数值(当然,这个值别是0、1就好),以此来判断它走没走过,是不是很巧妙 :D

最后实现起来大抵是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码void dfs(int[][] grid, int r, int c){
    if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) {
        return;
    }
    // 如果这个方格不是岛屿,也直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 标记遍历过的岛屿
    dfs(grid, r - 1, c); // 探索上边的方格
    dfs(grid, r + 1, c); // 下边的
    dfs(grid, r, c - 1); // 左边的
    dfs(grid, r, c + 1); // 右边的
}

除了上述两种应用较多外,还有其他的问题大抵上也都是用的 回溯 的思想

这里提到了 回溯 :从一条路往前走,能进则进,不能进则退回来,换一条路再试 或者说 自后向前,追溯曾经走过的路径

而想要实现 回溯,可以利用 栈 的先入后出的特性,也可以采用 递归 的方式,而递归本身就是基于方法调用栈来实现的

而相对的,BFS的实现关键在于 重放,也就是 将遍历过的结点按之前的遍历顺序重新回顾,可以利用队列的先入先出的特性来实现。说那么多 不如来看看下面的例子吧~

BFS应用一:二叉树的层序遍历

BFS在二叉树中的遍历使用队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码void bfs(TreeNode node){
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(node);
    while (!queue.isEmpty()){
        int n = queue.size();
        for (int i = 0; i < n; i++) {
            TreeNode cur = queue.poll();
            if(cur.left != null) {
                queue.add(cur.left);
            }
            if(cur.right != null) {
                queue.add(cur.right);
            }
        }
    }
}

相比于DFS,BFS就显得比较繁琐了,这是因为 递归 隐含的使用了系统的 栈,而我们就不需要自己维护一个数据结构了

除此之外,两者遍历结点的顺序也不同

BFS应用二:图的BFS

图的BFS

图的BFS

与层序遍历类似,同样需要使用队列,它的代码框架大概时这样的:

1
2
3
4
5
6
7
8
9
10
复制代码Queue<TreeNode> queue = new ArrayDeque<>();
while (!queue.isEmpty()){
    int n = queue.size();
    for (int i = 0; i < n; i++) {
        queue.poll();
        if () { // 若m结点没有访问过
            queue.add(m);
        }
    }
}

上面的岛屿问题也可以用BFS来实现,也与上面的图的BFS类似

BFS应用三:岛屿问题

图的BFS

图的BFS

同样的,0表示海洋,1表示陆地,从陆地依次向外遍历

BFS又是如何实现探索周围四个方块,这里可以用两个数组来间接实现:

1
2
3
4
5
复制代码void bfs(int[][] grid){
    //当前结点下标依次加上 dx[i], dy[i] 就可以得到新的下标
    int[] dx = {0, 0, 1, -1};
    int[] dy = {1, -1, 0, 0};
}

另外还需要队列来实现BFS,最后大概是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码void bfs(int[][] grid){
    int[] dx = {0, 0, 1, -1};
    int[] dy = {1, -1, 0, 0};
    
    Queue<int[]> queue = new ArrayDeque<>();
    int m = grid.length, n = grid[0].length;
    // 将所有的陆地都入队。
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 1) {
                queue.offer(new int[] {i, j});
            }
        }
    }
    
    // 然后从陆地开始一圈一圈的遍历
    int[] point = null;
    while (!queue.isEmpty()) {
        point = queue.poll();
        int x = point[0], y = point[1];
        // 取出队列的元素,将其四周的海洋入队。
        for (int i = 0; i < 4; i++) {
            int newX = x + dx[i];
            int newY = y + dy[i];
            // 边界的判断
            if (newX < 0 || newX >= m || newY < 0 || newY >= n || grid[newX][newY] != 0) {
                continue;
            }
            // 这里直接修改了原数组的值,以此来标记是否被访问
            grid[newX][newY] = grid[x][y] + 1; 
            queue.offer(new int[] {newX, newY});
        }
    }
}

感觉自己的输出效率蛮低的,可能还是不太会写博客吧,不过我会坚持下去的:D

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

极简策略模式

发表于 2020-11-23

思变

近期因为重构一个项目,有时间对老代码进行改造,发现很多接口初期设计良好,但是在各种需求的侵袭之下,也变得越加的混乱,最后就变成了所谓的「垃圾堆」,各种IF-ELSE满天飞,接手的人叫苦不迭,最后只能闻着恶臭,捏着鼻子继续往「垃圾堆」里扔垃圾。最后垃圾堆坍塌,重新开新项目

困境

  1. IF-ELSE代码块多,改造时间短,不宜「伤筋动骨」
  2. 使用策略模式消灭IF-ELSE代码块复杂且代码量大,增加许多策略类
  3. 尽量复用已有的逻辑,不增加新的代码

所得

对Java8的使用有一段时间了,非常喜欢「Steam」数据处理,感觉根本不是在写Java代码,复杂的代码木有啦,流线型代码写起来,额。。。跑题了,其实最好想说的是Java8新加的包「Function」,所谓的「函数式」编程,当然对于这种函数式编程是不是弱化版的不说,其实这个包下面有很多好的工具,可以简化我们的工作

注:以下代码只给出核心部分,其他方法请自行查看源码

  1. Predicate BiPredicate
1
2
3
4
5
6
7
8
9
复制代码
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u);
}

❝
第一个工具是「Predicate」,他的作用是什么呢?顾名思义,他提供一种判断逻辑,他可以替换「策略模式」中「钩子」方法,「钩子」方法就是决定使用何种策略去处理当前逻辑,当然「钩子」可以是由工厂方法提供,但是极简模式的策略选择将「钩子」置于策略内部
PS:这边给出两个「Predicate」是方便大家处理多种入参

❞

  1. Consumer Function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码//Consumer
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
}
//Function
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

❝
第二个工具是Consumer或者Function,两种工具代表策略处理的两种情况,即第一:纯消费(存库等),第二:有生产(过滤数据等),他们就是用来替换策略模式中的策略部分的,放弃编写复杂的策略接口以及策略实现类,简化策略模式的规模,减少代码以及时间成本

❞

  1. Pair
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码// 笔者使用org.springframework.data.util下的Pair工具
// 因为不需要引入其他依赖
// PS: Pair的实现很多,可以自行选择食用
public final class Pair<S, T> {
    @NonNull
    private final S first;
    @NonNull
    private final T second;

    public static <S, T> Pair<S, T> of(S first, T second) {
        return new Pair(first, second);
    }

    public S getFirst() {
        return this.first;
    }

    public T getSecond() {
        return this.second;
    }
}

❝
很明显,「Pair」是用来存放策略的,使得「钩子」和「策略逻辑」组装形成「策略处理单元」,说白了就是一个工具容器,这个容器种类很多,可以自己找一个自己喜欢的,笔者只是就近挑选

❞

试试

❝
经常有人在群里问一些其实自己写个Main方法就能测试出来的问题,然后大家的问答是:你自己试试呗!所以,程序员一定要会自己试试,所以试试先

❞

  1. 第一步找一个IF-ELSE块

一个简单权限过滤功能,可以写出3个IF嵌套,逻辑复杂一点呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码//PowerQuery结构在下方
public List<String> filterPowerByQuery(List<String> powers, PowerQuery query) {
        if ("ALL".equalsIgnoreCase(query.getType())) {
            //内部用户和外部用户ALL返回全部
            return powers;
        }
        if (new Integer(0).equals(query.getUserType())) {
            //内部用户
            if (StringUtils.isNotBlank(query.getPowerName())) {
                //内部用户可以查看 类型  权限 (以PowerName为前缀)
                return powers.stream()
                             .filter(s -> StringUtils.startsWithIgnoreCase(s, query.getPowerName()))
                             .collect(Collectors.toList());
            }
            //内部用户其他情况
            return powers;
        } else {
            //非ALL的情况下,外部用户一次只能查看一种权限的数据
            return powers.stream()
                         .filter(s -> StringUtils.equals(query.getPowerName(), s))
                         .collect(Collectors.toList());
        }
    }

PowerQuery的结构,简单三个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码@Data
public class PowerQuery {
    /**
     * ALL-全部
     * 其他值-无效
     */
    private String type;

    /**
     * 0-内部
     * 1-外部
     */
    private Integer userType;

    /**
     * 如果不是ALL角度查看
     * 外部用户一次只能查看一个权限
     */
    private String powerName;
}
  1. 盘他
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码public List<String> filterPowerByStrategy(List<String> allPowers, PowerQuery powerQuery) {
        //这个例子中策略模式有明显的链式的规则
        //但是使用List也可以很好地反应这种规则
        //类似Spring的DispatchServlet中的各种Resolver等也是List组织的
        List<Pair<Predicate<PowerQuery>, BiFunction<List<String>, PowerQuery, List<String>>>> chains = new ArrayList<>();
        //ALL的逻辑
        chains.add(Pair.of(query -> "ALL".equalsIgnoreCase(query.getType()), (powers, query) -> powers));
        //这里将外部用户的逻辑提到上部
        chains.add(Pair.of(query -> new Integer(1).equals(query.getUserType()), (powers, query) -> powers));
        //内部用户且PowerName有值
        chains.add(Pair.of(query -> new Integer(0).equals(query.getUserType()) && StringUtils.isNotBlank(query.getPowerName()),
                           (powers, query) -> powers.stream()
                                                    .filter(s -> StringUtils.startsWithIgnoreCase(s, query.getPowerName()))
                                                    .collect(Collectors.toList())));
        //最后增加一个收尾的策略 其他情况统一返回原全量权限
        chains.add(Pair.of(query -> true, (powers, query) -> powers));
        //使用策略List
        for (Pair<Predicate<PowerQuery>, BiFunction<List<String>, PowerQuery, List<String>>> chain : chains) {
            if (chain.getFirst().test(powerQuery)) {
                return chain.getSecond().apply(allPowers, powerQuery);
            }
        }
        //这个逻辑是不会走的
        return allPowers;
    }
  1. 方式描述

先梳理现有的逻辑,剥离策略的处理逻辑,将各个策略通过Predicate和Function组织起来,形成策略模式中的方法簇,最后通过「循环跳出」的方式进行策略「钩子命中」,策略逻辑「运行处理」,代理中其实有很多模仿的痕迹,比如策略使用「List」组织,「循环跳出」进行逻辑处理

总一个结

❝
笔者是很喜欢策略模式的,也会在日常的开发中尝试运用策略模式。在实践的过程中也体会到,策略模式有一定的运用门槛,且感觉策略模式体量较重,每次尝试运用实现,就是一个顶层「策略接口」,加下一大堆策略「实现方法簇」,但是其实平常最需要策略化的其实就是IF-ELSE,但是一搞就很麻烦,后面接手的兄弟也是一脸懵逼,大呼请容我看一会儿。。。这种极简的策略「贵在简单」,不增加太多的类和接口,简单的转化IF-ELSE,代码量没有明显的增加,同时也支持了「快速扩展」,如文中所言,确实是思变以后的成果。同时,也要指出这也是笔者「闭门造车」的结果,一家之言难免疏漏,今天分享出来也是希望给大家一些灵感,抛砖引玉、求同存异,有什么想法请在下方留言,大家讨论一下

❞

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaMail API 邮件发送与接收 一、概述 二、邮件

发表于 2020-11-22

一、概述

  JavaMail API 顾名思义,提供给开发者处理电子邮件相关的编程接口,它是Sun发布的用来处理email的API,其提供独立于平台且与协议无关的框架来构建邮件和消息传递应用。JavaMail API 提供了一组抽象类,用于定义组成邮件系统的对象,它是一个可选包(标准扩展名),用于阅读,撰写和发送电子信息。

开发人员使用JavaMail编写邮件程序时,不再需要考虑底层的通讯细节如:Socket,而是关注在逻辑层面。JavaMail可以发送各种复杂MIME格式的邮件内容,注意JavaMail仅支持JDK4及以上版本。虽然JavaMail是JDK的API但它并没有直接加入JDK中,所以我们需要另外添加依赖。

JavaMail 下载地址: github.com/javaee/java…

二、邮件协议

在收发邮件的过程中,需要遵守相关的协议,JavaMail并不是绝对支持每一个协议,其中主要有:

  1. 发送电子邮件的协议:SMTP;
  2. 接收电子邮件的协议:POP3和IMAP。

2.1 什么是SMTP?

  SMTP全称为Simple Mail Transfer Protocol(简单邮件传输协议),它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP认证要求必须提供账号和密码才能登陆服务器,其设计目的在于避免用户受到垃圾邮件的侵扰。

SMTP协议提供程序支持以下属性,这些属性可以在JavaMail会话对象进行设置。

名称 类型 描述
mail.smtp.user String SMTP的默认用户名
mail.smtp.host String 要连接的SMTP服务器
mail.smtp.port int 要连接的SMTP服务器端口,默认为25
mail.smtp.connectiontimeout int 套接字连接超时值,以毫秒为单位.默认为无限超时
mail.smtp.timeout int 套接字I/O超时值毫秒,默认为无限超时
mail.smtp.auth boolean 如果为true,则对用户进行身份验证,默认为false
mail.smtp.ssl.enable boolean 如果设置为true,则默认情况下使用SSL连接并使用SSL端口。对于”smtp”协议,默认为false;对于”smtps”协议,默认为true.

2.2 什么是IMAP?

  IMAP全称为Internet Message Access Protocol(互联网邮件访问协议),IMAP允许从邮件服务器上获取邮件的信息、下载邮件等。IMAP与POP类似,都是一种邮件获取协议。IMAP协议提供程序支持以下属性,这些属性可以在JavaMail会话对象进行设置。

名称 类型 描述
mail.imap.user String IMAP的默认用户名
mail.imap.host String 要连接的IMAP服务器
mail.imap.port int 要连接的IMAP服务器端口,默认为143
mail.imap.connectiontimeout int 套接字连接超时值,以毫秒为单位.默认为无限超时
mail.imap.timeout int 套接字I/O超时值毫秒,默认为无限超时
mail.imap.statuscachetimeout int 缓存的超时值(以毫秒为单位),默认值为1000(1秒),零禁用缓存
mail.imap.appendbuffersize int 要缓冲的邮件的最大大小附加到IMAP文件夹时的内存
mail.imap.connectionpoolsize int 最大可用数量连接池中的连接,默认值为1
mail.imap.connectionpooltimeout int 连接池连接的超时值(以毫秒为单位),默认值为45000(45秒).
mail.imap.ssl.enable boolean 如果设置如果为true,则默认使用SSL连接并使用SSL端口。”imap”协议默认为false,”imaps”协议默认为true.

2.3 什么是POP3?

  POP3全称为Post Office Protocol 3(邮局协议),POP3支持客户端远程管理服务器端的邮件。POP3常用于离线邮件处理,即允许客户端下载服务器邮件,然后服务器上的邮件将会被删除。目前很多POP3的邮件服务器只提供下载邮件功能,服务器本身并不删除邮件,这种属于改进版的POP3协议。

IMAP和POP3协议两者最大的区别在于,IMAP允许双向通信,即在客户端的操作会反馈到服务器上,例如在客户端收取邮件、标记已读等操作,服务器会跟着同步这些操作。而对于POP协议虽然也允许客户端下载服务器邮件,但是在客户端的操作并不会同步到服务器上面的,例如在客户端收取或标记已读邮件,服务器不会同步这些操作。

POP3协议提供程序支持以下属性,这些属性可以在JavaMail会话对象中设置

名称 类型 描述
mail.pop3.user String POP3的默认用户名
mail.pop3.host String 要连接的POP3服务器
mail.pop3.port int 要连接的POP3服务器端口,默认为110
mail.pop3.connectiontimeout int 套接字连接超时值,以毫秒为单位,默认为无限超时
mail.pop3.timeout int 套接字I/O超时值毫秒,默认为无限超时
mail.pop3.ssl.enable boolean 如果设置为true,则默认情况下使用SSL连接并使用SSL端口。”pop3”协议默认为false,”pop3s”协议默认为true.

三、核心类

  JavaMail API 包含一些用于发送,读取和删除电子邮件的接口和类.虽然 JavaMail API 中有许多软件包,但它们将涵盖 Java Mail API 中经常使用的两个主要软件包: javax.mail 和 javax.mail.internet 软件包.这些包包含所有JavaMail核心类.它们是:

Class 描述
javax.mail.Session API的关键类,多线程对象表示连接工厂。
javax.mail.Message 为电子邮件建模的抽象类,子类提供实际的实现。
javax.mail.Address 一个抽象类,用于对消息中的地址(来自地址和来自地址)进行建模,子类提供特定的实现。
javax.mail.Authenticator 用于保护邮件服务器上邮件资源的抽象类。
javax.mail.Transport 一个抽象类,它模拟用于发送电子邮件的邮件传输机制。
javax.mail.Store 为消息存储建模的抽象类及其访问协议,用于存储和检索消息.商店分为文件夹。
javax.mail.Folder 表示邮件消息文件夹的抽象类.它可以包含子文件夹。

3.1 Session

  javax.mail.Session 类定义了一个基本邮件会话(session),是Java Mail API最高层入口类,所有其它类都是经由这个session才得以生效。Session类用于定义整个应用程序所需的环境信息以及客户端与邮件服务器建立网络连接的会话信息,例如邮件服务器的主机名、端口号、采用的邮件发送和接收协议等。Session 对象用Java.util.Properties对象获取信息,根据这些信息构建用于邮件收发的 Transport 和 Store 对象,以及为客户端创建 Message 对象时提供信息支持。

Session类的构建方法是private 的,因此它提供了两个方法可以得到Session对象:

  • getDefualtInstance()
1
2
java复制代码public static Session getDefaultInstance(Properties props)
public static Session getDefaultInstance(Properties props,Authenticator authenticator)
  • getInstance()
1
2
java复制代码public static Session getInstance(Properties props)
public static Session getInstance(Properties props,Authenticator authenticator)

3.2 Message

  javax.mail.Message 类是创建和解析邮件的核心API,一旦获得Session对象,就可以继续创建要发送的消息,这由Message类来完成。这是一个抽象类,通常使用它的子类 javax.mail.internet.MimeMessage 类。客户端程序发送邮件时,首先使用创建邮件的 JavaMail API 创建出封装了邮件数据的 Message 对象,然后把这个对象传递给邮件发送API(Transport 类) 发送。客户端程序接收邮件时,邮件接收API把接收到的邮件数据封装在Message 类的实例中,客户端程序在使用邮件解析API从这个对象中解析收到的邮件数据。

为了建立一个MimeMessage对象,我们必须将Session对象作为MimeMessage构造方法的参数传入:

1
2
java复制代码// 通过session对象来创建一个MimeMessage对象
MimeMessage message=new MimeMessage(session);

  message对象一旦创建,就需要填充一些信息。Meesage类实现了 javax.mail.Part 接口,而MimeMessage类实现了 javax.mail.internet.MimePart 接口。

方法 描述
void setFrom(Address address) 用于设置发送者的邮件地址
void addRecipient(Message.RecipientType type,String address) 设置邮件的收件人、抄送人、密送人(通过type区分)
void addRecipients(Message.RecipientType type,Address[] addresses) 设置邮件的多个收件人、抄送人、密送人(通过type区分)
void setSubject(String subject) 设置邮件标题
void setText(String textmessage) 如果邮件内容是纯文本,可以使用此接口设置文本内容。
void setContent(Object obj,String type) 设置邮件内容

3.3 Address

  创建了Session和Message后,需要通过 Address 来设置信件的地址信息。由于 Address 是个抽象类,因此多数用它的实现类 javax.mial.internet.InternetAddress。

1
2
3
4
5
6
7
8
java复制代码// 直接通过一个邮箱地址来创建
Address address = new InternetAddress("dllwhcrawler@sina.com");

// 也可以通过邮箱地址和邮寄人姓名来创建
Address address = new InternetAddress("dllwhcrawler@sina.com", "dllwhcrawler");

// 多个地址
Address[] to = InternetAddress.parse((@NotNull String addresslist);

  为了设置收信人,我们使用addRecipient()方法增加收信人,此方法需要使用Message.RecipientType的常量来区分收信人的类型:

1
java复制代码message.addRecipient(type, address)

  下面是Message.RecipientType的三个常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public static class RecipientType implements Serializable {
/**
* 收信人
*/
public static final RecipientType TO = new RecipientType("To");
/**
* 抄送
*/
public static final RecipientType CC = new RecipientType("Cc");
/**
* 私密抄送
*/
public static final RecipientType BCC = new RecipientType("Bcc");
}

3.4 Authenticator

  javax.mail.Authenticator 代表一个知道如何获得网络连接认证的对象,是一个抽象类,通常通过创建它的子类PasswordAuthentication,传递用户名和密码来创建。JavaMail API 可以利用 Authenticator 通过用户名和密码访问受保护的资源,对于JavaMail API来说,这些资源就是邮件服务器。

1
2
3
4
5
6
7
8
java复制代码Properties props = new Properties();
Authenticator auth = new Authenticator() {
@Override
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("发件人邮箱名", "SDFA授权码FFHBF");
}
};
Session session = Session.getDefaultInstance(props, auth);

3.5 Store

  javax.mail.Store 是接收邮件的核心 API 类,它的实例对象代表实现了某个邮件接收协议的邮件接收对象,用来存储和查询消息。客户端程序接收邮件时,只需要使用邮件接收 API 得到 Store 对象,然后调用 Store 对象的接收方法,就可以从指定的 POP3 服务器获得邮件数据,并把这些邮件数据封装到表示邮件的 Message 对象中。

1
java复制代码Store store = session.getStore("pop3");

Store 将根据Session中Properties属性设置情况进行工作,属性包括:

属性名 说明
mail.store.protocol 默认的存储邮件协议,例如:pop3
mail.host 默认的邮件服务地址,例如:imap.sina.com
mail.user 默认的登陆用户名,例如:dllwhcrawler@sina.com
mail.port 默认的邮件服务端口

3.6 Transport

  javax.mail.Transport 是发送邮件的核心API 类,它的实例对象代表实现了某个邮件发送协议的邮件发送对象,例如 SMTP 协议,客户端程序创建好 Message 对象后,只需要使用邮件发送API 得到 Transport 对象,然后把 Message 对象传递给 Transport 对象,并调用它的发送方法,就可以把邮件发送给指定的 SMTP 服务器。

1
java复制代码Transport.send(message);

Transport 将根据Session中Properties属性设置情况进行工作,属性包括:

属性名 说明
mail.transport.protocol 默认的邮件传输协议,例如,smtp
mail.host 默认的邮件服务地址,例如:smtp.sina.com
mail.user 默认的登陆用户名,例如:dllwhcrawler@sina.com
mail.port 默认的邮件服务端口

3.7 Folder

  javax.mail.Folder是个抽象类,代表邮件消息的一个文件夹,其子类实现了特定的Folders。其实例:

1
java复制代码public abstract class Folder implements AutoCloseable

  由于 Folder 类唯一的构造函数是受保护的,但Session、Store都有一个类似的getFolder()方法获取Folder对象:

1
java复制代码public abstract Folder getFolder(String name) throws MessagingException;

四、邮件操作

4.1 准备工作

创建一个邮件的基本信息类MailInfo,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
java复制代码import lombok.Data;
import javax.mail.Authenticator;
import javax.mail.PasswordAuthentication;
import java.util.Properties;

@Data
public class MailInfo {
/**
* 是否需要身份验证
*/
private boolean validate = false;
/**
* 是否开启Session的debug模式,可以查看到程序发送Email的运行状态
*/
private boolean debugMode = false;
/**
* 发送邮件的服务器的IP(或主机地址)
*/
private String mailServerHost;
/**
* 发送邮件的服务器的端口
*/
private String mailServerPort;
/**
* 登陆邮件发送服务器的用户名
*/
private String userName;
/**
* 登陆邮件发送服务器的密码
*/
private String password;


/**
* 获得邮件会话属性
*/
public Properties getProperties() {
Properties properties = new Properties();
// 默认的邮件传输协议
properties.setProperty("mail.transport.protocol", "smtp");
// 默认的存储邮件协议
properties.setProperty("mail.store.protocol", "pop3");
// 设置邮件服务器主机名
properties.put("mail.host", this.mailServerHost);
properties.put("mail.port", this.mailServerPort);
// 设置是否安全验证,默认为false,一般情况都设置为true
properties.put("mail.smtp.auth", this.validate);
return properties;
}

/**
* JavaMail认证/验证
*
* @return
*/
public Authenticator getAuthenticator() {
// 判断是否需要身份认证
Authenticator authenticator = null;

if (isValidate()) {
// 如果需要身份认证,则创建一个密码验证器
authenticator = new Authenticator() {
@Override
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(userName, password);
}
};
}

return authenticator;
}
}

相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
xml复制代码<!-- JavaMail API-->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>

<!-- 引入lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<version>1.18.12</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>

<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.1</version>
</dependency>

4.2 发送邮件

  1. 发送邮件需要使用的基本信息:MailSenderInfo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码@Data
@EqualsAndHashCode
@ToString
public class MailSenderInfo extends MailInfo {
/**
* 发件人邮箱地址
*/
private String fromAddress;
/**
* 收件人邮箱地址(多个以逗号隔开)
*/
private String toAddress;
/**
* 抄送人邮箱地址(多个以逗号隔开)
*/
private String ccAddress;
/**
* 密送人邮箱地址(多个以逗号隔开)
*/
private String bccAddress;
/**
* 邮件主题
*/
private String subject;
/**
* 邮件的文本内容
*/
private String content;
/**
* 邮件附件的文件名
*/
private String[] attachFileNames;
}
  1. 发送邮件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
java复制代码import org.apache.commons.lang3.StringUtils;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import java.io.UnsupportedEncodingException;
import java.util.Date;

public final class JavaMailSendHelper {

private static Message getMessage(MailSenderInfo mailInfo) throws MessagingException, UnsupportedEncodingException {
// 根据邮件会话属性和密码验证器构造一个发送邮件的session
Session session = Session.getDefaultInstance(mailInfo.getProperties(), mailInfo.getAuthenticator());
// 开启Session的debug模式,这样就可以查看到程序发送Email的运行状态
session.setDebug(mailInfo.isDebugMode());
// 根据session创建一个邮件消息
Message message = new MimeMessage(session);
// 设置邮件消息的发送者
message.setFrom(new InternetAddress(mailInfo.getFromAddress()));
// 创建邮件的多个接收者地址
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(mailInfo.getToAddress()));
// Cc: 抄送(可选)
if (StringUtils.isNotBlank(mailInfo.getCcAddress())) {
message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(mailInfo.getCcAddress()));
}
// Bcc: 密送(可选)
if (StringUtils.isNotBlank(mailInfo.getBccAddress())) {
message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(mailInfo.getBccAddress()));
}
// 设置邮件消息的主题
message.setSubject(MimeUtility.encodeText(mailInfo.getSubject(), "utf-8", "B"));
// 设置邮件消息发送的时间
message.setSentDate(new Date());
return message;
}

/**
* 使用 JavaMail 发送简单的纯文本邮件
*
* @param mailInfo 待发送的邮件信息
* @return 成功返回true,否则返回false
*/
public static boolean sendTextMail(MailSenderInfo mailInfo) {
try {
Message message = getMessage(mailInfo);
// 设置邮件消息的主要内容
message.setText(mailInfo.getContent());
message.saveChanges();
// 发送邮件
Transport.send(message);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}

/**
* 使用 JavaMail 发送 HTML 格式的邮件
*
* @param mailInfo 待发送的邮件信息
* @return 成功返回true,否则返回false
*/
public static boolean sendHtmlMail(MailSenderInfo mailInfo) {
try {
Message message = getMessage(mailInfo);
// 设置邮件消息的主要内容
message.setContent(mailInfo.getContent(), "text/html;charset=UTF-8");
message.saveChanges();
// 发送邮件
Transport.send(message);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
  1. 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码import org.dllwh.javamail.mailsend.JavaMailSendHelper;
import org.dllwh.javamail.mailsend.MailSenderInfo;

public class JavaMailApiHelper {
/**
* 获取邮件发送属性信息
* @return
*/
private static MailSenderInfo getMailSenderInfo() {
String email = "dllwhcrawler@sina.com";
// 设置邮件服务器信息
MailSenderInfo mailInfo = new MailSenderInfo();
// 发送邮件的服务器的IP(或主机地址)
mailInfo.setMailServerHost("smtp.sina.com");
//有些端口在服务器上是没开放的 这里需要注意下
mailInfo.setMailServerPort("");
mailInfo.setValidate(true);
// 邮箱用户名(此处填写跟上面发送邮件服务器对应的邮箱)
mailInfo.setUserName("dllwhcrawler@sina.com");
// 邮箱密码(根据自己情况设置)
mailInfo.setPassword("4f38d72caa78ffec");
// 发件人邮箱(根据自己情况设置,如果你没对邮箱进行特别设置,应该和邮箱用户名一致)
mailInfo.setFromAddress("dllwhcrawler@sina.com");
// 收件人邮箱(根据自己情况设置)
mailInfo.setToAddress(email);
return mailInfo;
}

/**
* 发送简单本文邮件
*/
public static void sendTextMail() {
MailSenderInfo mailInfo = getMailSenderInfo();
// 邮件标题
mailInfo.setSubject("我是标题");
// 邮件内容
mailInfo.setContent("我是内容,正经的内容不是垃圾邮箱");
// 发送文体格式邮件
JavaMailSendHelper.sendTextMail(mailInfo);
}
/**
* 发送 HTML 格式的邮件
*/
public static void sendHtmlMail() {
MailSenderInfo mailInfo = getMailSenderInfo();
// 邮件标题
mailInfo.setSubject("测试HTML邮件");
// 邮件内容
mailInfo.setContent("<h1>Hello</h1><p>显示图片<img src='http://pic.jj20.com/up/allimg/1114/111220111638/201112111638-4.jpg'>1.jpg</p>");
// 发送文体格式邮件
JavaMailSendHelper.sendHtmlMail(mailInfo);
}
}

4.2 收取邮件

  • 接收邮件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
java复制代码import org.apache.commons.lang3.ArrayUtils;
import org.dllwh.javamail.MailInfo;
import org.joda.time.DateTime;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeUtility;

public class JavaMailReceiveHelper {
/**
* 使用 JavaMail 接收邮件
*
* @param mailInfo 邮件信息
* @return
*/
public static boolean recipientMail(MailInfo mailInfo) {
// 根据邮件会话属性和密码验证器构造一个发送邮件的session
Session emailSession = Session.getDefaultInstance(mailInfo.getProperties(), mailInfo.getAuthenticator());

// 开启Session的debug模式,这样就可以查看到程序发送Email的运行状态
emailSession.setDebug(mailInfo.isDebugMode());
try {

Store emailStore = emailSession.getStore("pop3");
emailStore.connect(mailInfo.getUserName(), mailInfo.getPassword());

Folder emailFolder = emailStore.getFolder("INBOX");
emailFolder.open(Folder.READ_ONLY);
System.out.println("未读邮件数: " + emailFolder.getUnreadMessageCount());

// 由于POP3协议无法获知邮件的状态,所以下面得到的结果始终都是为0
System.out.println("删除邮件数: " + emailFolder.getDeletedMessageCount());
System.out.println("新邮件: " + emailFolder.getNewMessageCount());

// 获得收件箱中的邮件总数
System.out.println("邮件总数: " + emailFolder.getMessageCount());

// 获取收件箱中的所有邮件并解析
Message[] messages = emailFolder.getMessages();
for (Message message:messages) {
System.out.println("------------------解析第" + message.getMessageNumber() + "封邮件-------------------- ");
System.out.println("Email Number " + message.getMessageNumber());
System.out.println("主题: " + MimeUtility.decodeText(message.getSubject()));
System.out.println("发件人: " + InternetAddress.toString(message.getFrom()));
System.out.println("邮件正文: " + message.getContent().toString());
System.out.println("接收时间:"+new DateTime(message.getReceivedDate()).toString("yyyy-MM-dd HH:mm:ss"));
System.out.println("发送时间:"+new DateTime(message.getSentDate()).toString("yyyy-MM-dd HH:mm:ss"));
System.out.println("是否已读:" + message.getFlags().contains(Flags.Flag.SEEN));
String[] headers = message.getHeader("X-Priority");
if(ArrayUtils.isNotEmpty(headers)){
System.out.println("邮件优先级:" + headers[0]);
}
String[] replySign = message.getHeader("Disposition-Notification-To");
System.out.println("是否需要回执:" + ArrayUtils.isNotEmpty(replySign));
}
//释放资源
emailFolder.close(false);
emailStore.close();
} catch (Exception exp) {
exp.printStackTrace();
}


return false;
}
}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import org.dllwh.javamail.mailreceive.JavaMailReceiveHelper;
import org.dllwh.javamail.mailsend.MailSenderInfo;

public class JavaMailApiHelper {
public static void recipientMail() {
// 设置邮件服务器信息
MailSenderInfo mailInfo = new MailSenderInfo();
// 发送邮件的服务器的IP(或主机地址)
mailInfo.setMailServerHost("imap.sina.com");
//有些端口在服务器上是没开放的 这里需要注意下
mailInfo.setMailServerPort("");
mailInfo.setValidate(true);
// 邮箱用户名(此处填写跟上面发送邮件服务器对应的邮箱)
mailInfo.setUserName("dllwhcrawler@sina.com");
// 邮箱密码(根据自己情况设置)
mailInfo.setPassword("4f38d72caa78ffec");
JavaMailReceiveHelper.recipientMail(mailInfo);
}
}

4.3 回复邮件

我们使用JavaMail API来回复电子邮件,下面的程序中的列出基本步骤:

  • 获取Session对象与POP和SMTP 服务器的细节属性。
  • 创建POP3存储对象,并连接到存储。
  • 创建文件夹对象,并在您的邮箱中打开相应的文件夹。
  • 检索消息。
  • 遍历的消息,如果你想回复键入“Y”或“y”。
  • 得到消息的所有信息(收件人,发件人,主题,内容)(To,From,Subject, Content) 。
  • 建立应答消息,使用Message.reply()方法。这个方法配置一个新的消息与适当的收件人和主题。该方法接受一个布尔参数,指示是否只回复给发送者 (false)或回复给所有人(true)。
  • 从设置,文本和回复到邮件中,并通过传输对象的实例发送。
  • 关闭传输,文件夹和存储对象分别。

4.5 转发邮件

我们将使用JavaMail API来转发电子邮件,下面的程序的基本步骤是:

  • 获取Session对象与POP和SMTP服务器的细节的属性。我们需要的POP细节来检索信息和SMPT详细信息发送邮件。
  • 创建POP3存储对象,并连接到存储。
  • 创建文件夹对象,并在您的邮箱中打开相应的文件夹。
  • 检索消息。
  • 遍历的消息,如果你想转发键入“Y”或“y”。
  • 得到消息的所有信息(收件人,发件人,主题,内容)。
  • 通过与组成消息的各个部分的工作建立转发消息。第一部分将是消息的文本和第二部分将要转发的邮件。结合两成多部分。那么你多部分添加到妥善处理消息并发送它。
  • 关闭传输,文件夹和存储对象分别。

4.6 删除邮件

我们将使用JavaMail API来删除电子邮件。删除信息涉及与该消息相关联的标志工作。有不同的标志为不同的状态,一些系统定义和一些用户定义的。预定义的标志在内部类中定义的标志。标志如下所列:

Flags.Flag.ANSWERED 邮件回复标记,标识邮件是否已回复。
Flags.Flag.DELETED 邮件删除标记,标识邮件是否需要删除。
Flags.Flag.DRAFT 草稿邮件标记,标识邮件是否为草稿。
Flags.Flag.FLAGGED 表示邮件是否为回收站中的邮件。
Flags.Flag.RECENT 新邮件标记,表示邮件是否为新邮件。
Flags.Flag.SEEN 邮件阅读标记,标识邮件是否已被阅读。
Flags.Flag.USER 底层系统是否支持用户自定义标记,应用程序只能检索这个属性,而不能设置。

其次在删除程序的基本步骤是:

  • 获取Session对象与POP和SMTP伺服器的细节的属性。
  • 创建POP3存储对象,并连接到存储。
  • 创建文件夹对象,并在READ_WRITE模式下邮箱打开相应的文件夹。
  • 从收件箱文件夹中检索邮件。
  • 遍历的消息,如果你想通过Message对象上调用方法setFlag(Flags.Flag.DELETED, true)以删除邮件中键入“Y”或“y”。
  • 这些消息标记DELETED 实际上并没有删除,直到我们调用Folder对象上expunge() 方法,或expunge 设置为true,关闭文件夹。
  • 关闭存储对象

4.7 邮件文件夹管理

下面列出一些常用的方法:

  • 获取 Folder 对象的方法
方法 描述
boolean exists() 检查文件夹是否真的存在。
void open(int mode) 以Folder.READ_ONLY或Folder.READ_WRITE模式打开文件夹。
boolean isOpen() 如果文件夹打开,此返回true,否则返回false。
void close(boolean expunge) 关闭文件夹。如果expunge为true,则会从服务器上的实际文件中删除该文件夹中的所有已删除邮件。否则,它们只是标记为已删除,但邮件仍然可以取消删除.
* 获取文件夹基本信息的方法
方法 描述
String getName() 返回文件夹的名称,例如”TutorialsPoint Mail”
String getFullName() 从根目录返回完整的层次结构名称,例如”books/Manisha/TutorialsPointMail”
URLName getURLName() 返回表示此文件夹的URLName
Folder getParent() 返回包含此文件夹的文件夹的名称,即父文件夹.例如,之前的”TutorialsPoint Mail”示例中的”Manisha”.
int getType() 返回一个int,指示文件夹是否可以包含消息和/或其他文件夹.
int getMode() 它返回两个命名常量Folder.READ_ONLY或Folder之一.当模式未知时,READ_WRITE或-1.
Store getStore() 返回Store对象从中检索此文件夹
char getSeparator() 返回分隔的分隔符此文件夹的路径名来自直接子文件夹的名称
* 管理文件夹
方法 描述
create(int type) 这会在此文件夹的Store中创建一个新文件夹.如果成功返回true,否则返回false。其中类型将是:Folder.HOLDS_MESSAGES或Folder.HOLDS_FOLDERS.
delete(boolean recurse) 仅当文件夹关闭时,才会删除文件夹,否则抛出IllegalStateException。如果recurse是true,则删除子文件夹.
renameTo(Folder f) 更改文件夹的名称,必须关闭文件夹才能重命名.否则,抛出IllegalStateException
appendMessages(Message[] messages) 顾名思义,数组中的消息放在此文件夹的末尾
copyMessages(Message[] msgs,Folder folder) 这会将此文件夹中的消息复制到作为参数给出的指定文件夹中
Message[] expunge() 从文件夹中物理删除已删除的邮件
* 列出文件夹的内容
方法 描述
Folder[] list() 返回一个数组,列出该文件夹包含的文件夹.
Folder[] listSubscribed() 返回一个数组,列出该文件夹包含的所有订阅文件夹.
* 检查邮件
方法 描述
int getMessageCount() 邮件总数
boolean hasNewMessages()
int getNewMessageCount()
int getUnreadMessageCount() 未读邮件数
int getDeletedMessageCount() 删除邮件数
* 从文件夹获取消息
方法 描述
Messageget Message(int msgnum) 这将返回文件夹中的第n条消息.文件夹中的第一条消息是数字 1
Message[] getMessages() 返回一组Message对象,表示此文件夹中的所有消息
Message[] getMessages(int start,int end)
Message[] getMessages(int[] msgnums)
* 修改文件夹标记
方法 描述
void setFlags(Message[] msgs,Flags flag,boolean value) 在数组中指定的消息上设置指定的标志
void setFlags(intstart,intend,Flags flag,boolean value) 设置从开始到结束编号的消息上的指定标志,包括起点和终点
void setFlags(int[] msgnums,Flags flag,boolean value) 设置消息号在数组中的消息的指定标志
Flags getPermanentFlags() 返回此文件夹支持所有邮件的标志

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

TTS(Text to Speech)初识&服务端应用(Ma

发表于 2020-11-22

背景

最近项目中有个用户输入英语输出音频的场景,因此开始学习了文本转语音的一些知识,并找到一些实现方案实现了需求。

TTS介绍

TTS(Text to Speech, 文本转语音)是指将一般语言文本转换为音频的技术。一般应用于智能助手、盲人助手等应用的语言交互模块中。

实现原理

输入文本(text),输出音频波形(waveform),一般流程分为两步:文本分析、声波生成。

1. 文本分析

提取文本中关于语音生成的信息。一般有3个流程:文字规范化、语音分析、还有韵律分析。

1
2
3
4
5
6
7
8
css复制代码a. 文字规范化
这一步主要是确定语言文字组成,确定词句的开始结束,将非语言文字转为对应文字或者过滤去除。

b. 语音分析
这一步主要是把词句中的发音音标标记出来,以便后续根据音标组成生成词句音频。

c. 韵律分析
这一步主要是分析出词句的语音语调(重音、边界、音长、主频率),以生成更真实音频。

2. 声波生成

根据提取的信息生成声波波形。一般有两种方法:拼接法、参数法。

1
2
3
4
5
6
7
8
9
markdown复制代码* 拼接法
需要准备大量音素或音节的音频库,根据文本分析出的信息,将词句对应的音素音节以适当的方式调节对齐拼接。
优点是音质比较高比较自然。
缺点是需要足够齐全的音频库和适合的拼接方式,不然输出的音频就会失真甚至错误。

* 参数法
使用经过学习的统计模型,输入文本分析结果,预测得到音频的波形参数,再合成生成结果音频。
优点是对音频库要求不高。
缺点是输出音质比较不自然。

服务端应用方案

参考了7 个开源的TTS(文本转语音)系统推荐一文,根据服务端需求选择使用了MaryTTS。

MaryTTS介绍

MaryTTS是一款基于Java语言的开源TTS系统。由德国人工智能研究中心DFKI的语言技术实验室和萨尔大学Saarland University的语音学机构合作创立,现由MMCI的多层次模型语音处理小组和DFKI维护。

当前5.2版本支持德语、英式美式英语、法语、意大利语、卢森堡语、俄语、瑞典语、泰卢固语和土耳其语,以及更多其他语言准备接入。同时,MaryTTS也带有便于快速添加新语言支持和生成语音的工具。

应用

MaryTTS在github(marytts)上提供开源的核心代码和服务端实现代码,也在bintray上提供了封装服务(附带一个美式女性英语音源)的依赖。同时,也有在线网站进行效果试用和可直接安装的应用(包含小工具)提供下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<repositories>
<repository>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>de.dfki.mary</groupId>
<artifactId>voice-cmu-slt-hsmm</artifactId>
<version>5.2</version>
</dependency>
</dependencies>

代码实现

分析源码,发现其中的主要类如下:

  • MaryConfig

音源、语言、合成参数等配置信息。

  • Mary

TTS系统的核心类,处理音源、配置等资源的加载,加载相关模块。

  • MaryRuntimeUtils

Mary的运行工具,提供给外部调用初始化、获取配置信息等方法。

  • LocalMaryInterface

默认实现的转码接口,可以选择配置一个环境中存在的音源,提供生成音频、生成声音信息文本等方法。

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
kotlin复制代码import marytts.LocalMaryInterface
import marytts.util.data.BufferedDoubleDataSource
import marytts.util.data.audio.DDSAudioInputStream
import marytts.util.data.audio.MaryAudioUtils
import java.io.File
import java.sql.Timestamp
import java.time.Instant
import javax.sound.sampled.AudioFileFormat
import javax.sound.sampled.AudioSystem

/**
* @author hac
* @description: 文本转Wav
*/
class TTWavUtil {

companion object {

private val ttsMap = MaryTTSVoiceType.values().associate {
val tts = LocalMaryInterface()
tts.voice = it.value // 设置音源
Pair(it, tts)
}

/**
* 文本转wav文件
* @param text 文本
* @param output 输出wav文件
* @param voiceType 声音类型
*/
fun ttWavFile(text: String, output: File, voiceType: MaryTTSVoiceType) {
val maryTTS = ttsMap[voiceType]!!
val audio = maryTTS.generateAudio(text)
val samples = MaryAudioUtils.getSamplesAsDoubleArray(audio)
val outputAudio = DDSAudioInputStream(BufferedDoubleDataSource(samples), audio.format)
AudioSystem.write(outputAudio, AudioFileFormat.Type.WAVE, output)
}

}

/**
* 声音类型
*/
enum class MaryTTSVoiceType(val code: Int, val value: String) {

CMU_SLT_HSMM(0, "cmu-slt-hsmm"),
DFKI_SPIKE_HSMM(1, "dfki-spike-hsmm"),
DFKI_POPPY_HSMM(2, "dfki-poppy-hsmm"),
;

}

}

添加音源

音源依赖会在MaryConfig类加载的时候通过ServiceLoader加载运行环境中存在的MaryConfig子类。

1
arduino复制代码private static final ServiceLoader<MaryConfig> configLoader = ServiceLoader.load(MaryConfig.class);

每个音源都可以以Jar包引入的方式添加到服务中。

而音源Jar包则可以通过MaryTTS提供的小工具生成的。将MaryTTS的应用软件包下载解压后,其bin目录下的marytts-component-installer就是生成音源Jar包的工具。


运行会出现GUI操作界面,选择需要的语言下需要的音源进行下载。

Jar包会被下载到解压目录下的lib目录下,将Jar包引入项目即可。

结语

初探了TTS知识并研究使用MaryTTS作为应用方案进行了开发,但是TTS背后还有更多值得深入学习的知识,如音频数字化技术、参数法中的HMM隐马尔可夫模型、新型声波生成方式中的神经网络算法Wavenet等,感兴趣也可再深入探索一番。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…763764765…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%