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

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


  • 首页

  • 归档

  • 搜索

Spring Security OAuth2入门踩坑

发表于 2021-11-21

吐槽Spring Security

刚开始使用的时候确实很简单,只需要添加依赖和几个配置类就行,但到后面只是想OAuth2和Spring Security默认登录功能一起用的时候,发现无从下手。看来不对Spring Security有个大概的了解,那么灵活是不存在的。

最简的配置

修改pom.xml

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!-- 若使用Spring Security则需要配置该依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 若使用OAuth2则还需额外配置该依赖 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>

新增Spring Security配置类

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
scala复制代码@EnableWebSecurity
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
@Override
public UserDetailsService userDetailsServiceBean() {
UserDetails user = User
// 这里配置个用户名是user1密码是123456的用户
.withUsername("user1")
.password(passwordEncoder().encode("123456"))
// 这里的USER是自定义,没有什么特殊的含义
.roles("USER")
.build();

// InMemoryUserDetailsManager设计目的主要是测试和功能演示
return new InMemoryUserDetailsManager(user);
}

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

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

}

新增OAuth2配置类(注意Spring Security过滤器会因为@Order顺序而在执行的时候有先后顺序,默认情况下过滤器执行顺序是OAuth2 Authorization -> OAuth2 Resource -> Spring Security)

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
java复制代码@EnableAuthorizationServer
@Configuration
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final PasswordEncoder passwordEncoder;

public OAuth2AuthorizationConfig(AuthenticationManager authenticationManager,
PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.passwordEncoder = passwordEncoder;
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 开启 /oauth/token_key 验证端口成无权限访问
security.tokenKeyAccess("permitAll()")
// 开启 /oauth/check_token 验证端口成认证权限访问
.checkTokenAccess("isAuthenticated()")
// 主要是让 /oauth/token 支持 client_id 以及 client_secret 做登录认证
.allowFormAuthenticationForClients();
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 这里配置个客户ID是client1密码是123456的客户端
clients.inMemory()
.withClient("client1")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("authorization_code", "password", "refresh_token")
// 这里的ALL是自定义,没有什么特殊的含义
.scopes("ALL")
.accessTokenValiditySeconds(3600);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scala复制代码@EnableResourceServer
@Configuration
public class OAuth2ResourceConfig extends ResourceServerConfigurerAdapter {

@Override
public void configure(HttpSecurity http) throws Exception {
// !!!这里我配置成只处理请求头里有"Authorization"属性的请求
http.requestMatcher((request) -> {
return request.getHeader("Authorization") != null;
});

http.authorizeRequests()
.anyRequest()
.authenticated();
}

}

通过Spring Security自带的登录页面登录http://127.0.0.1:8080/login

1.png

通过OAuth2获取token http://127.0.0.1:8080/oauth/token?username=user1&password=123456&grant_type=password&client_id=client1&client_secret=123456

2.png

通过获取的token访问资源

3.png

介绍Spring Security原理

通过DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain实现认证和授权,需要特别注意的是1.一次请求里只有最先匹配到的SecurityFilterChain会被执行。2.SecurityFilterChain里有一系列跟认证/授权相关的过滤器,其中包括FilterSecurityInterceptor。

4.png

5.png

Spring Security主要组成部分

  • SecurityContextHolder, 提供获取 SecurityContext 的方法。
  • SecurityContext, 安全上下文,提供获取 Authentication 等方法。
  • Authentication, 代表认证的对象。
  • GrantedAuthority, 代表赋予的权限。

SecurityContextHolder,可以获取安全上下文SecurityContext。

1
2
3
4
5
6
7
8
ini复制代码// 代码来自https://www.springcloud.cc/spring-security-zhcn.html
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

UserDetailsService用于根据用户名加载用户信息。

1
2
3
java复制代码public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

AuthenticationManager是认证管理器,其实现类ProviderManager通过一系列AuthenticationProvider实现类验证认证信息。

1
2
3
java复制代码public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

6.png

AccessDecisionManager是访问决策管理器,可以通过该类实现动态权限控制。其实现类AffirmativeBased通过一系列AccessDecisionVoter实现类决定授权结果。

1
2
3
4
5
6
7
java复制代码public interface AccessDecisionManager {
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);
}

以下是阅读过的其他作者写的文章

  • Spring Security中文参考手册 www.springcloud.cc/spring-secu…
  • blog.csdn.net/weixin_4387…

本文转载自: 掘金

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

Go 中 ioPipe 的用法

发表于 2021-11-21

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」

在这篇文章中,我想展示 Go 标准库 io 的一个函数 Pipe

1
Go复制代码func Pipe() (*PipeReader, *PipeWriter)

Pipe creates a synchronous in-memory pipe. It can be used to connect code expecting an io.Reader with code expecting an io.Writer.

Reads and Writes on the pipe are matched one to one except when multiple Reads are needed to consume a single Write. That is, each Write to the PipeWriter blocks until it has satisfied one or more Reads from the PipeReader that fully consume the written data. The data is copied directly from the Write to the corresponding Read (or Reads); there is no internal buffering.

It is safe to call Read and Write in parallel with each other or with Close. Parallel calls to Read and parallel calls to Write are also safe: the individual calls will be gated sequentially.

根据文档,io.Pipe 创建了一个同步内存管道,可用于将需要 io.Reader 的代码与需要 io.Writer 的代码连接起来。

调用时,io.Pipe() 返回一个 PipeReader 和一个 PipeWriter。它们是连接的(管道),因此写入 PipeWriter 的所有内容都可以从 PipeReader 读取。

下面将用三个栗子来展示 io.Pipe 的用法以及它与 I/O 结合带来的特性。

Example1: JSON to HTTP Request

当我们将一些数据编码为 JSON,并希望通过 http.Post 将其发送到 Web 端时,通常,我们会先将内存数据通过 json.Encoder 进行编码,然后再讲结果提供给 http.Post 作为输入。但JSON encoder 需要一个 io.Writer,而 http Post 方法需要一个 io.Reader 作为输入,所以我们不能只是将它们连接在一起。我们需要在他们中间通过 []bytes 进行转换。我们也可以使用 io.Pipe 来做这件事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Go复制代码pr, pw := io.Pipe()

go func() {
// close the writer, so the reader knows there's no more data
defer pw.Close()

// write json data to the PipeReader through the PipeWriter
if err := json.NewEncoder(pw).Encode(&PayLoad{Content: "Hello Pipe!"}); err != nil {
log.Fatal(err)
}
}()

// JSON from the PipeWriter lands in the PipeReader
// ...and we send it off...
if _, err := http.Post("http://example.com", "application/json", pr); err != nil {
log.Fatal(err)
}

首先,我们将结构体 PayLoad 编码为 JSON,并将数据写入通过调用 io.Pipe 创建的 PipeWriter。之后,我们创建一个 http POST 请求,该请求从 PipeReader 获取其数据。 PipeReader 被写入 PipeWriter 的数据填满。

这里需要注意的是,我们必须异步编码以防止死锁,因为如果我们没有读取器,我们将在没有读取器的情况下进行写入。

这个实际例子很好地展示了 io.Pipe 的多功能性。它确实激励 gophers 使用 io.Reader 和 io.Writer 构建组件,而不必担心它们一起使用。

Split up Data with TeeReader

我发现了另一种将 io.Pipe 与 TeeReader 相结合的使用方法,他可以产生类似数据流镜像的效果。国外网友的一个用法是:将视频文件转码为另一种格式并上传时,同时还上传了原始文件,通过 Pipe 与 TeeReader 结合,在最小的开销并且完全并行的情况下完成。

简化后的代码如下:

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
Go复制代码pr, pw := io.Pipe()

// we need to wait for everything to be done
wg := sync.WaitGroup{}
wg.Add(2)

// we get some file as input
f, err := os.Open("./fruit.txt")
if err != nil {
log.Fatal(err)
}

// TeeReader gets the data from the file and also writes it to the PipeWriter
tr := io.TeeReader(f, pw)

go func() {
defer wg.Done()
defer pw.Close()

// get data from the TeeReader, which feeds the PipeReader through the PipeWriter
_, err := http.Post("https://example.com", "text/html", tr)
if err != nil {
log.Fatal(err)
}
}()

go func() {
defer wg.Done()
// read from the PipeReader to stdout
if _, err := io.Copy(os.Stdout, pr); err != nil {
log.Fatal(err)
}
}()

wg.Wait()

我们有某种输入 io.Reader,在这种情况下是一个文件,并创建一个 TeeReader,它返回一个 Reader,该 Reader 将写入你提供的 Writer,它从你提供的 Reader 读取的所有内容。

现在我们启动两个 goroutine,一个只是将数据打印到 stdout,另一个将数据发送到 HTTP 端点。TeeReader 使用 io.Pipe 拆分给定的输入。当使用 TeeReader 时,PipeReader 也会接收到这些相同的字节。

Example 3: Piping the output of Shell commands

第三种方式是将 io.Pipe 与 os.Exec 结合在一起。基本上,它执行大多数 CI 服务(如 Jenkins 或 Travis CI)中的任务运行器所做的工作,即执行一些 shell 命令并在某些网站上显示其输出。

简化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Go复制代码pr, pw := io.Pipe()
defer pw.Close()

// tell the command to write to our pipe
cmd := exec.Command("cat", "fruit.txt")
cmd.Stdout = pw

go func() {
defer pr.Close()
// copy the data written to the PipeReader via the cmd to stdout
if _, err := io.Copy(os.Stdout, pr); err != nil {
log.Fatal(err)
}
}()

// run the command, which writes all output to the PipeWriter
// which then ends up in the PipeReader
if err := cmd.Run(); err != nil {
log.Fatal(err)
}

首先,我们定义命令 —— cat 一个名为 fruit.txt 的文件,它会在标准输出上文件的内容。然后,我们将命令的标准输出设置为我们的 PipeWriter。

所以我们将命令的输出重定向到我们的管道,就像以前一样,这将使我们可以在另一个点通过我们的 PipeReader 读取它。

本文转载自: 掘金

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

【力扣-二叉树】19、二叉搜索树的最近公共祖先(235) 2

发表于 2021-11-21

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」

235. 二叉搜索树的最近公共祖先

题目描述

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]

示例 1:

1
2
3
less复制代码输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。

示例 2:

1
2
3
less复制代码输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。

解析

自底向上查找就可以找到公共祖先了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
css复制代码    二叉树回溯的过程可以实现自底向上查找

后序遍历就是天然的回溯过程,最先处理的一定是叶子节点

如果找到一个节点,该节点的左子树上出现节点p ,右子树上出现节点q
那么这个节点就是p q 的公共祖先


递归三部曲
1、确定递归函数的参数与返回值
需要递归函数的返回值来确定是否找到了p 或 q节点
那么返回值类型可以是bool类型
但是还需要返回最近的公共节点,所以可以直接返回p 或 q
返回值不为空,就说明找到了 p 或 q

2、确定终止条件
如果遇到了节点p 或 q 或是NULL ,就直接返回

3、单层递归的逻辑
本题递归函数需要返回值,使用 left接左子树的递归结果,使用 right 接右子树的递归结果。

代码

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
c++复制代码class Solution
{
public:
TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p.TreeNode *q)
{
if (root == p || root == q || root == NULL)
{
return root;
}

TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);

// 如果 left和right都不为空,则说明 root 就是最近的公共节点
if (left != NULL && right != NULL)
{
return root;
}
// 如果 left 为空 right 不为空,则返回right
else if (left == NULL && right != NULL)
{
return right;
}
// 如果 right 为空 left 不为空,则返回left

else if (left != NULL && right == NULL)
{
return right;
}
else
{
return NULL;
}
}
};

本文转载自: 掘金

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

最长递增子序列问题解法分析!使用动态规划和二分查找实现最长递

发表于 2021-11-21

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

基本概念

  • 动态规划的通用技巧 : 数学归纳思想
  • 最长递增子序列LIS问题:
    • 动态规划解法. 时间复杂度是 O(N^2^)
    • 二分查找解法. 时间复杂度是 O(Nlog⁡N)O(N\log N)O(NlogN)
  • 注意: 子序列和子串之间的区别
    • 子序列不一定是连续的
    • 子串一定是连续的

动态规划解法

  • 动态规划的核心思想: 数学归纳法
  • 想要证明一个数学结论成立:
    • 先假设这个结论在 k<nk < nk<n 时成立
    • 然后证明 k=nk = nk=n 时此结论也成立
    • 那么就说明这个结论对于 kkk 等于任何数时都是成立的
  • 动态规划算法: 需要一个DP数组
    • 可以假设 dp[0,…,i−1]dp[0,…,i - 1]dp[0,…,i−1] 都已经计算出来
    • 通过这些结果计算出dp[i]dp[i]dp[i]
  • 最长递增子序列LIS问题:
    • 首先要定义清楚 dpdpdp 数组的含义
    • 要清楚 dp[i]dp[i]dp[i] 的值代表的含义
  • 定义: dp[i]dp[i]dp[i] 表示以 nums[i]nums[i]nums[i] 这个数结尾的最长递增子序列的长度
  • 根据定义,可知最终结果的子序列的最大长度就是dp数组中的最大值
1
2
3
4
5
c复制代码int res = 0;
for (int i = 0; i < dp.size(); i ++) {
res = Math.max(res, dp[i]);
}
return res;
  • 设计动态规划算法正确计算每个 dp[i]dp[i]dp[i] : 使用数学归纳法思考如何进行状态转移

    • 根据对dp数组的定义,已知 dp[0,…,4]dp[0,…,4]dp[0,…,4] 的结果,要求 dp[5]dp[5]dp[5] 的值,也就是要求 nums[5]nums[5]nums[5] 为结尾的最长递增子序列

    • nums[5]=3nums[5]=3nums[5]=3,因为是递增子序列,只要找到之前结尾比3小的递增子序列,然后将3接到最后,就可以形成一个新的递增子序列,并且这个新的序列长度会增加1

    • 形成的子序列有多种,只是需要最长的,将最长子序列的长度作为 dp[5]dp[5]dp[5] 的值

      1
      2
      3
      4
      5
      c复制代码for (int j = 0; j < i, j++) {
      if (nums[i] > nums[j]) {
      dp[i] = Math.max(dp[i], dp[j] + 1);
      }
      }
    • 使用数学归纳法,可以计算出其余的dp数组的值

      1
      2
      3
      4
      5
      6
      7
      c复制代码for (int i = 0; i < nums.length; i++) {
      for (int j = 0; j < i; j++) {
      if (nums[i] > nums[j]) {
      dp[i] = Math.max(dp[i], dp[j] + 1);
      }
      }
      }
    • dp数组应该全部初始化为1, 因为子序列长度最少也要包含自己,所以最小长度为1而不为0

  • 最长递增子序列完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码public int lengthOfLIS() {
int[] dp = new int[nums.length];
// dp数组全部初始化为1
Arrays.fill(dp, 1);
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int res = 0;
for (int i = 0; i < dp.length(); i++) {
res = Math.max(res, dp[i]);
}
return res;
}
  • 最长递增子序列的DP数组算法的时间复杂度为O(N^2^)
  • 动态规划设计流程:
    • 首先明确DP数组所存数据的含义
      • 这步很重要,如果含义不明确,会导致后续步骤计算混乱
    • 然后根据DP数组的定义,计算出 dp[i]dp[i]dp[i]
      • 运用数学归纳法的思想,假设 dp[0,…,i−1]dp[0,…,i-1]dp[0,…,i−1] 的值都是已知,根据 dp[0,…,i−1]dp[0,…,i-1]dp[0,…,i−1] 的值求出 dp[i]dp[i]dp[i]
        • 一旦完成这个步骤,整个题目就基本解决了
        • 如果无法完成这一步骤,就要重新思考DP数组的定义
        • 或者可能是DP数组存储的信息不完全,无法推导出下一步的答案,就需要将DP数组扩大成为二维数组甚至三维数组
    • 最后确定问题的base case
      • 使用base case初始化数组,保证算法正确运行

二分查找解法

  • 最长递增子序列的二分查找解法的算法时间复杂度为 O(Nlog⁡N)O(N\log N)O(NlogN)
  • 最长递增子序列和一种叫作patience game的纸牌游戏有关,有一种排序算法就叫做耐心排序patience sorting
  • 场景分析: 给定一排纸牌,然后从左到右像遍历数组那样一张一张处置这些纸牌,最终将这些纸牌分成若干堆
    • 只能将点数小的牌压到点数大的牌上
    • 如果当前牌点数较大没有可以放置的堆,则新建一个堆,将这张牌放置进去
    • 如果当前牌有多个堆可供选择,则选择最左边的堆放置
      • 选择最左边的堆放置的原因是为了保证堆顶的牌有序
  • 按照上述规则,可以算出最长递增子序列,牌堆数就是最长递增子序列的长度
  • 二分查找算法求解最长递增子序列:
    • 将处理扑克牌的过程使用编程的方式表达出来
    • 因为每次处理一张扑克牌要找到一个合适的牌堆顶放置,牌堆顶的牌是有序的.所以可以使用二分查找
    • 使用二分查找来搜索当前牌应该放置的位置
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
c复制代码public int LengthOfLIS(int[] nums) {
int[] top = new Int[nums.length];
// 牌的堆数初始化为0
int piles = 0;

for (int i = 0; i < nums.length; i++) {
// 需要处理的牌
int poker = nums[i];

int left = 0, right = piles;
while (left < right) {
int mid = left + (right -left) / 2;
if (top[mid] > poker) {
right = mid;
} else if (top[mid] < poker) {
left = mid + 1;
} else {
right = mid;
}
}
// 没有找到合适的牌堆则新建一个牌堆
if (left == piles) {
piles++;
}
// 选择最左边的牌堆放置
top[left] = piles;
}
// 牌堆数就是最长递增子串的长度
return piles;
}
  • 二分查找解法:
    • 首先涉及数学证明,要证明出按照这些规则的执行,就能得到最长递增子序列
    • 其次是二分查找算法的应用,要理解二分查找方法的细节
  • 动态规划设计方法:
    • 假设之前的答案为已知
    • 利用数学归纳法的思想正确进行状态转移
    • 最后得到答案
  • 动态规划解法:
1
2
3
4
5
6
7
8
9
10
11
12
13
> python复制代码def lengthOfLIS(self, nums : List[int]) -> int:
> n = len(nums)
> dp = [1 for x in range(0, n)]
> for i in range(0, n):
> for j in range(0, i):
> if nums[i] > num[j]:
> dp[i] = max(dp[i], dp[j] + 1)
> res = 0
> for temp in dp:
> res = max(temp, res)
> return res
>
>
  • 二分查找解法:
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
> python复制代码def lengthOfLIS(self, nums : List[int]) -> int:
> top = []
> # 牌堆初始化为0
> piles = 0
> # num为需要处理的牌
> for num in nums:
> left, right = 0,
> while left < right:
> mid = left + (right - left) / 2
> # 搜索左侧边界
> if top[mid] > num:
> right = mid
> # 搜索右侧边界
> elif top[mid] < num:
> left = mid + 1
> else
> right = mid
> if left == piles:
> # 如果没有找到合适的牌堆,就新建一个牌堆
> piles += 1
> # 将该牌放到新建的牌堆顶
> top[left] = num
> # 牌堆数就是最长递增子序列的长度
> return piles
>
>

本文转载自: 掘金

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

【力扣-二叉树】18、二叉树中的众数 501 二叉搜索树中

发表于 2021-11-21

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」

501. 二叉搜索树中的众数

题目描述

给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。

假定 BST 有如下定义:

  • 结点左子树中所含结点的值小于等于当前结点的值
  • 结点右子树中所含结点的值大于等于当前结点的值
  • 左子树和右子树都是二叉搜索树

例如:

给定 BST [1,null,2,2],

1
2
3
4
5
markdown复制代码   1
\
2
/
2

返回[2].

提示:如果众数超过1个,不需考虑输出顺序

进阶: 你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)

解析(针对二叉搜索树)

中序遍历

  • 在中序遍历的过程中,求出众数
  • 二叉搜索树中,左子树节点与根节点值相同,或者右子树节点值与根节点值相同
  • 使用指针pre记录遍历的前一个节点,当pre==NULL时表示遍历的第一个节点,此时pre为空
  • 使用int maxCount记录众数,使用int count记录每个值出现的次数
  • 使用vector<int> result记录众数
  • 在遍历的过程中更新maxCount
  • 只要出现count大于maxCount,就把result数组清空,将当前节点值加入到结果集result

daima

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
c++复制代码// 中序遍历
class Solution
{
public:
vector<int> findMode(TreeNode *root)
{
result.clear();
searchBST(root);
return result;
}

private:
// 记录最大值
int maxCount = 0;
// 记录每个元素出现的次数
int count = 0;
// 结果集
vector<int> result;
// 指向前一个节点的指针
TreeNode *pre = NULL;
// 中序遍历
void searchBST(TreeNode *node)
{
if (node == NULL)
{
return;
}
// 左
searchBST(node->left);

// 遍历的第一个节点
if (pre == NULL)
{
count = 1;
}
else if (pre->val == node->val) // 当前一个节点值与当前节点值相同
{
count++;
}
else // 第一次出现的节点值,计数置为1
{
count = 1;
}
pre = node;

// 若计数大于计数的最大值,更新最大值
// 并且清空之前记录的节点值
if (count > maxCount)
{
result.clear();
// 更新最大值
maxCount = count;
// 重新记录新的节点值
result.push_back(node->val);
}
else if (count == maxCount)
{
// 出现的众数可能不止一个
result.push_back(node->val);
}
// 右
searchBST(node->right);
}
};

解析(针对普通二叉树)

  • 对于普通的二叉树,节点值得分布没有规律
  • 遍历二叉树
  • 使用map记录每个节点值出现的次数
  • 对 map 进行排序
  • 将出现次数最多的节点值存入结果集

代码

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
c++复制代码// 针对普通二叉树
// 内存会溢出(leetcode上)
class Solution
{
public:
vector<int> findMode(TreeNode *root)
{
vector<int> result;
// 前序遍历
traversal(root);
// 将map转为vector,vector中存储的仍然是pair
vector<pair<int, int>> tmp(mp.begin(), mp.end());
// 对 vector进行排序
sort(tmp.begin(), tmp.end(), compare);

// 将排序后的vector中的第一个pair的 first 元素加入到 结果集中
result.push_back(tmp[0].first);
// 遍历剩余的元素
for (int i = 0; i < tmp.size(); i++)
{
// 过滤掉相同的元素
if (tmp[i].second == tmp[0].second && tmp[i].first != tmp[0].first)
{
result.push_back(tmp[i].first);
}
}

return result;
}

private:
map<int, int> mp; // 统计每个节点值出现的次数
void traversal(TreeNode *node)
{
if (node == NULL)
{
return;
}
// 节点出现次数+1
mp[node->val]++;
// 左
traversal(node->left);
// 右
traversal(node->right);
}
// 比较pair<int,int>类型的数据,返回第二个值较大的 pair
bool static compare(const pair<int, int> &a, const pair<int, int> &b)
{
return a.second >= b.second;
}
};
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
c++复制代码
// 修改后
class Solution {
private:

void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍历
if (cur == NULL) return ;
map[cur->val]++; // 统计元素频率
searchBST(cur->left, map);
searchBST(cur->right, map);
return ;
}
bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
public:
vector<int> findMode(TreeNode* root) {
unordered_map<int, int> map; // key:元素,value:出现频率
vector<int> result;
if (root == NULL) return result;
searchBST(root, map);
vector<pair<int, int>> vec(map.begin(), map.end());
sort(vec.begin(), vec.end(), cmp); // 给频率排个序
result.push_back(vec[0].first);
for (int i = 1; i < vec.size(); i++) {
// 取最高的放到result数组中
if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
else break;
}
return result;
}
};

本文转载自: 掘金

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

【力扣-二叉树】17二叉搜索树的最小绝对差 530 二叉

发表于 2021-11-21

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」

530. 二叉搜索树的最小绝对差

题目描述

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

差值是一个正数,其数值等于两值之差的绝对值。

示例 1:

1
2
ini复制代码输入: root = [4,2,6,1,3]
输出: 1

示例 2:

1
2
csharp复制代码输入: root = [1,0,48,null,null,12,49]
输出: 1

递归法

遇到在二叉搜索树上求最值,求差值之类的问题,都要考虑到二叉搜索树有序的,要充分利用好这一特点。利用中序遍历可以得到一个有序的递增数组,在通过数组来获得想要的值

  • 递归法1 : 中序递归遍历,使用数组存储每个节点的值。遍历数组,相邻的两个数作差,得到差值的最小值。
  • 递归法2 : 中序递归遍历,使用 pre 指针,指向当前遍历结点的前一个结点,在遍历的过程中计算最小差值。

递归法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
c++复制代码#define MAX_INT 65536

class Solution
{
public:
int getMinimumDifference(TreeNode *root)
{
// 清空数组
vec.clear();
// 中序遍历,将节点的值加入到数组中
traversal(root);
int minDiff = MAX_INT;
// 遍历数组,求出最小的差值
for (int i = 1; i < vec.size(); i++)
{
if (vec[i] - vec[i - 1] < minDiff)
{
minDiff = vec[i] - vec[i - 1];
}
}
return minDiff;
}

private:
// 定义全局数组,用来存储每个节点的值
vector<int> vec;
// 中序遍历数组
void traversal(TreeNode *node)
{
if (node == NULL)
{
return;
}
// 左
traversal(node->left);
// 中
vec.push_back(node->val);
// 右
traversal(node->right);
}
};

递归法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
c++复制代码#define MAX_INT 65536

class Solution
{
private:
// 记录最小的差值
int result = MAX_INT;

// 记录遍历结点的前一个结点
TreeNode *pre;
void traversal(TreeNode *cur)
{
if (cur == NULL)
{
return;
}
// 左
traversal(cur->left);
// 中
if (pre != NULL)
{
result = min(result, cur->val - pre->val);
}
// 记录前一个结点
pre = cur;
// 右
traversal(cur->right);
}

public:
int getMinimumDifference(TreeNode *root)
{
traversal(root);
return result;
}
};

本文转载自: 掘金

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

隐藏了2年的Bug,终于连根拔起,悲观锁并没有那么简单

发表于 2021-11-21

接手的新项目,接二连三的出现账不平的问题,作为程序员中比较执着的人,不解决誓不罢休。最终,经过两次,历时多日终于将其连根拔起。实属不易,特写篇文章记录一下。

文章中不仅会讲到使用悲观锁踩到的坑,以及本人是如何排查问题的,某些思路和方法或许能对大家有所帮助。

事情的起源

运营同事时不时就提出查账调账的需求,原因很简单,账不平,不查不行。如果你有过财务相关系统的工作经历,账务问题始终是最难攻克的。

虽然刚接手项目,虽然很多业务逻辑还不了解,但出现这样的技术挑战,还是要坚决攻克的。

其实,这类问题的原因很简单:热点账户。当很多服务或线程操作同一个用户的账户时,就会出现一个更新把另外一个更新覆盖掉的情况。

账户不平

上图可轻易看出,当两个服务或线程同时查询数据库的一条数据(热点账户),然后内存中做修改,最后更新到数据库。如果出现并发情况,两个线程都读取了100,一个计算得80,一个计算得60,后更新的就有可能将前面的覆盖掉。

解决方案通常有:

  • 单服务线程锁;
  • 集群分布式锁;
  • 集群数据库悲观锁;

项目中已采用了悲观锁,就基于来进行排查追踪原因。

何谓悲观锁

悲观锁是在对数据被的修改持悲观态度,在整个数据处理过程中会将数据锁定。

悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在应用层中实现了加锁机制,也无法保证外部系统不会修改数据)。

通常会使用select … for update语句来实现对数据的枷锁。

for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。

如下示例展示了悲观锁的基本使用流程:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码set autocommit=0;  
//设置完autocommit后,执行正常业务。具体如下:
//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;

因为关闭了数据库自动提交,这里通过begin/commit来管理事务。

使用select…for update的方式通过数据库实现了悲观锁。其中,id为1的那条数据就被锁定,其它的事务必须等本次事务提交之后才能执行。这样就保证了在操作期间数据不会被其它事务修改。

原因初步分析

在了解了账不平的原因和悲观锁的基本原理之后,就可以进行问题的排查了。既然系统已经使用了悲观锁,竟然还会出现问题,那肯定是哪里漏掉了什么。

于是,排查了所有账户(account表)更新的地方,还真找到一处bug。

大多数地方都使用了悲观锁,先for update查询一下,然后计算新的余额,再进行更新数据库。但有一处竟然先查询到了计算了余额,然后再进行加锁,最后更新。

基本流程如下:

错误加锁

在上述情况中,虽然线程B进行了加锁处理,但由于计算新余额并未在锁中,导致虽然使用了悲观锁,但依旧存在问题。正确的使用方式就是将计算余额的逻辑放在锁中。

当然,如果线程B完全被遗忘加锁了,也会出现同样的问题。

在排查解决了上述bug,我开始嘚瑟了,以为彻底解决了账不平的问题。

一个月之后

结果一个月之后,运营同事又来找了,偶尔依旧会出现账不平的问题。刚开始我还以为是不是搞错了,历史的账不平导致现在最终的不平。但最终还是下定决心再排查一次。

第一天,把账不平的账户的账务流水、涉及到代码、日志全部捋一遍。这期间还遇到了很多小困难,最终注意克服。

困难一:数据查不动

账务记录表数据太多,上千万的数据,最初的设计者并没有创建索引。这就要了老命了,根据筛选条件根本查不出数据来。

这里就用到SQL优化的两个技能点:limit限制查询条数和高效的分页策略。

关于limit限制查询条件这一点很明显,不仅减少了结果集,而且在遇到符合条件的数据之后会立马返回。

高效的分页策略在列表页在查询数据经常遇到,为了避免一次性返回过多的数据影响接口性能,一般会对查询接口做分页处理。

在Mysql中分页一般用的limit关键字:

1
sql复制代码select id,name,age from user limit 10,20;

少量数据时,limit分页没啥问题。但如果表中数据量很多,就会出现性能问题。

比如分页参数变成了:

1
sql复制代码select id,name,age from user limit 1000000,20;

Mysql会查到1000020条数据,然后丢弃前面的1000000条,只查后面的20条数据,非常浪费资源。

优化sql:

1
bash复制代码select id,name,age from user where id > 1000000 limit 20;

当然还可以使用between优化分页:

1
2
sql复制代码select id,name,age 
from user where id between 1000000 and 1000020;

值得庆幸的是那张表的ID是自增的,于是用了id大于的条件,只差了最近的交易记录,才勉强把数据查询出来。

困难二:日志过多

由于系统日志打的比较详细,一个项目每天大概几个G的日志。要在这中间查询到有用的日志,也是一个调整。

排查问题时,先使用了grep 命令找到出问题交易的账号日志:

1
c复制代码grep 123 info.log

当大概定位的到日志输出时间了,再利用区间缩小日志范围:

1
c复制代码grep '2021-11-17 19:23:23' info.log > temp.log

这里同样使用grep命令查找对应时间区间的日志,并将查找到日志输出到temp.log文件中,然后通过sz命令,下载到本地进行筛选分析。

这里大家可以善用grep命令。同时也要善用输出到新文件,这样比每次查几个G的内容方便多了。当然更方便的就是把筛选之后的日志下载本地,再次比对分析。

其他

关于代码筛选这块,没有什么诀窍,除了从头到位的捋一捋,没有别的好方法。不过这个过程善用IDE的搜索和“Find usages”功能即可。

日终收获

经过上述排查,最终在临下班时,定位到了问题的原因:一个线程将余额更新之后,另外一个线程将其覆盖了。在账务流水记录中存在了两笔紧邻,且计算前余额一样的记录。

得出结果之后,再排查其他的同类问题就方便多了,比如可采用group by来进行快速筛选:

1
csharp复制代码select count(id) as num , balance from account group by balance having num > 1;

通过上述语句就可以快速查出有同样计算前余额的记录。当然,上述语句还可以添加条件和结果维度。

虽然找到的问题发生的地方,但并未完全找到问题的原因。

更深层次的Bug

本以为找到了问题发生的点,就能快速解决问题的,但的确小觑了这个Bug,又是一整天才排查出根本原因。

模拟高并发

找到出问题的代码,看了实现逻辑,没问题啊,也加了悲观锁,数据库事务也没失效,也没有同Service的方法调用。怎么就会出现问题呢?

既然肉眼看不出来,那就用程序跑。于是,写了一个单元测试,创建一个线程池,来调用对应加锁方法。结果,依旧没问题。

由于跑的是测试库,生产库用的是云服务,担心是数据库的差异,于是在Navicat验证了悲观锁是否生效:

1
2
sql复制代码START transaction ;
select * from account where id = 1 for update;

然后在另外一个查询窗口执行:

1
sql复制代码select * from account where id = 1 for update;

发现,数据库的锁的确是生效的,在没有执行commit操作之前,是查不到数据的。

僵局与希望

此时,完全陷入僵局。于是就开始大量搜索资料,多次阅读代码。

最终,在一篇写得很水,但给了一个Hibernate javadoc文档链接的文中,无意点了一下链接,获得了巨大的启发。

在javadoc看了一下session实现悲观锁的方法。项目中用了已经废弃的get方法:

get

1
2
3
4
5
6
> vbnet复制代码@Deprecated
> Object get(Class clazz,
>                   Serializable id,
>                   LockMode lockMode)
>
>

Deprecated.* LockMode parameter should be replaced with LockOptions*

Return the persistent instance of the given entity class with the given identifier, or null if there is no such persistent instance. (If the instance is already associated with the session, return that instance. This method never returns an uninitialized instance.) Obtain the specified lock mode if the instance exists.

其中的“If the instance is already associated with the session, return that instance”让我眼前一亮。难道是缓存在作祟?

上面的重点是:如果session中已经存在这么个对象实例,会直接返回这个实例。

感觉回去看代码,还真是的,伪代码如下:

1
2
3
4
5
6
ini复制代码Account account = accountService.getAccount(type, userNo);
if(account == null){
//...
}
accountService.getAccountAndLock(account.getId());
// ...

上述代码首先值得肯定的有两点:第一,在加锁之前先查了一次对象,这样能避免因为对象不存在,锁住全表;第二,就是锁一条数据库记录时尽量采用id,精确定位到具体的记录,避免锁住其他记录或整张表。

那么,是不是因为前面的查询导致后面getAccountAndLock方法的实效呢?再来验证一下。

于是,在单元测试中添加了前面的查询,再次执行。哈哈,Bug终于复现了!

为了进一步证实,在底层的公共方法中添加了clear操作:

1
2
3
4
5
6
7
scss复制代码  public T findAndLock(Class cls, String primaryKey) throws DataAccessException {
Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
// 添加验证是否缓存问题
session.clear();
Object object = session.load(cls, primaryKey, LockOptions.UPGRADE);
return (T) object;
}

再次执行单元测试,可正常加锁。至此,Bug定位完毕。

问题的解决

既然已经定位问题,解决起来就非常方便了。上面使用session.clear()只是为了验证,真实生产使用这种方法影响太大,而且是事后处理。

解决方案:将基于Hibernate的普通查询,改为基于原生SQL的查询。因为前面的普通查询只需要id,那么只用一条SQL查询ID即可,如果id为空,则不存在;如果id非空,则再进行下一步处理。

至此,问题完美解决。

小结

在解决上述问题的过程中,看似只是很简单的悲观锁,但在排查的过程中还用到和涉及到了大量的其他知识,比如@Transactional事务失效场景的排查、事务的隔离级别、Hibernate的多级缓存、Spring的事物管理、多线程、Linux操作、Navicat手动事务、SQL优化、单元测试、Javadoc查阅等。

所以,在解决问题之后,觉得十分有必要分享给大家。通过这个案例,你又学到了什么呢?

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

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

Maven 依赖调解源码解析(七):总结

发表于 2021-11-21

本文是系列文章《Maven 源码解析:依赖调解是如何实现的?》第七篇,也是最后一篇,主要做个总结。

请按顺序阅读其他系列文章,系列文章总目录参见:juejin.cn/post/703292…

总结

在本系列文章中,我们搭建了一个简单的多模块项目,以实验的形式,从源码角度解析了四种依赖调节原则。涉及到了传递依赖的两种调解原则、一种同文件内的覆盖原则,以及 dependencyManagement 依赖锁定原则。其中,传递依赖的两种调解原则涉及到 NearestConflictResolver 冲突调解器;而同文件内的覆盖原则最简单,就是简单的 Map 覆盖;最后,dependencyManagement 依赖锁定原则稍有些复杂,因为它涉及到了 dependencyManagement 的版本解析,并以解析出来的版本号为准。

在现实工作中,这几种依赖关系可能同时存在。尤其在大型工程中,dependencyManagement 版本锁定运用非常广泛,如果能从源码角度掌握其运行原理,一定会提升你对 Maven 的运用能力。

资源

本文中用到的源码已上传至 Github,地址:github.com/xiaoxi666/m…,需要的小伙伴请自行下载。

在阅读源码的过程中,我们学到了什么?

首先,我们了解了 Maven 依赖调解实现原理,以后面对各种输出信息,能够做到心中有数。稍微拓展一下,各种依赖管理工具的核心原理其实都差不多,无非就是管理各种依赖版本。希望本文能为你理解包管理工具的实现思路提供些许参考。

其次是设计方面的考量。Maven 提供了核心实现,并且预留了各种扩展点,可以让不同的插件实现不同的功能。这种开发模式可以非常方便功能扩展,对于一款软件的成长是很有利的。业界也有很多采用这种思路开发的产品,比如你熟悉的 Atom。

再说说算法,其实就是很经典的递归算法,以及常提到的备忘录(Map 存储已经解析过的依赖)。

最后,谈谈设计模式的体现。上面提到的源码就有几种,比如模板模式(不同冲突调解器的实现)、访问者模式(参照 visit 方法)、观察者模式(各种 Listener,事件产生时打印一些信息),以及桥接模式(dependency:tree 的实现其实是一个桥,类似于 slf4j 的模式)等等。

当然,你还学会了一种简单方便的 Maven 源码调试方法,哈哈。

参考

《Maven 实战》

Maven 官网:maven.apache.org/guides

本文转载自: 掘金

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

一个简单的库,可让你轻松处理 Swift 声音

发表于 2021-11-21

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」

🌊 作者主页:海拥

🌊 作者简介:🏆CSDN全栈领域优质创作者、🥇HDZ核心组成员

🌊 粉丝福利:粉丝群 每周送六本书,不定期送各种小礼品

概述

SwiftySound 是一个简单的库,可让你轻松处理 Swift 声音。

静态方法

1
python复制代码Sound.play(file: "dog.wav")
1
python复制代码Sound.play(url: fileURL)

更高级的例子:

1
python复制代码Sound.play(file: "dog", fileExtension: "wav", numberOfLoops: 2)

以上将播放声音三遍。

指定负数循环以无限循环连续播放声音:

1
python复制代码Sound.play(file: "dog", fileExtension: "wav", numberOfLoops: -1)

停止当前播放声音:

1
python复制代码Sound.stopAll()

启用/禁用所有声音:

1
2
python复制代码Sound.enabled = true
Sound.enabled = false

Sound.enabled属性的值将在UserDefaults你的应用程序下次启动时自动保留并恢复。

更改声音类别。SwiftySound 提供了一种更改声音类别的简单方法:

1
python复制代码Sound.category = .ambient

这会更改底层共享AVAudioSession实例的类别。默认值为SoundCategory.ambient。由于AVAudioSession体系结构的原因,此属性在 macOS 上不可用。

创建Sound类的实例

你还可以创建 Sound 类的实例并将其存储在应用程序中的某个位置。

1
2
python复制代码let mySound = Sound(url: fileURL)
mySound.play()

创建实例有更多好处,例如可以调整音量和播放回调。

改变音量

你可以更改每个Sound实例的音量。

1
python复制代码mySound.volume = 0.5

volume属性的值应该在 0.0 到 1.0 之间,其中 1.0 是最大值。

回调

你可以将回调传递给该play方法。它会在声音播放完毕后播放。对于循环声音,将在播放最后一个循环后调用一次回调。

1
2
3
python复制代码mySound.play { completed in
print("completed: (completed)")
}

如果声音停止、中断或播放错误,则不会调用回调。

特点

  • 播放单个声音
  • 循环
  • 无限循环
  • 同时多次播放相同的声音
  • 使用全局静态方法停止所有声音
  • 能够暂停和恢复
  • 调节音量
  • 回调
  • 启用/禁用所有声音的全局静态变量

要求

  • Swift 5
  • Xcode 10.2 或更高版本
  • iOS 8.0 或更高版本
  • tvOS 9.0 或更高版本
  • macOS 10.9 或更高版本

对于 Xcode 8 和 Swift 3 支持,请使用 SwiftySound 版本0.7.0。对于 Xcode 9 和 Swift 4 支持,请使用 SwiftySound 版本1.0.0。

安装

使用 CocoaPods 安装

CocoaPods是一个依赖管理器,它自动化并简化了在项目中使用第三方库的过程。

Podfile

1
2
3
python复制代码platform :ios, '8.0'
use_frameworks!
pod 'SwiftySound'

使用 Carthage 安装

Carthage是 Swift 和 Objective-C 的轻量级依赖管理器。它利用 CocoaTouch 模块并且比 CocoaPods 的侵入性更小。

要使用 carthage 安装,请按照Carthage上的说明进行操作

Cartfile

1
python复制代码github "adamcichy/SwiftySound"

使用 Swift 包管理器安装

Swift Package Manager 是一个用于管理 Swift 代码分发的工具。只需将此 repo 的 url 添加到你的Package.swift文件中作为依赖项:

1
2
3
4
5
6
7
8
9
python复制代码import PackageDescription

let package = Package(
name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/adamcichy/SwiftySound.git",
majorVersion: 0)
]
)

然后运行swift build并等待 SPM 安装 SwiftySound。

手动安装

将Sound.swift文件放入你的项目中,链接AVFoundation.framework,就可以开始了。

执照

SwiftySound 是根据[MIT 许可证获得许可的。

GitHub

github.com/adamcichy/S…

写在最后的

作者立志打造一个拥有100个小游戏的摸鱼网站,更新进度:41/100

我已经写了很长一段时间的技术博客,并且主要通过掘金发表,这是我的一篇关于一个简单的库,可让你轻松处理 Swift 声音。我喜欢通过文章分享技术与快乐。你可以访问我的博客: juejin.cn/user/204034… 以了解更多信息。希望你们会喜欢!😊

💌 欢迎大家在评论区提出意见和建议!💌

本文转载自: 掘金

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

Maven 依赖调解源码解析(五):同一个文件内声明,后者覆

发表于 2021-11-21

本文是系列文章《Maven 源码解析:依赖调解是如何实现的?》第五篇,主要介绍同一个文件内声明,后者覆盖前者的原则。

请按顺序阅读其他系列文章,系列文章总目录参见:juejin.cn/post/703292…

场景

这次我们让 A 直接依赖 X,且在 A 的 pom.xml 中声明两次 X,分别为 1.0 和 2.0 版本。内容如下:

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
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">
<parent>
<artifactId>mavenDependencyDemo</artifactId>
<groupId>org.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>A</artifactId>
<version>1.0</version>

<dependencies>

<dependency>
<groupId>org.example</groupId>
<artifactId>X</artifactId>
<version>1.0</version>
</dependency>

<dependency>
<groupId>org.example</groupId>
<artifactId>X</artifactId>
<version>2.0</version>
</dependency>


</dependencies>

</project>

源码

这个场景比较简单,不涉及调解器,我们直接对着图看一下主流程:

小结

从源码可以看到,如果在同一个 pom 文件内,声明了两个 groupId 和 artifactId 完全相同的依赖,则会以最后一个声明的依赖为准。因为在实现层面,它们是保存在 Map 中的,后一个依赖会把前一个依赖覆盖掉。这也印证了该原则的名称:同一个文件内声明,后者覆盖前者。

其实,控制台已经输出了警告,如果你仔细观察的话就会发现:

对应的源码在这里:

本文转载自: 掘金

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

1…251252253…956

开发者博客

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