注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

奇怪的电梯广搜做法~

一、题目描述:一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例...
继续阅读 »

一、题目描述:

一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 Ki(K1=3, K2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 -2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?

来源:洛谷 http://www.luogu.com.cn/problem/P11…

输入格式

共二行。 第一行为三个用空格隔开的正整数,表示 N, A, B(1≤N≤200,1≤A,B≤N )。 第二行为 N 个用空格隔开的非负整数,表示 Ki

5 1 5
3 3 1 2 5

输出格式

一行,即最少按键次数,若无法到达,则输出 -1

3

二、思路分析:

首先看一下输入数据是什么意思,首先输入一个N, A, B,也就是分别输入楼层数(N)、开始楼层(A)、 终点楼层(B)。 在例子中,我们的 楼层数N是5,也就是说有5层楼,第二行就是这5层楼的每层楼的数字k。

1、题目中说到只有四个按钮:开,关,上,下,上下的层数等于当前楼层上的那个数字,众所周知,电梯的楼层到了的时候啊,它是会自动打开的,没有人进来的时候,也会自动关上,这里求的是最少按几个按钮,所以我们在这里不用看开关,也就是可以看作该楼层只有两个按钮 +k 、 -k

2、题目中提到最少按几次,很明显,这是一个搜索题。当出现最少的时候,我们就可以考虑用广搜了(也可以用深搜做的啦)

3、这里注意一下,就是我们在不同的按钮次数时遇到停在同一楼层,这时候就会出现一个重复的且没有必要的搜索,所以我们需要在搜索的时候加个条件。

三、AC 代码:

import java.util.*;

public class Main {
public static void main(String[] args) {
Scanner sr = new Scanner (System.in);
int N = sr.nextInt();              
int A = sr.nextInt();            
int B = sr.nextInt();              

// 广搜必备队列
Queue<Fset> Q = new LinkedList<Fset>();
// 一个记忆判断,看看这层楼是不是来过
boolean[] visit = new boolean[N+1];
// 来存楼梯按钮的,假设第3层的k是2, 那么 k[3][0]=2 (向上的按钮)、 k[3][1]=-2 (向下的按钮)
int[][] k = new int [N+1][2];
for(int i = 1 ; i <= N ; i++){
k[i][0] = sr.nextInt();
k[i][1] = -k[i][0];
}

// 存一个起始楼层和按钮次数到队列
Q.add(new Fset(A,0));
// 当队列为空也就是所以能走的路线都走过了,没有找到就可以返回-1了
while(!Q.isEmpty()){
Fset t = Q.poll();
// 找到终点楼层,不用找了直接输出并退出搜索
if(t.floor == B){
System.out.println(t.count);
return;
}
//
for(int j = 0 ; j < 2 ; j++){
// 按键后到的楼层
int f = t.floor + k[t.floor][j];
// 判断按键后到的楼层是否有效和是否走过
if(f >= 1 && f <= N && visit[f]!=true) {
Q.add(new Fset(f,t.count+1));
// 做标记
visit[f]=true;
}  
      }
      }
       // 没找到
       System.out.println(-1);
}
}

class Fset{
int floor; // 当前楼层
int count; // 当前按键次数
public Fset(int floor, int count) {
this.floor = floor;
this.count = count;
}
}


四、总结:

为什么用的队列呢? 因为队列的排队取出的,首先判断的一定是按钮次数最少的,感觉这道题用广搜或者深搜效果其实差不多,我写的深搜多一个判断,就是当当前次数超过我找到的最少按钮次数,我就丢弃这个。 广搜像晕染吧,往四周分散搜索,

嗯,就酱~


作者:d粥
来源:https://juejin.cn/post/7073817170618089479

收起阅读 »

Google 大佬们为什么要开发 Go 这门新语言?

Go
大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?难不成是造轮子,其他语言不香吗?背景Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施...
继续阅读 »

大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?

难不成是造轮子,其他语言不香吗?

背景

Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施时遇到的一些问题。


图上三位是 Go 语言最初的设计者,功力都非常的深厚,按序从左起分别是:

  • Robert Griesemer:参与过 Google V8 JavaScript 引擎和 Java HotSpot 虚拟机的研发。

  • Rob Pike:Unix 操作系统早期开发者之一,UTF-8 创始人之一,Go 语言吉祥物设计者是 Rob Pike 的媳妇。

  • Ken Thompson:图灵奖得主,Unix 操作系统早期开发者之一,UTF-8 创始人之一,C 语言(前身 B 语言)的设计者。

遇到的问题

曾经在早期的采访中,Google 大佬们反馈感觉 "编程" 太麻烦了,他们很不喜欢 C++,对于现在工作所用的语言和环境感觉比较沮丧,充满着许多不怎么好用的特性。

具体遭遇到的问题。如下:

  • 软件复杂:多核处理器、网络系统、大规模计算集群和网络编程模型所带来的问题只能暂时绕开,没法正面解决。

  • 软件规模:软件规模也发生了变化,今天的服务器程序由数千万行代码组成,由数百甚至数千名程序员进行工作,而且每天都在更新(据闻 Go 就是在等编译的 45 分钟中想出来的)。

  • 编译耗时:在大型编译集群中,构建时间也延长到了几分钟,甚至几小时。

设计目的

为了实现上述目标,在既有语言上改造的话,需要解决许多根本性的问题,因此需要一种新的语言。

这门新语言需要符合以下需求:

  • 目的:设计和开发 Go 是为了使在这种环境下能够提高工作效率

  • 设计:在 Go 的设计上,除了比较知名的方面:如内置并发和垃圾收集。还考虑到:严格的依赖性管理,随着系统的发展,软件架构的适应性,以及跨越组件之间边界的健壮性。

这门新语言就是现在的 Go。

Go 在 Google

Go 是 Google 设计的一种编程语言,用于帮助解决谷歌的问题,而 Google 的问题很大。

Google 整体的应用软件很庞大,硬件也很庞大,有数百万行的软件,服务器主要是 C++ 语言,其他部分则是大量的 Java 和 Python。

数以千计的工程师在代码上工作,在一个由所有软件组成的单一树的 "头 " 上工作,所以每天都会对该树的所有层次进行重大改变。

一个大型的定制设计的分布式构建系统使得这种规模的开发是可行的,但它仍然很大。

当然,所有这些软件都在几十亿台机器上运行,这些机器被视为数量不多的独立、联网的计算集群。


简而言之,Google 的开发规模很大,速度可能是缓慢的,而且往往是笨拙的。但它是有效的。

Go 项目的目标是:消除 Google 软件开发的缓慢和笨拙,从而使这个过程更富有成效和可扩展。这门语言是由编写、阅读、调试和维护大型软件系统的人设计的,也是为他们设计的

因此 Go 的目的不是为了研究编程语言的设计,而是为了改善其设计者及其同事的工作环境。

Go 更多的是关于软件工程而不是编程语言研究。或者换个说法,它是为软件工程服务的语言设计。

痛点

当 Go 发布时,有些人声称它缺少被认为是现代语言的必要条件的特定功能或方法。在缺乏这些设施的情况下,Go怎么可能有价值?

我们的答案是:Go 所拥有的特性可以解决那些使大规模软件开发变得困难的问题。

这些问题包括:

  • 构建速度缓慢。

  • 不受控制的依赖关系。

  • 每个程序员使用不同的语言子集。

  • 对程序的理解不透彻(代码可读性差,文档不全等)。

  • 工作的重复性。

  • 更新的成本。

  • 版本偏移(version skew)。

  • 编写自动工具的难度。

  • 跨语言的构建。

纯粹一门语言的单个功能并不能解决这些问题,我们需要对软件工程有一个更大的看法。因此在 Go 的设计中,我们试图把重点放在这些问题的解决方案上。

总结

软件工程指导了 Go 的设计。

与大多数通用编程语言相比,Go 的设计是为了解决我们在构建大型服务器软件时接触到的一系列软件工程问题。这可能会使 Go 听起来相当沉闷和工业化。

但事实上,整个设计过程中对清晰、简单和可组合性的关注反而导致了一种高效、有趣的语言,许多程序员发现它的表现力和力量。

为此产生的 Go 特性包括:

  • 清晰的依赖关系。

  • 清晰的语法。

  • 清晰的语义。

  • 相对于继承的组合。

  • 编程模型提供的简单性(垃圾收集、并发)。

  • 简单的工具(Go工具、gofmt、godoc、gofix)。

这就是为什么要开发 Go 的由来,以及为什么会产生如此的设计和特性的原因。

你学会了吗?:)

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

参考


作者:煎鱼eddycjy
来源:https://juejin.cn/post/7054028466060001288

收起阅读 »

本着什么原则,才能写出优秀的代码?

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。在这个...
继续阅读 »

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。

有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。

风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。

在这个时候,大部分程序员的想法是:这烂代码真是不想改,还不如直接重写。

但有的时候,我们看一些著名的开源项目时,又会感叹,代码写的真好,优雅。为什么好呢?又有点说不出来,总之就是好。

那么,这篇文章就试图分析一下好代码都有哪些特点,以及本着什么原则,才能写出优秀的代码。

初级阶段

先说说比较基本的原则,只要是程序员,不管是高级还是初级,都会考虑到的。

img

这只是列举了一部分,还有很多,我挑选四项简单举例说明一下。

  1. 格式统一

  2. 命名规范

  3. 注释清晰

  4. 避免重复代码

以下用 Python 代码分别举例说明:

格式统一

格式统一包括很多方面,比如 import 语句,需要按照如下顺序编写:

  1. Python 标准库模块

  2. Python 第三方模块

  3. 应用程序自定义模块

然后每部分间用空行分隔。

import os
import sys

import msgpack
import zmq

import foo
复制代码

再比如,要添加适当的空格,像下面这段代码;

i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
复制代码

代码都紧凑在一起了,很影响阅读。

i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
复制代码

添加空格之后,立刻感觉清晰了很多。

还有就是像 Python 的缩进,其他语言的大括号位置,是放在行尾,还是另起新行,都需要保证统一的风格。

有了统一的风格,会让代码看起来更加整洁。

命名规范

好的命名是不需要注释的,只要看一眼命名,就能知道变量或者函数的作用。

比如下面这段代码:

a = 'zhangsan'
b = 0
复制代码

a 可能还能猜到,但当代码量大的时候,如果满屏都是 abcd,那还不得原地爆炸。

把变量名稍微改一下,就会使语义更加清晰:

username = 'zhangsan'
count = 0
复制代码

还有就是命名要风格统一。如果用驼峰就都用驼峰,用下划线就都用下划线,不要有的用驼峰,有点用下划线,看起来非常分裂。

注释清晰

看别人代码的时候,最大的愿望就是注释清晰,但在自己写代码时,却从来不写。

但注释也不是越多越好,我总结了以下几点:

  1. 注释不限于中文或英文,但最好不要中英文混用

  2. 注释要言简意赅,一两句话把功能说清楚

  3. 能写文档注释应该尽量写文档注释

  4. 比较重要的代码段,可以用双等号分隔开,突出其重要性

举个例子:

# =====================================
# 非常重要的函数,一定谨慎使用 !!!
# =====================================

def func(arg1, arg2):
   """在这里写函数的一句话总结(如: 计算平均值).

  这里是具体描述.

  参数
  ----------
  arg1 : int
      arg1的具体描述
  arg2 : int
      arg2的具体描述

  返回值
  -------
  int
      返回值的具体描述

  参看
  --------
  otherfunc : 其它关联函数等...

  示例
  --------
  示例使用doctest格式, 在`>>>`后的代码可以被文档测试工具作为测试用例自动运行

  >>> a=[1,2,3]
  >>> print [x + 3 for x in a]
  [4, 5, 6]
  """
复制代码

避免重复代码

随着项目规模变大,开发人员增多,代码量肯定也会增加,避免不了的会出现很多重复代码,这些代码实现的功能是相同的。

虽然不影响项目运行,但重复代码的危害是很大的。最直接的影响就是,出现一个问题,要改很多处代码,一旦漏掉一处,就会引发 BUG。

比如下面这段代码:

import time


def funA():
   start = time.time()
   for i in range(1000000):
       pass
   end = time.time()

   print("funA cost time = %f s" % (end-start))


def funB():
   start = time.time()
   for i in range(2000000):
       pass
   end = time.time()

   print("funB cost time = %f s" % (end-start))


if __name__ == '__main__':
   funA()
   funB()
复制代码

funA()funB() 中都有输出函数运行时间的代码,那么就适合将这些重复代码抽象出来。

比如写一个装饰器:

def warps():
   def warp(func):
       def _warp(*args, **kwargs):
           start = time.time()
           func(*args, **kwargs)
           end = time.time()
           print("{} cost time = {}".format(getattr(func, '__name__'), (end-start)))
       return _warp
   return warp
复制代码

这样,通过装饰器方法,实现了同样的功能。以后如果需要修改的话,直接改装饰器就好了,一劳永逸。

进阶阶段

当代码写时间长了之后,肯定会对自己有更高的要求,而不只是格式注释这些基本规范。

但在这个过程中,也是有一些问题需要注意的,下面就来详细说说。

炫技

第一个要说的就是「炫技」,当对代码越来越熟悉之后,总想写一些高级用法。但现实造成的结果就是,往往会使代码过度设计。

这不得不说说我的亲身经历了,曾经有一段时间,我特别迷恋各种高级用法。

有一次写过一段很长的 SQL,而且很复杂,里面甚至还包含了一个递归调用。有「炫技」嫌疑的 Python 代码就更多了,往往就是一行代码包含了 N 多魔术方法。

然后在写完之后漏出满意的笑容,感慨自己技术真牛。

结果就是各种被骂,更重要的是,一个星期之后,自己都看不懂了。

img

其实,代码并不是高级方法用的越多就越牛,而是要找到最适合的。

越简单的代码,越清晰的逻辑,就越不容易出错。而且在一个团队中,你的代码并不是你一个人维护,降低别人阅读,理解代码的成本也是很重要的。

脆弱

第二点需要关注的是代码的脆弱性,是否细微的改变就可能引起重大的故障。

代码里是不是充满了硬编码?如果是的话,则不是优雅的实现。很可能导致每次性能优化,或者配置变更就需要修改源代码。甚至还要重新打包,部署上线,非常麻烦。

而把这些硬编码提取出来,设计成可配置的,当需要变更时,直接改一下配置就可以了。

再来,对参数是不是有校验?或者容错处理?假如有一个 API 被第三方调用,如果第三方没按要求传参,会不会导致程序崩溃?

举个例子:

page = data['page']
size = data['size']
复制代码

这样的写法就没有下面的写法好:

page = data.get('page', 1)
size = data.get('size', 10)
复制代码

继续,项目中依赖的库是不是及时升级更新了?

积极,及时的升级可以避免跨大版本升级,因为跨大版本升级往往会带来很多问题。

还有就是在遇到一些安全漏洞时,升级是一个很好的解决办法。

最后一点,单元测试完善吗?覆盖率高吗?

说实话,程序员喜欢写代码,但往往不喜欢写单元测试,这是很不好的习惯。

有了完善,覆盖率高的单元测试,才能提高项目整体的健壮性,才能把因为修改代码带来的 BUG 的可能性降到最低。

重构

随着代码规模越来越大,重构是每一个开发人员都要面对的功课,Martin Fowler 将其定义为:在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改。

重构的收益是明显的,可以提高代码质量和性能,并提高未来的开发效率。

但重构的风险也很大,如果没有理清代码逻辑,不能做好回归测试,那么重构势必会引发很多问题。

这就要求在开发过程中要特别注重代码质量。除了上文提到的一些规范之外,还要注意是不是滥用了面向对象编程原则,接口之间设计是不是过度耦合等一系列问题。

那么,在开发过程中,有没有一个指导性原则,可以用来规避这些问题呢?

当然是有的,接着往下看。

高级阶段

最近刚读完一本书,Bob 大叔的《架构整洁之道》,感觉还是不错的,收获很多。

img

全书基本上是在描述软件设计的一些理论知识。大体分成三个部分:编程范式(结构化编程、面向对象编程和函数式编程),设计原则(主要是 SOLID),以及软件架构(其中讲了很多高屋建翎的内容)。

总体来说,这本书中的内容可以让你从微观(代码层面)和宏观(架构层面)两个层面对整个软件设计有一个全面的了解。

其中 SOLID 就是指面向对象编程和面向对象设计的五个基本原则,在开发过程中适当应用这五个原则,可以使软件维护和系统扩展都变得更容易。

五个基本原则分别是:

  1. 单一职责原则(SRP)

  2. 开放封闭原则(OCP)

  3. 里氏替换原则(LSP)

  4. 接口隔离原则(ISP)

  5. 依赖倒置原则(DIP)

单一职责原则(SRP)

A class should have one, and only one, reason to change. – Robert C Martin

一个软件系统的最佳结构高度依赖于这个系统的组织的内部结构,因此每个软件模块都有且只有一个需要被改变的理由。

这个原则非常容易被误解,很多程序员会认为是每个模块只能做一件事,其实不是这样。

举个例子:

假如有一个类 T,包含两个函数,分别是 A()B(),当有需求需要修改 A() 的时候,但却可能会影响 B() 的功能。

这就不是一个好的设计,说明 A()B() 耦合在一起了。

开放封闭原则(OCP)

Software entities should be open for extension, but closed for modification. – Bertrand Meyer, Object-Oriented Software Construction

如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。

通俗点解释就是设计的类对扩展是开放的,对修改是封闭的,即可扩展,不可修改。

看下面的代码示例,可以简单清晰地解释这个原则。

void DrawAllShape(ShapePointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawSquare((struct Circle*)s);
break;
default:
break;
}
}
}
复制代码

上面这段代码就没有遵守 OCP 原则。

假如我们想要增加一个三角形,那么就必须在 switch 下面新增一个 case。这样就修改了源代码,违反了 OCP 的封闭原则。

缺点也很明显,每次新增一种形状都需要修改源代码,如果代码逻辑复杂的话,发生问题的概率是相当高的。

class Shape
{
public:
virtual void Draw() const = 0;
}

class Square: public Shape
{
public:
virtual void Draw() const;
}

class Circle: public Shape
{
public:
virtual void Draw() const;
}

void DrawAllShapes(vector<Shape*>& list)
{
vector<Shape*>::iterator I;
for (i = list.begin(): i != list.end(); i++)
{
(*i)->Draw();
}
}
复制代码

通过这样修改,代码就优雅了很多。这个时候如果需要新增一种类型,只需要增加一个继承 Shape 的新类就可以了。完全不需要修改源代码,可以放心扩展。

里氏替换原则(LSP)

Require no more, promise no less.– Jim Weirich

这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。

里氏替换原则可以从两方面来理解:

第一个是继承。如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。

子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。

第二个是多态,而多态的前提就是子类覆盖并重新定义父类的方法。

为了符合 LSP,应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法。当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里,也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

举个例子:

看下面这段代码:

class A{
public int func1(int a, int b){
return a - b;
}
}

public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50=" + a.func1(100, 50));
System.out.println("100-80=" + a.func1(100, 80));
}
}
复制代码

输出;

100-50=50
100-80=20
复制代码

现在,我们新增一个功能:完成两数相加,然后再与 100 求和,由类 B 来负责。即类 B 需要完成两个功能:

  1. 两数相减

  2. 两数相加,然后再加 100

现在代码变成了这样:

class B extends A{
public int func1(int a, int b){
return a + b;
}

public int func2(int a, int b){
return func1(a,b) + 100;
}
}

public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50=" + b.func1(100, 50));
System.out.println("100-80=" + b.func1(100, 80));
System.out.println("100+20+100=" + b.func2(100, 20));
}
}
复制代码

输出;

100-50=150
100-80=180
100+20+100=220
复制代码

可以看到,原本正常的减法运算发生了错误。原因就是类 B 在给方法起名时重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原本运行正常的功能出现了错误。

这样做就违反了 LSP,使程序不够健壮。更通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

接口隔离原则(ISP)

Clients should not be forced to depend on methods they do not use. –Robert C. Martin

软件设计师应该在设计中避免不必要的依赖。

ISP 的原则是建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法要尽量少。

也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。

单一职责与接口隔离的区别:

  1. 单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。

  2. 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节; 而接口隔离原则主要约束接口。

举个例子:

img

首先解释一下这个图的意思:

「犬科」类依赖「接口 I」中的方法:「捕食」,「行走」,「奔跑」; 「鸟类」类依赖「接口 I」中的方法「捕食」,「滑翔」,「飞翔」。

「宠物狗」类与「鸽子」类分别是对「犬科」类与「鸟类」类依赖的实现。

对于具体的类:「宠物狗」与「鸽子」来说,虽然他们都存在用不到的方法,但由于实现了「接口 I」,所以也 必须要实现这些用不到的方法,这显然是不好的设计。

如果将这个设计修改为符合接口隔离原则的话,就必须对「接口 I」进拆分。

img

在这里,我们将原有的「接口 I」拆分为三个接口,拆分之后,每个类只需实现自己需要的接口即可。

依赖倒置原则(DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.– Robert C. Martin

高层策略性的代码不应该依赖实现底层细节的代码。

这话听起来就让人听不明白,我来翻译一下。大概就是说在写代码的时候,应该多使用稳定的抽象接口,少依赖多变的具体实现。

举个例子:

看下面这段代码:

public class Test {

public void studyJavaCourse() {
System.out.println("张三正在学习 Java 课程");
}

public void studyDesignPatternCourse() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

上层直接调用:

public static void main(String[] args) {
Test test = new Test();
test.studyJavaCourse();
test.studyDesignPatternCourse();
}
复制代码

这样写乍一看并没有什么问题,功能也实现的好好的,但仔细分析,却并不简单。

第一个问题:

如果张三又新学习了一门课程,那么就需要在 Test() 类中增加新的方法。随着需求增多,Test() 类会变得非常庞大,不好维护。

而且,最理想的情况是,新增代码并不会影响原有的代码,这样才能保证系统的稳定性,降低风险。

第二个问题:

Test() 类中方法实现的功能本质上都是一样的,但是却定义了三个不同名字的方法。那么有没有可能把这三个方法抽象出来,如果可以的话,代码的可读性和可维护性都会增加。

第三个问题:

业务层代码直接调用了底层类的实现细节,造成了严重的耦合,要改全改,牵一发而动全身。

基于 DIP 来解决这个问题,势必就要把底层抽象出来,避免上层直接调用底层。

img

抽象接口:

public interface ICourse {
void study();
}
复制代码

然后分别为 JavaCourseDesignPatternCourse 编写一个类:

public class JavaCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习 Java 课程");
}
}

public class DesignPatternCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

最后修改 Test() 类:

public class Test {

   public void study(ICourse course) {
       course.study();
  }
}
复制代码

现在,调用方式就变成了这样:

public static void main(String[] args) {
   Test test = new Test();
   test.study(new JavaCourse());
   test.study(new DesignPatternCourse());
}
复制代码

通过这样开发,上面提到的三个问题得到了完美解决。

其实,写代码并不难,通过什么设计模式来设计架构才是最难的,也是最重要的。

所以,下次有需求的时候,不要着急写代码,先想清楚了再动手也不迟。

这篇文章写的特别辛苦,主要是后半部分理解起来有些困难。而且有一些原则也确实没有使用经验,单靠文字理解还是差点意思,体会不到精髓。

其实,文章中的很多要求我都做不到,总结出来也相当于是对自己的一个激励。以后对代码要更加敬畏,而不是为了实现功能草草了事。写出健壮,优雅的代码应该是每个程序员的目标,与大家共勉。


作者:yongxinz
链接:https://mp.weixin.qq.com/s/xWZmP4qBI8cm68UZH6AXOg

收起阅读 »

十分钟搞懂手机号码一键登录

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只...
继续阅读 »

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只要确认登录用的手机号码是在绑定了此号码的手机上发起的即可认证成功,从这一点来看,它和短信验证码登录并无本质区别,都是一种设备认证登录方式。这篇文章就来捋一下其中的技术门道。

这几年为了保护用户的隐私安全,Android和iOS系统都限制了应用获取本机号码的能力,即使通过某些技术手段获取到了本机号码,这个号码还可能是被篡改的,所以应用直接读取本机号码用于登录是不可行的。那么这些应用是怎么获取到真实的本机号码的呢?答案是电信运营商,手机要打电话、要上网、要计费,运营商肯定能对应到正确的手机号码。国内的运营商就是移动、联通、电信这三家,它们都开放了这种能力。对于在互联网大潮中被管道化的运营商来说,不失为一种十分有意义的积极进取。

手机流量上网的原理

手机号码一键登录是借助手机流量上网来实现,所以先要搞清楚流量上网的原理。

目前网上已有很多关于一键登录的技术文章,但是内容基本雷同,关于获取手机号码的部分,所述都是通过运营商的数据网关能力,语焉不详,对于有追求的技术人来说,难以忍受。这个章节就来介绍下这种从数据网关获取手机号码的能力是如何实现的,因为通信专业知识十分繁杂,我也没有经过专业的学习,大家也不想接触到很多的专业名词,所以这里只保留一些关键的专业名词,尽量以通俗易懂的方式来理清这个机制。

五层网络模型

对网络比较熟悉的同学,应该了解五层协议,那么手机流量上网时的五层网络模型有何不同呢?


从上图可以看出,手机流量上网的主要区别在数据链路层和物理层。在数据链路层,流量上网没有MAC地址的概念,它采用一种点对点协议(PPP),手机端通过拨号方式建立这种PPP连接,然后发送数据。在物理层,流量上网通过手机内置的基带模块进行无线信号的调制、解调工作,从而实现与移动基站之间的电磁波通信。

流量上网的机制

点对点协议支持身 验证功能,手机端发起连接时会携带自己的身 粉证明,一般就是手机卡内置的IMSI,这个IMSI也会保存在运营商的数据库中,因此基站就可以验证连接用户的身 ,当然这个验证过程不是简单的对比IMSI,会有更多安全机制。为了更清楚的了解流量上网机制,下面再来一张4G流量上网时手机与运营商的交互示意图:


核心组件

手机:这其中对流量上网起到关键作用的就是手机卡和基带模块。手机卡中保存了IMSI,全称International Mobile Subscriber Identification Number,国际移动用户识别码。IMSI是手机卡的身 标识。

基站:就是外边常见的铁架子信号塔,是一种能覆盖一定范围的无线电收发信息电台,手机会连接到它,然后它再通过光纤连接到运营商网络,从而实现移动通信。

MME:Mobility Management Entity,移动控制单元。手机建立连接时会先访问到这里,负责:手机与基站的接入控制,手机卡的鉴权、会话管理、安全传输,漫游控制、跨运营商通信等。

HSS:Home Subscriber Server,归属签约用户服务器。保存本地签约的手机卡信息,包括手机卡IMSI与手机号的对应关系,手机号的套餐信息、手机号的归属地信息等。

S-GW:Service Gateway,服务网关。4G环境下,用户侧与运营商核心网之间的业务网关。访问能不能进入,能做什么业务,去哪里做业务,是在这里控制的。跨运营商计费、漫游计费等也在这里完成。

P-GW:PDN Gateway,PDN网关。运营商核心网与互联网之间的网关,手机真正上网就是通过它了。它会给手机分配一个IP地址,控制上网的速度,对流量进行计费等。

PCRF:Policy and Charging Rules Function,策略与计费控制单元,保存每个用户的网络访问策略和计费规则。

上网过程

为了方便理解,这里将上网的过程大致分为两个部分(和上图的1、2对应):

  • 1 接入:建立连接时,手机携带IMSI信息,通过基站访问到MME,MME通过HSS验证IMSI信息,然后MME进行一些初始化工作,返回一些鉴权参数给手机,手机再进行一些计算,然后把计算结果返回给MME,MME验证手机的计算结果,验证通过则允许接入。这个过程保证了接入的安全,MME还为后续的数据传输提供了加密传输支持,保护数据不被窃听和篡改,有兴趣的同学可以去详细了解下。

    如果手机卡销售的时候没有写入手机号,手机卡首次注册登记的时候,运营商会从HSS中取出手机号,然后再写入手机卡中。

    实际应用中,为了防止跟踪和攻击,不是每次通信时都要携带IMSI,MME会生成一个临时的GUTI对应到IMSI,就像Web程序中的SessionId。MME还有一定的机制控制GUIT的重新分配。

  • 2 传输:手机网络流量的传输,还是先要通过基站,然后下一步进入S-GW,S-GW会检查用户的授权,就像Web程序中检查前端提交过来的SessionId,再看看用户有没有权限进行其提交的业务,这里就是看看用户有没有开通流量上网,这是S-GW通过连接MME实现的。S-GW处理完毕后,数据包会进入P-GW,P-GW在手机使用流量上网时会给用户分配一个IP地址,然后数据包通过网关进入互联网,访问到相关的资源。P-GW还会对上网行为进行速率控制、流量计费等操作,这些策略来源于PCRF,PCRF中的规则是根据HSS中的用户套餐、用户等级等计算出来的。

    对P-GW来说S-GW屏蔽了用户的移动性,手机在多个基站切换时,S-GW不变。

以上就是手机流量上网的基本原理了,可以看到,运营商通过IMSI或者GUTI完全有能力获取到当前上网用户的手机号码。对于运营商的一键登录具体是怎么实现的,我并没有找到相关的介绍,但是可以设想下:手机应用通过运营商的SDK发起获取手机号码的业务请求,此时会携带IMSI或者GUTI,业务请求到达S-GW,S-GW鉴权通过,然后将这个业务请求路由到运营商核心网中获取手机号码的服务,服务根据业务规则从HSS中取出手机号码并进行若干处理。

一键登录的原理

理解了手机流量上网的原理,再来看下一键登录业务是如何实现的,这个部分属于上层应用程序开发,大家应该相对熟悉一些。

如果你接入过微信的第三方应用登录,或者其他类似的第三方应用登录,过程是差不多的。还是先来看图:


这里对一些关键步骤进行说明:

  • 2预取手机号掩码:这个手机号掩码需要在请求用户授权的页面展示给用户看,因为获取这个信息要通过电信运营商的网络,所以可能会比较慢,为了提升用户体验,可以在应用启动的时候就去获取,然后缓存一段时间。

  • 8授权请求:因为应用获取用户手机号这个事比较敏感,必须让用户清楚的了解并授权之后才能进行,为了确保这件事,运营商的认证SDK提供了这个授权请求页面,用户确认授权后,SDK直接向运营商认证服务发起请求认证,认证服务会返回一个认证Token给应用。应用再通过自己的服务端拿着这个Token找运营商获取手机号码。

  • 17生成应用授权Token:应用要维护自己用户的登录状态,这里可以采用传统的Session机制,也可以使用JWT机制。

  • 3预取手机号掩码 和 11请求认证,都需要通过手机蜂窝网络通信,也就说需要通过手机流量上网。如果手机同时开启了流量和WIFI,认证SDK会将手机短暂切换到流量上网模式。如果手机没有开启流量,有些SDK还会在上次成功取号之后多缓存一个临时Token,这样也能成功实现一次一键登录,不过这个限制性很大。

这里其实还有一个安全问题

14登录请求:用户如果随便造一个认证Token,然后就向应用服务提交请求,应用服务再向认证服务提交请求,这属于一种跨站攻击。虽然这个Token可以被阻止,但是不免浪费资源,给服务端带来压力。

这一点微信第三方应用登录做的比较好,用户登录前,应用服务端先生成一个随机数,然后应用前端向应用服务端提交时,带着这个随机数,应用服务端可以验证这个随机数。

号码验证场景

除了用于登录,运营商网关的这种取号能力,还可以用在验证手机号上,在某些关键业务上,比如支付过程中,要求用户输入本机手机号码或者其中的某几位,然后通过运营商认证服务验证手机号是否本机号码。

隐私保护问题

设备唯一标识问题

现在大家对隐私问题关注的越来越多了,经常会出现这种情况:你在某电商网站搜索了某个商品,然后访问其它网站时,都向你推荐这类商品的广告。还有一种感觉很恐怖的情况,你刚和某个人谈论了某件事,然后就在某个App上看到了关于这件事的推荐,有人猜测是App在偷听,不过基于目前的舆论和监督,偷听风险太大,这其中的原因可能真的只是算法太厉害了。

最近几年Android和iOS系统都对App获取手机唯一标识进行了限制,比如IMEI、Mac地址、序列号、广告Id等,目的就是防止用户的信息在多个App之间进行关联,导致泄漏用户的隐私,产生一些安全问题和法律风险,前述跨App的广告行为也自然受到了抑制。

在了解一键登录的技术原理时,看到某运营商提供了一种和SIM卡绑定的设备唯一Id服务,宣传语就是为了应对移动操作系统限制访问手机唯一标识的问题,在现今越来越重视隐私保护的前提下,如果这种能力开放给了广告平台,就是开历史的倒车了。

手机号作为身 份标识的问题

对于国内普遍使用手机号登录的方式,从技术上很难限制App之间进行手机号关联,然后综合分析用户的行为。比如某家大厂运营了多款不同种类的热门App,它就有能力更全面的了解某个用户,如果要限制可能就得通过法律层面来解决了。至于不同厂商之间的手机号关联行为,基于商业利益的保护,不太可能会出现。

在国内这种商业环境下,如果你真的对自己的隐私很关注,最好只使用账号密码的方式登录,否则经常更换手机号可能是一种没办法的办法。

手机号重新销售问题

手机号的总量是有限的,为了有效利用手机号资源,手机号注销以后,经过一段时间就会被运营商重新销售。如果新的手机号拥有者拿着这个手机号登录某个APP,而这个手机号之前已经在这个App上注册过,产生了大量的使用记录,那么此手机号前拥有者的隐私就会被泄漏。所以大家现在都不太敢随便更换手机号,因为注册过的地方太多了,留下了数不清的使用痕迹。

在了解一键登录的技术原理时,还看到某运营商提供了一种“手机号更换绑定SIM卡通知”的服务,应用可以据此解绑重新销售的手机号与应用账号之间的关系,从而保护用户的隐私。在上文中已经提过手机卡使用IMSI进行标识,如果手机号被重新销售,就会绑定新的IMSI,运营商可以据此产生通知。当然运营商还需要排除手机卡更换和携号转网的情况,这些情况下手机号也会绑定新的IMSI。

不得不说运营商的这个服务还是挺赞的👍。


作者:萤火架构
来源:https://juejin.cn/post/7059182505101885471

收起阅读 »

一些著名的软件都用什么语言编写?

1、操作系统Microsoft Windows :汇编 -> C -> C++备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。相...
继续阅读 »

1、操作系统

Microsoft Windows :汇编 -> C -> C++


备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。

相信很多朋友都知道Windows Vista,这个系统开发早期比尔盖茨想全部用C#写,但最终因为执行慢而放弃,结果之前无数软件工程师日夜劳作成果一夜之间被宣告作废。

Linux :C


Apple MacOS : 主要为C,部分为C++。

备注:之前用的语言比较杂,最早是汇编和Pascal。


Sun Solaris : C

HP-UX : C

Symbian OS : 汇编,主要为C++(诺基亚手机)

Google Android :2008 年推出:C语言(有传言说是用Java开发的操作系统,但最近刚推出原生的C语言SDK)

RIM BlackBerry OS 4.x :黑莓 C++

2、图形界面层

Microsoft Windows UI :C++

Apple MacOS UI (Aqua) : C++

Gnome (Linux图形界面之一,大脚): C和C++, 但主要是C

KDE (Linux图形界面): C++

3、桌面搜索工具

Google Desktop Search : C++


Microsoft Windows Desktop Search : C++

Beagle (Linux/Windows/UNIX 下): C# (基于开源的 .net : Mono)

4、办公软件

Microsoft Office :在 汇编 -> C -> 稳定在C++


Sun Open Office : 部分JAVA(对外接口),主要为C++ (开源,可下载其源代码)

Corel Office/WordPerfect Office : 1996年尝试过Java,次年被抛弃,重新回到C/C++

Adobe Systems Acrobat Reader/Distiller : C++

5、关系型数据库

Oracle : 汇编、C、C++、Java。主要为C++


MySQL : C++


IBM DB2 :汇编、C、C++,但主要为C


Microsoft SQL Server : 汇编 -> C->C++

IBM Informix : 汇编、C、C++,但主要为C

SAP DB/MaxDB : C++

6、Web Browsers/浏览器

Microsoft Internet Explorer : C++


Mozilla Firefox : C++


Netscape Navigator :The code of Netscape browser was written in C, and Netscape engineers, all bought to Java (see M. Cusumano book and article) redeveloped the browser using Java. It was too slow and abandoned. Mozilla, the next version, was later developed using C++.

Safari : (2003年1月发布)C++

Google Chrome : (2008的发布)C++


Sun HotJava : Java (死于1999年)

Opera : C++ (手机上占用率比较大)

Opera Mini : Opera Mini (2007) has a very funny architecture, and is indeed using both C++ and Java. The browser is split in two parts, an ultra thin (less than 100Kb) “viewer” client part and a server side responsible of rendering. The first uses Java and receives the page under the OBML format, the latter reuses classical Opera (C++) rendering engine plus Opera’s Small Screen Rendering, on the server. This allows Opera to penetrate various J2ME-enabled portable devices, such as phones, while preserving excellent response time. This comes obviously with a few sacrifices, for instance on JavaScript execution.

Mosaic : 鼻祖(已死) C 语言

7、邮件客户端

Microsoft Outlook : C++


IBM Lotus Notes : Java


Foxmail : Delphi


8、软件开发集成环境/IDE

Microsoft Visual Studio :C++


Eclipse : Java (其图形界面SWT基于C/C++)


Code::Blocks :C++


易语言:C++


火山中文:C++

火山移动:C++

9、虚拟机

Microsoft .Net CLR (.NET的虚拟机): C++


Java Virtual Machine (JVM) : Java 虚拟机:C++


10、ERP软件 (企业应用)

SAP mySAP ERP : C,后主要为“ABAP/4”语言

Oracle Peoplesoft : C++ -> Java


Oracle E-Business Suite : Java

11、商业智能(Business Intelligence )

Business Objects : C++

12、图形处理

Adobe Photoshop : C++


The GIMP : C

13、搜索引擎

Google : 汇编 与 C++,但主要为C++

14、著名网站

eBay : 2002年为C++,后主要迁至Java

facebook : C++ 和 PHP

This line is only about facebook, not its plugins. Plugins can be developed in many different technologies, thanks to facebook’s ORB/application server, Thrift. Thrift contains a compiler coded in C++. facebook people write about Thrift: “The multi-language code generation is well suited for search because it allows for application development in an efficient server side language (C++) and allows the Facebook PHP-based web application to make calls to the search service using Thrift PHP libraries.” Aside the use of C++, facebook has adopted a LAMP architecture.


阿里巴巴和淘宝:php->C++/Java(主要用)


15、游戏

汇编、C、C++

星际争霸、魔兽争霸、CS、帝国时代、跑跑卡丁车、传奇、魔兽世界… 数不胜数了,自己数吧


都是用C开发的,C语言靠近系统地称,执行速度最快。比如你的两个朋友与你分别玩用VB、Java、与C编写的“跑跑卡丁车”,你玩C编写的游戏已经跑玩结束了,发现你的两个朋友还没开始跑呢,那是相当的卡啊。

16、编译器

Microsoft Visual C++ 编译器: C++

Microsoft Visual Basic 解释、编译器:C++

Microsoft Visual C# :编译器: C++

gcc (GNU C compiler) : C

javac (Sun Java compiler) : Java

Perl : C++

PHP : C

17、3D引擎

Microsoft DirectX : C++


OpenGL : C


OGRE 3D : C++


18、Web Servers (网页服务)

Apache : C和C++,但主要为C


Microsoft IIS : C++

Tomcat : Java


Jboss : Java


19、邮件服务

Microsoft Exchange Server : C->C++

Postfix : C

hMailServer : C++

Apache James : Java

20、CD/DVD刻录

Nero Burning ROM : C++


K3B : C++

21、媒体播放器

Nullsoft Winamp : C++


Microsoft Windows Media Player : C++


22、Peer to Peer (P2P软件)

eMule : C++

μtorrent : C++

Azureus : Java (图形界面使用基于C/C++的SWT,类Eclipse)

23、全球定位系统(GPS)

TomTom : C++


Hertz NeverLost : C++

Garmin : C++

Motorola VIAMOTO : 2007年6月,停止服务,Java

24、3D引擎

Microsoft DirectX : C++(相信玩游戏的同学都知道这个,现在最高版本是DX11)

OpenGL : C

OGRE 3D : C++

25、服务器软件

Apache:C

Nginx:C


IIS:C

26、其它

OpenStack:Python


作者:土豆居士
来源:一口Linux

收起阅读 »

代码对比工具,我就用这6个

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。 支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通...
继续阅读 »

WinMerge

pic_2c55c38b.png

WinMerge是一款运行于Windows系统下的文件比较和合并工具,使用它可以非常方便地比较多个文档内容,适合程序员或者经常需要撰写文稿的朋友使用。

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。

Diffuse

pic_73d4bebc.png

Diffuse在命令行中的速度是相当快的,支持像 C++、Python、Java、XML 等语言的语法高亮显示。可视化比较,非常直观,支持两相比较和三相比较。这就是说,使用 Diffuse 你可以同时比较两个或三个文本文件。

支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通过 Diffuse 直接从版本控制系统获取源代码,以便对其进行比较和合并。

Beyond Compare

pic_45e693a5.png

Beyond Compare可以很方便地对比出两份源代码文件之间的不同之处,相差的每一个字节用颜色加以表示,查看方便,支持多种规则对比。

Beyond Compare选择最好的方法来突出不同之处,文本文件可以用语法高亮和设置比较规则的方法进行查看和编辑,适用于用于文档、源代码和HTML。

Altova DiffDog

pic_4afb57c3.png

pic_b3fd72aa.png

是一款用于文件、目录、数据库模式与表格对比与合并的使用工具。

这个强大易用的对比/合并工具可以让你通过其直观的可视化界面快速比较和合并文本或源代码文件,同步目录以及比较数据库模式与表格。DiffDog还提供了先进XML的差分和编辑功能。

AptDiff

pic_5bfb9a16.png

AptDiff是一个文件比较工具,可以对文本和二进制文件进行比较和合并,适用于软件开发、网络设计和其它的专业领域。

它使用方便,支持键盘快捷键,可以同步进行横向和纵向卷动,支持Unicode格式和大于4GB的文件,可以生成HTML格式的比较报告。

Code Compare

pic_1ff7b983.png

Code Compare是一款用于程序代码文件的比较工具,目前Code Compare支持的对比语言有:C#、C++、CSS、HTML、Java、JavaScrip等代码语言。

Code Compare的运行环境为Visual Studio,而Visual Studio可以方便所有的程序开发设计。

作者:小白学视觉
来源:https://mp.weixin.qq.com/s/I5__jnuDAIJlWbCHJsPDRQ

收起阅读 »

Java之父独家专访:我可太想简化一下 Java了

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Ja...
继续阅读 »

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Java 语言将如何发展?在亚马逊云科技 re:Invent 十周年之际,InfoQ 有幸对 Java 父 James Gosling 博士进行了一次独家专访。James Gosling 于 2017 年作为“杰出工程师”加入亚马逊云科技,负责为产品规划和产品发布之类的工作提供咨询支持,并开发了不少原型设计方案。在本次采访中,James Gosling 谈到了 Java 的诞生与发展、他对众多编程语言的看法、编程语言的未来发展趋势以及云计算带来的改变等问题。


Java 的诞生与发展

InfoQ:Java 语言是如何诞生的?是什么激发您创建一门全新的语言?

James Gosling:Java 的诞生其实源于物联网的兴起。当时,我在 Sun 公司工作,同事们都觉得嵌入式设备很有发展前景,而且随着设备数量的激增,整个世界正逐渐向智能化的方向发展。我们投入大量时间与不同行业的从业者进行交流,也拜访了众多东南亚、欧洲的从业者,结合交流心得和行业面临的问题,决定构建一套设计原型。正是在这套原型的构建过程中,我们深刻地意识到当时主流的语言 C++ 存在问题。

最初,我们只打算对 C++ 做出一点小调整,但随着工作的推进、一切很快“失控”了。我们构建出不少非常有趣的设备原型,也从中得到了重要启示。因此,我们及时对方向进行调整,希望设计出某种适用于主流业务和企业计算的解决方案,这正是一切故事的开端。

InfoQ:Java 作为一门盛行不衰的语言,直到现在依旧稳居编程语言的前列,其生命力何在?

James Gosling:Java 得以拥有顽强的生命力背后有诸多原因。

首先,采用 Java 能够非常便捷地进行多线程编程,能大大提升开发者的工作效率。

其次,Java 提供多种内置安全功能,能够帮助开发者及时发现错误、更加易于调试,此外,各种审查机制能够帮助开发者有效识别问题。

第三,热修复补丁功能也非常重要,亚马逊开发者开发出的热补丁修复程序,能够在无须停机的前提下修复正在运行的程序,这是 Java 中非常独特的功能。

第四,Java 拥有很好的内存管理机制,自动垃圾收集大大降低了内存泄露或者双重使用问题的几率。总之,Java 的设计特性确实提升了应用程序的健壮性,特别是极为强大的现代垃圾收集器方案。如果大家用过最新的长期支持版本 JDK17,应该对其出色的垃圾收集器印象深刻。新版本提供多种强大的垃圾收集器,适配多种不同负载使用。另外,现代垃圾收集器停顿时间很短、运行时的资源消耗也非常低。如今,很多用户会使用体量极为庞大的数据结构,而只要内存能容得下这种 TB 级别的数据,Java 就能以极快的速度完成庞大数据结构的构建。

InfoQ:Java 的版本一直以来更新得比较快,几个月前发布了最新的 Java17 版本,但 Java8 仍然是开发人员使用的主要版本,新版本并未“得宠”,您认为主要的原因是什么?

James Gosling:对继续坚守 Java8 的朋友,我想说“是时候作出改变了”。新系统全方位性更强、速度更快、错误也更少、扩展效率更高。无论从哪个角度看,大家都有理由接纳 JDK17。确实,大家在从 JDK8 升级到 JDK9 时会遇到一个小问题,这也是 Java 发展史中几乎唯一一次真正重大的版本更替。大多数情况下,Java 新旧版本更替都非常简单。只需要直接安装新版本,一切就能照常运作。长久以来,稳定、非破坏性的升级一直是 Java 的招牌特性之一,我们也不希望破坏这种良好的印象。

InfoQ:回顾当初,你觉得 Java 设计最成功的点是什么?相对不太满意的地方是什么?

James Gosling:这其实是一种博弈。真正重要的是 Java 能不能以更便利的方式完成任务。我们没办法设想,如果放弃某些问题域,Java 会不会变得更好?或者说,如果我现在重做 Java,在取舍上会有不同吗?区别肯定会有,但我估计我的取舍可能跟大多数人都不一样,毕竟我的编程风格也跟多数人不一样。不过总的来讲,Java 确实还有改进空间。

InfoQ:有没有考虑简化一下 Java?

James Gosling:我可太想简化一下 Java 了。毕竟简化的意义就是放下包袱、轻装上阵。所以 JavaScript 刚出现时,宣传的就是精简版 Java。但后来人们觉得 JavaScript 速度太慢了。在 JavaScript 的早期版本中,大家只是用来执行外部验证之类的简单事务,所以速度还不太重要。但在人们打算用 JavaScript 开发高性能应用时,得出的解决方案就成了 TypeScript。其实我一直觉得 TypeScript 的定位有点搞笑——JavaScript 就是去掉了 Type 的 Java,而 TypeScript 在 JavaScript 的基础上又把 type 加了回来。Type 系统有很多优势,特别是能让系统运行得更快,但也确实拉高了软件开发者的学习门槛。但如果你想成为一名专业的软件开发者,那最好能克服对于学习的恐惧心理。

Java 之父的编程语言之见

InfoQ:一款优秀的现代化编程语言应该是怎样的?当下最欣赏哪一种编程语言的设计理念?

James Gosling:我个人还是会用最简单的评判标准即这种语言能不能改善开发者的日常工作和生活。我尝试过很多语言,哪种更好主要取决于我想干什么。如果我正要编写低级设备驱动程序,那我可能倾向于选择 Rust。但如果需要编写的是用来为自动驾驶汽车建立复杂数据结构的大型导航系统,那我几乎肯定会选择 Java。

InfoQ:数据科学近两年非常热门,众所周知,R 语言和 Python 是数据科学领域最受欢迎的两门编程语言,那么,这两门语言的发展前景怎么样?因具体的应用领域产生专用的编程语言,会是接下来编程语言领域的趋势之一吗?

James Gosling:我是领域特定语言的铁粉,也深切认同这些语言在特定领域中的出色表现。大多数领域特定语言的问题是,它们只能在与世隔绝的某一领域中发挥作用,而无法跨越多个领域。这时候大家更愿意选择 Java 这类语言,它虽然没有针对任何特定领域作出优化,但却能在跨领域时表现良好。所以,如果大家要做的是任何形式的跨领域编程,肯定希望单一语言就能满足所有需求。有时候,大家也会尝试其他一些手段,希望在两种不同的领域特定语言之间架起一道桥梁,但一旦涉及两种以上的语言,我们的头脑通常就很难兼顾了。

InfoQ:Rust 一直致力于解决高并发和高安全性系统问题,这也确实符合当下绝大部分应用场景的需求,对于 Rust 语言的现在和未来您怎么看?

James Gosling:在我看来,Rust 太过关注安全了,这让它出了名的难学。Rust 解决问题的过程就像是证明定理,一步也不能出错。如果我们只需要编写一小段代码,用于某种固定不变的设备,那 Rust 的效果非常好。但如果大家需要构建一套具有高复杂度动态数据结构的大规模系统,那么 Rust 的使用难度就太高了。

编程语言的学习和发展

InfoQ:编程语言倾向于往更加低门槛的方向发展,开发者也更愿意选择学习门槛低的开发语言,一旦一门语言的学习成本过高,开发者可能就不愿意去选择了。对于这样的现象,您怎么看?

James Gosling:要具体问题具体分析。我到底需要 Rust 中的哪些功能特性?我又需要 Java 中的哪些功能特性?很多人更喜欢 Python,因为它的学习门槛真的很低。但跑跑基准测试,我们就会发现跟 Rust 和 Java 相比,Python 的性能实在太差了。如果关注性能,那 Rust 或 Java 才是正确答案。另外,如果你需要的是只有 Rust 能够提供的那种致密、安全、严谨的特性,代码的编写体量不大,而且一旦出问题会造成严重后果,那 Rust 就是比较合适的选择。只能说某些场景下某些语言更合适。Java 就属于比较折衷的语言,虽然不像 Python 那么好学,但也肯定不算难学。

InfoQ:当前,软件项目越来越倾向采用多语言开发,对程序员的要求也越来越高。一名开发人员,应该至少掌握几种语言?最应该熟悉和理解哪些编程语言?

James Gosling:我刚刚入行时,市面上已经有很多语言了。我学了不少语言,大概有几十种吧。但很多语言的诞生本身就很荒谬、很没必要。很多语言就是同一种语言的不同方言,因为它们只是在用不同的方式实现基本相同的语言定义。最让我振奋的是我生活在一个能够致力于解决问题的世界当中。Java 最大的吸引力也正在于此,它能帮助我们解决几乎任何问题。具有普适性的语言地位高些、只适用于特定场景的语言则地位低些,对吧?所以到底该学什么语言,取决于你想解决什么问题、完成哪些任务。明确想要解决什么样的问题才是关键。

InfoQ:2021 年,技术圈最热门的概念非元宇宙莫属,您认为随着元宇宙时代的到来,新的应用场景是否会对编程语言有新的需求?可否谈谈您对未来编程语言的理解?

James Gosling:其实人们从很早开始就在构建这类虚拟世界系统了,所以我觉得元宇宙概念对编程不会有什么影响。唯一的区别是未来我们可以漫步在这些 3D 环境当中,类似于大型多人游戏那种形式。其实《我的世界》就是用户构建型元宇宙的雏形嘛,所以这里并没有什么真正新鲜的东西,仍然是游戏粉加上社交互动机制的组合。我还想强调一点,虚拟现实其实没什么意思。我更重视与真实人类的面对面互动,真的很难想象自己有一天会跟独角兽之类的虚拟形象聊天。

写在最后:云计算带来的改变

InfoQ:您最初是从什么时候或者什么具体事件开始感受到云计算时代的到来的?

James Gosling:云计算概念的出现要远早出云计算的真正实现。因为人们一直把计算机摆在大机房里,再通过网络连接来访问,这其实就是传统的 IT 服务器机房,但这类方案维护成本高、建造成本高、扩展成本也高,而且对于人员技能等等都有着很高的要求。如果非要说,我觉得多租户云的出现正是云计算迎来飞跃的关键,这时候所有的人力与资本支出都由云服务商负责处理,企业客户再也不用为此烦心了。他们可以单纯关注自己的业务重心,告别那些没完没了又没有任何差异性可言的繁重工作。

InfoQ:云计算如今已经形成巨大的行业和生态,背后的根本驱动力是什么?

James Gosling:云计算的驱动力实际上与客户当前任务的实际规模有很大关系。过去几年以来,数字化转型已经全面掀起浪潮,而这波转型浪潮也凸显出新的事实,即我们还有更多的探索空间和机遇,例如,现在人们才刚刚开始探索真正的机器学习能做些什么,能够以越来越有趣且多样的方法处理大规模数据,开展数据分析,获取洞见并据此做出决策,而这一切既是客户需求,也为我们指明了接下来的前进方向。亚马逊云科技做为云科技领导者,引领着云科技的发展,改变着 IT 世界,切实解决了企业客户的诸多痛点。

作者:张雅文
来源:https://mp.weixin.qq.com/s/B4_YaVrnltm54aV4cW1XpA

收起阅读 »

这才是Yaml的语法精髓, 不要再只有字符串了

文章目录什么是YAML基本语法数据类型标量对象数组文本块显示指定类型引用单文件多配置什么是YAMLYAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Ma...
继续阅读 »

文章目录

  • 什么是YAML

  • 基本语法

  • 数据类型

    • 标量

    • 对象

    • 数组

  • 文本块

  • 显示指定类型

  • 引用

  • 单文件多配置

什么是YAML

YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。主要强度这种语音是以数据为中心,而不是以标记语音为重心,例如像xml语言就会使用大量的标记。

YAML是一个可读性高,易于理解,用来表达数据序列化的格式。它的语法和其他高级语言类似,并且可以简单表达清单(数组)、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件等。

YAML的配置文件后缀为 .yml,例如Springboot项目中使用到的配置文件 application.yml

基本语法

  • YAML使用可打印的Unicode字符,可使用UTF-8或UTF-16。

  • 数据结构采用键值对的形式,即 键名称: 值,注意冒号后面要有空格。

  • 每个清单(数组)成员以单行表示,并用短杠+空白(- )起始。或使用方括号([]),并用逗号+空白(, )分开成员。

  • 每个散列表的成员用冒号+空白(: )分开键值和内容。或使用大括号({ }),并用逗号+空白(, )分开。

  • 字符串值一般不使用引号,必要时可使用,使用双引号表示字符串时,会转义字符串中的特殊字符(例如\n)。使用单引号时不会转义字符串中的特殊字符。

  • 大小写敏感

  • 使用缩进表示层级关系,缩进不允许使用tab,只允许空格,因为有可能在不同系统下tab长度不一样

  • 缩进的空格数可以任意,只要相同层级的元素左对齐即可

  • 在单一文件中,可用连续三个连字号(—)区分多个文件。还有选择性的连续三个点号(…)用来表示文件结尾。

  • '#'表示注释,可以出现在一行中的任何位置,单行注释

  • 在使用逗号及冒号时,后面都必须接一个空白字符,所以可以在字符串或数值中自由加入分隔符号(例如:5,280或http://www.wikipedia.org)而不需要使用引号。

数据类型

  • 纯量(scalars):单个的、不可再分的值

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)

  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)

标量

标量是最基础的数据类型,不可再分的值,他们一般用于表示单个的变量,有以下七种:

  1. 字符串

  2. 布尔值

  3. 整数

  4. 浮点数

  5. Null

  6. 时间

  7. 日期

# 字符串
string.value: Hello!我是陈皮!
# 布尔值,true或false
boolean.value: true
boolean.value1: false
# 整数
int.value: 10
int.value1: 0b1010_0111_0100_1010_1110 # 二进制
# 浮点数
float.value: 3.14159
float.value1: 314159e-5 # 科学计数法
# Null,~代表null
null.value: ~
# 时间,时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
datetime.value: !!timestamp 2021-04-13T10:31:00+08:00
# 日期,日期必须使用ISO 8601格式,即yyyy-MM-dd
date.value: !!timestamp 2021-04-13

这样,我们就可以在程序中引入了,如下:

@RestController
@RequestMapping("demo")
public class PropConfig {
   
   @Value("${string.value}")
   private String stringValue;

   @Value("${boolean.value}")
   private boolean booleanValue;

   @Value("${boolean.value1}")
   private boolean booleanValue1;

   @Value("${int.value}")
   private int intValue;

   @Value("${int.value1}")
   private int intValue1;

   @Value("${float.value}")
   private float floatValue;

   @Value("${float.value1}")
   private float floatValue1;

   @Value("${null.value}")
   private String nullValue;

   @Value("${datetime.value}")
   private Date datetimeValue;

   @Value("${date.value}")
   private Date datevalue;
}

对象

我们知道单个变量可以用键值对,使用冒号结构表示 key: value,注意冒号后面要加一个空格。可以使用缩进层级的键值对表示一个对象,如下所示:

person:
 name: 陈皮
 age: 18
 man: true

然后在程序对这几个属性进行赋值到Person对象中,注意Person类要加get/set方法,不然属性会无法正确取到配置文件的值。使用@ConfigurationProperties注入对象,@value不能很好的解析复杂对象。

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "my.person")
@Getter
@Setter
public class Person {
   private String name;
   private int age;
   private boolean man;
}

当然也可以使用 key:{key1: value1, key2: value2, ...}的形式,如下:

person: {name: 陈皮, age: 18, man: true}

数组

可以用短横杆加空格 -开头的行组成数组的每一个元素,如下的address字段:

person:
 name: 陈皮
 age: 18
 man: true
 address:
   - 深圳
   - 北京
   - 广州

也可以使用中括号进行行内显示形式,如下:

person:
 name: 陈皮
 age: 18
 man: true
 address: [深圳, 北京, 广州]

在代码中引入方式如下:

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {
 
   
   
   private String name;
   private int age;
   private boolean man;
   private List<String> address;
}

如果数组字段的成员也是一个数组,可以使用嵌套的形式,如下:

person:
name: 陈皮
age: 18
man: true
address: [深圳, 北京, 广州]
twoArr:
-
- 2
- 3
- 1
-
- 10
- 12
- 30
package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {



private String name;
private int age;
private boolean man;
private List<String> address;
private List<List<Integer>> twoArr;
}

如果数组成员是一个对象,则用如下两种形式形式:

childs:
-
name: 小红
age: 10
-
name: 小王
age: 15
childs: [{name: 小红, age: 10}, {name: 小王, age: 15}]

文本块

如果你想引入多行的文本块,可以使用|符号,注意在冒号:|符号之间要有空格。

person:
name: |
Hello Java!!
I am fine!
Thanks! GoodBye!

它和加双引号的效果一样,双引号能转义特殊字符:

person:
name: "Hello Java!!\nI am fine!\nThanks! GoodBye!"

显示指定类型

有时我们需要显示指定某些值的类型,可以使用 !(感叹号)显式指定类型。!单叹号通常是自定义类型,!!双叹号是内置类型,例如:

# 指定为字符串
string.value: !!str HelloWorld!
# !!timestamp指定为日期时间类型
datetime.value: !!timestamp 2021-04-13T02:31:00+08:00

内置的类型如下:

  • !!int:整数类型

  • !!float:浮点类型

  • !!bool:布尔类型

  • !!str:字符串类型

  • !!binary:二进制类型

  • !!timestamp:日期时间类型

  • !!null:空值

  • !!set:集合类型

  • !!omap,!!pairs:键值列表或对象列表

  • !!seq:序列

  • !!map:散列表类型

引用

引用会用到 &锚点符合和 *星号符号,&用来建立锚点,<< 表示合并到当前数据,* 用来引用锚点。

xiaohong: &xiaohong
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
<<: *xiaohong

上面最终相当于如下:

xiaohong:
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
name: 小红
age: 20

还有一种文件内引用,引用已经定义好的变量,如下:

base.host: https://chenpi.com
add.person.url: ${base.host}/person/add

单文件多配置

可以在同一个文件中,实现多文档分区,即多配置。在一个yml文件中,通过 — 分隔多个不同配置,根据spring.profiles.active 的值来决定启用哪个配置

#公共配置
spring:
profiles:
active: pro # 指定使用哪个文档块
---
#开发环境配置
spring:
profiles: dev # profiles属性代表配置的名称

server:
port: 8080
---
#生产环境配置
spring:
profiles: pro

server:
port: 8081

作者:陈皮的JavaLib
来源:https://blog.csdn.net/chenlixiao007/article/details/115654824

收起阅读 »

掉了两根头发,可算是把volatile整明白了

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底...
继续阅读 »

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来

volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解

相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点

先说它的两个作用:

  • 保证变量在内存中对线程的可见性

  • 禁用指令重排

每个字都认识,凑在一起就麻了

这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile

关于可见性

不多bb,码来

public class VolatileTest {
   private static volatile int count = 0;
   
   private static void increase() {
   count++;
  }

   public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 10; i++) {
           new Thread(() -> {
               for (int j = 0; j < 10000; j++) {
                   increase();
              }
          }).start();
      }
       // 所有线程累加完成后输出
       while (Thread.activeCount() > 2) Thread.yield();
       System.out.println(count);
  }
}

代码很好理解,开了十个线程对同一个共享变量count做累加,每个线程累加1w次

count我们已经用volatile修饰,已经保证了count对十个线程在内存中的可见性,按理说十个线程执行完毕count的值应该10w

然鹅,运行多次,结果都远小于期望值


是哪个环节出了问题?


你肯定听过一句话:volatile只保证可见性,不保证原子性

这句话就是答案,但是依旧很多人没搞懂其中的奥秘

说来话长我长话短说,简单来讲就是 count++这个操作不是原子的,它是分三步进行

  1. 从内存读取 count 的值

  2. 执行 count + 1

  3. 将 count 的新值写回

要彻底搞懂这个问题,我们得从字节码入手

下面是increase方法编译后的字节码


看不懂没关系,我们一行一行来看:

  1. GETSTATIC:读取 count 的当前值

  2. ICONST_1:将常量 1 加载到栈顶

  3. IADD:执行+1

  4. PUTSTATIC:写入count最新值

ICONST_1和IADD其实就是真正的++操作

关键点来了,volatile只能保证线程在GETSTATIC这一步拿到的值是最新的,但当该线程执行到下面几行指令时,这期间可能就有其它线程把count的值修改了,最终导致旧值把真正的新值覆盖

懂我意思吗

所以,并发编程中,只靠volatile修饰共享变量是不可靠的,最终还是要通过对关键方法加锁来保证线程安全

就如上面的demo,稍加修改就能实现真正的线程安全

最简单的,给increase方法加个synchronized (synchronized怎么实现线程安全的我就不啰嗦了,我以前讲过 synchronized底层实现原理)

private synchronized static void increase() {
   ++count;
}

run几下


这不就妥了嘛

到现在,对于以下两点你应该有了新的认知

  1. volatile保证变量在内存中对线程的可见性

  2. volatile只保证可见性,不保证原子性

关于指令重排

并发编程中,cpu自身和虚拟机为了提高执行效率,都会采用指令重排(在保证不影响结果的前提下,将某些代码乱序执行)

  1. 关于cpu:为了从分利用cpu,实际执行指令时会做优化;

  2. 关于虚拟机:在HotSpot vm中,为了提升执行效率,JIT(即时编译)模式也会做指令优化

指令重排在大部分场景下确实能提升执行效率,但有些场景对代码执行顺序是强依赖的,此时我们需要禁用指令重排,如下面这个场景


伪代码取自《深入理解Java虚拟机》:

其描述的场景是开发中常见配置读取过程,只是我们在处理配置文件时一般不会出现并发,所以没有察觉这会有问题。
试想一下,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一条代码“initialized=true”被提前执行(这里虽然使用Java作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile通过禁止指令重排则可以避免此类情况发生

禁用指令重排只需要将变量声明为volatile,是不是很神奇

我们来看看volatile是如何实现禁用指令重排的

也借用《深入理解Java虚拟机》的一个例子吧,比较好理解


这是个单例模式的实现,下面是它的部分字节码,红框中 mov%eax,0x150(%esi) 是对instance赋值


可以看到,在赋值后,还执行了 lock addl$0x0,(%esp) 指令,关键点就在这儿,这行指令相当于此处设置了个 内存屏障 ,有了内存屏障后,cpu或虚拟机在指令重排时就不能把内存屏障后面的指令提前到内存屏障前面,好好捋一下这段话

最后,留一个能加深大家对volatile理解的问题,兄弟们好好思考下:

Java代码明明是从上往下依次执行,为什么会出现指令重排这个问题?

ok我话说完
————————————————
作者:负债程序猿
来源:https://blog.csdn.net/qq_33709582/article/details/122415754

收起阅读 »

什么样的问题应该使用动态规划

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:你对动态规划相关问题的套路和思想还没有完全...
继续阅读 »

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:

  • 你对动态规划相关问题的套路和思想还没有完全掌握;

  • 你没有系统地总结过究竟有哪些问题可以用动态规划解决。

知己知彼,你想把动态规划作为你的面试武器之一,就得足够了解它;而应对面试,总结、归类问题其实是个不错的选择,这在我们刷题的时候其实也能感觉得到。那么,我们就针对以上两点,系统地谈一谈究竟什么样的问题可以用动态规划来解。

一、动态规划是一种思想

动态规划算法,这种叫法我想你应该经常听说。嗯,从道理上讲这么叫我觉得也没错,首先动态规划它不是数据结构,这一点毋庸置疑,并且严格意义上来说它就是一种算法。但更加准确或者更加贴切的提法应该是说动态规划是一种思想。那算法和思想又有什么区别呢?

一般来说,我们都会把算法和数据结构放一起来讲,这是因为它们之间密切相关,而算法也往往是在特定数据结构的基础之上对解题方案的一种严谨的总结。

比如说,在一个乱序数组的基础上进行排序,这里的数据结构指的是什么呢?很显然是数组,而算法则是所谓的排序。至于排序算法,你可以考虑使用简单的冒泡排序或效率更高的快速排序方法等等来解决问题。

没错,你应该也感觉到了,算法是一种简单的经验总结和套路。那什么是思想呢?相较于算法,思想更多的是指导你我来解决问题。

比如说,在解决一个复杂问题的时候,我们可以先将问题简化,先解决简单的问题,再解决难的问题,那么这就是一种指导解决问题的思想。另外,我们常说的分治也是一种简单的思想,当然它在诸如归并排序或递归算法当中会常常被提及。

而动态规划就是这样一个指导我们解决问题的思想:你需要利用已经计算好的结果来推导你的计算,即大规模问题的结果是由小规模问题的结果运算得来的

总结一下:算法是一种经验总结,而思想则是用来指导我们解决问题的。既然动态规划是一种思想,那它实际上就是一个比较抽象的概念了,也很难和实际的问题关联起来。所以说,弄清楚什么样的问题可以使用动态规划来解就显得十分重要了。

二、动态规划问题的特点

动态规划作为运筹学上的一种最优化解题方法,在算法问题上已经得到广泛应用。接下来我们就来看一下动归问题所具备的一些特点。

2.1 最优解问题

除非你碰到的问题是简单到找出一个数组中最大的值这样,对这种问题来说,你可以对数组进行排序,然后取数组头或尾部的元素,如果觉得麻烦,你也可以直接遍历得到最值。不然的话,你就得考虑使用动态规划来解决这个问题了。这样的问题一般都会让你求最大子数组、求最长递增子数组、求最长递增子序列或求最长公共子串、子序列等等。

如果碰到求最值问题,我们可以使用下面的套路来解决问题:

  • 优先考虑使用贪心算法的可能性;

  • 然后是暴力递归进行穷举,针对数据规模不大的情况;

  • 如果上面两种都不适合,那么再选择动态规划。

可以看到,求解动态规划的核心问题其实就是穷举。当然了,动态规划问题也不会这么简单了事,我们还需要考虑待解决的问题是否存在重叠子问题、最优子结构等特性。

清楚了动态规划算法的特点,接下来我们就来看一下哪些问题适合用动态规划思想来解题。

1. 乘积最大子数组

给你一个整数数组 numbers,找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),返回该子数组的乘积。

示例1:
输入: [2,7,-2,4]
输出: 14
解释: 子数组 [2,7] 有最大乘积 14。


示例2:
输入: [-5,0,3,-1]
输出: 3
解释: 结果不能为 15, 因为 [-5,3,-1] 不是子数组,是子序列。

首先,很明显这个题目当中包含一个“最”字,使用动态规划求解的概率就很大。这个问题的目的就是从数组中寻找一个最大的连续区间,确保这个区间的乘积最大。由于每个连续区间可以划分成两个更小的连续区间,而且大的连续区间的结果是两个小连续区间的乘积,因此这个问题还是求解满足条件的最大值,同样可以进行问题分解,而且属于求最值问题。同时,这个问题与求最大连续子序列和比较相似,唯一的区别就是你需要在这个问题里考虑正负号的问题,其它就相同了。

对应实现代码:

class Solution {
public:
   int maxProduct(vector<int>& nums) {
       if(nums.empty()) return 0;

       int curMax = nums[0];
       int curMin = nums[0];
       int maxPro = nums[0];
       for(int i=1; i<nums.size(); i++){
           int temp = curMax;    // 因为curMax在下一行可能会被更新,所以保存下来
           curMax = max(max(curMax*nums[i], nums[i]), curMin*nums[i]);
           curMin = min(min(curMin*nums[i], nums[i]), temp*nums[i]);
           maxPro = max(curMax, maxPro);
       }
       return maxPro;
   }
};

2. 最长回文子串

问题:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例1:
输入: "babad"
输出: "bab"


示例2:
输入: "cbbd"
输出: "bb"

【回文串】是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。这个问题依然包含一个“最”字,同样由于求解的最长回文子串肯定包含一个更短的回文子串,因此我们依然可以使用动态规划来求解这个问题。

对应实现代码:

class Solution {
      public boolean isPalindrome(String s, int b, int e){//判断s[b...e]是否为回文字符串
      int i = b, j = e;
      while(i <= j){
          if(s.charAt(i) != s.charAt(j)) return false;
          ++i;
          --j;
      }
      return true;
  }
  public String longestPalindrome(String s) {
      if(s.length() <=1){
          return s;
      }
      int l = 1, j = 0, ll = 1;
      for(int i = 1; i < s.length(); ++i){
            //下面这个if语句就是用来维持循环不变式,即ll恒表示:以第i个字符为尾的最长回文子串的长度
            if(i - 1 - ll >= 0 && s.charAt(i) == s.charAt(i-1-ll)) ll += 2;
            else{
                while(true){//重新确定以i为边界,最长的回文字串长度。确认范围为从ll+1到1
                    if(ll == 0||isPalindrome(s,i-ll,i)){
                        ++ll;
                        break;
                    }
                    --ll;
                }
            }
            if(ll > l){//更新最长回文子串信息
              l = ll;
              j = i;
          }
      }
      return s.substring(j-l+1, j+1);//返回从j-l+1到j长度为l的子串
  }
}

3. 最长上升子序列

问题:给定一个无序的整数数组,找到其中最长上升子序列的长度。可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

示例:
输入: [10,9,2,5,3,7,66,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,66],它的长度是 4。

这个问题依然是一个最优解问题,假设我们要求一个长度为 5 的字符串中的上升自序列,我们只需要知道长度为 4 的字符串最长上升子序列是多长,就可以根据剩下的数字确定最后的结果。
对应实现代码:

class Solution {
   public int lengthOfLIS(int[] nums) {
       if(nums.length == 0) return 0;
       int[] dp = new int[nums.length];
       int res = 0;
       Arrays.fill(dp, 1);
       for(int i = 0; i < nums.length; i++) {
           for(int j = 0; j < i; j++) {
               if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
           }
           res = Math.max(res, dp[i]);
       }
       return res;
   }
}

2.2 求可行性

如果有这样一个问题,让你判断是否存在一条总和为 x 的路径(如果找到了,就是 True;如果找不到,自然就是 False),或者让你判断能否找到一条符合某种条件的路径,那么这类问题都可以归纳为求可行性问题,并且可以使用动态规划来解。

1. 凑零兑换问题

问题:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

示例1:
输入: c1=1, c2=2, c3=5, c4=7, amount = 15
输出: 3
解释: 11 = 7 + 7 + 1。


示例2:
输入: c1=3, amount =7
输出: -1
解释: 3怎么也凑不到7这个值。

这个问题显而易见,如果不可能凑出我们需要的金额(即 amount),最后算法需要返回 -1,否则输出可能的硬币数量。这是一个典型的求可行性的动态规划问题。

对于示例代码:

class Solution {
   public int coinChange(int[] coins, int amount) {
       if(coins.length == 0)
           return -1;
       //声明一个amount+1长度的数组dp,代表各个价值的钱包,第0个钱包可以容纳的总价值为0,其它全部初始化为无穷大
       //dp[j]代表当钱包的总价值为j时,所需要的最少硬币的个数
       int[] dp = new int[amount+1];
       Arrays.fill(dp,1,dp.length,Integer.MAX_VALUE);
       for (int coin : coins) {
           for (int j = coin; j <= amount; j++) {
               if(dp[j-coin] != Integer.MAX_VALUE) {
                   dp[j] = Math.min(dp[j], dp[j-coin]+1);
              }
          }
      }
       if(dp[amount] != Integer.MAX_VALUE)
           return dp[amount];
       return -1;
  }
}

2. 字符串交错组成问题

问题:给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

示例1:
输入: s1="aabcc",s2 ="dbbca",s3="aadbbcbcac"
输出: true
解释: 可以交错组成。


示例2:
输入: s1="aabcc",s2="dbbca",s3="aadbbbaccc"
输出: false
解释:无法交错组成。

这个问题稍微有点复杂,但是我们依然可以通过子问题的视角,首先求解 s1 中某个长度的子字符串是否由 s2 和 s3 的子字符串交错组成,直到求解整个 s1 的长度为止,也可以看成一个包含子问题的最值问题。
对应示例代码:

class Solution {
  public boolean isInterleave(String s1, String s2, String s3) {
      int length = s3.length();
      // 特殊情况处理
      if(s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) return true;
      if(s1.isEmpty()) return s2.equals(s3);
      if(s2.isEmpty()) return s1.equals(s3);
      if(s1.length() + s2.length() != length) return false;

      int[][] dp = new int[s2.length()+1][s1.length()+1];
      // 边界赋值
      for(int i = 1;i < s1.length()+1;i++){
          if(s1.substring(0,i).equals(s3.substring(0,i))){
              dp[0][i] = 1;
          }
      }
      for(int i = 1;i < s2.length()+1;i++){
          if(s2.substring(0,i).equals(s3.substring(0,i))){
              dp[i][0] = 1;
          }
      }
       
      for(int i = 2;i <= length;i++){
          // 遍历 i 的所有组成(边界除外)
          for(int j = 1;j < i;j++){
              // 防止越界
              if(s1.length() >= j && i-j <= s2.length()){
                  if(s1.charAt(j-1) == s3.charAt(i-1) && dp[i-j][j-1] == 1){
                      dp[i-j][j] = 1;
                  }
              }
              // 防止越界
              if(s2.length() >= j && i-j <= s1.length()){
                  if(s2.charAt(j-1) == s3.charAt(i-1) && dp[j-1][i-j] == 1){
                      dp[j][i-j] = 1;
                  }
              }
          }
      }
      return dp[s2.length()][s1.length()]==1;
  }
}

2.3 求总数

除了求最值与可行性之外,求方案总数也是比较常见的一类动态规划问题。比如说给定一个数据结构和限定条件,让你计算出一个方案的所有可能的路径,那么这种问题就属于求方案总数的问题。

1. 硬币组合问题

问题:英国的英镑硬币有 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), 和 £2 (200p)。比如我们可以用以下方式来组成 2 英镑:1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p。问题是一共有多少种方式可以组成 n 英镑? 注意不能有重复,比如 1 英镑 +2 个 50P 和 50P+50P+1 英镑是一样的。

示例1:
输入: 2
输出: 73682

这个问题本质还是求满足条件的组合,只不过这里不需要求出具体的值或者说组合,只需要计算出组合的数量即可。

public class Main {
  public static void main(String[] args) throws Exception {
       
      Scanner sc = new Scanner(System.in);
      while (sc.hasNext()) {
           
          int n = sc.nextInt();
          int coin[] = { 1, 5, 10, 20, 50, 100 };
           
          // dp[i][j]表示用前i种硬币凑成j元的组合数
          long[][] dp = new long[7][n + 1];
           
          for (int i = 1; i <= n; i++) {
              dp[0][i] = 0; // 用0种硬币凑成i元的组合数为0
          }
           
          for (int i = 0; i <= 6; i++) {
              dp[i][0] = 1; // 用i种硬币凑成0元的组合数为1,所有硬币均为0个即可
          }
           
          for (int i = 1; i <= 6; i++) {
               
              for (int j = 1; j <= n; j++) {
                   
                  dp[i][j] = 0;
                  for (int k = 0; k <= j / coin[i - 1]; k++) {
                       
                      dp[i][j] += dp[i - 1][j - k * coin[i - 1]];
                  }
              }
          }
           
          System.out.print(dp[6][n]);
      }
      sc.close();
  }
}

2. 路径规划问题

问题:一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角,共有多少路径?

示例1:
输入: 2 2
输出: 2


示例1:
输入: 3 3
输出: 6

这个问题还是一个求满足条件的组合数量的问题,只不过这里的组合变成了路径的组合。我们可以先求出长宽更小的网格中的所有路径,然后再在一个更大的网格内求解更多的组合。这和硬币组合的问题相比没有什么本质区别。

这里有一个规律或者说现象需要强调,那就是求方案总数的动态规划问题一般都指的是求“一个”方案的所有具体形式。如果是求“所有”方案的具体形式,那这种肯定不是动态规划问题,而是使用传统递归来遍历出所有方案的具体形式。

为什么这么说呢?因为你需要把所有情况枚举出来,大多情况下根本就没有重叠子问题给你优化。即便有,你也只能使用备忘录对遍历进行一个简单加速。但本质上,这类问题不是动态规划问题。

对应示例代码:

package com.qst.Tesst;

import java.util.Scanner;

public class Test12 {
  public static void main(String[] args) {
      Scanner scanner = new Scanner(System.in);
      while (scanner.hasNext()) {
          int x = scanner.nextInt();
          int y = scanner.nextInt();

          //设置路径
          long[][] path = new long[x + 1][y + 1];
          //设置领导数量
          int n = scanner.nextInt();

          //领导位置
          for (int i = 0; i < n; i++) {
              int a = scanner.nextInt();
              int b = scanner.nextInt();
              path[a][b] = -1;
          }

          for (int i = 0; i <= x; i++) {
              path[i][0] = 1;
          }
          for (int j = 0; j <= y; j++) {
              path[0][j] = 1;
          }

          for (int i = 1; i <= x; i++) {
              for (int j = 1; j <= y; j++) {
                  if (path[i][j] == -1) {
                      path[i][j] = 0;
                  } else {
                      path[i][j] = path[i - 1][j] + path[i][j - 1];
                  }

              }

          }
          System.out.println(path[x][y]);
      }
  }
}

三、 如何确认动态规划问题

从前面我所说来看,如果你碰到了求最值、求可行性或者是求方案总数的问题的话,那么这个问题就八九不离十了,你基本可以确定它就需要使用动态规划来解。但是,也有一些个别情况需要注意:

3.1 数据不可排序

假设我们有一个无序数列,希望求出这个数列中最大的两个数字之和。很多初学者刚刚学完动态规划会走火入魔到看到最优化问题就想用动态规划来求解,事实上,这个问题不是简单做一个排序或者做一个遍历就可以求解出来的。对于这种问题,我们应该先考虑一下能不能通过排序来简化问题,如果不能,才极有可能是动态规划问题。

最小的 k 个数

问题:输入整数数组 arr ,找出其中最小的 k 个数。例如,输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。

示例1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

我们发现虽然这个问题也是求“最”值,但其实只要通过排序就能解决,所以我们应该用排序、堆等算法或者数据结构就可以解决,而不应该用动态规划。

对应的示例代码:

public class Solution {
  public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
              int t;
      boolean flag;
      ArrayList result = new ArrayList();
      if(k>input.length){
          return result;
      }
      for(int i =0;i<input.length;i++){
          flag = true;
          for(int j = 0; j < input.length-i;j++)
              if(j<input.length-i-1){
                  if(input[j] > input[j+1]) {
                      t = input[j];
                      input[j] = input[j+1];
                      input[j+1] = t;
                      flag = false;
                  }
              }
          if(flag)break;
      }
      for(int i = 0; i < k;i++){
          result.add(input[i]);
      }
      return result;
  }
}

3.2 数据不可交换

还有一类问题,可以归类到我们总结的几类问题里去,但是不存在动态规划要求的重叠子问题(比如经典的八皇后问题),那么这类问题就无法通过动态规划求解。

全排列

问题:给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

这个问题虽然是求组合,但没有重叠子问题,更不存在最优化的要求,因此可以使用回溯方法处理。

对应的示例代码:

public class Main {
   public static void main(String[] args) {
       perm(new int[]{1,2,3},new Stack<>());
  }
   public static void perm(int[] array, Stack<Integer> stack) {
       if(array.length <= 0) {
           //进入了叶子节点,输出栈中内容
           System.out.println(stack);
      } else {
           for (int i = 0; i < array.length; i++) {
               //tmepArray是一个临时数组,用于就是Ri
               //eg:1,2,3的全排列,先取出1,那么这时tempArray中就是2,3
               int[] tempArray = new int[array.length-1];
               System.arraycopy(array,0,tempArray,0,i);
               System.arraycopy(array,i+1,tempArray,i,array.length-i-1);
               stack.push(array[i]);
               perm(tempArray,stack);
               stack.pop();
          }
      }
  }
}

总结一下,哪些问题可以使用动态规划呢,通常含有下面情况的一般都可以使用动态规划来解决:

  • 求最优解问题(最大值和最小值);

  • 求可行性(True 或 False);

  • 求方案总数;

  • 数据结构不可排序(Unsortable);

  • 算法不可使用交换(Non-swappable)。

如果面试题目出现这些特征,那么在 90% 的情况下你都能断言它就是一个动归问题。除此之外,还需要考虑这个问题是否包含重叠子问题与最优子结构,在这个基础之上你就可以 99% 断言它是否为动归问题,并且也顺势找到了大致的解题思路。

作者:xiangzhihong

来源:https://segmentfault.com/a/1190000041300090

收起阅读 »

10 个让人头疼的 bug

那个谁,今天又写 bug 了,没错,他说的好像就是我。。。。。。作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各种空指针异常,在 ArrayList 的迭代中进行删除操作引发异常,数组下标越界...
继续阅读 »

那个谁,今天又写 bug 了,没错,他说的好像就是我。。。。。。

作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各种空指针异常,在 ArrayList 的迭代中进行删除操作引发异常,数组下标越界异常等。

如果你不小心看到同事的代码出现了我所描述的这些 bug 后,那你就把我这篇文章甩给他!!!你甩给他一篇文章,并让他关注了一波 cxuan,你会收获他在后面像是如获至宝并满眼崇拜大神的目光。

废话不多说,下面进入正题。

错误一:Array 转换成 ArrayList

Array 转换成 ArrayList 还能出错?这是哪个笨。。。。。。

等等,你先别着急说,先来看看是怎么回事。

如果要将数组转换为 ArrayList,我们一般的做法会是这样

List<String> list = Arrays.asList(arr);

Arrays.asList() 将返回一个 ArrayList,它是 Arrays 中的私有静态类,它不是 java.util.ArrayList 类。如下图所示


Arrays 内部的 ArrayList 只有 set、get、contains 等方法,但是没有能够像是 add 这种能够使其内部结构进行改变的方法,所以 Arrays 内部的 ArrayList 的大小是固定的。


如果要创建一个能够添加元素的 ArrayList ,你可以使用下面这种创建方式:

ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

因为 ArrayList 的构造方法是可以接收一个 Collection 集合的,所以这种创建方式是可行的。


错误二:检查数组是否包含某个值

检查数组中是否包含某个值,部分程序员经常会这么做:

Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);

这段代码虽然没错,但是有额外的性能损耗,正常情况下,不用将其再转换为 set,直接这么做就好了:

return Arrays.asList(arr).contains(targetValue);

或者使用下面这种方式(穷举法,循环判断)

for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;

上面第一段代码比第二段更具有可读性。

错误三:在 List 中循环删除元素

这个错误我相信很多小伙伴都知道了,在循环中删除元素是个禁忌,有段时间内我在审查代码的时候就喜欢看团队的其他小伙伴有没有犯这个错误。


说到底,为什么不能这么做(集合内删除元素)呢?且看下面代码

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);

这个输出结果你能想到么?是不是蠢蠢欲动想试一波了?

答案其实是 [b,d]

为什么只有两个值?我这不是循环输出的么?

其实,在列表内部,当你使用外部 remove 的时候,一旦 remove 一个元素后,其列表的内部结构会发生改变,一开始集合总容量是 4,remove 一个元素之后就会变为 3,然后再和 i 进行比较判断。。。。。。所以只能输出两个元素。

你可能知道使用迭代器是正确的 remove 元素的方式,你还可能知道 for-each 和 iterator 这种工作方式类似,所以你写下了如下代码

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));

for (String s : list) {
if (s.equals("a"))
list.remove(s);
}

然后你充满自信的 run xxx.main() 方法,结果。。。。。。ConcurrentModificationException

为啥呢?

那是因为使用 ArrayList 中外部 remove 元素,会造成其内部结构和游标的改变。

在阿里开发规范上,也有不要在 for-each 循环内对元素进行 remove/add 操作的说明。


所以大家要使用 List 进行元素的添加或者删除操作,一定要使用迭代器进行删除。也就是

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
String s = iter.next();

if (s.equals("a")) {
iter.remove();
}
}

.next() 必须在 .remove() 之前调用。在 foreach 循环中,编译器会在删除元素的操作后调用 .next(),导致ConcurrentModificationException。

错误四:Hashtable 和 HashMap

这是一条算法方面的规约:按照算法的约定,Hashtable 是数据结构的名称,但是在 Java 中,数据结构的名称是 HashMap,Hashtable 和 HashMap 的主要区别之一就是 Hashtable 是同步的,所以很多时候你不需要 Hashtable ,而是使用 HashMap。

错误五:使用原始类型的集合

这是一条泛型方面的约束:

在 Java 中,原始类型和无界通配符类型很容易混合在一起。以 Set 为例,Set 是原始类型,而 Set<?> 是无界通配符类型。

比如下面使用原始类型 List 作为参数的代码:

public static void add(List list, Object o){
list.add(o);
}
public static void main(String[] args){
List<String> list = new ArrayList<String>();
add(list, 10);
String s = list.get(0);
}

这段代码会抛出 java.lang.ClassCastException 异常,为啥呢?


使用原始类型集合是比较危险的,因为原始类型会跳过泛型检查而且不安全,Set、Set<?> 和 Set<Object> 存在巨大的差异,而且泛型在使用中很容易造成类型擦除。

大家都知道,Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除

如在代码中定义List<Object>List<String>等类型,在编译后都会变成List,JVM 看到的只是List,而由泛型附加的类型信息对 JVM 是看不到的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是 Java 的泛型与 C++ 模板机制实现方式之间的重要区别。

比如下面这段示例

public class Test {

  public static void main(String[] args) {

      ArrayList<String> list1 = new ArrayList<String>();
      list1.add("abc");

      ArrayList<Integer> list2 = new ArrayList<Integer>();
      list2.add(123);

      System.out.println(list1.getClass() == list2.getClass());
  }

}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型StringInteger都被擦除掉了,只剩下原始类型。

所以,最上面那段代码,把 10 添加到 Object 类型中是完全可以的,然而将 Object 类型的 "10" 转换为 String 类型就会抛出类型转换异常。

错误六:访问级别问题

我相信大部分开发在设计 class 或者成员变量的时候,都会简单粗暴的直接声明 public xxx,这是一种糟糕的设计,声明为 public 就很容易赤身裸体,这样对于类或者成员变量来说,都存在一定危险性。

错误七:ArrayList 和 LinkedList

哈哈哈,ArrayList 是我见过程序员使用频次最高的工具类,没有之一。


当开发人员不知道 ArrayList 和 LinkedList 的区别时,他们经常使用 ArrayList(其实实际上,就算知道他们的区别,他们也不用 LinkedList,因为这点性能不值一提),因为看起来 ArrayList 更熟悉。。。。。。

但是实际上,ArrayList 和 LinkedList 存在巨大的性能差异,简而言之,如果添加/删除操作大量且随机访问操作不是很多,则应首选 LinkedList。如果存在大量的访问操作,那么首选 ArrayList,但是 ArrayList 不适合进行大量的添加/删除操作。

错误八:可变和不可变

不可变对象有很多优点,比如简单、安全等。但是不可变对象需要为每个不同的值分配一个单独的对象,对象不具备复用性,如果这类对象过多可能会导致垃圾回收的成本很高。在可变和不可变之间进行选择时需要有一个平衡。

一般来说,可变对象用于避免产生过多的中间对象。比如你要连接大量字符串。如果你使用一个不可变的字符串,你会产生很多可以立即进行垃圾回收的对象。这会浪费 CPU 的时间和精力,使用可变对象是正确的解决方案(例如 StringBuilder)。如下代码所示:

String result="";
for(String s: arr){
result = result + s;
}

所以,正确选择可变对象还是不可变对象需要慎重抉择。

错误九:构造函数

首先看一段代码,分析为什么会编译不通过?


发生此编译错误是因为未定义默认 Super 的构造函数。在 Java 中,如果一个类没有定义构造函数,编译器会默认为该类插入一个默认的无参数构造函数。如果在 Super 类中定义了构造函数,在这种情况下 Super(String s),编译器将不会插入默认的无参数构造函数。这就是上面 Super 类的情况。

要想解决这个问题,只需要在 Super 中添加一个无参数的构造函数即可。

public Super(){
  System.out.println("Super");
}

错误十:到底是使用 "" 还是构造函数

考虑下面代码:

String x = "abc";
String y = new String("abc");

上面这两段代码有什么区别吗?

可能下面这段代码会给出你回答

String a = "abcd";
String b = "abcd";
System.out.println(a == b); // True
System.out.println(a.equals(b)); // True

String c = new String("abcd");
String d = new String("abcd");
System.out.println(c == d); // False
System.out.println(c.equals(d)); // True

这就是一个典型的内存分配问题。

后记

今天我给你汇总了一下 Java 开发中常见的 10 个错误,虽然比较简单,但是很容易忽视的问题,细节成就完美,看看你还会不会再犯了,如果再犯,嘿嘿嘿。


作者:cxuan
来源:https://mp.weixin.qq.com/s/uF0p8MGDhfvke4gdRv44iA

收起阅读 »

Ngnix之父突然离职,程序员巅峰一代落幕

当地时间 1 月 18 日,Nginx 公司副总裁兼总经理 Rob Whiteley 在 Nginx 官网发布了一篇「告别信」,正式宣告 Nginx 的作者和 Nginx Inc. 的联合创始人 Igor Sysoev 退出 Nginx 和 F5 Networ...
继续阅读 »

当地时间 1 月 18 日,Nginx 公司副总裁兼总经理 Rob Whiteley 在 Nginx 官网发布了一篇「告别信」,正式宣告 Nginx 的作者和 Nginx Inc. 的联合创始人 Igor Sysoev 退出 Nginx 和 F5 Networks。

此事很快登上 Hacker News 的热搜榜,有网友留言道:

我看过 Igor 参加某个会议的视频,他一说:“你好,我是 Nginx 的创建者 Igor Sysoev ”,观众席就会‘爆发’绵延不绝的掌声。他甚至不得不告诉他们“Come on guys, 你们还没听我的讲演呢。”。

不少开发者对 Igor 所做出的贡献表达了崇敬和感谢,也有网友感慨“巅峰一代落幕”。从 2002 年发展至今日,Nginx 已经成为全球最受欢迎的 Web 服务器。据 W3Techs 统计,截至 2022 年 1 月上旬,Nginx 占据了全球 Web 服务器市场 33% 的份额。排在第二位的是 Apache,份额为 31%。

pic_d710133a.png

一直以来,Nginx 常被拿来跟 Apache 对比,也有观点认为,Nginx 和 Apache 不算真正意义上的竞争者,很多地方会同时使用两者。但无论如何,Igor 和 Nginx 的成功确实鼓舞了不少开源人。

作为一名开源开发者和商业 OSS 初创公司创始人,Nginx 给了我很大的挑战现状的信心。Apache 是如此受人尊敬,以至于你会认为可以改进它的想法是很疯狂的,但他( Igor )做到了,这对我产生了真正的影响。——yesimahuman

Igor 早期曾在采访中分享对于开源和商业产品找平衡的观点,他表示不想创建单独的商业产品,而是希望对 Nginx 的主要开源产品进行商业扩展,社区想要的新功能将出现在其中。商业扩展更多的是有助于处理数千个实例、添加扩展性能监控、托管、云和 CDN 基础设施的附加功能等。

很多客户会说愿意付钱让 Igor 增加他们所需要的新功能,而 Igor 等人收集此类请求后会将其与从用户社区收到的需求进行比较,并寻找交叉点——“如果我们意识到每个人都需要某些功能,而不仅仅是某些公司,我们会将这些功能包含在开源版本中。我们从中了解我们可以销售什么,而不会惹恼开源产品的支持者,也不会损害整个项目的信誉。”

Nginx 如今归属于 F5 Networks。2019 年 3 月,F5 Networks 宣布将以 6.7 亿美元收购 Nginx,根据交易条款,Nginx 品牌被保留,而 Igor 和 合伙人 Konovalov 作为 F5 的一部分继续致力于该项目。但这笔交易很快就触发了利益纷争,同年 12 月,Igor 陷入版权纠纷,前东家 Rambler 集团对 NGINX Inc. 提出了侵犯版权的诉讼,声称拥有 Nginx Web 服务器代码的全部所有权,但 Igor 辩称是在业余时间开发了 Nginx。

此事随即引发热议,业余项目究竟属于开发者个人、还是属于开发者所在的企业,目前没有明确的统一的法律来判定。2020 年 4 月,Rambler 驳回针对 Nginx 的刑事诉讼。但 Rambler 并未就此停下,只是不再是以刑事诉讼的方式,而是通过民事法院,并于 2020 年 6 月初宣布授权旗下 Lynwood Investments 在美国对 F5 Networks、Igor 本人发起民事诉讼,要求索赔 7.5 亿美元。6 月末,俄罗斯内政部因缺乏犯罪记录证据,结案了有关 Nginx 版权的案件。

告别信有提到 Igor 从 Nginx 离职后将从事个人项目,目前我们尚不清楚他具体会涉及哪些项目。

以下是「告别信」全文:

挥别 Igor:
感谢你为 Nginx 付出的一切

怀着深深的感激之情,我们今天宣布,Nginx 的作者和 Nginx 公司联合创始人 Igor Sysoev 选择退出 Nginx 和 F5,以便花更多的时间与他的朋友和家人在一起,并追求个人项目。

2002 年的春天,Igor Sysoev 开始开发 Nginx。互联网的早期飞速发展让他萌生出一个念头:用一套全新架构改进网络流量的处理方式,帮助高流量网站从容应对数万个并发连接,并将照片、视频等各类可能严重拖慢页面加载速度的内容统统塞进缓存。

二十年过去,Igor 写下的代码已经在为世界上大部分网站提供支持。除了直接使用外,也被作为 Cloudflare、OpenResty、Tengine 等流行服务器的底层软件。很多人认为,Igor 最初的梦想就是把 Web 塑造成如今的样貌。Igor 所秉持的意志与价值观则汇聚成 Nginx 公司,结合开源与技术社区之力成就高透明度、质量卓越的代码,最终转化为客户喜闻乐见的商业产品。

但其中的平衡往往很难把握。Igor 之所以受到开发者、企业客户以及 Nginx 工程师们的高度赞扬,依靠的正是他谦逊的内心、不断探索的激情以及在开发工作中勇攀高峰的意志。

Igor 的成长与 Nginx 的诞生

Igor 的人生起点不高。他出生于苏联时期的一个哈萨克斯坦小镇,父亲是一名军官。一岁时,他们全家迁往首都阿拉木图。Igor 从小痴迷计算机,1980 年代中期就在 Yamaha MSX 上写下了人生第一行代码。而伴随着早期互联网产业的快速发展,Igor 也从著名的鲍曼莫斯科国立技术大学计算机科学系顺利毕业。

Igor 毕业后先找了份系统管理员工作,但写代码的好习惯一直没有丢下。1999 年,他用汇编语言开发出自己的第一个程序,这款反病毒软件能抵御当时最常见的十种计算机病毒。Igor 免费开放了程序的二进制文件,这款工具也在俄罗斯国内风靡一时。之后,敏感的他发觉 Apache HTTP 服务器的连接处理方式过于原始,根本无法满足不断发展的万维网需求。于是他决定开展相关研究,这也正是后来 Nginx 项目的雏形。

彼时,Igor 将目光投向了 C10k 问题,即如何在单一服务器上处理 10000 个并发连接。此外,他还希望让自己的 Web 服务器更快、更高效地处理照片或者音乐文件等极占传输带宽的元素。在获得俄罗斯国内外多家公司的肯定和采用之后,Igor 于 2004 年 10 月 4 日(即苏联发射全球首颗人造卫星「斯普特尼克」号的四十七周年纪念日)对这个名为 Nginx 的项目进行了许可开源。

七年来,Igor 一直是唯一的开发者。他独力写下数十万行代码,并把 Nginx 从简单的 Web 服务器加反向代理工具,扩展成一把能满足各类 Web 应用与服务需求的“瑞士军刀”。随着项目发展,负载均衡、缓存、安全和内容加速等关键功能也在他的指尖一一成形。

没有队伍的 Igor 当时自然没精力宣传项目,甚至连说明文档也不够完备。但 Nginx 仍然凭借着出色的表现迅速占领了市场。更神奇的是,新用户发现就算没有全面的使用指南、自己仍然能轻松玩转 Nginx,于是项目就在口口相传之下普及开来。越来越多的开发者和系统管理员利用 Nginx 解决自己面对的现实问题,提升网站响应速度。对于 Igor 的贡献,我们已经不需要刻意赞美或者宣扬,他的代码已经说明了一切。

Nginx 开启商业化之路,但开源定位永不动摇

2011 年,Igor 与 Maxim Konovalov、Andrew Alexeev 两位联合创始人共同成立了 Nginx 公司,希望借众人之力加快项目开发速度。但 Igor 也很清楚,从这一刻起他和团队得想办法赚钱了。不过他们坚持发布 Nginx 完整开源版本、恪守开源许可的承诺不会动摇。君子一诺值千金,自公司成立以来,Igor 引领 Nginx 通过 140 多个版本不断完善自我,始终以开源姿态为全球数亿网站提供支持。

pic_93439c65.png

奔波在为 Nginx 公司筹集风险投资的路上——(右起)Igor、公司 CEO Gus Robertson、联合创始人 Andrew Alexeev 以及 Maxim Konovalov

2011 年的时候,以专有模块的形式向商业版本中添加新功能的想法还属于开时代之先河。但如今,很多开源后起之秀已经可以站在巨人的肩膀上享受这种商业模式。在商业版 Nginx Plus 于 2013 年首次推出时,市场立刻抱以热烈欢迎。四年之后,Nginx 已经拥有超过 1000 家付费客户和数千万收入,Nginx 开源项目与技术社区的规模也在同步发展壮大。截至 2019 年底,Nginx 已经在为全球超过 4.75 亿个网站提供支持;到 2021 年,Nginx 正式成为世界上应用范围最广的 Web 服务器方案。

着眼于未来需求,Igor 还一路打造出多个 Nginx 相关项目,包括 Nginx JavaScript(njs)与 Nginx Unit。他还为 sendfile(2)系统调用设计了全新实现,将其整合到开源 FreeBSD 操作系统当中。随着 Nginx 工程师队伍的壮大和 Nginx 公司正式加入 F5,Igor 一直是团队背后稳健的领导者,保证 Nginx 始终方向明确、斗志坚定。

接过 Igor 手中的旗帜

今天,Igor 希望退居幕后享受生活,独余我们继续前行。但 Igor 的精神和他一路塑造的文化不会消失。伟大的企业、产品和项目中,创始人的 DNA 是永恒不变的。我们对于产品、社区、透明度、开源和创新的态度皆继承自 Igor,我们也将继续在 Maxim 和 Nginx 领导团队的指引下接过这面旗帜、发挥这份传统。

Igor 在 Nginx 与 F5 时代的奋斗与付出凝结成了我们今天所看到的项目代码,多年以来一直默默支撑起整个互联网世界。时间会考验我们、鞭策我们,证明我们能否像 Igor 那样创造出历久弥新、影响深远的产品。这当然是一条极高的标准,但 Igor 也用实际行动为我们指明了达成目标的方法。感恩多年来的指引与教导,Igor,祝你在人生的新阶段写下新的传奇故事。

来源:https://mp.weixin.qq.com/s/GANdlnXt1_vuUm3j97Njg

收起阅读 »

城市数字孪生标准化白皮书(2022版)

当 前,城市数字孪生已经发展成为支撑智慧城市的重要技术手段。全文共计3026字,预计阅读时间8分钟来源| 全国信标委智慧城市标准工作组编辑 | 蒲蒲城市数字孪生通过在数字空间对城市物理空间和社会空间进行全要素表达、全过程呈现、全周期可溯,实现城市全面感知、虚实...
继续阅读 »

当 前,城市数字孪生已经发展成为支撑智慧城市的重要技术手段。

pic_02b697b8.png
全文共计3026字,预计阅读时间8分钟

来源| 全国信标委智慧城市标准工作组

编辑 | 蒲蒲

城市数字孪生通过在数字空间对城市物理空间和社会空间进行全要素表达、全过程呈现、全周期可溯,实现城市全面感知、虚实交互、智能决策、精准控制,推动城市智能化、智慧化发展。

当前,城市数字孪生已经发 展成为支撑智慧城市的重要技术手段。 为做好城市数字孪生标准化工作整体规划,有序推动相关标准制定与应用实施工作,全国信标 委智慧城市标准工作组组建了城市数字孪生专题组,并联合相关单位编制了《城市数字孪生标准化白皮书(2022版)》。

白皮书在系统研究城市数字孪生内涵、典型特征、相关方等基础上,构建了城市数字孪生技术参考架构,梳理了城市数字孪生关键技术和典型应用场景,总结了城市数字孪生发展现状、发展趋势、面临的问题与挑战及国际国内标准化现状。在此基础上,白皮书探索形成了“城市数字孪生标准体系总体框架(1.0版)”,并提出了拟研制标准建议和标准化工作建议。白皮书构建了城市数字孪生标准化路线图,为后续相关标准研制、应用实施指明了方向。

城市数字孪生典型特征

全面感知:城市数字孪生以全面感知为前提。城市是一个复杂巨系统,时刻处于发展变化中,必须时刻掌握物理城市的全局发展与精细变化,实现孪生环境下的数字城市与物理城市同步运行。

精准映射是构建数字世界并建立数字世界与物理世界紧密关系的过程。

智能推演是城市数字孪生具备智慧能力的体现,是实现对物理城市进行科学预测、指导与优化的关键。

动态可视:指通过将感知的多源数据进行数字化建模和可视化渲染,城市数字孪生提供了全要素、全范围、全精度真实的渲染效果,实现全空间信息和城市实时运行

虚实互动:指物理空间与数字空间的互操作和双向互动,借助物联网、图形/图像、AR/VR、人机交互等领域技术的协同和融合,实现城市级虚实空间融合、控制与反馈等能力。态势的动态展示。

协同演进是城市数字孪生具有高阶智慧能力的体现。城市数字孪生过程中,物理城市与数字城市在城市运行、数据、技术、机制等方面存在长期协同关系,长期相互反馈、相互影响。

pic_1f49ab65.png

多维度构建参考架构,立体刻画城市数字孪生内涵

白皮书对“城市数字孪生”概念进行了系统地梳理和分析,创新性地从概念、技术、相关方等不同视角构建了城市数字孪生参考架构。

白皮书认为,城市数字孪生是利用数字孪生技术,以数字化方式创建城市物理实体的虚拟映射,借助历史数据、实时数据、空间数据以及算法模型等,仿真、预测、交互、控制城市物理实体全生命周期过程的技术手段,可以实现城市物理空间和社会空间中物理实体对象以及关系、活动等在数字空间的多维映射和连接。

从概念视角,城市数字孪生以城市物理空间、社会空间以及数字空间在时间维度和空间维度更加精准的映射、更加紧密的联接和更加多维的联动,实现三元空间的协同演进和共生共智,满足“人”在城市生活、生产、生态的各类需求,服务“以人为本”的智慧城市建设初心。

pic_49a1d280.png

图1 城市数字孪生概念模型

从技术视角,需对物理空间以及社会空间中的物理实体对象、事件对象以及关系对象进行数字空间的虚拟表达以及映射,通过信息基础设施的转化传输以及处理形成数据资源,在通用服务能力的支撑下进一步融合数字孪生技术形成能够对外提供的数字孪生服务,并通过交互服务实现与上层应用场景的融合。同时,需提供立体化安全管理以及全生命周期的运营管理,保障数字空间各类资产以及服务的安全高效运行。

pic_5800ae7f.png图2 城市数字孪生技术参考架构

从相关方视角,城市数字孪生由城市数字孪生咨询服务提供方、建设技术提供方、运营服务方三方多类主体联动构建。

pic_6b1cf3f0.png图3 城市数字孪生相关方

pic_6240dbd6.png

梳理发展现状,总结城市数字孪生趋势与问题

白皮书梳理了城市数字孪生国家及地方相关政策,分析了产业生态发展现状。同时,提出了城市数字孪生发展的总体发展趋势及面临的主要问题和挑战。

pic_17724c61.png图4 城市数字孪生产业生态

随着各地城市数字孪生探索与落地,智慧城市建设也将进入新的发展阶段。随供需双发力,城市数字孪生的技术创新、产业发展、标准规范等将快速发展,呈现物理城市和数字城市并行共生的新发展格局。

  • 城市数字孪生将成为智慧城市建设技术底座。城市数字孪生将成为智慧城市发展新阶段的核心底座,为城市构建虚实共生的数字基础设施能力。
  • 城市数字孪生将在智慧城市中迎来深度应用。城市数字孪生相关产业快速发展,市场规模不断扩大,以城市大脑、城运中心、城市信息模型、城市数字孪生运营管理为主的相关领域市场迅速升温。
  • 城市数字孪生将形成跨行业协作生态共融。数据融合、技术融合和业务融合推动城市数字孪生产业链上下游的多元主体在竞争中发展出共生关系,生态共融正成为行业共识。

城市数字孪生从概念培育逐步走向建设实施,各项支撑技术日渐成熟,但仍面临着供应链安全性不足、数据支撑不足、应用深度不足、产业联动不足和标准支撑不足等问题与挑战。

pic_bb1e559f.png

构建标准体系,以标准化助力高质量发展

当前,城市数字孪生标准化处于起步阶段,亟需开展标准体系顶层设计。白皮书探索构建了“城市数字孪生标准体系总体框架(1.0版)”,将城市数字孪生标准划分为“01总体”“02数据”“03技术与平台”“04安全”“05运维/运营”“06应用”六大类。

pic_852cf2b0.png图5 城市数字孪生标准体系总体框架(1.0版)

pic_99774322.png图6 城市数字孪生标准体系结构

为充分发挥标准基础性、引领性作用,助力城市数字孪生相关产业高质量发展,白皮书建议城市数字孪生标准化工作主要从四方面开展。

  • 完善工作机制,强化统筹与协同。城市数字孪生涉及技术、应用、相关方众多,是复杂的系统工程,其标准化工作开展需统筹布局、协同各方。
  • 研制重点标准,完善标准体系建设。以“规划引领、需求牵引”为原则,推动重点标准研制工作,不断完善城市数字孪生标准体系。
  • 挖掘优秀案例,发挥示范引领作用。建立城市数字孪生典型案例与标准的良性互动机制,充分发挥先进性、代表性案例的引领与示范作用。
  • 强化国际交流,推动国际标准制定。城市数字孪生承载了一系列关键技术和核心产业,要借助国际标准带动我国产品和方案走出去。

下一步,全国信标委智慧城市标准工作组将以此白皮书为基础,与各界共同推动城市数字孪生技术、理论、标准研究与制定工作,凝共识、聚合力,助力城市数字孪生产业生态培育,推动“以人为本”的智慧城市向智能化、智慧化迈进。

具体内容如下

pic_26d37640.png

pic_5e18ea32.png

pic_819d1a52.png

pic_41f0df74.png

pic_83d7532c.png

pic_0e4ecd8e.png

pic_3f946dd0.png

pic_6b04c7bc.png

pic_dcc0daaf.png

pic_0f29152f.png

pic_fdd8dedd.png

pic_66735768.png

pic_b20e947b.png

pic_b3b47ca6.png

pic_228d6a38.png

pic_7e0ae0aa.png

pic_13e8029d.png

pic_198b3a43.png

pic_38d1988e.png

pic_822ed03b.png

pic_4013dc97.png

pic_1a552d51.png

pic_18768e2d.png

pic_a42a5335.png

pic_92e9e847.png

pic_3cbad5fe.png

pic_b349d1bc.png

pic_ac929dd1.png

pic_9c257eca.png

pic_a126168b.png

pic_6e02a89e.png

pic_36202e9e.png

pic_7d0afa1c.png

pic_0988e5d8.png

pic_2bf91f18.png

pic_59d7d9ac.png

pic_dc59256b.png

pic_6402fd89.png

pic_9f8fb217.png

pic_69a13f05.png

pic_0b680970.png

pic_35a51d84.png

pic_590b48db.png

pic_d425aa26.png

pic_5a62839c.png

pic_3a23fd3e.png

pic_ecf8dfa3.png

pic_1bf22b4c.png

pic_ddb0d741.png

pic_a156ccc9.png

pic_1af6169c.png

pic_1b160c3a.png

pic_97466946.png

pic_36139833.png

pic_dbaae499.png

pic_133108f1.png

pic_e4108299.png

pic_e64220e2.png

pic_c4cb36d3.png

pic_0a2c9b5b.png来源:全国信标委智慧城市标准工作组    

收起阅读 »

还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS!

本文介绍策略模式的具体应用以及Map+函数式接口如何 “更完美” 的解决 if-else的问题。 需求 最近写了一个服务:根据优惠券的类型resourceType和编码resourceId来 查询 发放方式grantType和领取规则实现方式:根据优惠券类型...
继续阅读 »

本文介绍策略模式的具体应用以及Map+函数式接口如何 “更完美” 的解决 if-else的问题。

需求

最近写了一个服务:根据优惠券的类型resourceType和编码resourceId来 查询 发放方式grantType和领取规则

实现方式:

  1. 根据优惠券类型resourceType -> 确定查询哪个数据表

  2. 根据编码resourceId -> 到对应的数据表里边查询优惠券的派发方式grantType和领取规则

优惠券有多种类型,分别对应了不同的数据库表:

  • 红包 —— 红包发放规则表

  • 购物券 —— 购物券表

  • QQ会员

  • 外卖会员

实际的优惠券远不止这些,这个需求是要我们写一个业务分派的逻辑

第一个能想到的思路就是if-else或者switch case:

switch(resourceType){

case "红包"
查询红包的派发方式 
break;
case "购物券"
查询购物券的派发方式
break;
case "QQ会员" :
break;
case "外卖会员" :
break;
......
default : logger.info("查找不到该优惠券类型resourceType以及对应的派发方式");
break;
}

如果要这么写的话, 一个方法的代码可就太长了,影响了可读性。(别看着上面case里面只有一句话,但实际情况是有很多行的)

而且由于 整个 if-else的代码有很多行,也不方便修改,可维护性低。

策略模式

策略模式是把 if语句里面的逻辑抽出来写成一个类,如果要修改某个逻辑的话,仅修改一个具体的实现类的逻辑即可,可维护性会好不少。

以下是策略模式的具体结构(详细可看这篇博客: 策略模式.):


策略模式在业务逻辑分派的时候还是if-else,只是说比第一种思路的if-else 更好维护一点。。。

switch(resourceType){

case "红包"
String grantType=new Context(new RedPaper()).ContextInterface();
break;
case "购物券"
String grantType=new Context(new Shopping()).ContextInterface();
break;

......
default : logger.info("查找不到该优惠券类型resourceType以及对应的派发方式");
break;

但缺点也明显:

  • 如果 if-else的判断情况很多,那么对应的具体策略实现类也会很多,上边的具体的策略实现类还只是2个,查询红包发放方式写在类RedPaper里边,购物券写在另一个类Shopping里边;那资源类型多个QQ会员和外卖会员,不就得再多写两个类?有点麻烦了

  • 没法俯视整个分派的业务逻辑

Map+函数式接口

用上了Java8的新特性lambda表达式

  • 判断条件放在key中

  • 对应的业务逻辑放在value中

这样子写的好处是非常直观,能直接看到判断条件对应的业务逻辑

需求: 根据优惠券(资源)类型resourceType和编码resourceId查询派发方式grantType

上代码:

@Service
public class QueryGrantTypeService {

   @Autowired
   private GrantTypeSerive grantTypeSerive;
   private Map<StringFunction<String,String>> grantTypeMap=new HashMap<>();

   /**
    * 初始化业务分派逻辑,代替了if-else部分
    * key: 优惠券类型
    * value: lambda表达式,最终会获得该优惠券的发放方式
    */
   @PostConstruct
   public void dispatcherInit(){

       grantTypeMap.put("红包",resourceId->grantTypeSerive.redPaper(resourceId));
       grantTypeMap.put("购物券",resourceId->grantTypeSerive.shopping(resourceId));
       grantTypeMap.put("qq会员",resourceId->grantTypeSerive.QQVip(resourceId));
  }

   public String getResult(String resourceType){
  
    
    
       //Controller根据 优惠券类型resourceType、编码resourceId 去查询 发放方式grantType
       Function<String,String> result=getGrantTypeMap.get(resourceType);
       if(result!=null){

      //传入resourceId 执行这段表达式获得String型的grantType
           return result.apply(resourceId);
      }
       return "查询不到该优惠券的发放方式";
  }
}

如果单个 if 语句块的业务逻辑有很多行的话,我们可以把这些 业务操作抽出来,写成一个单独的Service,即:

//具体的逻辑操作

@Service
public class GrantTypeSerive {

   public String redPaper(String resourceId){

       //红包的发放方式
       return "每周末9点发放";
  }
   public String shopping(String resourceId){

       //购物券的发放方式
       return "每周三9点发放";
  }
   public String QQVip(String resourceId){

       //qq会员的发放方式
       return "每周一0点开始秒杀";
  }
}

入参String resourceId是用来查数据库的,这里简化了,传参之后不做处理。

用http调用的结果:

@RestController
public class GrantTypeController {

   @Autowired
   private QueryGrantTypeService queryGrantTypeService;

   @PostMapping("/grantType")
   public String test(String resourceName){

       return queryGrantTypeService.getResult(resourceName);
  }
}


用Map+函数式接口也有弊端:
你的队友得会lambda表达式才行啊,他不会让他自己百度去

最后捋一捋本文讲了什么:

  1. 策略模式通过接口、实现类、逻辑分派来完成,把 if语句块的逻辑抽出来写成一个类,更好维护。

  2. Map+函数式接口通过Map.get(key)来代替 if-else的业务分派,能够避免策略模式带来的类增多、难以俯视整个业务逻辑的问题。

    ————————————————
    作者:zhongh Jim
    来源:https://blog.csdn.net/qq_44384533/article/details/109197926

收起阅读 »

DDD划分领域、子域、核心域、支撑域的目的

名词解释在DDD兴起的原因以及与微服务的关系中曾举了一个研究桃树的例子,如果要研究桃树,将桃树根据器官分成根、茎、叶、花、果实、种子,这每一种器官都可以认为是一个研究领域,而领域又有更加具体的细分,分成子域、核心域、通用域、支撑域等,下面回顾桃树这个例子看上面...
继续阅读 »

名词解释

DDD兴起的原因以及与微服务的关系中曾举了一个研究桃树的例子,如果要研究桃树,将桃树根据器官分成根、茎、叶、花、果实、种子,这每一种器官都可以认为是一个研究领域,而领域又有更加具体的细分,分成子域、核心域、通用域、支撑域等,下面回顾桃树这个例子


看上面这张图 ,如果研究桃树是我们的业务,那么如何更加快速有效的研究桃树呢? 根据回忆,初中课本是这样研究的:

第一步: 确定研究的对象,即研究领域 ,这里是一棵桃树。

第二步: 根据研究对象的某些维度,对其进行进一步的拆分,例如拆分成器官,而器官又可以分成营养器官,生殖器官,其中营养器官包括根、茎、叶,生殖器官包括花、果实、种子,那么这些就是我们要研究的子域。

第三步: 现在就可以最子域进行划分了,找出核心域,通用域,支撑域,至于为什么要这么划分,后面再解释,当我们找到核心域之后,再各个子域进行深一步的划分,划分成组织,例如分成保护组织,营养组织,疏导组织,这就儿也可以理解成将领域继续划分为子域的过程。

第四步:对组织进行进一步的划分,可以分成细胞,例如根毛细胞、导管细胞等等

我们有没有必要继续拆分细胞呢?这个取决于我们研究的业务,例如在之前光学显微镜时,研究到细胞也就截止了,具体到其他业务,也是研究到某一步就不需要继续拆分,而这最小层次的领域,通常就是我们所说的实体,聚合、聚合根、实体以及值对象等内容会在后面深入了解。


下面归纳一下上面提到的几个名词的概念 :

领域: 往往就是业务的某一个部分 , 例如电商的销售部分、物流部分、供应链部分等, 这些对于电商来说就是各个领域(模块),领域主要作用就是用来驱动范围, DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。

子域:相对的一个概念, 我们可以将领域进行进一步的划分 , 这时候就是子域, 甚至可以对子域继续划分形成 子子域(依旧叫子域),就好比当我们研究植物时,如果研究的对象是桃树,那么果实根茎叶是领域,可是如果不仅仅要研究果实,还要研究组织甚至细胞,那么研究的就是果实的子域、组织的子域。

核心域:所有领域中最关键的部分 , 什么意思呢, 就是最核心的部分, 对于业务来说, 核心域是企业根本竞争力, 也是创造利润里最关键的部分 , 例如电商里面那么多领域, 最重要的是什么? 就是销售系统, 无论你是2B还是2C, 还是PDD ,这些核心模块就是核心域。

通用域:除了核心域之外, 还需要自己做的一些领域, 例如鉴权、日志等, 特点是可能被多个领域公用的部分。

支撑域:系统中业务分析阶段最不重点关注的领域, 也就是非核心域非通用域的领域, 例如电商里面的支付、物流,仅仅是为了支撑业务的运转而存在, 甚至可以去购买别人的服务, 这类的领域就是支撑域。

需要注意的是,这些名词在实际的微服务设计和开发过程中不一定用得上,但是可以帮助理解DDD的核心设计思想以及理念,而这些思想和理念在实际的IT战略设计业务建模和微服务设计上都是可以借鉴的。

为什么要划分核心域、通用域、支撑域 ?

通过上面可以知道,决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。

这三类子域相较之下,核心域是最重要的,我们下面讲目的的时候还会以核心域为例详细介绍。通用域和支撑域如果对应到企业系统,举例来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。

那么为什么要划分出这些新的名词呢? 先想一个问题,对于桃树而言,根、茎、叶、花、果实、种子六个领域哪一个是核心域?

是不是有不同的理解? 有人说是种子,有人说是根,有人说是叶子,也有人说是茎等等,为什么会有这种情况呢?

因为每个人站的角度不一样,你如果是果农,那么果实就是核心域,你的大部分操作应该都是围绕提高果实产量进行,如果你是景区管理员,那么芳菲四月桃花盛开才是你重点关注,如果比是林场工作人员,那么树干才应该是你重点关注的领域,看到没,对于同一个领域划分的子域,每个人都有不同的理解,那么要通过讨论确定核心域,确保大家认同一致,对于实际业务开发来说,参与的人员众多,有业务方面的,有架构师,有后端开发人员,营销市场等等,势必要最开始就确定我们的核心域,除了统一大家的认识之外还有什么好处呢?

对于一个企业来说,预算以及时间是有限的,也就意味着时间以及精力甚至金钱要尽可能多的花在核心的的地方。就好比电商,电商企业那么多,每一家核心域都有所差别,造成的市场结果也千差万别,那么公司战略重点和商业模式应该找到核心域,且重点关注核心域。

总的来说,核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能
属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

作者:等不到的口琴
来源:https://www.cnblogs.com/Courage129/p/14853600.html


收起阅读 »

API安全接口安全设计

如何保证外网开放接口的安全性。使用加签名方式,防止数据篡改信息加密与密钥管理搭建OAuth2.0认证授权使用令牌方式搭建网关实现黑名单和白名单一、令牌方式搭建搭建API开放平台方案设计:1.第三方机构申请一个appId,通过appId去获取accessToke...
继续阅读 »

如何保证外网开放接口的安全性。

  • 使用加签名方式,防止数据篡改

  • 信息加密与密钥管理

  • 搭建OAuth2.0认证授权

  • 使用令牌方式

  • 搭建网关实现黑名单和白名单

一、令牌方式搭建搭建API开放平台


方案设计:

1.第三方机构申请一个appId,通过appId去获取accessToken,每次请求获取accessToken都要把老的accessToken删掉

2.第三方机构请求数据需要加上accessToken参数,每次业务处理中心执行业务前,先去dba持久层查看accessToken是否存在(可以把accessToken放到redis中,这样有个过期时间的效果),存在就说明这个机构是合法,无需要登录就可以请求业务数据。不存在说明这个机构是非法的,不返回业务数据。

3.好处:无状态设计,每次请求保证都是在我们持久层保存的机构的请求,如果有人盗用我们accessToken,可以重新申请一个新的taken.

二、基于OAuth2.0协议方式

原理

第三方授权,原理和1的令牌方式一样

1.假设我是服务提供者A,我有开发接口,外部机构B请求A的接口必须申请自己的appid(B机构id)

2.当B要调用A接口查某个用户信息的时候,需要对应用户授权,告诉A,我愿同意把我的信息告诉B,A生产一个授权token给B。

3.B使用token获取某个用户的信息。

联合微信登录总体处理流程

  1. 用户同意授权,获取code

  2. 通过code换取网页授权access_token

  3. 通过access_token获取用户openId

  4. 通过openId获取用户信息

三、信息加密与密钥管理

  • 单向散列加密

  • 对称加密

  • 非对称加密

  • 安全密钥管理

1.单向散列加密

散列是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。任何输入信息的变化,哪怕仅一位,都将导致散列结果的明显变化,这称之为雪崩效应。

散列还应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改。

单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:

  • MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值;

SHA-1与MD5的比较

因为二者均由MD4导出,SHA-1和MD5彼此很相似。相应的,他们的强度和其他特性也是相似,但还有以下几点不同:

  • 对强行供给的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2128数量级的操作,而对SHA-1则是2160数量级的操作。这样,SHA-1对强行攻击有更大的强度。

  • 对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。

  • 速度:在相同的硬件上,SHA-1的运行速度比MD5慢。

1、特征:雪崩效应、定长输出和不可逆。

2、作用是:确保数据的完整性。

3、加密算法:md5(标准密钥长度128位)、sha1(标准密钥长度160位)、md4、CRC-32

4、加密工具:md5sum、sha1sum、openssl dgst。

5、计算某个文件的hash值,例如:md5sum/shalsum FileName,openssl dgst –md5/-sha

2.对称加密

秘钥:加密解密使用同一个密钥、数据的机密性双向保证、加密效率高、适合加密于大数据大文件、加密强度不高(相对于非对称加密)

对称加密优缺点

  • 优点:与公钥加密相比运算速度快。

  • 缺点:不能作为身份验证,密钥发放困难


DES是一种对称加密算法,加密和解密过程中,密钥长度都必须是8的倍数

public class DES {
public DES() {
}

// 测试
public static void main(String args[]) throws Exception {
// 待加密内容
String str = "123456";
// 密码,长度要是8的倍数 密钥随意定
String password = "12345678";
byte[] encrypt = encrypt(str.getBytes(), password);
System.out.println("加密前:" +str);
System.out.println("加密后:" + new String(encrypt));
// 解密
byte[] decrypt = decrypt(encrypt, password);
System.out.println("解密后:" + new String(decrypt));
}

/**
* 加密
*
* @param datasource
*           byte[]
* @param password
*           String
* @return byte[]
*/
public static byte[] encrypt(byte[] datasource, String password) {
try {
  SecureRandom random = new SecureRandom();
  DESKeySpec desKey = new DESKeySpec(password.getBytes());
  // 创建一个密匙工厂,然后用它把DESKeySpec转换成
  SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
  SecretKey securekey = keyFactory.generateSecret(desKey);
  // Cipher对象实际完成加密操作
  Cipher cipher = Cipher.getInstance("DES");
  // 用密匙初始化Cipher对象,ENCRYPT_MODE用于将 Cipher 初始化为加密模式的常量
  cipher.init(Cipher.ENCRYPT_MODE, securekey, random);
  // 现在,获取数据并加密
  // 正式执行加密操作
  return cipher.doFinal(datasource); // 按单部分操作加密或解密数据,或者结束一个多部分操作
} catch (Throwable e) {
  e.printStackTrace();
}
return null;
}

/**
* 解密
*
* @param src
*           byte[]
* @param password
*           String
* @return byte[]
* @throws Exception
*/
public static byte[] decrypt(byte[] src, String password) throws Exception {
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
// 创建一个DESKeySpec对象
DESKeySpec desKey = new DESKeySpec(password.getBytes());
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");// 返回实现指定转换的
                  // Cipher
                  // 对象
// 将DESKeySpec对象转换成SecretKey对象
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, random);
// 真正开始解密操作
return cipher.doFinal(src);
}
}

输出

加密前:123456
加密后:>p.72|
解密后:123456

3.非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。

公钥与私钥是一对

  • 公钥对数据进行加密,只有用对应的私钥才能解密

  • 私钥对数据进行加密,只有用对应的公钥才能解密

过程:

  • 甲方生成一对密钥,并将公钥公开,乙方使用该甲方的公钥对机密信息进行加密后再发送给甲方;

  • 甲方用自己私钥对加密后的信息进行解密。

  • 甲方想要回复乙方时,使用乙方的公钥对数据进行加密

  • 乙方使用自己的私钥来进行解密。

  • 甲方只能用其私钥解密由其公钥加密后的任何信息。

特点:

  • 算法强度复杂

  • 保密性比较好

  • 加密解密速度没有对称加密解密的速度快。

  • 对称密码体制中只有一种密钥,并且是非公开的,如果要解密就得让对方知道密钥。所以保证其安全性就是保证密钥的安全,而非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多

  • 适用于:金融,支付领域

RSA加密是一种非对称加密

import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import org.apache.commons.codec.binary.Base64;


/**
* RSA加解密工具类
*
*
*/
public class RSAUtil {

public static String publicKey; // 公钥
public static String privateKey; // 私钥

/**
* 生成公钥和私钥
*/
public static void generateKey() {
// 1.初始化秘钥
KeyPairGenerator keyPairGenerator;
try {
  keyPairGenerator = KeyPairGenerator.getInstance("RSA");
  SecureRandom sr = new SecureRandom(); // 随机数生成器
  keyPairGenerator.initialize(512, sr); // 设置512位长的秘钥
  KeyPair keyPair = keyPairGenerator.generateKeyPair(); // 开始创建
  RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
  RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
  // 进行转码
  publicKey = Base64.encodeBase64String(rsaPublicKey.getEncoded());
  // 进行转码
  privateKey = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
}

/**
* 私钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByprivateKey(String content, String privateKeyStr, int opmode) {
// 私钥要用PKCS8进行处理
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyStr));
KeyFactory keyFactory;
PrivateKey privateKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, privateKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }

} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

/**
* 公钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByPublicKey(String content, String publicKeyStr, int opmode) {
// 公钥要用X509进行处理
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyStr));
KeyFactory keyFactory;
PublicKey publicKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, publicKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }
} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

// 测试方法
public static void main(String[] args) {
/**
  * 注意: 私钥加密必须公钥解密 公钥加密必须私钥解密
  * // 正常在开发中的时候,后端开发人员生成好密钥对,服务器端保存私钥 客户端保存公钥
  */
System.out.println("-------------生成两对秘钥,分别发送方和接收方保管-------------");
RSAUtil.generateKey();
System.out.println("公钥:" + RSAUtil.publicKey);
System.out.println("私钥:" + RSAUtil.privateKey);

System.out.println("-------------私钥加密公钥解密-------------");
  String textsr = "11111111";
  // 私钥加密
  String cipherText = RSAUtil.encryptByprivateKey(textsr,
  RSAUtil.privateKey, Cipher.ENCRYPT_MODE);
  System.out.println("私钥加密后:" + cipherText);
  // 公钥解密
  String text = RSAUtil.encryptByPublicKey(cipherText,
  RSAUtil.publicKey, Cipher.DECRYPT_MODE);
  System.out.println("公钥解密后:" + text);

System.out.println("-------------公钥加密私钥解密-------------");
// 公钥加密
String textsr2 = "222222";

String cipherText2 = RSAUtil.encryptByPublicKey(textsr2, RSAUtil.publicKey, Cipher.ENCRYPT_MODE);
System.out.println("公钥加密后:" + cipherText2);
// 私钥解密
String text2 = RSAUtil.encryptByprivateKey(cipherText2, RSAUtil.privateKey, Cipher.DECRYPT_MODE);
System.out.print("私钥解密后:" + text2 );
}

}


四、使用加签名方式,防止数据篡改

客户端:请求的数据分为2部分(业务参数,签名参数),签名参数=md5(业务参数)

服务端:验证md5(业务参数)是否与签名参数相同
————————————————
作者:单身贵族男
来源:https://blog.csdn.net/zhou920786312/article/details/95536556

收起阅读 »

一个向上帝买了挂的男人!

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。 在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不...
继续阅读 »

pic_8ae2a872.png

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。

在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不知道他是谁。 约翰·冯·诺依曼是20世纪最有影响力的人物之一。他可能比过去150年中任何一位伟大的思想更直接地影响了你的生活,他的研究涉及从量子力学到气候科学的一切。 pic_1f8dc88d.png 冯·诺依曼最大的贡献是现代计算机,他采用了图灵奠定的卓越理论框架,并实际提出了为几乎所有数字计算机提供动力的架构:冯·诺依曼架构。 更有争议的是,冯·诺依曼在二战期间对曼哈顿计划做出了重大贡献,包括完善原子弹本身的设计和对其功能至关重要的机制。 pic_7ec06076.png 与曼哈顿计划的其他一些老兵不同,冯·诺依曼从未对自己在该计划中的角色表示遗憾,甚至在冷战期间推动了「同归于尽」的政策。 至少可以说,约翰·冯·诺依曼是一个复杂的人物,但他在20世纪几乎无人能及,可以说他对现代世界的责任比他同时代的任何人都大。

神童降世

约翰·冯·诺依曼于1903年12月28日出生在匈牙利首都布达佩斯。 冯·诺依曼的父亲是银行家,母亲是奥匈贵族的女儿,他的父母富有且受人尊敬。
pic_34745df7.png
1913年,奥匈帝国皇帝弗朗茨·约瑟夫授予冯·诺依曼的父亲贵族地位,并给了这个家族一个世袭的头衔「马吉塔」,即现在的罗马尼亚马吉塔。 这个头衔纯粹是尊称,因为这个家庭与这个地方没有任何联系,但这是冯·诺依曼一生都会坚持的。 年轻的冯·诺依曼在同龄人中,被认为是真正的神童,尤其是在数学方面。人们认为他有摄影一般的记忆力,这帮助他从很小的时候就吸收了大量的知识。 pic_9118426a.png 冯·诺伊曼11岁时与表妹 Katalin Alcsuti在一起 六岁时,他就开始在头脑中进行两个八位数的除法,八岁时,他已经掌握了微积分。 pic_1875c648.png 他的父亲认为,他所有的孩子都需要说除母语匈牙利语以外的欧洲主要语言,所以冯·诺依曼学习了英语、法语、意大利语和德语。 小时候,他对历史也有很深的兴趣,并阅读了德国历史学家威廉·昂肯的整部46卷专著《通史》。
pic_b93227b9.png
德国历史学家威廉·昂肯 在老师的鼓励下,冯·诺依曼在学习上取得了优异的成绩,但他的父亲不相信数学家的职业会带来经济上的利益。 相反,冯·诺依曼和他的父亲达成共识,同意冯·诺依曼从事化学工程,他17岁去柏林学习,后来在苏黎世学习。 化学似乎是冯·诺依曼少数几个不感兴趣的领域之一,尽管他确实获得了苏黎世化学工程文凭,同时还获得了数学博士学位。
pic_c0043b57.png

职业生涯早期

约翰·冯·诺依曼很早就发表了论文,从20岁开始,他写了一篇定义序数的论文,这仍然是我们今天使用的定义。 他写了关于集合论的博士论文,并在一生中对该领域做出了若干贡献。
到1927年,冯·诺依曼已经发表了12篇著名的数学论文。到1929年,他已经出版了32部作品,以大约一个月一篇学术论文的速度写出了很多重要的工作。 pic_1cb6f516.png 1928年,他成为柏林大学的一名私 人教师,也是柏林大学历史上获得该职位最年轻的人。 这个职位使他能够在大学里讲课,直到1929年他成为汉堡大学的一名私人教师。 冯·诺依曼在父亲于1929年去世后,皈依了天主教。1930年元旦,约翰·冯·诺依曼与布达佩斯大学经济学学生玛丽埃塔·科维西结婚,并于1935年与她生下了他唯一的孩子玛丽娜。 虽然冯·诺依曼似乎注定要在德国科学院从事一个有前途的职业,但在1929年10月,他获得了新泽西州普林斯顿大学的一个职位,他最终接受了这个职位,并于1930年与妻子一起前往美国。

移民美国

到1933年,约翰·冯·诺依曼成为新成立的普林斯顿高等研究院最初的六名数学教授之一,他也将在这个岗位上度过余生。 当他搬到新泽西时,像他之前的许多美国移民一样,冯·诺依曼将他的匈牙利名字英语化了(由玛吉塔伊·诺依曼·亚诺斯变成约翰·冯·诺依曼,使用德国式的世袭尊称)。 1937年,他和妻子离婚,第二年冯·诺依曼再婚,这次是和克拉拉·丹,他在第二次世界大战前最后一次访问匈牙利时,在布达佩斯第一次见到了克拉拉·丹。 pic_75b34244.png 1937年,冯·诺依曼成为美国公民,1939年,他的母亲、兄弟姐妹和姻亲也都移民到了美国(他的父亲早些时候去世了)。

战争年代与「曼哈顿计划」

约翰·冯·诺依曼对历史最重要的贡献之一是他在第二次世界大战期间对曼哈顿计划的研究。 一如既往,冯·诺依曼不能让数学挑战得不到解决,更困难的问题之一是如何模拟爆炸的影响。 冯·诺依曼在20世纪30年代投身于这些问题的研究,并成为该领域的专家。如果他有特长的话,那应该是研究聚能装药(Shaped Charges,用于爆破)领域的数学问题,聚能装药是用来控制和引导爆炸能量的。 pic_434c04b8.png 这使他与美国军方,特别是美国海军进行了相当多的定期磋商。当曼哈顿计划在20世纪40年代初开始工作时,冯·诺依曼因其专业知识而被招募。 1943年,冯·诺依曼对曼哈顿计划产生了最重大、最持久的影响。 pic_dd9691a8.png 曼哈顿计划成员 当时,设计原子弹的洛斯阿拉莫斯实验室发现,钚-239(该项目使用的裂变材料之一)与实验室的工作炸弹设计不兼容。 实验室的物理学家塞斯·尼德迈尔一直在研究一种独立的内爆型炸弹设计,这种设计很有希望,但许多人认为它不可行。
pic_bbad35a1.png
核弹内爆机制的动画 要引爆核爆炸,需要在炸弹的反应物中引发失控的裂变链式反应。链式反应的速度是指数级的,因此控制爆炸足够长的时间,使足够的裂变材料进行所需的反应,是一项重大挑战。 内爆型炸弹需要更复杂的控制来产生反应,但它也不需要像洛斯阿拉莫斯开发的枪型炸弹设计那样耗费那么多的材料。 内爆型装置使用一系列受控的常规爆炸来压缩其核心中的裂变反应物。 在这种压力下,裂变材料迅速开始核裂变链式反应,通过内爆的力量保持在原位,并允许更多的裂变材料释放其能量。
pic_32a52999.png
控制这些爆炸以产生精确的内爆力来产生期望的反应是一项很大的挑战,而冯·诺依曼以极大的热情接受了它。 他认为,使用较少的球形材料并通过内爆力适当压缩,可以产生更有效的爆炸,尽可能多地使用现有的裂变材料。 他经常是少数几个主张内爆方法的人之一,并最终计算出了一个数学公式,表明如果内爆能以至少95%的精度保持球形的几何形状,该方法就是可以实现的。 冯·诺依曼还计算出,如果爆炸在目标上方一定距离引爆,而不是在击中地面时引爆,爆炸的有效性将会提高。 这大大增加了原子弹的杀伤力,也减少了爆炸产生的尘埃量。 之后,冯·诺依曼被选为科学顾问团队的一员,他们就炸弹的可能目标咨询军方。 冯·诺依曼建议将目标定为日本京都,因为京都作为文化之都,其毁灭可能足以迫使战争迅速结束。 提出这一建议的不止他一个人,但战争部长亨利·史汀生否决了将目标对准京都这个提议,因为那里有许多历史建筑和重要的宗教圣地,所以最后选择了广岛和长崎。 1945年7月16日的三位一体试验中,冯·诺依曼在场,当时第一枚原子武器被引爆。广岛和长崎被炸后,日本投降,第二次世界大战结束。 pic_87622a6c.png 三位一体试验 与曼哈顿计划中同时代的一些人不同,冯·诺依曼似乎没有任何反思的痛苦、遗憾,甚至对他在原子弹方面的工作也没有一丝怀疑。 事实上,冯·诺依曼是核武器发展和相互保证毁灭(MAD)理论的最有力支持者之一,认为这是防止另一场灾难性世界大战的唯一方法。

核武器的「轻量化」思想

像战后初期的许多美国人一样,冯·诺依曼担心美国在核武器发展方面落后于苏联。 到上世纪40 年代末到50 年代初,用战略轰炸机向敌人投掷更多原子弹的理念,逐渐被新的火箭技术所取代。
冯·诺依曼认为,导弹是核武器的未来,由于他与参与苏联武器研制的德国科学家有过接触,他知道苏联对此问题的看法与他是一样的。 军备竞赛开始了,美苏两国把氢弹越做越小,可以装入洲际弹道导弹的弹头,冯·诺依曼积极为美国效力,努力缩小与苏联的「导弹差距」。 战后,冯·诺依曼在原子能委员会任职,为政府和军方提供核技术开发和战略方面的建议,并被广泛认为是「确保相互毁灭」理论(MAD)的设计者,而 MAD 在冷战期间确实被政府采纳,成为事实上的美国国策。

建造第一台真正的计算机

pic_75fe701f.png 冯·诺依曼在上世纪30年代初遇到了艾伦·图灵,当时图灵正在普林斯顿攻读博士学位。1937年,图灵发表了具有里程碑意义的论文《论可计算数》,奠定了现代计算的理论基础。
pic_3623b10a.png 冯·诺依曼很快认识到了图灵这一发现的重要性,并在30年代推动了计算机科学的发展。在普林斯顿大学,他和图灵围绕人工智能的思想曾进行了长时间的讨论。 作为一名数学家,冯·诺依曼从更抽象的角度研究计算机科学,另一个原因也是因为在30年代时,并没有真正可以工作的计算机。 在二战结束后,这种情况很快就改变了。 pic_bf84e043.png 冯·诺依曼深入参与了第一台可编程电子计算机「ENIAC」的开发,这台计算机能够识别和决定其他数据操作规则集,而不是最初使用的规则集。是冯诺依曼将 ENIAC 修改为作为存储程序机器运行。 后者让使我们今天理解的现代程序成为可能。冯·诺依曼本人编写了几个在 ENIAC 上运行的首批程序,并用这些程序模拟原子能委员会的部分核武器研究。 毫无疑问,冯·诺依曼对计算机科学领域最持久的贡献是在当今运行的每台计算机中使用的两个基本概念:冯·诺依曼体系架构和存储程序概念。 pic_e701e538.png 冯·诺依曼架构涉及构成计算机的物理电子电路的组织方式。按照这种方式构建的计算机被称为「冯·诺依曼机」。该架构由算术和逻辑单元 (ALU)、控制单元和临时存储器寄存器组成,它们共同构成了中央处理器 (CPU)。 CPU 连接到内存单元,该内存单元包含将要由CPU处理和操作的所有数据。CPU还连接到输入和输出设备,以根据需要更改数据,并检索运行程序的结果。 自 1945 年冯诺依曼提出这一架构以来,直到今天,它基本上仍是当今大多数通用计算机的运行方式,几乎没有改变。 另一项重大创新与冯·诺依曼架构有关,即存储程序概念,也就是说,被操作或处理的数据,以及描述如何操纵和处理该数据的程序,都存储在计算机的内存中。 这两项相互交织的创新实现了图灵机的理论框架,实际上将它们变成了可以用来计算工资、火炮轨迹、游戏、互联网等几乎所有一切数据的机器。

对其他领域的杰出贡献

除了数学和计算机科学之外,冯·诺依曼一生都对其他几个领域也做出了重大贡献。
在早期的职业生涯中,冯·诺依曼为新兴的量子力学领域做出了重大贡献。 pic_e52acca3.png 1932年,他和保罗·狄拉克在《量子力学的数学基础》一书中发表了狄拉克-冯·诺依曼公理,这是该领域的第一个完整的数学框架。在这本书中,他还提出了量子逻辑的形式系统,也是同类体系中的首创。 冯·诺依曼还将博弈论确立为一门严谨的数学学科,这无疑影响了他后来关于MAD理论的地缘政治战略工作。 pic_a9fc558f.png 冯·诺依曼的博弈论中包含这样一个观点,即在广泛的博弈类别中,总是有可能找到一个平衡,任何参与者都不应单方面偏离这个平衡。 在生命科学领域,冯·诺依曼对元胞自动机的自我复制进行了彻底的数学分析,主要是构造函数、正在构建的事物以及构造函数构建所讨论事物所遵循的蓝图之间的关系。该分析描述了一种自我复制的机器,它是在40年代设计的,没有使用计算机。 冯·诺依曼的数学造诣也惠及气候科学。1950年,他编写了第一个气候建模程序,并使用 ENIAC 使用数值数据进行了世界上第一个气象预测。 冯·诺依曼预计,全球变暖是人类活动的结果,他在1955年写道: 「工业燃烧煤和石油释放到大气中的二氧化碳,可能已经充分改变了大气的成分,导致全球普遍变暖约1华氏度。」 冯·诺依曼也被认为是第一个描述「技术奇点」的人。冯·诺依曼的朋友斯坦·乌拉姆 (Stan Ulam) 后来描述了与他的一次对话,这次对话在今天听起来非常有先见之明。 pic_ccf72d4b.png 斯坦·乌拉姆、理查德·费曼和冯·诺依曼在一起 乌拉姆回忆说:「有一次谈话集中在不断加速的技术进步和人类生活方式的变化上,这让我们看到了人类历史上一些本质上的奇点。一旦超越了这些奇点,我们所熟知的人类事务就将无法继续下去了。」

冯·诺依曼的去世,和他的光辉遗产

1955 年,冯·诺依曼在看医生时发现他的锁骨上长了一块肉,他被诊断患有癌症,但他并没有充分接受这个事实。 众所周知,冯·诺依曼对即将到来的结局感到恐惧。他的一生好友尤金·维格纳 (Eugene Wigner) 写道:
pic_4c140e80.png 「当冯·诺依曼意识到自己病入膏肓时,他的逻辑迫使他意识到,自己即将不复存在,因此也不再有思想……亲眼目睹这一过程是令人心碎的,所有的希望都消失了,即将到来的命运尽管难以接受,但已经不可避免。」 冯·诺依曼的病情在1956年持续恶化,最终被送进了华盛顿特区的沃尔特里德陆军医疗中心。为防止泄密,军方对他实施了特殊的安全措施。 冯·诺依曼邀请一位天主教神父在他临终前商量,并接受了他的临终仪式安排。不过这位神父本人表示,冯诺依曼看上去似乎并没有从仪式中得到安慰。 1957年2月8日,冯·诺依曼因癌症逝世,享年53岁,他被安葬在新泽西州的普林斯顿公墓。 关于冯·诺依曼的癌症是否与他在「曼哈顿计划」期间遭受辐射有关,人们一直存在争议,但毫无争议的是,人类过早地失去了当代最伟大的科学巨人之一。 pic_1628bda4.png 冯·诺依曼的助手 P.R. Halmos 在1973年写道: 「人类的英雄有两种:一种和我们所有人一样,但更加相似,另一种显然具有一些「超人」的特质。 我们都可以跑步,我们中的一些人可以在不到4分钟的时间内跑完一英里。但有些事,我们大多数人一辈子都无法做到。冯·诺依曼的伟大贡献是惠及全人类的。在某些时候,我们或多或少都能清晰地思考,但冯·诺依曼的清晰思维始终比我们大多数人高出好几个数量级。」 冯·诺依曼的才华是毋庸置疑的,尽管他留下的遗产,尤其是核武器方面的贡献,比他的朋友和崇拜者愿意承认的要复杂得多。 无论我们最终如何看待冯·诺依曼和他的成就,我们都可以肯定地说,在未来一代人甚至几代人的时间里,都不太可能出现像他一样,对人类历史产生如此重大影响的人了。

转自:新智元 | David 小咸鱼
原文链接:https://interestingengineering.com/john-von-neumann-human-the-computer-behind-project-manhattan

收起阅读 »

使用 Nginx 构建前端日志统计服务

之前的几篇文章都是关于低代码平台的。这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。先放一下整体流程图吧:日志收集在常见的埋点方案中,...
继续阅读 »

背景

之前的几篇文章都是关于低代码平台的。


这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。

今天就来说一下其中的统计服务:目的主要是为了实现 H5 页面的分渠道统计(其实不仅仅是分渠道统计,核心是想做一个自定义事件统计服务,只是目前有分渠道统计的需求),查看每个渠道具体的 PV 情况。(具体会在 url 上面体现,会带上页面名称、id、渠道类型等)

先放一下整体流程图吧:


日志收集

常见的日志收集方式有手动埋点和自动埋点,这里我们不关注于如何收集日志,而是如何将收集的日志的发送到服务器。

在常见的埋点方案中,通过图片来发送埋点请求是一种经常被采纳的,它有很多优势:

  • 没有跨域

  • 体积小

  • 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)

  • 执行过程无阻塞

这里的方案就是在 nginx 上放一张 1px * 1px 的静态图片,然后通过访问该图片(http://xxxx.png?env=xx&event=xxx),并将埋点数据放在query参数上,以此将埋点数据落到nginx日志中。

iOS 上会限制 get 请求的 url 长度,但我们这里真实场景发送的数据不会太多,所以目前暂时采用这种方案

这里简单阐述一下为什么图片地址的query key 要这么设计,如果单纯是为了统计渠道和作品,很有可能会把key设计为channelworkId这种,但上面也说到了,我们是想做一个自定义事件统计服务,那么就要考虑字段的可扩展性,字段应更有通用语义。所以参考了很多统计服务的设计,这里采用的字段为:

  • env

  • event

  • key

  • value

之后每次访问页面,nginx就会自动记录日志到access_log中。

有了日志,下面我们来看下如何来对其进行拆分。

日志拆分

为何要拆分日志

access.log日志默认不会拆分,会越积累越多,系统磁盘的空间会被消耗得越来越多,将来可能面临着日志写入失败、服务异常的问题。

日志文件内容过多,对于后续的问题排查和分析也会变得很困难。

所以日志的拆分是有必要也是必须的。

如何拆分日志

我们这里拆分日志的核心思路是:将当前的access.log复制一份重命名为新的日志文件,之后清空老的日志文件。

视流量情况(流量越大日志文件积累的越快),按天、小时、分钟来拆分。可以把access.log按天拆分到某个文件夹中。

log_by_day/2021-12-19.log
log_by_day/2021-12-20.log
log_by_day/2021-12-21.log

但上面的复制 -> 清空操作肯定是要自动处理的,这里就需要启动定时任务,在每天固定的时间(我这里是在每天凌晨 00:00)来处理。

定时任务

其实定时任务不仅在日志拆分的时候会用到,在后面的日志分析和日志清除都会用到,这里先简单介绍一下,最终会整合拆分、分析和清除。

linux中内置的cron进程就是来处理定时任务的。在node中我们一般会用node-schedulecron来处理定时任务。

这里使用的是cron

/**
   cron 定时规则 https://www.npmjs.com/package/cron
   *    *    *    *    *    *
           
           
            day of week (0 - 6) (Sun-Sat)
          └───── month (1 - 12)
        └────────── day of month (1 - 31)
      └─────────────── hour (0 - 23)
    └──────────────────── minute (0 - 59)
  └───────────────────────── second (0 - 59)
*/

具体使用方式就不展开说明了。

编码

有了上面这些储备,下面我就来写一下这块代码,首先梳理下逻辑:

1️⃣ 读取源文件 access.log

2️⃣ 创建拆分后的文件夹(不存在时需自动创建)

3️⃣ 创建日志文件(天维度,不存在时需自动创建)

4️⃣ 拷贝源日志至新文件

5️⃣ 清空 access.log

/**
* 拆分日志文件
*
* @param {*} accessLogPath
*/
function splitLogFile(accessLogPath) {
 const accessLogFile = path.join(accessLogPath, "access.log");

 const distFolder = path.join(accessLogPath, DIST_FOLDER_NAME);
 fse.ensureDirSync(distFolder);

 const distFile = path.join(distFolder, genYesterdayLogFileName());
 fse.ensureFileSync(distFile);
 fse.outputFileSync(distFile, ""); // 防止重复,先清空

 fse.copySync(accessLogFile, distFile);

 fse.outputFileSync(accessLogFile, "");
}

日志分析

日志分析就是读取上一步拆分好的文件,然后按照一定规则去处理、落库。这里有一个很重要的点要提一下:node在处理大文件或者未知内存文件大小的时候千万不要使用readFile,会突破 V8 内存限制。正是考虑到这种情况,所以这里读取日志文件的方式应该是:createReadStream创建一个可读流交给 readline 逐行读取处理

readline

readline 模块提供了用于从可读流每次一行地读取数据的接口。 可以使用以下方式访问它:

const readline = require("readline");

readline 的使用也非常简单:创建一个接口实例,传入对应的参数:

const readStream = fs.createReadStream(logFile);
const rl = readline.createInterface({
 input: readStream,
});

然后监听对应事件即可:

rl.on("line", (line) => {
 if (!line) return;

 // 获取 url query
 const query = getQueryFromLogLine(line);
 if (_.isEmpty(query)) return;

 // 累加逻辑
 // ...
});
rl.on("close", () => {
 // 逐行读取结束,存入数据库
 const result = eventData.getResult();
 resolve(result);
});

这里用到了lineclose事件:

  • line事件:每当 input 流接收到行尾输入(\n、\r 或 \r\n)时,则会触发 'line' 事件

  • close事件:一般在传输结束时会触发该事件

逐行分析日志结果

了解了readline 的使用,下面让我们来逐行对日志结果进行分析吧。

首先来看下access.log中日志的格式:

我们取其中一行来分析:

127.0.0.1 - - [19/Feb/2021:15:22:06 +0800] "GET /event.png?env=h5&event=pv&key=24&value=2 HTTP/1.1" 200 5233 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" "-"

我们要拿到的就是urlquery部分,也就是我们在h5中自定义的数据。

通过正则匹配即可:

const reg = /GET\s\/event.png\?(.+?)\s/;
const matchResult = line.match(reg);
console.log("matchResult", matchResult);

const queryStr = matchResult[1];
console.log("queryStr", queryStr);

打印结果为:

queryStr可通过node中的querystring.parse()来处理:

const query = querystring.parse(queryStr);

console.log('query', query)
{
env: 'h5',
event: 'pv',
key: '24',
value: '2'
}

剩下的就是对数据做累加处理了。

但如何去做累加,我们要想一下,最开始也说了是要去做分渠道统计,那么最终的结果应该可以清晰的看到两个数据:

  • 所有渠道的数据

  • 每个渠道单独的数据

只有这样的数据对于运营才是有价值的,数据的好坏也直接决定了后面在每个渠道投放的力度。

这里我参考了 Google Analytics中的多渠道漏斗的概念,由上到下分维度记录每个维度的数据,这样就可以清晰的知道每个渠道的情况了。

具体实现也不麻烦,我们先来看下刚刚从一条链接中得到的有用数据:

{
 env: 'h5',
 event: 'pv',
 key: '24',
 value: '2'
}

这里的env代表环境,这里统计的都是来源于h5页面,所以envh5,但是为了扩展,所以设置了这个字段。

event表示事件名称,这里主要是统计访问量,所以为pv

key是作品 id。

value是渠道 code,目前主要有:1-微信、2-小红书、3-抖音。

再来看下最终统计得到的结果吧:

{
 date: '2021-12-21',
 key: 'h5',
 value: { num: 1276}
}
{
 date: '2021-12-21',
 key: 'h5.pv',
 value: { num: 1000}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12',
 value: { num: 200}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.1',
 value: { num: 56}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.2',
 value: { num: 84}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.3',
 value: { num: 60}
}

这是截取了2021-12-21当天的数据,我给大家分析一波:

1️⃣ h5:当天 h5 页面的自定义事件上报总数为 1276

2️⃣ h5.pv:其中 所有 pv(也就是 h5.pv)为 1000

3️⃣ h5.pv.12:作品 id 为 12 的 pv 一共有 200

4️⃣ h5.pv.12.1:作品 id 为 12 的在微信渠道的 pv 为 56

5️⃣ h5.pv.12.2:作品 id 为 12 的在小红书渠道的 pv 为 84

6️⃣ h5.pv.12.2:作品 id 为 12 的在抖音渠道的 pv 为 60

这样就能清楚的得到某一天某个作品在某条渠道的访问情况了,后续再以这些数据为支撑做成可视化报表,效果就一目了然了。

统计结果入库

目前这部分数据是放在了mongoDB中,关于node中使用mongoDB就不展开说了,不熟悉的可以参考我另外一篇文章Koa2+MongoDB+JWT 实战--Restful API 最佳实践

这里贴下model吧:

/**
* @description event 数据模型
*/
const mongoose = require("../db/mongoose");

const schema = mongoose.Schema(
{
   date: Date,
   key: String,
   value: {
     num: Number,
  },
},
{
   timestamps: true,
}
);

const EventModel = mongoose.model("event_analytics_data", schema);

module.exports = EventModel;

日志删除

随着页面的持续访问,日志文件会快速增加,超过一定时间的日志文件存在的价值也不是很大,所以我们要定期清除日志文件。

这个其实比较简单,遍历文件,因为文件名都是以日期命名的(格式:2021-12-14.log),所以只要判断时间间隔大于 90 天就删除日志文件。

贴一下核心实现:

// 读取日志文件
const fileNames = fse.readdirSync(distFolder);
fileNames.forEach((fileName) => {
 try {
   // fileName 格式 '2021-09-14.log'
   const dateStr = fileName.split(".")[0];
   const d = new Date(dateStr);
   const t = Date.now() - d.getTime();
   if (t / 1000 / 60 / 60 / 24 > 90) {
     // 时间间隔,大于 90 天,则删除日志文件
     const filePath = path.join(distFolder, fileName);
     fse.removeSync(filePath);
  }
} catch (error) {
   console.error(`日志文件格式错误 ${fileName}`, error);
}
});

定时任务整合

到这里,日志的拆分、分析和清除都说完了,现在要用cron来对他们做整合了。

首先来创建定时任务:

function schedule(cronTime, onTick) {
 if (!cronTime) return;
 if (typeof onTick !== "function") return;

 // 创建定时任务
 const c = new CronJob(
   cronTime,
   onTick,
   null, // onComplete 何时停止任务
   true, // 初始化之后立刻执行
   "Asia/Shanghai" // 时区
);

 // 进程结束时,停止定时任务
 process.on("exit", () => c.stop());
}

然后每一阶段都在不同的时间阶段去处理(定时拆分 -> 定时分析 -> 定时删除)

定时拆分

function splitLogFileTiming() {
 const cronTime = "0 0 0 * * *"; // 每天的 00:00:00
 schedule(cronTime, () => splitLogFile(accessLogPath));
 console.log("定时拆分日志文件", cronTime);
}

定时分析并入库

function analysisLogsTiming() {
 const cronTime = "0 0 3 * * *"; // 每天的 3:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => analysisLogsAndWriteDB(accessLogPath));
 console.log("定时分析日志并入库", cronTime);
}

定时删除

function rmLogsTiming() {
 const cronTime = "0 0 4 * * *"; // 每天的 4:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => rmLogs(accessLogPath));
 console.log("定时删除过期日志文件", cronTime);
}

然后在应用入口按序调用即可:

// 定时拆分日志文件
splitLogFileTiming();
// 定时分析日志并入库
analysisLogsTiming();
// 定时删除过期日志文件
rmLogsTiming();

总结

ok,到这里,一个简易的统计服务就完成了。

作者:前端森林
来源:https://segmentfault.com/a/1190000041184489

收起阅读 »

偷偷看了同事的代码找到了优雅代码的秘密

真正的大师永远怀着一颗学徒的心引言对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程...
继续阅读 »

真正的大师永远怀着一颗学徒的心

引言

对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程序猿在这个工程中写好代码的初始热情都没了。相反,如果clone下的代码结构清晰,代码优雅易懂,那么你在写代码的时候都不好意思写烂代码。这其中的差别相信工作过的同学都深有体会,那么我们看了那么多代码之后,到底什么样的代码才是好代码呢?它们有没有一些共同的特征或者原则?本文通过阐述优雅代码的设计原则来和大家聊聊怎么写好代码。

代码设计原则

好代码是设计出来的,也是重构出来的,更是不断迭代出来的。在我们接到需求,经过概要设计过后就要着手进行编码了。但是在实际编码之前,我们还需要进行领域分层设计以及代码结构设计。那么怎么样才能设计出来比较优雅的代码结构呢?有一些大神们总结出来的优雅代码的设计原则,我们分别来看下。

SRP

所谓SRP(Single Responsibility Principle)原则就是职责单一原则,从字面意思上面好像很好理解,一看就知道什么意思。但是看的会不一定就代表我们就会用,有的时候我们以为我们自己会了,但是在实际应用的时候又会遇到这样或者那样的问题。原因就是实际我们没有把问题想透,没有进行深度思考,知识还只是知识,并没有转化为我们的能力。就比如这里所说的职责单一原则指的是谁的单一职责,是类还是模块还是域呢?域可能包含多个模块,模块也可以包含多个类,这些都是问题。

为了方便进行说明,这里以类来进行职责单一设计原则的说明。对于一个类来说,如果它只负责完成一个职责或者功能,那么我们可以说这个类时符合单一职责原则。请大家回想一下,其实我们在实际的编码过程中,已经有意无意的在使用单一职责设计原则了。因为实际它是符合我们人思考问题的方式的。为什么这么说呢?想想我们在整理衣柜的时候,为了方便拿衣服我们会把夏天的衣服放在一个柜子中,冬天的衣服放在一个柜子。这样季节变化的时候,我们只要到对应的柜子直接拿衣服就可以了。否则如果冬天和夏天的衣服都放在一个柜子中,我们找衣服的时候可就费劲了。放到软件代码设计中,我们也需要采用这样的分类思维。在进行类设计的时候,要设计粒度小、功能单一的类,而不是大而全的类。

举个栗子,在学生管理系统中,如果一个类中既有学生信息的操作比如创建或者删除动作,又有关于课程的创建以及修改动作,那么我们可以认为这个类时不满足单一职责的设计原则的,因为它将两个不同业务域的业务混杂在了一起,所以我们需要进行拆分,将这个大而全的类拆分为学生以及课程两个业务域,这样粒度更细,更加内聚。

笔者根据自身的经验,总结了需要考虑进行单一职责拆分的几个场,希望对大家判断是否需要进行拆分有个简单的判断的标准: 1、不同的业务域需要进行拆分,就像上面的例子,另外如果与其他类的依赖过多,也需要考虑是不是应该进行拆分; 2、如果我们在类中编写代码的时候发现私有方法具有一定的通用性,比如判断ip是不是合法,解析xml等,那我们可以考虑将这些方法抽出来形成公共的工具类,这样其他类也可以方便的进行使用。 另外单一职责的设计思想不止在代码设计中使用,我们在进行微服务拆分的时候也会一定程度的遵循这个原则。

OCP

OCP(Open Closed Principle)即对修改关闭,对扩展开放原则,个人觉得这是设计原则中最难的原则。不仅理解起来有一定的门槛,在实际编码过程中也是不容易做到的。 首先我们得先搞清楚这里的所说的修改以及扩展的区别在什么地方,说实话一开始看到这个原则的时候,我总觉得修改和开放说的不是一个意思嘛?想来想去都觉得有点迷糊。后来在不断的项目实践中,对这个设计原则的理解逐渐加深了。

设计原则中所说的修改指的是对原有代码的修改,而扩展指的是在原有代码基础上的能力的扩展,并不修改原先已有的代码。这是修改与扩展的最大的区别,一个需要修改原来的代码逻辑,另一个不修改。因此才叫对修改关闭但是对扩展开放。弄清楚修改和扩展的区别之后,我们再想一想为什么要对修改关闭,而要对扩展开放呢? 我们都知道软件平台都是不断进行更新迭代的,因此我们需要不断在原先的代码中进行开发。那么就会涉及到一个问题如果我们的代码设计的不好,扩展性不强,那么每次进行功能迭代的时候都会修改原先已有的代码,有修改就有可能引入bug,造成系统平台的不稳定。因此我们为了平台的稳定性,需要对修改关闭。但是我们要添加新的功能怎么办呢?那就是通过扩展的方式来进行,因此需要实现对扩展开放。

这里我们以一个例子来进行说明,否则可能还是有点抽象。在一个监控平台中,我们需要对服务所占用CPU、内存等运行信息进行监控,第一版代码如下。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

代码逻辑很简单,就是根据对应的告警规则中的阈值判断是否达到触发告警通知的条件。如果此时来了个需求,需要增加判断的条件,就是根据服务对应的状态,判断需不需要进行告警通知。我们来先看下比较low的修改方法。我们在checkServiceStatus方法中增加了服务状态的参数,同事在方法中增加了判断状态的逻辑。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory, int status) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

很显然这种修改方法非常的不友好,为什么这么说呢?首先修改了方法参数,那么调用该方法的地方可能也需要修改,另外如果改方法有单元测试方法的话,单元测试用例必定也需要修改,在原有测试过的代码中添加新的逻辑,也增加了bug引入的风险。因此这种修改的方式我们需要进行避免。那么怎么修改才能够体现对修改关闭以及对扩展开放呢? 首先我们可以先将关于服务状态的属性抽象为一个ServiceStatus 实体,在对应的检查方法中以ServiceStatus 为入参,这样以后如果还有服务状态的属性增加的话,只需要在ServiceStatus 中添加即可,并不需要修改方法中的参数以及调用方法的地方,同样单元测试的方法也不用修改。

@Data
public class ServiceStatus {
   String serviecName;
   int cpu;
   int memory;
   int status;

}

另外在检测方法中,我们怎么修改才能体现可扩展呢?而不是在检测方法中添加处理逻辑。一个比较好的实现方式就是通过抽象检测方法,具体的实现在各个实现类中。这样即使新增检测逻辑,只需要扩展检测实现方法就可,不需要在修改原先代码的逻辑,实现代码的可扩展。

LSP

LSP(Liskov Substitution Principle)里氏替换原则,这个设计原则我觉得相较于前面的两个设计原则来说要简单些。它的内容为子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

我们怎么判断有没有违背LSP呢?我觉得有两个关键点可以作为判断的依据,一个是子类有没有改变父类申明需要实现的业务功能,另一个是否违反父类关于输入、输出以及异常抛出的规定。

ISP

ISP(Interface Segregation Principle)接口隔离原则,简单理解就是只给调用方需要的接口,它不需要的就不要硬塞给他了。这里我们举个栗子,以下是关于产品的接口,其中包含了创建产品、删除产品、根据ID获取产品以及更新产品的接口。如果此时我们需要对外提供一个根据产品的类别获取产品的接口,我们应该怎么办?很多同学会说,这还不简单,我们直接在这个接口里面添加根据类别查询产品的接口就OK了啊。大家想想这个方案有没有什么问题。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public class UserServiceImpl implements UserService { //...}

这个方案看上去没什么问题,但是再往深处想一想,外部系统只需要一个根据产品类别查询商品的功能,,但是实际我们提供的接口中还包含了删除、更新商品的接口。如果这些接口被其他系统误调了可能会导致产品信息的删除或者误更新。因此我们可以将这些第三方调用的接口都隔离出来,这样就不存在误调用以及接口能力被无序扩散的情况了。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public interface ThirdSystemProductService{
   List<Product> getProductByType(int type);
}

public class UserServiceImpl implements UserService { //...}

LOD

LOD(Law of Demeter)即迪米特法则,这是我们要介绍的最后一个代码设计法则了,光从名字上面上看,有点不明觉厉的感觉,看不出来到底到底表达个什么意思。我们可以来看下原文是怎么描述这个设计原则的。 Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers. 按照我自己的理解,这迪米特设计原则的最核心思想或者说最想达到的目的就是尽最大能力减小代码修改带来的对原有的系统的影响。所以需要实现类、模块或者服务能够实现高内聚、低耦合。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。打个比方这就像抗战时期的的地下组织一样,相关联的聚合到一起,但是与外部保持尽可能少的联系,也就是低耦合。

总结

本文总结了软件代码设计中的五大原则,按照我自己的理解,这五大原则就是程序猿代码设计的内功,而二十三种设计模式实际就是内功催生出来的编程招式,因此深入理解五大设计原则是我们用好设计模式的基础,也是我们在平时设计代码结构的时候需要遵循的一些常见规范。只有不断的在设计代码-》遵循规范-》编写代码-》重构这个循环中磨砺,我们才能编写出优雅的代码。

作者:慕枫技术笔记
来源:https://juejin.cn/post/7046404022143549447

收起阅读 »

二维码扫码登录是什么原理

前几天看了极客时间一个二维码的视频,写的不错,这里总结下在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码...
继续阅读 »

前几天看了极客时间一个二维码的视频,写的不错,这里总结下

在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码,防止上当受骗。

二维码,大家再熟悉不过了

购物扫个码,吃饭扫个码,坐公交也扫个码

在扫码的过程中,大家可能会有疑问:这二维码安全吗?会不会泄漏我的个人信息?更深度的用户还会考虑:我的系统是不是也可以搞一个二维码来推广呢?

这时候就需要了解一下二维码背后的技术和逻辑了!

二维码最常用的场景之一就是通过手机端应用扫描PC或者WEB端的二维码,来登录同一个系统。 比如手机微信扫码登录PC端微信,手机淘宝扫码登录PC端淘宝。 那么就让我们来看一下,二维码登录是怎么操作的!

二维码登录的本质

二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情!

  1. 告诉系统我是谁

  2. 向系统证明我是谁

比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁; 比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;

那么扫码登录是怎么做到这两件事情的呢?我们一起来考虑一下

手机端应用扫PC端二维码,手机端确认后,账号就在PC端登录成功了!这里,PC端登录的账号肯定与手机端是同一个账号。不可能手机端登录的是账号A,而扫码登录以后,PC端登录的是账号B。

所以,第一件事情,告诉系统我是谁,是比较清楚的!

通过扫描二维码,把手机端的账号信息传递到PC端,至于是怎么传的,我们后面再说

第二件事情,向系统证明我是谁。扫码登录过程中,用户并没有去输入密码,也没有输入验证码,或者其他什么码。那是怎么证明的呢?

有些同学会想到,是不是扫码过程中,把密码传到了PC端呢? 但这是不可能的。因为那样太不安全的,客户端也根本不会去存储密码。我们仔细想一下,其实手机端APP它是已经登录过的,就是说手机端是已经通过登录认证。所说只要扫码确认是这个手机且是这个账号操作的,其实就能间接证明我谁。

认识二维码

那么如何做确认呢?我们后面会详细说明,在这之前我们需要先认识一下二维码! 在认识二维码之前我们先看一下一维码!

所谓一维码,也就是条形码,超市里的条形码--这个相信大家都非常熟悉,条形码实际上就是一串数字,它上面存储了商品的序列号。

二维码其实与条形码类似,只不过它存储的不一定是数字,还可以是任何的字符串,你可以认为,它就是字符串的另外一种表现形式,

在搜索引擎中搜索二维码,你可以找到很多在线生成二维码的工具网站,这些网站可以提供字符串与二维码之间相互转换的功能,比如 草料二维码网站

在左边的输入框就可以输入你的内容,它可以是文本、网址,文件........。然后就可以生成代表它们的二维码

你也可以把二维码上传,进行”解码“,然后就可以解析出二维码代表的含义

系统认证机制

认识了二维码,我们了解一下移动互联网下的系统认证机制。

前面我们说过,为了安全,手机端它是不会存储你的登录密码的。 但是在日常使用过程中,我们应该会注意到,只有在你的应用下载下来后,第一次登录的时候,才需要进行一个账号密码的登录, 那之后呢 即使这个应用进程被杀掉,或者手机重启,都是不需要再次输入账号密码的,它可以自动登录。

其实这背后就是一套基于token的认证机制,我们来看一下这套机制是怎么运行的,

  1. 账号密码登录时,客户端会将设备信息一起传递给服务端,

  2. 如果账号密码校验通过,服务端会把账号与设备进行一个绑定,存在一个数据结构中,这个数据结构中包含了账号ID,设备ID,设备类型等等

const token = {
 acountid:'账号ID',
 deviceid:'登录的设备ID',
 deviceType:'设备类型,如 iso,android,pc......',
}

然后服务端会生成一个token,用它来映射数据结构,这个token其实就是一串有着特殊意义的字符串,它的意义就在于,通过它可以找到对应的账号与设备信息,

  1. 客户端得到这个token后,需要进行一个本地保存,每次访问系统API都携带上token与设备信息。

  2. 服务端就可以通过token找到与它绑定的账号与设备信息,然后把绑定的设备信息与客户端每次传来的设备信息进行比较, 如果相同,那么校验通过,返回AP接口响应数据, 如果不同,那就是校验不通过拒绝访问

从前面这个流程,我们可以看到,客户端不会也没必要保存你的密码,相反,它是保存了token。可能有些同学会想,这个token这么重要,万一被别人知道了怎么办。实际上,知道了也没有影响, 因为设备信息是唯一的,只要你的设备信息别人不知道, 别人拿其他设备来访问,验证也是不通过的。

可以说,客户端登录的目的,就是获得属于自己的token。

那么在扫码登录过程中,PC端是怎么获得属于自己的token呢?不可能手机端直接把自己的token给PC端用!token只能属于某个客户端私有,其他人或者是其他客户端是用不了的。在分析这个问题之前,我们有必要先梳理一下,扫描二维码登录的一般步骤是什么样的。这可以帮助我们梳理清楚整个过程,

扫描二维码登录的一般步骤

大概流程

  1. 扫码前,手机端应用是已登录状态,PC端显示一个二维码,等待扫描

  2. 手机端打开应用,扫描PC端的二维码,扫描后,会提示"已扫描,请在手机端点击确认"

  3. 用户在手机端点击确认,确认后PC端登录就成功了

可以看到,二维码在中间有三个状态, 待扫描,已扫描待确认,已确认。 那么可以想象

  1. 二维码的背后它一定存在一个唯一性的ID,当二维码生成时,这个ID也一起生成,并且绑定了PC端的设备信息

  2. 手机去扫描这个二维码

  3. 二维码切换为 已扫描待确认状态, 此时就会将账号信息与这个ID绑定

  4. 当手机端确认登录时,它就会生成PC端用于登录的token,并返回给PC端

好了,到这里,基本思路就已经清晰了,接下来我们把整个过程再具体化一下

二维码准备

按二维码不同状态来看, 首先是等待扫描状态,用户打开PC端,切换到二维码登录界面时。

  1. PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端

  2. 服务端收到请求后,它生成二维码ID,并将二维码ID与PC端设备信息进行绑定

  3. 然后把二维码ID返回给PC端

  4. PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID)

  5. 为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息

二维码已经准好了,接下来就是扫描状态

扫描状态切换

  1. 用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID

  2. 再调用服务端API将移动端的身份信息与二维码ID一起发送给服务端

  3. 服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成临时token。然后返回给手机端

  4. 因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描

那么为什么需要返回给手机端一个临时token呢?临时token与token一样,它也是一种身份凭证,不同的地方在于它只能用一次,用过就失效。

在第三步骤中返回临时token,为的就是手机端在下一步操作时,可以用它作为凭证。以此确保扫码,登录两步操作是同一部手机端发出的,

状态确认

最后就是状态的确认了。

  1. 手机端在接收到临时token后会弹出确认登录界面,用户点击确认时,手机端携带临时token用来调用服务端的接口,告诉服务端,我已经确认

  2. 服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成用户PC端登录的token

  3. 这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到用户登录的token

  4. 到这里,登录就成功了,后端PC端就可以用token去访问服务端的资源了

扫码动作的基础流程都讲完了,有些细节还没有深入介绍,

比如二维码的内容是什么?

  • 可以是二维码ID

  • 可以是包含二维码ID的一个url地址

在扫码确认这一步,用户取消了怎么处理? 这些细节都留给大家思考

总结

我们从登陆的本质触发,探索二维码扫码登录是如何做到的

  1. 告诉系统我是谁

  2. 向系统证明我谁

在这个过程中,我们先简单讲了两个前提知识,

  • 一个是二维码原理,

  • 一个是基于token的认证机制。

然后我们以二维码状态为轴,分析了这背后的逻辑: 通过token认证机制与二维码状态变化来实现扫码登录.

需要指出的是,前面的讲的登录流程,它适用于同一个系统的PC端,WEB端,移动端。

平时我们还有另外一种场景也比较常见,那就是通过第三方应用来扫码登录,比如极客时间/掘金 都可以选择微信/QQ等扫码登录,那么这种通过第三方应用扫码登录又是什么原理呢?

感兴趣的同学可以思考研究一下,欢迎在评论区留下你的见解。


作者:望道同学
来源:https://juejin.cn/post/6940976355097985032

收起阅读 »

Nginx 配置在线一键生成“神器”

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。网址:https://nginxconfig.io/操...
继续阅读 »

Nginx作为一个轻量级的HTTP服务器,相比Apache优势也是比较明显的,在性能上它占用资源少,能支持更高更多的并发连接,从而达到提高访问效率;在功能上它是一款非常优秀的代理服务器与负载均衡服务器;在安装配置上它安装,配置都比较简单。

但在实际的生产配置环境中,肯定会经常遇到需要修改、或者重新增加Nginx配置的问题,有的时候需求更是多种多样,修修改改经常会出现这样、那样的一些错误,特别的烦索。

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。

网址:https://nginxconfig.io/

NGINX Config 支持 HTTP、HTTPS、PHP、Python、Node.js、WordPress、Drupal、缓存、逆向代理、日志等各种配置选项。在线生成 Web 服务器 Nginx 配置文件。

操作配置也非常简单,你需要做的只需要2步:

  • 打开官方网站

  • 按需求配置相关参数

系统就会自动生成特定的配置文件。虽然界面是英文的,但是功能的页面做的非常直观,生成的Nginx格式规范。

案例展示

配置域名:mingongge.com 实现用户访问*.mingongge.com 域名时会自动跳转到 mingongge.com 此配置,并且开启http强制跳转到https的配置。

这时,Nginx的配置就会实时自动生成在下面,我把生成的配置复制过来,如下:

/etc/nginx/sites-available/mingongge.com.conf#文件名都给你按规则配置好了
server {
listen 443 ssl http2;

server_name mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

# security
include nginxconfig.io/security.conf;

# additional config
include nginxconfig.io/general.conf;
}

# subdomains redirect
server {
listen 443 ssl http2;

server_name *.mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

return 301 https://mingongge.com$request_uri;
}

# HTTP redirect
server {
listen 80;

server_name .mingongge.com;

include nginxconfig.io/letsencrypt.conf;

location / {
return 301 https://mingongge.com$request_uri;
}
}

非常的方便与快速。

官方还提供一些Nginx的基础优化配置,如下:

/etc/nginx/nginx.conf
# Generated by nginxconfig.io

user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
multi_accept on;
worker_connections 65535;
}

http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
client_max_body_size 16M;

# MIME
include mime.types;
default_type application/octet-stream;

# logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;

# load configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

还有基于安全的配置,如下:

/etc/nginx/nginxconfig.io/security.conf
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;

# . files
location ~ /\.(?!well-known) {
deny all;
}

都相当于是提供一些基础的模板配置,可以根据自己的实际需求去修改。

有了这个神器在手,再也不用为配置Nginx的各类配置而烦恼了!!

作者:民工哥

来源:https://www.bbsmax.com/A/kjdw09OwzN/

收起阅读 »

求解“微信群覆盖”的三种方法:暴力,染色,链表,并查集

(1) 题目简介;(3) 思路二:染色法;(5) 思路四:并查集法;(1) 每个微信群由一个唯一的gid标识;(3) 一个用户可以加入多个群;g1{u1, u2, u3}可以看到,用户u1加入了g1与g2两个群。gid和uid都是uint64;(1) ...
继续阅读 »



这是一篇聊算法的文章,从一个小面试题开始,扩展到一系列基础算法,包含几个部分:

(1) 题目简介;

(2) 思路一:暴力法;

(3) 思路二:染色法;

(4) 思路三:链表法;

(5) 思路四:并查集法;

除了聊方案,重点分享思考过程。文章较长,可提前收藏。


第一部分:题目简介


问题提出:求微信群覆盖


微信有很多群,现进行如下抽象:

(1) 每个微信群由一个唯一的gid标识;

(2) 微信群内每个用户由一个唯一的uid标识;

(3) 一个用户可以加入多个群;

(4) 群可以抽象成一个由不重复uid组成的集合,例如:

g1{u1, u2, u3}

g2{u1, u4, u5}

可以看到,用户u1加入了g1与g2两个群。

画外音:

gid和uid都是uint64;

集合内没有重复元素;


假设微信有M个群(M为亿级别),每个群内平均有N个用户(N为十级别).


现在要进行如下操作:

(1) 如果两个微信群中有相同的用户则将两个微信群合并,并生成一个新微信群;

例如,上面的g1和g2就会合并成新的群:

g3{u1, u2, u3, u4, u5};

画外音:集合g1中包含u1,集合g2中包含u1,合并后的微信群g3也只包含一个u1。

(2) 不断的进行上述操作,直到剩下所有的微信群都不含相同的用户为止

将上述操作称:求群的覆盖。


设计算法,求群的覆盖,并说明算法时间与空间复杂度。

画外音:你遇到过类似的面试题吗?


对于一个复杂的问题,思路肯定是“先解决,再优化”,大部分人不是神,很难一步到位。先用一种比较“笨”的方法解决,再看“笨方法”有什么痛点,优化各个痛点,不断升级方案。


第二部分:暴力法


拿到这个问题,很容易想到的思路是:

(1) 先初始化M个集合,用集合来表示微信群gid与用户uid的关系;

(2) 找到哪两个(哪些)集合需要合并;

(3) 接着,进行集合的合并;

(4) 迭代步骤二和步骤三,直至所有集合都没有相同元素,算法结束;


第一步,如何初始化集合?

set这种数据结构,大家用得很多,来表示集合:

(1) 新建M个set来表示M个微信群gid;

(2) 每个set插入N个元素来表示微信群中的用户uid;


set有两种最常见的实现方式,一种是树型set,一种是哈希型set


假设有集合:

s={7, 2, 0, 14, 4, 12}


树型set的实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(lg(n));

(2) 能实现有序查找;

(3) 省空间;


哈希型set实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(1);

(2) 不能实现有序查找;

画外音:求群覆盖,哈希型实现的初始化更快,复杂度是O(M*N)。


第二步,如何判断两个(多个)集合要不要合并?

集合对set(i)和set(j),判断里面有没有重复元素,如果有,就需要合并,判重的伪代码是:

// 对set(i)和set(j)进行元素判断并合并

(1)    foreach (element in set(i))

(2)    if (element in set(j))

         merge(set(i), set(j));


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)判断element是否在第二个集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第三步,如何合并集合?

集合对set(i)和set(j)如果需要合并,只要把一个集合中的元素插入到另一个集合中即可:

// 对set(i)和set(j)进行集合合并

merge(set(i), set(j)){

(1)    foreach (element in set(i))

(2)    set(j).insert(element);

}


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)把element插入到集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第四步:迭代第二步与第三步,直至结束

对于M个集合,暴力针对所有集合对,进行重复元素判断并合并,用两个for循环可以暴力解决:

(1)for(i = 1 to M)

(2)    for(j= i+1 to M)

         //对set(i)和set(j)进行元素判断并合并

         foreach (element in set(i))

         if (element in set(j))

         merge(set(i), set(j));


递归调用,两个for循环,复杂度是O(M*M)。


综上,如果这么解决群覆盖的问题,时间复杂度至少是:

O(M*N) // 集合初始化的过程

+

O(M*M) // 两重for循环递归

*

O(N) // 判重

*

O(N) // 合并

画外音:实际复杂度要高于这个,随着集合的合并,集合元素会越来越多,判重和合并的成本会越来越高。


第三部分:染色法


总的来说,暴力法效率非常低,集合都是一个一个合并的,同一个元素在合并的过程中要遍历很多次。很容易想到一个优化点,能不能一次合并多个集合?


暴力法中,判断两个集合set和set是否需要合并,思路是:遍历set中的所有element,看在set中是否存在,如果存在,说明存在交集,则需要合并。


哪些集合能够一次性合并?

当某些集合中包含同一个元素时,可以一次性合并。


怎么一次性发现,哪些集合包含同一个元素,并合并去重呢?


回顾一下工作中的类似需求:

M个文件,每个文件包含N个用户名,或者N个手机号,如何合并去重?

最常见的玩法是:

cat file_1 file_2 … file_M | sort | uniq > result


这里的思路是什么?

(1) 把M*N个用户名/手机号输出;

(2) sort排序,排序之后相同的元素会相邻

(3) uniq去重,相邻元素如果相同只保留一个;


排序之后相同的元素会相邻”,就是一次性找出所有可合并集合的关键,这是染色法的核心。


举一个栗子

假设有6个微信群,每个微信群有若干个用户:

s1={1,0,5} s2={3,1} s3={2,9}

s4={4,6} s5={4,7} s6={1,8}

假设使用树形set来表示集合。

首先,给同一个集合中的所有元素染上相同的颜色,表示来自同一个集合。

然后,对所有的元素进行排序,会发现:

(1) 相同的元素一定相邻,并且一定来自不同的集合;

(2) 同一个颜色的元素被打散了;

这些相邻且相同的元素,来自哪一个集合,这些集合就是需要合并的,如上图:

(1) 粉色的1来自集合s1,紫色的1来自集合s2,黄色的1来自集合s6,所以s1s2s6需要合并;

(2) 蓝色的4来自集合s4,青色的4来自集合s5,所以s4s5需要合并;


不用像暴力法遍历所有的集合对,而是一个排序动作,就能找到所有需要合并的集合。

画外音:暴力法一次处理2个集合,染色法一次可以合并N个集合。

集合合并的过程,可以想象为,相同相邻元素所在集合,染成第一个元素的颜色

(1) 紫色和黄色,染成粉色;

(2) 青色,染成蓝色;


最终,剩余三种颜色,也就是三个集合:

s1={0,1,3,5,8}

s3={2,9}

s4={4,6,7}


神奇不神奇!!!


染色法有意思么?但仍有两个遗留问题

(1) 粉色1,紫色1,黄色1,三个元素如何找到这三个元素所在的集合s1s2s6呢?

(2) s1s2s6三个集合如何快速合并

画外音:假设总元素个数n=M*N,如果使用树形set,合并的复杂度为O(n*lg(n)),即O(M*N*lg(M*N))。


我们继续往下看。


第四部分:链表法


染色法遗留了两个问题:

步骤(2)中,如何通过元素快速定位集合

步骤(3)中,如何快速合并集合

我们继续聊聊这两个问题的优化思路。


问题一:如何由元素快速定位集合?

普通的集合,只能由集合根(root)定位元素,不能由元素逆向定位root,如何支持元素逆向定位root呢?

很容易想到,每个节点增加一个父指针即可。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* parent;    // 指向父节点

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向它的上级,而只有root->parent=NULL。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         element* temp=x;

         while(temp->parent != NULL){

                   temp= temp->parent;

         }

         return temp;

}


很容易发现,由元素找集合根的时间复杂度是树的高度,即O(lg(n))


有没有更快的方法呢?

进一步思考,为什么每个节点要指向父节点,直接指向根节点是不是也可以。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* root;         // 指向集合根

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向集合的根。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         return x->root;

}


很容易发现,升级后,由元素找集合根的时间复杂度是O(1)

画外音:不能更快了吧。


另外,这种方式,能在O(1)的时间内,判断两个元素是否在同一个集合内

bool in_the_same_set(element* a, element* b){

         return (a->root == b->root);

}

甚为方便。

画外音:两个元素的根相同,就在同一个集合内。


问题二:如何快速进行集合合并? 

暴力法中提到过,集合合并的伪代码为:

merge(set(i), set(j)){

         foreach(element in set(i))

                   set(j).insert(element);

}

把一个集合中的元素插入到另一个集合中即可。


假设set(i)的元素个数为n1,set(j)的元素个数为n2,其时间复杂度为O(n1*lg(n2))。


在“微信群覆盖”这个业务场景下,随着集合的不断合并,集合高度越来越高,合并会越来越慢,有没有更快的集合合并方式呢?


仔细回顾一下:

(1) 树形set的优点是,支持有序查找,省空间;

(2) 哈希型set的优点是,快速插入与查找;


而“微信群覆盖”场景对集合的频繁操作是:

(1) 由元素找集合根;

(2) 集合合并;


那么,为什么要用树形结构或者哈希型结构来表示集合呢?

画外音:优点完全没有利用上嘛。


让我们来看看,这个场景中,如果用链表来表示集合会怎么样,合并会不会更快?

s1={7,3,1,4}

s2={1,6}

如上图,分别用链表来表示这两个集合。可以看到,为了满足“快速由元素定位集合根”的需求,每个元素仍然会指向根。


s1和s2如果要合并,需要做两件事:

(1) 集合1的尾巴,链向集合2的头(蓝线1);

(2) 集合2的所有元素,指向集合1的根(蓝线2,3);


合并完的效果是:

变成了一个更大的集合。


假设set(1)的元素个数为n1,set(2)的元素个数为n2,整个合并的过程的时间复杂度是O(n2)。

画外音:时间耗在set(2)中的元素变化。


咦,我们发现:

(1) 将短的链表,接到长的链表上;

(2) 将长的链表,接到短的链表上;

所使用的时间是不一样的。


为了让时间更快,一律使用更快的方式:“元素少的链表”主动接入到“元素多的链表”的尾巴后面。这样,改变的元素个数能更少一些,这个优化被称作“加权合并”。


对于M个微信群,平均每个微信群N个用户的场景,用链表的方式表示集合,按照“加权合并”的方式合并集合,最坏的情况下,时间复杂度是O(M*N)。

画外音:假设所有的集合都要合并,共M次,每次都要改变N个元素的根指向,故为O(M*N)。


于是,对于“M个群,每个群N个用户,微信群求覆盖”问题,使用“染色法”加上“链表法”,核心思路三步骤:

(1) 全部元素全局排序

(2) 全局排序后,不同集合中的相同元素,一定是相邻的,通过相同相邻的元素,一次性找到所有需要合并的集合

(3) 合并这些集合,算法完成;


其中:

步骤(1),全局排序,时间复杂度O(M*N);

步骤(2),染色思路,能够迅猛定位哪些集合需要合并,每个元素增加一个属性指向集合根,实现O(1)级别的元素定位集合;

步骤(3),使用链表表示集合,使用加权合并的方式来合并集合,合并的时间复杂度也是O(M*N);


总时间复杂度是:

O(M*N)    //排序

+

O(1)        //由元素找到需要合并的集合

*

O(M*N)    //集合合并


神奇不神奇!


神奇不止一种,还有其他方法吗?我们接着往下看。


第五部分:并查集法


分离集合(disjoint set)是一种经典的数据结构,它有三类操作:

Make-set(a):生成一个只有一个元素a的集合;

Union(X, Y):合并两个集合X和Y;

Find-set(a):查找元素a所在集合,即通过元素找集合;


这种数据结构特别适合用来解决这类集合合并与查找的问题,又称为并查集


能不能利用并查集来解决求“微信群覆盖”问题呢?


一、并查集的链表实现


链表法里基本聊过,为了保证知识的系统性,这里再稍微回顾一下。

如上图,并查集可以用链表来实现。


链表实现的并查集,Find-set(a)的时间复杂度是多少?

集合里的每个元素,都指向“集合的句柄”,这样可以使得“查找元素a所在集合S”,即Find-set(a)操作在O(1)的时间内完成


链表实现的并查集,Union(X, Y)的时间复杂度是多少?

假设有集合:

S1={7,3,1,4}

S2={1,6}


合并S1和S2两个集合,需要做两件事情:

(1) 第一个集合的尾元素,链向第二个集合的头元素(蓝线1);

(2) 第二个集合的所有元素,指向第一个集合的句柄(蓝线2,3);


合并完的效果是:

变成了一个更大的集合S1。


集合合并时,将短的链表,往长的链表上接,这样变动的元素更少,这个优化叫做“加权合并”。

画外音:实现的过程中,集合句柄要存储元素个数,头元素,尾元素等属性,以方便上述操作进行。


假设每个集合的平均元素个数是nUnion(X, Y)操作的时间复杂度是O(n)


能不能Find-set(a)与Union(X, Y)都在O(1)的时间内完成呢?

可以,这就引发了并查集的第二种实现方法。


二、并查集的有根树实现


什么是有根树,和普通的树有什么不同?

常用的set,就是用普通的二叉树实现的,其元素的数据结构是:

element{

         int data;

         element* left;

         element* right;

}

通过左指针与右指针,父亲节点指向儿子节点。


而有根树,其元素的数据结构是:

element{

         int data;

         element* parent;

}

通过儿子节点,指向父亲节点。


假设有集合:

S1={7,3,1,4}

S2={1,6}


通过如果通过有根树表示,可能是这样的:

所有的元素,都通过parent指针指向集合句柄,所有元素的Find-set(a)的时间复杂度也是O(1)。

画外音:假设集合的首个元素,代表集合句柄。


有根树实现的并查集,Union(X, Y)的过程如何?时间复杂度是多少?

通过有根树实现并查集,集合合并时,直接将一个集合句柄,指向另一个集合即可。

如上图所示,S2的句柄,指向S1的句柄,集合合并完成:S2消亡,S1变为了更大的集合。


容易知道,集合合并的时间复杂度为O(1)


会发现,集合合并之后,有根树的高度变高了,与“加权合并”的优化思路类似,总是把节点数少的有根树,指向节点数多的有根树(更确切的说,是高度矮的树,指向高度高的树),这个优化叫做“按秩合并”。


新的问题来了,集合合并之后,不是所有元素的Find-set(a)操作都是O(1)了,怎么办?

如图S1与S2合并后的新S1,首次“通过元素6来找新S1的句柄”,不能在O(1)的时间内完成了,需要两次操作。


但为了让未来“通过元素6来找新S1的句柄”的操作能够在O(1)的时间内完成,在首次进行Find-set(“6”)时,就要将元素6“寻根”路径上的所有元素,都指向集合句柄,如下图。

某个元素如果不直接指向集合句柄,首次Find-set(a)操作的过程中,会将该路径上的所有元素都直接指向句柄,这个优化叫做“路径压缩”。

画外音:路径上的元素第二次执行Find-set(a)时,时间复杂度就是O(1)了。


实施“路径压缩”优化之后,Find-set的平均时间复杂度仍是O(1)


稍微总结一下。


通过链表实现并查集:

(1) Find-set的时间复杂度,是O(1)常数时间;

(2) Union的时间复杂度,是集合平均元素个数,即线性时间;

画外音:别忘了“加权合并”优化。


通过有根树实现并查集:

(1) Union的时间复杂度,是O(1)常数时间;

(2) Find-set的时间复杂度,通过“按秩合并”与“路径压缩”优化后,平均时间复杂度也是O(1);


即,使用并查集,非常适合解决“微信群覆盖”问题。


知其然,知其所以然,思路往往比结果更重要

算法,其实还是挺有意思的。

作者:58沈剑
来源:https://mp.weixin.qq.com/s/2MNL4vDpXQR94KGts4JUhA

收起阅读 »

一张图看懂开源许可协议,开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别 以下是上述协议的简单介绍:BSD开源协议BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由...
继续阅读 »

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别



以下是上述协议的简单介绍:
BSD开源协议
BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。

但”为所欲为”的前提当你发布使用了BSD协议的代码,或则以BSD协议代码为基础做二次开发自己的产品时,需要满足三个条件:

    如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。
    如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。
    不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

BSD 代码鼓励代码共享,但需要尊重代码作者的著作权。BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。

Apache Licence 2.0
Apache Licence是著名的非盈利开源组织Apache采用的协议。该协议和BSD类似,同样鼓励代码共享和尊重原作者的著作权,同样允许代码修改,再发布(作为开源或商业软件)。需要满足的条件也和BSD类似:

    需要给代码的用户一份Apache Licence
    如果你修改了代码,需要再被修改的文件中说明。
    在延伸的代码中(修改和有源代码衍生的代码中)需要带有原来代码中的协议,商标,专利声明和其他原来作者规定需要包含的说明。
    如果再发布的产品中包含一个Notice文件,则在Notice文件中需要带有Apache Licence。你可以在Notice中增加自己的许可,但不可以表现为对Apache Licence构成更改。

Apache Licence也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布/销售。
GPL

我们很熟悉的Linux就是采用了GPL。GPL协议和BSD, Apache Licence等鼓励代码重用的许可很不一样。GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。这也就是为什么我们能用免费的各种linux,包括商业公司的linux和linux上各种各样的由个人,组织,以及商业软件公司开发的免费软件了。

GPL协议的主要内容是只要在一个软件中使用(”使用”指类库引用,修改后的代码或者衍生代码)GPL 协议的产品,则该软件产品必须也采用GPL协议,既必须也是开源和免费。这就是所谓的”传染性”。GPL协议的产品作为一个单独的产品使用没有任何问题,还可以享受免费的优势。

由于GPL严格要求使用了GPL类库的软件产品必须使用GPL协议,对于使用GPL协议的开源代码,商业软件或者对代码有保密要求的部门就不适合集成/采用作为类库和二次开发的基础。

其它细节如再发布的时候需要伴随GPL协议等和BSD/Apache等类似。

LGPL
LGPL是GPL的一个为主要为类库使用设计的开源协议。和GPL要求任何使用/修改/衍生之GPL类库的的软件必须采用GPL协议不同。LGPL 允许商业软件通过类库引用(link)方式使用LGPL类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。

但是如果修改LGPL协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用LGPL协议。因此LGPL协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。

GPL/LGPL都保障原作者的知识产权,避免有人利用开源代码复制并开发类似的产品

MIT
MIT是和BSD一样宽范的许可协议,作者只想保留版权,而无任何其他了限制.也就是说,你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码发布的.

MPL
MPL是The Mozilla Public License的简写,是1998年初Netscape的 Mozilla小组为其开源软件项目设计的软件许可证。MPL许可证出现的最重要原因就是,Netscape公司认为GPL许可证没有很好地平衡开发者对源代码的需求和他们利用源代码获得的利益。同著名的GPL许可证和BSD许可证相比,MPL在许多权利与义务的约定方面与它们相同(因为都是符合OSIA 认定的开源软件许可证)。但是,相比而言MPL还有以下几个显著的不同之处:

◆ MPL虽然要求对于经MPL许可证发布的源代码的修改也要以MPL许可证的方式再许可出来,以保证其他人可以在MPL的条款下共享源代码。但是,在MPL 许可证中对“发布”的定义是“以源代码方式发布的文件”,这就意味着MPL允许一个企业在自己已有的源代码库上加一个接口,除了接口程序的源代码以MPL 许可证的形式对外许可外,源代码库中的源代码就可以不用MPL许可证的方式强制对外许可。这些,就为借鉴别人的源代码用做自己商业软件开发的行为留了一个豁口。
◆ MPL许可证第三条第7款中允许被许可人将经过MPL许可证获得的源代码同自己其他类型的代码混合得到自己的软件程序。
◆ 对软件专利的态度,MPL许可证不像GPL许可证那样明确表示反对软件专利,但是却明确要求源代码的提供者不能提供已经受专利保护的源代码(除非他本人是专利权人,并书面向公众免费许可这些源代码),也不能在将这些源代码以开放源代码许可证形式许可后再去申请与这些源代码有关的专利。
◆ 对源代码的定义
而在MPL(1.1版本)许可证中,对源代码的定义是:“源代码指的是对作品进行修改最优先择取的形式,它包括:所有模块的所有源程序,加上有关的接口的定义,加上控制可执行作品的安装和编译的‘原本’(原文为‘Script’),或者不是与初始源代码显著不同的源代码就是被源代码贡献者选择的从公共领域可以得到的程序代码。”
◆ MPL许可证第3条有专门的一款是关于对源代码修改进行描述的规定,就是要求所有再发布者都得有一个专门的文件就对源代码程序修改的时间和修改的方式有描述。

作者:微wx笑
翻译:https://blog.csdn.net/testcs_dn/article/details/38496107
原文:http://www.mozilla.org/MPL/MPL-1.1.html

收起阅读 »

拒绝白嫖,开源项目作者删库跑路,数千个应用程序无限输出乱码

「我删我自己的开源项目代码,需要经过别人允许吗?」几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「co...
继续阅读 »



「我删我自己的开源项目代码,需要经过别人允许吗?」

几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。

更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「colors.js」的作者 Marak Squires 本人。

一夜之间,Marak Squires 主动删除了「faker.js」和「colors.js」项目仓库的所有代码,让正在使用这两个开源项目的数千位开发者直接崩溃。

「faker.js」和「colors.js」

faker.js 在 npm 上的每周下载量接近 250 万,color.js 每周的下载量约为 2240 万,本次删库的影响是极其严重的,使用这两个项目开发的工具包括 AWS CDK 等。

如果在构建和测试应用时,真实的数据量远远不够,那么 Faker 类工具将帮助开发者生成伪数据。faker.js 就是可为多个领域生成伪数据的 Node.js 库,包括地址、商业、公司、日期、财务、图像、随机数、名称等。

faker.js 支持生成英文、中文等多语种信息,包含丰富的 API,此前版本通常一个月迭代更新一次。faker.js 不仅可以使用在服务器端的 JavaScript,还可以应用在浏览器端的 JavaScript。

现在,faker.js 项目的所有 commit 信息都被改为「endgame」,在 README 中,作者写下这样一句话:「What really happened with Aaron Swartz?」

Swartz 是一位杰出的开发人员,帮助建立了 Creative Commons、RSS 和 Reddit。2011 年,Swartz 被指控从学术数据库 JSTOR 中窃取文件,目的是免费访问这些文件。Swartz 在 2013 年自杀,Squires 提到 Swartz 可能意指围绕这一死亡疑云。

Marak Squires 向 colors.js 提交了恶意代码,添加了一个「a new American flag module」,然后将其发布到了 GitHub 和 npm。

随后他在 GitHub 和 npm 发布了 faker.js 6.6.6,这两个动作引发了同样的破坏性事件。破坏后的版本导致应用程序无限输出奇怪的字母和符号,从三行写着「LIBERTY LIBERTY LIBERTY」的文本开始,后面跟着一系列非 ASCII 字符:

目前,color.js 已经更新了一个可以使用的版本。faker.js 项目尚未恢复,开发者只能通过降级到此前的 5.5.3 版本来解决问题。

为了解决问题,Squires 在 GitHub 上还发布了更新以解决「zalgo 问题」,该问题是指损坏文件产生的故障文本。

「我们注意到在 v1.4.44-liberty-2 版本的 colors 中有一个 zalgo 错误,」Squires 以一种讽刺的语气写道。「我们现在正在努力解决这个问题,很快就会有解决方案。」

在将更新推送到 faker.js 两天后,Squires 发了一条推文,表示自己存储了数百个项目的 GitHub 账户已经被封。Squires 在 1 月 4 日发布了 faker.js 的最新 commit,在 1 月 6 日被封,直到 1 月 7 日推送了 colors.js 的「liberty」版本。然而,从 faker.js 和 colors.js 的更新日志来看,他的账户似乎被解封过。目前尚不清楚 Squires 的帐户是否再次被封。

至此,故事并没有就此结束。Squires 2020 年 11 月发在 GitHub 上的一篇帖子被挖出来,在帖子中他写道自己不再想做免费的工作了。「恕我直言,我不想再用我的免费工作来支持财富 500 强(和其他小型公司),以此为契机,向我发送一份六位数的年度合同,或者 fork 项目并让其他人参与其中。」

Squires 的大胆举动引起了人们对开源开发者的道德和财务困境的关注,这可能是 Marak Squires 行动的目标。大量网站、软件和应用程序依赖开源开发人员来创建基本工具和组件,而所有这些都是免费的,无偿开发人员经常不知疲倦地工作,努力修复其开源软件中的安全问题。

开发者们怎么看

软件工程师 Sergio Gómez 表示:「从 GitHub 删除自己的代码违反了他们的服务条款?WTF?这是绑架。我们需要开始分散托管免费软件源代码。」

「不知道发生了什么,但我将我所有的项目都托管在 GitLab 私有 instance 上,永远不要相信任何互联网服务提供商。」

有网友认为 faker.js 团队的反应有些夸张了,并说道:「没有人会用一个只生成一些虚假数据的包赚大钱。faker.js 的确为开发者生成伪数据节省了一些时间,但我们也可以让实习生编写类似程序来生成数据。这对企业来说并没有那么重要。」

甚至有人认为 Marak 这么做是一种冲动行为,不够理性,并和他之前「卖掉房子购买 NFT」的传闻联系起来,认为 Marak 需要学会控制自己的情绪:

这种说法很快带偏部分网友的看法,有人原本同情开源项目被「白嫖」,但现在已转向认为 Marak 是恶意删库,并指出:「停止维护他的项目或完全删除都是他的权利,但故意提交有害代码是不对的。」

当然,也有人为开源软件(FOSS)开发者的待遇鸣不平:「希望有相关的基金会位 FOSS 开发人员提供资金支持」,而软件的可靠性和稳定性也是至关重要的

有人表示:一些大公司确实不尊重开源项目的版权,滥用开源项目对于 FOSS 开发者来说是绝对不公平的。但 Marak 对 faker.js 的做法并不可取,不是正面例子,存在 Marak 的个人负面原因。

对此,你有什么看法?

作者:机器之心Pro

来源:https://www.163.com/dy/article/GTC8PE5M0511AQHO.html

收起阅读 »

崩溃的一天,西安一码通崩溃背后的技术问题

12月20号,算得上西安崩溃的一天。西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。足足瘫痪超过 15 个多小时!到了下午,新闻甚至提示:这是解决问题的方法吗?今天,我们就试着分析一下这个业务、以及对应的技术问题。西安一码通其它业务我们暂且不分析...
继续阅读 »

1.崩溃的一天

12月20号,算得上西安崩溃的一天。

12月19号新增病例21个,20号新增病例42个,并且有部分病例已经在社区内传播.

西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。

在这样严峻的情况下,作为防控最核心的系统:西安一码通竟然崩溃了,并且崩溃得是那么的彻底。

足足瘫痪超过 15 个多小时!

整整一天的时间呀,多少上班族被堵在地铁口,多少旅客被冻在半路上,进退不能...

到了下午,新闻甚至提示:

为了减轻系统压力,建议广大市民非必要不展码、亮码,在出现系统卡顿时,请耐心等待,尽量避免反复刷新,也感谢广大市民朋友们的理解配合。

这是解决问题的方法吗?

如果真的需要限流来防止系统崩溃,用技术手段来限流是不是会更简单一些,甚至前面加一个 nginx 就能解决的问题。

今天,我们就试着分析一下这个业务、以及对应的技术问题。

2.产品分析

西安一码通其它业务我们暂且不分析,那并不是重点,并且当天也没有完全崩溃,崩溃的仅有扫码功能。

其实这是一个非常典型的大量查询、少数更新的业务,闭着眼睛分析一下,可以说, 90% 以上的流量都是查询。

我们先来看看第一版的产品形态,扫码之后展示个人部分姓名和身份政信息,同时下面展示绿、黄、红码。

这是西安一码通最开始的样子,业务流程仅仅只需要一个请求,甚至一个查询的 SQL 就可以搞定。

到了后来,这个界面做了2次比较大的改版。

第一次改版新增了疫苗接种信息,加了一个边框;第二次改版新增了核酸检测信息,在最下方展示核酸检测时间、结果。

整个页面增加了2个查询业务,如果系统背后使用的是关系数据库,可能会多增加至少2个查询SQL。

基本上就是这样的一个需求,据统计西安有1300万人口,按照最大10%的市民同时扫码(我怀疑不会有这么多),也就是百万的并发量。

这样一个并发量的业务,在互联网公司很常见,甚至比这个复杂的场景也多了去了。

那怎么就崩了呢?

3.技术分析

在当天晚上的官方回复中,我们看到有这样一句话:

12月20日早7:40分左右,西安“一码通”用户访问量激增,每秒访问量达到以往峰值的10倍以上,造成网络拥塞,致使包括“一码通”在内的部分应用系统无法正常使用。“

一码通”后台监控第一时间报警,各24小时驻场通信、网络、政务云、安全和运维团队立即开展排查,平台应用系统和数据库运行正常,判断问题出现在网络接口侧。

根据上面的信息,数据库和平台系统都正常,是网络出现了问题。

我之前在文章《一次dns缓存引发的惨案》画过一张访问示意图,用这个图来和大家分析一下,网络出现问题的情况。

一般用户的请求,会先从域名开始,经过DNS服务器解析后拿到外网IP地址,经过外网IP访问防火墙和负载之后打到服务器,最后服务器响应后将结果返回到浏览器。

如果真的是网络出现问题,一般最常见的问题就是 DNS 解析错误,或者外网的宽带被打满了。

DNS解析错误一定不是本次的问题,不然可能不只是这一个功能出错了;外网的宽带被打满,直接增加带宽就行,不至于一天都没搞定。

如果真的是网络侧出现问题,一般也不需要改动业务,但实际上系统恢复的时候,大家都发现界面回到文章开头提到了第一个版本了。

也就是说系统“回滚”了。

界面少了接种信息和核酸检测信息的内容,并且在一码通的首页位置,新增加了一个核酸查询的页面。

所以,仅仅是网络接口侧出现问题吗?我这里有一点点的疑问。

4.个人分析

根据我以往的经验,这是一个很典型的系统过载现象,也就是说短期内请求量超过服务器响应。

说人话就是,外部请求量超过了系统的最大处理能力。

当然了,系统最大处理能力和系统架构息息相关,同样的服务器不同的架构,系统负载量差异极大。

应对这样的问题,解决起来无非有两个方案,一个是限流,另外一个就是扩容了。

限流就是把用户挡在外面,先处理能处理的请求;扩容就是加服务器、增加数据库承载能力。

上面提到官方让大家没事别刷一码通,也算是人工限流的一种方式;不过在技术体系上基本上不会这样做。

技术上的限流方案有很多,但最简单的就是前面挂一个 Nginx 配置一下就能用;复杂一点就是接入层自己写算法。

当然了限流不能真正的解决问题,只是负责把一部分请求挡在外面;真正解决问题还是需要扩容,满足所有用户。

但实际上,根据解决问题的处理和产品回滚的情况来看,一码通并没有第一时间做扩容,而是选择了回滚。

这说明,在系统架构设计上,没有充分考虑扩容的情况,所以并不能支持第一时间选择这个方案。

5.理想的方案?

上面说那么多也仅仅是个人推测,实际上可能他们会面临更多现实问题,比如工期紧张、老板控制预算等等...

话说回来,如果你是负责一码通公司的架构师,你会怎么设计整个技术方案呢?欢迎大家留言,这里说说我的想法。

第一步,读写分离、缓存。

至少把系统分为2大块,满足日常使用的读业务单独抽取出来,用于承接外部的最大流量。

单独抽出一个子系统负责业务的更新,比如接种信息的更新、核酸信息的变化、或者根据业务定时变更码的颜色。

同时针对用户大量的单查询,上缓存系统,优先读取缓存系统的信息,防止压垮后面的数据库。

第二步,分库分表、服务拆分。

其实用户和用户之间的单个查询是没有关系的,完全可以根据用户的属性做分库分表。

比如就用用户ID取模分64个表,甚至可以分成64个子系统来查询,在接口最前端将流量分发掉,减轻单个表或者服务压力。

上面分析没有及时扩容,可能就是没有做服务拆分,如果都是单个的业务子服务的话,遇到过载的问题很容易做扩容。

当然,如果条件合适的话,上微服务架构就更好了,有一套解决方案来处理类似的问题。

第三步,大数据系统、容灾。

如果在一个页面中展示很多信息,还有一个技术方案,就是通过异步的数据清洗,整合到 nosql 的一张大表中。

用户扫描查询等相关业务,直接走 nosql 数据库即可。

这样处理的好处是,哪怕更新业务完全挂了,也不会影响用户扫码查询,因为两套系统、数据库都是完全分开的。

使用异地双机房等形式部署服务,同时做好整体的容灾、备灾方案,避免出现极端情况,比如机房光缆挖断等。

还有很多细节上的优化,这里就不一一说明了,这里也只是我的一些想法,欢迎大家留言补充。

6.最后

不管怎么分析,这肯定是人祸而不是天灾。

系统在没有经过严格测试之下,就直接投入到生产,在强度稍微大一点的环境中就崩溃了。

比西安大的城市很多,比西安现在疫情还要严重的情况,其它城市也遇到过,怎么没有出现类似的问题?

西安做为一个科技大省,出现这样的问题真的不应该,特别是我看了这个小程序背后使用的域名地址之后。

有一种无力吐槽的感觉,虽然说这和程序使用没有关系,但是从细节真的可以看出一个技术团队的实力。

希望这次能够吸取教训,避免再次出现类似的问题!

作者:纯洁的微笑
出处:https://www.cnblogs.com/ityouknow/p/15719395.html

收起阅读 »

利用好 git bisect 这把利器,帮助你快速定位疑难 bug

Git
使用git bisect二分法定位问题的基本步骤:git bisect start [最近的出错的commitid] [较远的正确的commitid]测试相应的功能git bisect good 标记正确直到出现问题则 标记错误 git bisect bad提...
继续阅读 »

使用git bisect二分法定位问题的基本步骤:

  1. git bisect start [最近的出错的commitid] [较远的正确的commitid]

  2. 测试相应的功能

  3. git bisect good 标记正确

  4. 直到出现问题则 标记错误 git bisect bad

  5. 提示的commitid就是导致问题的那次提交

问题描述

我们以Vue DevUI组件库的一个bug举例子🌰

5d14c34b这一次commit,执行yarn build报错,报错信息如下:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

我可以确定的是上一次发版本(d577ce4)是可以build成功的。

git bisect 简介

git bisect命令使用二分搜索算法来查找提交历史中的哪一次提交引入了错误。它几乎能让你闭着眼睛快速定位任何源码导致的问题,非常实用。

你只需要告诉这个命令一个包含该bug的坏commit ID和一个引入该bug之前的好commit ID,这个命令会用二分法在这两个提交之间选择一个中间的commit ID,切换到那个commit ID的代码,然后询问你这是好的commit ID还是坏的commit ID,你告诉它是好还是坏,然后它会不断缩小范围,直到找到那次引入bug的凶手commit ID

这样我们就只需要分析那一次提交的代码,就能快速定位和解决这个bug(具体定位的时间取决于该次提交的代码量和你的经验),所以我们提交代码时一定要养成小批量提交的习惯,每次只提交一个小的独立功能,这样出问题了,定位起来会非常快。

接下来我就以Vue DevUI之前出现过的一个bug为例,详细介绍下如何使用git bisect这把利器。

定位过程

git bisect start 5d14c34b d577ce4
or
git bisect start HEAD d577ce4

其中5d14c34b这次是最近出现的有bug的提交,d577ce4这个是上一次发版本没问题的提交。

执行完启动bisect之后,马上就切到中间的一次提交啦,以下是打印结果:

kagol:vue-devui kagol$ git bisect start 5d14c34b d577ce4
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[1cfafaaa58e03850e0c9ddc4246ae40d18b03d71] fix: read-tip icon样式泄露 (#54)

可以看到已经切到以下提交:

[1cfafaaa] fix: read-tip icon样式泄露 (#54)

执行命令:

yarn build

构建成功,所以标记下good

git bisect good
kagol:vue-devui kagol$ git bisect good
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0] feat(drawer): add service model (#27)

标记万good,马上又通过二分法,切到了一次新的提交:

[c0c4cc1a] feat(drawer): add service model (#27)

再次执行build命令:

yarn build

build失败了,出现了我们最早遇到的报错:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

标记下bad,再一次切到中间的提交:

kagol:vue-devui kagol$ git bisect bad
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[86634fd8efd2b808811835e7cb7ca80bc2904795] feat: add scss preprocessor in docs && fix:(Toast) single lifeMode bug in Toast

以此类推,不断地验证、标记、验证、标记...最终会提示我们那一次提交导致了这次的bug,提交者、提交时间、提交message等信息。

kagol:vue-devui kagol$ git bisect good
c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0 is the first bad commit
commit c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0
Author: nif <lnzhangsong@163.com>
Date: Sun Dec 26 21:37:05 2021 +0800

feat(drawer): add service model (#27)

* feat(drawer): add service model

* docs(drawer): add service model demo

* fix(drawer): remove 'console.log()'

packages/devui-vue/devui/drawer/index.ts | 7 +++--
.../devui-vue/devui/drawer/src/drawer-service.ts | 33 ++++++++++++++++++++++
packages/devui-vue/devui/drawer/src/drawer.tsx | 3 ++
packages/devui-vue/docs/components/drawer/index.md | 29 +++++++++++++++++++
4 files changed, 69 insertions(+), 3 deletions(-)
create mode 100644 packages/devui-vue/devui/drawer/src/drawer-service.ts

最终定位到出问题的commit:

c0c4cc1a is the first bad commit

github.com/DevCloudFE/…

整个定位过程几乎是机械的操作,不需要了解项目源码,不需要了解最近谁提交了什么内容,只需要无脑地:验证、标记、验证、标记,最后git会告诉我们那一次提交出错。

这么香的工具,赶紧来试试吧!

问题分析

直到哪个commit出问题了,定位起来范围就小了很多。

如果平时提交代码又能很好地遵循小颗粒提交的话,bug呼之欲出。

这里必须表扬下我们DevUI的田主(Contributor)们,他们都养成了小颗粒提交的习惯,这次导致bug的提交c0c4cc1a,只提交了4个文件,涉及70多行代码。

我们在其中搜索下document关键字,发现了两处,都在drawer-service.ts整个文件中:

一处是12行的:

static $body: HTMLElement | null = document.body

另一处是17行的:

this.$div = document.createElement('div')

最终发现罪魁祸首就是12行的代码!

破案!

作者:DevUI团队
来源:https://juejin.cn/post/7046409685561245733

收起阅读 »

手把手带你配置MySQL主备环境

为了保障生产数据的安全性,我们往往需要对数据库做备份,而通过MySQL主备配置,则是一种MySQL数据库备份的好的实现方案。本文将一步步带你搭建MySQL主备环境。MySQL主备搭建现有两台虚拟机,192.168.56.11(主)和192.168.56.12(...
继续阅读 »



为了保障生产数据的安全性,我们往往需要对数据库做备份,而通过MySQL主备配置,则是一种MySQL数据库备份的好的实现方案。本文将一步步带你搭建MySQL主备环境。

MySQL主备搭建

现有两台虚拟机,192.168.56.11(主)和192.168.56.12(备)

1 MySQL安装

离线安装解压版MySQL

1 上传压缩包
  • 以root用户上传mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz到/root/Downloads/下

2 解压
  • cd /root/Downloads

  • tar -zxvf mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz

  • mv mysql-5.7.29-linux-glibc2.12-x86_64 mysql5.7

  • mkdir /usr/database

  • mv mysql5.7 /usr/database

3 创建系统用户组和用户
  • groupadd mysql

  • useradd -r -g mysql mysql

  • id mysql

4 创建mysql data目录
  • cd /usr/database/mysql5.7

  • mkdir data

4 设置data目录权限
  • chown -R mysql:mysql /usr/database/mysql5.7/

  • ll /usr/database

5 修改my.cnf文件

删除并重新创建/etc/my.cnf:

rm -rf /etc/my.cnf
vim /etc/my.cnf
[client]
port = 3306
socket = /tmp/mysql.sock

[mysqld]
init-connect='SET NAMES utf8'
basedir=/usr/database/mysql5.7     #根据自己的安装目录填写
datadir=/usr/database/mysql5.7/data #根据自己的mysql数据目录填写
socket=/tmp/mysql.sock
max_connections=200                 # 允许最大连接数
character-set-server=utf8           # 服务端使用的字符集默认为8比特编码的latin1字符集
default-storage-engine=INNODB       # 创建新表时将使用的默认存储引擎

最大连接数

  • max_connections<=16384

  • 管理员(SUPER)登录的连接,不计其中

  • mysql会为每个连接提供连接缓冲区,连接越多内存开销越大

  • 查询:show variables like '%_connections';show status like '%_connections';

  • max_used_connections / max_connections * 100% (理想值≈ 85%)

  • 数值过小会经常出现ERROR 1040: Too many connections错误

  • 修改方法:

    • 永久:在配置文件my.cnf中设置max_connections的值

    • 临时:以root登录mysql:set GLOBAL max_connections=xxx;->flush privileges;

mysql存储引擎

  • InnoDB:5.5及之后版本的默认引擎

    • 支持事务

    • 聚集索引,文件存放在主键索引的叶子节点上,必须要有主键。通过主键索引效率很高,但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。

    • 不支持全文类型索引

    • 支持外键

    • 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。

    • 最小锁粒度是行锁

    • 适用场景:支持事物、较多写操作、系统崩溃后相对易恢复

  • MyISAM:5.5版本之前的默认引擎

    • 不支持事务

    • 非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。

    • 保存了整个表的行数,执行select count(*) 时只需要读出该变量即可,速度很快;

    • 支持全文类型索引

    • 不支持外键

    • 最小锁粒度是表锁,一个更新语句会锁住整张表,导致其他查询和更新都被阻塞,因此并发访问受限

    • 表以文件形式保存,跨平台使用较方便

    • 适用场景:非事物型、读操作、小型应用

6 mysql初始化
  • /usr/database/mysql5.7/bin/mysqld --initialize-insecure --user=mysql --basedir=/usr/database/mysql5.7 --datadir=/usr/database/mysql5.7/data

#注意:mysqld --initialize-insecure初始化后的mysql是没有密码的

  • chown -R root:root /usr/database/mysql5.7/ #把安装目录的目录的权限所有者改为root

  • chown -R mysql:mysql /usr/database/mysql5.7/data/ #把data目录的权限所有者改为mysql

7 启动mysql
/usr/database/mysql5.7/bin/mysqld_safe --user=mysql &
8 修改root密码
  • cd /usr/database/mysql5.7/bin

  • ./mysql -u root -p # 默认没有密码,直接回车就行

  • use mysql;

  • update user set authentication_string=password('``rootpasswd``') where user='root';

  • flush privileges;

  • exit;

9 登录测试
  • /usr/database/mysql5.7/bin/mysql mysql -u root -p

  • (输入密码)

  • show databases;

  • exit;

10 启动设置
  • cp /usr/database/mysql5.7/support-files/mysql.server /etc/init.d/mysql

  • chkconfig --add mysql # 添加服务

  • chkconfig --list # 查看服务列表

  • chkconfig --level 345 mysql on # 设置开机启动

11 测试服务命令是否可用
  • systemctl status mysql

  • systemctl start mysql

  • systemctl stop mysql

12 设置远程访问
  • 登录数据库:mysql -uroot -p[password]

  • use mysql;

  • select host,user from user;

  • update user set host='%' where user='root';

  • flush privileges;

  • 如果还是无法访问,检查防火墙

13 创建新用户
  • 创建用户app:create user 'app'@'%' identified by 'password';

  • 用户赋权(具有数据库appdb的所有权限,并可远程不限ip访问):grant all on appdb.* to 'app'@'%';

  • flush privilegesl;

14 其他问题
  • -bash: mysql: command not found

    • 临时方案,重新登录后失效:alias mysql=/usr/database/mysql5.7/bin/mysql

    • 永久方案,将命令路径添加到PATH中:

      • vim /etc/profile

      • PATH="$PATH:/usr/database/mysql5.7/bin"

      • source /etc/profile

2 主备配置

1 生产环境为什么需要MySQL集群
  • 高可用性,在主节点失效时自动切换,不需要技术人员紧急处理

  • 高吞吐,可以多个节点同时提供读取数据服务,降低主节点负载,实现高吞吐

  • 可扩展性强,支持在线扩容

  • 无影响备份,在备节点进行备份操作不会对业务产生影响

MySQL集群的缺点:

  • 架构复杂,在部署、管理方面对技术人员要求高

  • 备节点拉取主节点日志时会对主节点服务器性能有一定影响

  • 如果配置了半同步复制,会对事物提交有一定影响

2 修改my.cnf
  • 主备服务器均创建日志文件路径:mkdir /data/mysql_log

  • 修改master下的/etc/my.cnf:

[client]
port = 3306
default-character-set=utf8mb4
socket = /usr/database/mysql5.7/mysql.sock

[mysqld]
basedir = /usr/database/mysql5.7
datadir = /usr/database/mysql5.7/data
tmpdir = /tmp
socket = /tmp/mysql.sock
# pid-file = /data/mysql_db/mysql_seg_3306/mysql.pid
skip-external-locking = 1
skip-name-resolve = 1
port = 3306
server-id = 113306

default-storage-engine = InnoDB
character-set-server = utf8mb4
default_password_lifetime=0

#### log ####
log_timestamps=system
log_bin = /data/mysql_log/mysql-bin
log_bin_index = /data/mysql_log/mysql-bin.index
binlog_format = row
relay_log_recovery=ON
relay_log=/data/mysql_log/mysql-relay-bin
relay_log_index=/data/mysql_log/mysql-relay-bin.index
log_error = /data/mysql_log/mysql-error.log

#### replication ####
replicate_wild_ignore_table = information_schema.%,performance_schema.%,sys.%

#### semi sync replication settings #####
plugin_dir=/usr/database/mysql5.7/lib/plugin
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
loose_rpl_semi_sync_master_enabled = 1
loose_rpl_semi_sync_slave_enabled = 1
loose_rpl_semi_sync_master_timeout = 5000
  • 修改slave下的/etc/my.cnf:

[client]
port = 3306
default-character-set=utf8mb4
socket = /usr/database/mysql5.7/mysql.sock

[mysqld]
basedir = /usr/database/mysql5.7
datadir = /usr/database/mysql5.7/data
tmpdir = /tmp
socket = /tmp/mysql.sock
# pid-file = /data/mysql_db/mysql_seg_3306/mysql.pid
skip-external-locking = 1
skip-name-resolve = 1
port = 3306
server-id = 123306
read-only=1

default-storage-engine = InnoDB
character-set-server = utf8mb4
default_password_lifetime=0

#### log ####
log_timestamps=system
log_bin = /data/mysql_log/mysql-bin
log_bin_index = /data/mysql_log/mysql-bin.index
binlog_format = row
relay_log_recovery=ON
relay_log=/data/mysql_log/mysql-relay-bin
relay_log_index=/data/mysql_log/mysql-relay-bin.index
log_error = /data/mysql_log/mysql-error.log

#### replication ####
replicate_wild_ignore_table = information_schema.%,performance_schema.%,sys.%

#### semi sync replication settings #####
plugin_dir=/usr/database/mysql5.7/lib/plugin
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
loose_rpl_semi_sync_master_enabled = 1
loose_rpl_semi_sync_slave_enabled = 1
loose_rpl_semi_sync_master_timeout = 5000
  • utf8:最大unicode字符是0xffff,仅支持Unicode 中的基本多文种平面(BMP),任何不在基本多文本平面的 Unicode字符,都无法使用 Mysql 的 utf8 字符集存储。

  • utf8mb4(most bytes 4)专门用来兼容四字节的unicode,是utf8的超集,包含很多不常用的汉字、Emoji表情,以及任何新增的 Unicode 字符等等

3 master上创建同步用户
  • mysql -uroot -proot

  • use mysql;

  • create user 'repl'@'%' identified by 'repl';

  • grant replication slave on . to 'repl'@'%';

  • flush privileges;

4 备份master数据库
/usr/database/mysql5.7/bin/mysqldump -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -A > mysql_backup_full.sql

上述命令报错,1045,增加登录信息:

/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -A > mysql_backup_full.sql

导出指定数据库(仅表结构):

/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -d testdb1 testdb2 > mysql_backup_test.sql

导出指定数据库(表结构+数据):

**/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 testdb1 testdb2 > mysql_backup_testdatas.sql**
5 slave上恢复master数据库

master推送sql:rsync -avzP mysql_backup_test.sql 192.168.56.12:/root/Downloads/

slave导入sql:/usr/database/mysql5.7/bin/mysqldump -S /tmp/mysql.sock < mysql_backup_test.sql

上述命令不成功,需先创建数据库,改为下述操作:

**mysql -uroot -proot**
**use test;**
**source /root/Downloads/**``**mysql_backup_test.sql**
6 开启同步
  • mysql命令行中查看master status中的File和Position参数:show master status;

    • 查看进程:show processlist\G

  • slave的mysql命令行执行:

mysql> CHANGE MASTER TO
   -> MASTER_HOST='192.168.41.83',
   -> MASTER_PORT=3306,
   -> MASTER_USER='repl',
   -> MASTER_PASSWORD='repl',
   -> MASTER_LOG_FILE='mysql-bin.000004',
   -> MASTER_LOG_POS=154;
mysql> start slave;
mysql> show slave status\G

状态中注意这几项:

Slave_IO_Running:取 Master 日志的线程, Yes 为正在运行

Slave_SQL_Running:从日志恢复数据的线程, Yes 为正在运行

Seconds_Behind_Master:当前数据库相对于主库的数据延迟, 这个值是根据二进制日志的时间戳计算得到的(秒)

7 同步测试

master数据库插入一条数据,可用看到slave同步更新了

主备有了,要是能够故障自动切换就完美了,这正是下一篇内容。

引用请注明出处!

收起阅读 »

动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?

90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。今天的面试题是:如何实现动态代理?JDK Proxy 和 CGLib 有什...
继续阅读 »



90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。

今天的面试题是:如何实现动态代理?JDK Proxy 和 CGLib 有什么区别?

典型回答

动态代理的常用实现方式是反射。反射机制 是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。

但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。

JDK Proxy 和 CGLib 的区别主要体现在以下几个方面:

  • JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;

  • Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;

  • JDK Proxy 是通过拦截器加反射的方式实现的;

  • JDK Proxy 只能代理继承接口的类;

  • JDK Proxy 实现和调用起来比较简单;

  • CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;

  • CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。

考点分析

此面试题考察的是你对反射、动态代理及 CGLib 的了解,很多人经常会把反射和动态代理划为等号,但从严格意义上来说,这种想法是不正确的,真正能搞懂它们之间的关系,也体现了你扎实 Java 的基本功。和这个问题相关的知识点,还有以下几个:

  • 你对 JDK Proxy 和 CGLib 的掌握程度。

  • Lombok 是通过反射实现的吗?

  • 动态代理和静态代理有什么区别?

  • 动态代理的使用场景有哪些?

  • Spring 中的动态代理是通过什么方式实现的?

知识扩展

1.JDK Proxy 和 CGLib 的使用及代码分析

JDK Proxy 动态代理实现

JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可,整个实现代码如下所示:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* JDK Proxy 相关示例
*/
public class ProxyExample {
  static interface Car {
      void running();
  }

  static class Bus implements Car {
      @Override
      public void running() {
          System.out.println("The bus is running.");
      }
  }

  static class Taxi implements Car {
      @Override
      public void running() {
          System.out.println("The taxi is running.");
      }
  }

  /**
    * JDK Proxy
    */
  static class JDKProxy implements InvocationHandler {
      private Object target; // 代理对象

      // 获取到代理对象
      public Object getInstance(Object target) {
          this.target = target;
          // 取得代理对象
          return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                  target.getClass().getInterfaces(), this);
      }

      /**
        * 执行代理方法
        * @param proxy 代理对象
        * @param method 代理方法
        * @param args   方法的参数
        * @return
        * @throws InvocationTargetException
        * @throws IllegalAccessException
        */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)
              throws InvocationTargetException, IllegalAccessException {
          System.out.println("动态代理之前的业务处理.");
          Object result = method.invoke(target, args); // 执行调用方法(此方法执行前后,可以进行相关业务处理)
          return result;
      }
  }

  public static void main(String[] args) {
      // 执行 JDK Proxy
      JDKProxy jdkProxy = new JDKProxy();
      Car carInstance = (Car) jdkProxy.getInstance(new Taxi());
      carInstance.running();
}

以上程序的执行结果是:

动态代理之前的业务处理.
The taxi is running.

可以看出 JDK Proxy 实现动态代理的核心是实现 Invocation 接口,我们查看 Invocation 的源码,会发现里面其实只有一个 invoke() 方法,源码如下:

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

这是因为在动态代理中有一个重要的角色也就是代理器,它用于统一管理被代理的对象,显然 InvocationHandler 就是这个代理器,而 invoke() 方法则是触发代理的执行方法,我们通过实现 Invocation 接口来拥有动态代理的能力。

CGLib 的实现

在使用 CGLib 之前,我们要先在项目中引入 CGLib 框架,在 pom.xml 中添加如下配置:

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

CGLib 实现代码如下:

package com.lagou.interview;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLibExample {

  static class Car {
      public void running() {
          System.out.println("The car is running.");
      }
  }

  /**
    * CGLib 代理类
    */
  static class CGLibProxy implements MethodInterceptor {
      private Object target; // 代理对象

      public Object getInstance(Object target) {
          this.target = target;
          Enhancer enhancer = new Enhancer();
          // 设置父类为实例类
          enhancer.setSuperclass(this.target.getClass());
          // 回调方法
          enhancer.setCallback(this);
          // 创建代理对象
          return enhancer.create();
      }

      @Override
      public Object intercept(Object o, Method method,
                              Object[] objects, MethodProxy methodProxy) throws Throwable {
          System.out.println("方法调用前业务处理.");
          Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用
          return result;
      }
  }

  // 执行 CGLib 的方法调用
  public static void main(String[] args) {
      // 创建 CGLib 代理类
      CGLibProxy proxy = new CGLibProxy();
      // 初始化代理对象
      Car car = (Car) proxy.getInstance(new Car());
      // 执行方法
      car.running();
}

以上程序的执行结果是:

方法调用前业务处理.
The car is running.

可以看出 CGLib 和 JDK Proxy 的实现代码比较类似,都是通过实现代理器的接口,再调用某一个方法完成动态代理的,唯一不同的是,CGLib 在初始化被代理类时,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因此被代理类不能被关键字 final 修饰,如果被 final 修饰,再使用 Enhancer 设置父类时会报错,动态代理的构建会失败。

2.Lombok 原理分析

在开始讲 Lombok 的原理之前,我们先来简单地介绍一下 Lombok,它属于 Java 的一个热门工具类,使用它可以有效的解决代码工程中那些繁琐又重复的代码,如 Setter、Getter、toString、equals 和 hashCode 等等,向这种方法都可以使用 Lombok 注解来完成。

例如,我们使用比较多的 Setter 和 Getter 方法,在没有使用 Lombok 之前,代码是这样的:

public class Person {
  private Integer id;
  private String name;
  public Integer getId() {
      return id;
  }
  public void setId(Integer id) {
      this.id = id;
  }
  public String getName() {
      return name;
  }
  public void setName(String name) {
      this.name = name;
  }
}

在使用 Lombok 之后,代码是这样的:

@Data
public class Person {
  private Integer id;
  private String name;
}

可以看出 Lombok 让代码简单和优雅了很多。

小贴士:如果在项目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在编码阶段成功调用对象的 set 或 get 方法,我们需要在 IDE 中安装 Lombok 插件才行,比如 Idea 的插件如下图所示:

接下来讲讲 Lombok 的原理。

Lombok 的实现和反射没有任何关系,前面我们说了反射是程序在运行期的一种自省(introspect)能力,而 Lombok 的实现是在编译期就完成了,为什么这么说呢?

回到我们刚才 Setter/Getter 的方法,当我们打开 Person 的编译类就会发现,使用了 Lombok 的 @Data 注解后的源码竟然是这样的:

可以看出 Lombok 是在编译期就为我们生成了对应的字节码。

其实 Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下:

从流程图中可以看出,在编译期阶段,当 Java 源码被抽象成语法树(AST)之后,Lombok 会根据自己的注解处理器动态修改 AST,增加新的代码(节点),在这一切执行之后就生成了最终的字节码(.class)文件,这就是 Lombok 的执行原理。

3.动态代理知识点扩充

当面试官问动态代理的时候,经常会问到它和静态代理的区别?静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。

动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。

Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 <aop:aspectj-autoproxy proxy-target-/> 即可。

小结

今天我们介绍了 JDK Proxy 和 CGLib 的区别,JDK Proxy 是 Java 语言内置的动态代理,必须要通过实现接口的方式来代理相关的类,而 CGLib 是第三方提供的基于 ASM 的高效动态代理类,它通过实现被代理类的子类来实现动态代理的功能,因此被代理的类不能使用 final 修饰。

除了 JDK Proxy 和 CGLib 之外,我们还讲了 Java 中常用的工具类 Lombok 的实现原理,它其实和反射是没有任何关系的;最后讲了动态代理的使用场景以及 Spring 中动态代理的实现方式,希望本文可以帮助到你。

作者:Java面试真题解析
来源:https://mp.weixin.qq.com/s/UIMeWRerV9FhtEotV8CF9w

收起阅读 »

Java内存区域异常

一、内存区域划分Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。 JVM内存自动管理是Java...
继续阅读 »



一、内存区域划分

Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。

JVM内存自动管理是Java语言的一大优良特性,也是Java语言在软件市场长青的一大优势,有了JVM的自动内存管理,程序员不再为令人抓狂的内存泄漏担忧,但深入理解Java虚拟机内存区域依然至关重要,JVM内存自动管理并非永远万无一失,不当的使用也会造成OOM等内存异常,本文尝试列举Java内存溢出相关的常见异常并提供使用建议。

二、栈溢出

  • 方法调用深度过大

我们知道Java虚拟机栈及本地方法栈中存放的是方法调用所需的栈帧,栈帧是一种包括局部变量表、操作数栈、动态链接和方法返回地址的数据结构,每一次方法的调用和返回对应着压栈及出栈操作。Java虚拟机栈及本地方法栈由每个Java执行线程独享,当方法嵌套过深直至超过栈的最大空间时,当再次执行压栈操作,将抛出StackOverflowError异常。为演示栈溢出,假设存在如下代码:

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
  * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
  */
  public static void increment() {
      count++;
      increment();
  }

  public static void main(String[] args) {
      try {
          increment();
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

对于示例中的increment()方法,每次方法调用将深度+1,通过不断的栈帧压栈操作,当栈中空间无法再进行扩展时,程序将抛出StackOverflowError异常。Java虚拟机栈的空间大小可以通过JVM参数调整,我们先设置栈空间大小为512K(-Xss512k),程序执行结果如下:(方法调用5355次后触发栈溢出) 我们再将栈空间缩小至256k(-Xss256k),程序执行结果如下:(方法调用2079次时将触发栈溢出): 由此可见,Java虚拟机栈的空间是有限的,当我们进行程序设计时,应尽量避免使用递归调用。生产环境抛出StackOverflowError异常时,可以排查系统中是否存在调用深度过大的方法。方法的不断嵌套调用,不但会占用更多的内存空间,也会影响程序的执行效率。当我们进行程序设计时需要充分考虑并设置合理的栈空间大小,一般情况下虚拟机默认配置即满足大部分应用场景。

  • 局部变量表过大

局部变量表用于存放Java方法中的局部变量,我们需要合理的设置局部变量,避免过多的冗余变量产生,否则可能会导致栈溢出,沿用刚刚的实例代码,我们定义一个创建大量局部变量的重载方法,则在栈空间不变的情况下(-Xss512k),创建大量局部变量的方法将降低栈调用深度,更容易触发StackOverflowError异常,通过分别执行increment及其重载方法,其中代码及执行结果如下:

package org.learn.jvm.basic;

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
    * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
    */
  public static void increment() {
      count++;
      increment();
  }

  /**
    * 创建大量局部变量,导致局部变量表过大,影响栈调用深度
    *
    * @param a
    * @param b
    * @param c
    */
  public static void increment(long a, long b, long c) {
      long d = 1, e = 2, f = 3, g = 4, h = 5, i = 6,
              j = 7, k = 8, l = 9, m = 10;
      count++;
      increment(a, b, c);
  }

  public static void main(String[] args) {
      try {
          //increment();
          increment(1, 2, 3);
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

执行increment()时,最大栈深度为5355(栈空间大小-Xss512k)

执行increment(1,2,3)时,最大栈深度为1487(栈空间大小-Xss512k)

通过对比我们知道,创建大量的局部变量将使得局部变量表膨胀从而引发StackOverflowError异常,程序设计时应当尽量避免同一个方法中包含大量局部变量,实在无法避免可以考虑方法的重构及拆分。

三、OOM

对于Java程序员而言,OOM算是比较常见的生产环境异常,OOM往往容易引发线上事故,因此有必要梳理常见可能导致OOM的场景,并尽量规避保障服务的稳定性。

  • 创建大量的类

方法区主要的职责是用于存放类型相关信息,如类名、接口名、父类、访问修饰符、字段描述、方法描述等,Java8以后方法区从永久区移到了Metaspace,若系统中创建过多的类时,可能会引发OOM。Java开发中经常会利用字节码增强技术,通过创建代理类从而实现系统功能,以我们熟知的Spring框架为例,经常通过Cglib运行时创建代理类实现类的动态性,通过设置虚拟机参数,-XX:MaxMetaspaceSize=10M,则示例程序运行一段时间时间后,将触发Full GC并最终抛出OOM异常,因此日常开发过程中要特别留意,系统中创建的类的数量是否合理,以免产生OOM。实例代码如下:

/**
* 大量创建类导致方法区OOM
*/
public class JavaMethodAreaOOM {
  /**
    * VM Args: -XX:MaxMetaspaceSize=10M(设置Metaspace空间为10MB)
    *
    * @param args
    */
  public static void main(String[] args) {
      while (true) {
          Enhancer enhancer = new Enhancer();
          enhancer.setSuperclass(OOMObject.class);
          enhancer.setUseCache(false);
          enhancer.setCallback(new MethodInterceptor() {
              @Override
              public Object intercept(Object obj, Method method,
                                      Object[] args, MethodProxy proxy) throws Throwable {
                  return proxy.invokeSuper(obj, args);
              }
          });
          Object object = enhancer.create();
          System.out.println("hashcode:" + object.hashCode());
      }
  }

  /**
    * 对象
    */
  static class OOMObject {

  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定元数据区为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下:

  • 创建大对象

Java堆内存空间非常宝贵,若系统中存在数组、复杂嵌套对象等大对象,将会引发FullGc并最终引发OOM异常,因此程序设计时需要合理设置类结构,避免产生过大的对象,示例代码如下:

/**
* 堆内存溢出
*/
public class HeapOOM {
  /**
    *
    */
  static class InnerClass {
      private int value;
      private byte[] bytes = new byte[1024];
  }

  /**
    * VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
    *
    * @param args
    */
  public static void main(String[] args) {
      List<InnerClass> innerClassList = new ArrayList<>();
      while (true) {
          innerClassList.add(new InnerClass());
      }
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 常量池溢出

Java8开始,常量池从永久区移至堆区,当系统中存在大量常量并超过常量池最大容量时,将引发OOM异常。String类的intern()方法内部逻辑是:若常量池中存在字符串常量,则直接返回字符串引用,否则创建常量将其加入常量池中并返回常量池引用。通过每次创建不同的字符串,常量池将因为无法容纳新创建的字符串常量而最终引发OOM,示例代码如下:

/**
* VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
*
* @param args
*/
public static void main(String[] args) {
  Set<String> set = new HashSet<>();
  int i = 0;
  while (true) {
      set.add(String.valueOf(i++).intern());
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 直接内存溢出

直接内存是因NIO技术而引入的独立于堆区的一块内存空间,对于读写频繁的场景,通过直接操作直接内存可以获得更高的执行效率,但其内存空间受制于操作系统本地内存大小,超过最大限制后,也将抛出OOM 异常,以下代码通过UnSafe类,直接操作内存,程序执行一段时间后,将引发OOM异常。

public class DirectMemoryOOM {
  private static final int _1MB = 1024 * 1024;

  /**
    * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
    * @param args
    * @throws IllegalAccessException
    */
  public static void main(String[] args) throws IllegalAccessException {
      Field field = Unsafe.class.getDeclaredFields()[0];
      field.setAccessible(true);
      Unsafe unsafe = (Unsafe) field.get(null);
      while (true) {
          unsafe.allocateMemory(_1MB*1024);
      }
  }
}

设置虚拟机参数:-Xmx20M -XX:MaxDirectMemorySize=10M 限定最大直接内存空间为10M,程序最终执行结果如下:

四、总结

本文通过实际的案例列举了常见的OOM异常,由此我们可以得知,程序设计过程中应当合理的设置栈区、堆区、直接内存的大小,从实际情况出发,合理设计数据结构,从而避免引发OOM故障,此外通过分析引发OOM的原因也有利于我们针对深入理解JVM并对现有系统进行系统调优。

作者:洞幺幺洞
来源:https://juejin.cn/post/7043442191619850277

收起阅读 »

消息队列的使用场景是什么样的?

本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。什么是消息中间件作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、...
继续阅读 »



本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。

什么是消息中间件

作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、异步和事务等特性的消息通信服务。应用消息代理组件可以降低系统间耦合度,提高系统的吞吐量、可扩展性和高可用性。

分布式消息服务主要涉及五个核心角色,消息发布者(Publisher)、可靠消息组件(MsgBroker)、消息订阅者(Subscriber)、消息类型(Message Type)和订阅关系(Binding),具体描述如下:

  1. 消息发布者,指发送消息的应用系统,一个应用系统可以发送一种或者多种消息类型,发布者发送消息到可靠消息组件 (MsgBroker)。

  2. 可靠消息组件,即 MsgBroker,负责接收发布者发送的消息,根据消息类型和订阅关系将消息分发投递到一个或多个消息订阅者。整个过程涉及消息类型校验、消息持久化存储、订阅关系匹配、消息投递和消息恢复等核心功能。

  3. 消息订阅者,指订阅消息的应用系统,一个应用系统可以订阅一种或者多种消息类型,消息订阅者收到的消息来自可靠消息组件 (MsgBroker)。

  4. 消息类型:一种消息类型由 TOPIC 和 EVENTCODE 唯一标识。

  5. 订阅关系,用来描述一种消息类型被订阅者订阅,订阅关系也被称为 Binding。

核心功能特色

可为不同应用系统间提供可靠的消息通信,降低系统间耦合度并提高整体架构的可扩展性和可用性。

可为不同应用系统间提供异步消息通信,提高系统吞吐量和性能。

发布者系统、消息代理组件以及订阅者系统均支持集群水平扩展,可依据业务消息量动态部署计算节点。

支持事务型消息,保证消息与本地数据库事务的一致性。

远程调用RPC和消息MQ区别

谈到消息队列,有必要看下RPC和MQ的本质区别,从两者的定义和定位来看,RPC(Remote Procedure Call)远程过程调用,主要解决远程通信间的问题,不需要了解底层网络的通信机制;消息队列(MQ)是一种能实现生产者到消费者单向通信的通信模型。核心区别在于RPC是双向直接网络通讯,MQ是单向引入中间载体的网络通讯。单纯去看队列,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。在队列前面增加限定词“消息”,意味着通过消息驱动来进行整体的架构实现。RPC和MQ本质上是网络通讯的两种不同的实现机制,RPC同步等待结果对比于MQ在异步、解耦、削峰填谷等上的特征显著差异主要有以下几点差异:

  1. 在架构上,RPC和MQ的差异点是,Message有一个中间结点Message Queue,可以把消息存储起来。

  2. 同步调用:对于要立即等待返回处理结果的场景,RPC是首选。

  3. MQ的使用,一方面是基于性能的考虑,比如服务端不能快速的响应客户端(或客户端也不要求实时响应),需要在队列里缓存;另外一方面,它更侧重数据的传输,因此方式更加多样化,除了点对点外,还有订阅发布等功能。

  4. 随着业务增长,有的处理端调用下游服务太多或者处理量会成为瓶颈,会进行同步调用改造为异步调用,这个时候可以考虑使用MQ。

核心应用场景

针对MQ的核心场景,我们从异步、解耦、削峰填谷等特性进行分析,区别于传统的RPC调用。尤其在引入中间节点的情况下,通过空间(拥有存储能力)换时间(RPC同步等待响应)的思想,增加更多的可能性和能力。

异步通信

针对不需要立即处理消息,尤其那种非常耗时的操作,通过消息队列提供了异步处理机制,通过额外的消费线程接管这部分进行异步操作处理。

解耦

在应用和应用之间,提供了异构系统之间的消息通讯的机制,通过消息中间件解决多个系统或异构系统之间除了RPC之外另一种单向通讯的机制。

扩展性

因为消息队列解耦了主流程的处理过程,只要另外增加处理过程即可,不需要改变代码、不需要调整参数,便于分布式扩容。

分布式事务一致性

在2个应用系统之间的数据状态同步,需要考虑数据状态的最终一致性的场景下,利用消息队列所提供的事务消息来实现系统间的数据状态一致性。

削峰填谷

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提前预知;如果为了能处理这类瞬间峰值访问提前准备应用资源无疑是比较大的浪费。使用消息队列在突发事件下的防脉冲能力提供了一种保障,能够接管前台的大脉冲请求,然后异步慢速消费。

可恢复性

系统的一部分组件失效时,不会影响到整个系统。消息队列降低了应用间的耦合度,所以即使一个处理消息的应用挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

顺序保证

在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来进行处理。

大量堆积

通过消息堆积能力处理数据迁移场景,针对旧数据进行全量迁移的同时开启增量消息堆积,待全量迁移完毕,再开启增量,保证数据最终一致性且不丢失。

数据流处理

分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后导入到大数据实时计算引擎,通过消息队列解决异构系统的数据对接能力。

业界消息中间件对比

详细的对比可以参考:blog.csdn.net/wangzhipeng…

消息中间件常用协议

AMQP协议

AMQP即Advanced Message Queuing Protocol,提供统一消息服务的高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。

优点:可靠、通用

MQTT协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。

优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统

STOMP协议

STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。

优点:命令模式(非topic/queue模式)

XMPP协议

XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。

优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大

基于TCP/IP自定义的协议

有些特殊框架(如:redis、kafka、rocketMQ等)根据自身需要未严格遵循MQ规范,而是基于TCP/IP自行封装了一套二进制编解码协议,通过网络socket接口进行传输,实现了MQ的标准规范相关功能。

消息中间件推和拉模式对比

Push推模式:服务端除了负责消息存储、处理请求,还需要保存推送状态、保存订阅关系、消费者负载均衡;推模式的实时性更好;如果push能力大于消费能力,可能导致消费者崩溃或大量消息丢失

Push模式的主要优点是:

  1. 对用户要求低,方便用户获取需要的信息

  2. 及时性好,服务器端即时地向客户端推送更行的动态信息

Push模式的主要缺点是:

  1. 推送的信息可能并不能满足客户端的个性化需求

  2. Push消息大于消费者消费速率额,需要有协调QoS机制做到消费端反馈

Pull拉模式:客户端除了消费消息,还要保存消息偏移量offset,以及异常情况下的消息暂存和recover;不能及时获取消息,数据量大时容易引起broker消息堆积。

Pull拉模式的主要优点是:

  1. 针对性强,能满足客户端的个性化需求

  2. 客户端按需获取,服务器端只是被动接收查询,对客户端的查询请求做出响应

Pull拉模式主要的缺点是:

  1. 实时较差,针对于服务器端实时更新的信息,客户端难以获取实时信息

  2. 对于客户端用户的要求较高,需要维护位点

相关资料

建议学习以下的技术文档,了解更多详细的技术细节和实现原理,加深对消息中间件的理解和应用,同时可以下载开源的源代码,本地调试相应的代码,加深对技术原理的理解和概念的掌握,以及在实际生产中更多的掌握不同的消息队列应用的场景下,高效和正确地使用消息中间件。

RocketMQ资料: github.com/apache/rock…

Kafka资料: kafka.apache.org/documentati…

阿里云RocketMQ文档: help.aliyun.com/document_de…


作者:阿里巴巴淘系技术
来源:https://juejin.cn/post/7025955365812437028

收起阅读 »

业务实战中经典算法的应用

有网友提问:各种机器学习算法的应用场景分别是什么(比如朴素贝叶斯、决策树、K 近邻、SVM、逻辑回归最大熵模型)?这些在一般工作中分别用到的频率多大?一般用途是什么?需要注意什么?根据问题,核心关键词是基础算法和应用场景,比较担忧的点是这些基础算法能否学有所用...
继续阅读 »



有网友提问:各种机器学习算法的应用场景分别是什么(比如朴素贝叶斯、决策树、K 近邻、SVM、逻辑回归最大熵模型)?

这些在一般工作中分别用到的频率多大?一般用途是什么?需要注意什么?

根据问题,核心关键词是基础算法和应用场景,比较担忧的点是这些基础算法能否学有所用?毕竟,上述算法是大家一上来就接触的,书架上可能还放着几本充满情怀的《数据挖掘导论》《模式分类》等经典书籍,但又对在深度学习时代基础算法是否有立足之地抱有担忧。

网上已经有很多内容解答了经典算法的基本思路和理论上的应用场景,这些场景更像是模型的适用范围,这与工业界算法实际落地中的场景其实有很大区别。

从工业界的角度看,业务价值才是衡量算法优劣的金钥匙,而业务场景往往包含业务目标、约束条件和实现成本。如果我们只看目标,那么前沿的算法往往占据主导,可如果我们需兼顾算法运行复杂度、快速迭代试错、各种强加的业务限制等,经典算法往往更好用,因而就占据了一席之地。

针对这个问题,淘系技术算法工程师感知同学写出本文详细解答。

在实际业务价值中,算法模型的影响面大致是10%

在工业界,算法从想法到落地,你不是一个人在战斗。我以推荐算法为例,假设我们现在接到的任务是支持某频道页feeds流的推荐。我们首先应该意识到对业务来说,模型的影响面大致是10%,其他几个重要影响因子是产品设计(40%)、数据(30%)、领域知识的表示和建模(20%)。

这意味着,你把普通的LR模型升级成深度模型,即使提升20%,可能对业务的贡献大致只有2%。当然,2%也不少,只是这个折扣打的让人脑壳疼。

当然,上面的比例划分并不是一成不变的,在阿里推荐算法元年也就是2015年起,个性化推荐往往对标运营规则,你要是不提升个20%都不好意思跟人打招呼。那么一个算法工程师的日常除了接需求,就是做优化:业务给我输入日志、特征和优化目标,剩下的事情就交给我吭哧吭哧。

可随着一年年的水涨船高,大家所用模型自然也越来越复杂,从LR->FTRL->WDL->DeepFM->MMOE,大家也都沿着前辈们躺过的路径一步步走下去。这时候你要是问谁还在用LR或是普通的决策树,那确实会得到一个尴尬的笑容。

但渐渐的,大家也意识到了,模型优化终究是一个边际收益递减的事情。当我们把业务方屏蔽在外面而只在一个密闭空间中优化,天花板就已经注定。于是渐渐的,淘系的推荐慢慢进入第二阶段,算法和业务共建阶段。业务需求和算法优化虽然还是分开走,但已经开始有融合的地方。

集团对算法工程师的要求也在改变:一个高大上的深度模型,如果不能说清楚业务价值,或带来特别明显提升,那么只能认为是自嗨式的闭门造车。这时,一个优秀的算法工程师,需要熟悉业务,通过和业务反复交流中,能够弄清楚业务痛点。

注意,业务方甚至可能当局者迷,会提出既要又要还要的需求给你,而你需要真正聚焦到那个最值得做的问题上。然后,才是对问题的算法描述。做到这一步你会发现,并不是你来定哪个模型牛逼,而是跟着问题走来选择模型。这个模型的第一版极大可能是一个经典算法,因为,你要尽快跑通链路,快速验证你的这个idea是有效的。后面的模型迭代提升,只是时间问题。

经典算法在淘系的应用场景示例:TF-IDF、K近邻、朴素贝叶斯、逻辑回归等

因而现阶段,在淘系的大多数场景中,并不是算法来驱动业务,而是配合业务一起来完成增长。一个只懂技术的算法工程师,最多只能拿到那10%的满分。为了让大家有体感,这里再举几个小例子:

比如业务问题是对用户进行人群打标,人群包括钓鱼控、豆蔻少女、耳机发烧友、男神style等。在实操中我们不仅需考虑用户年龄、性别、购买力等属性,还需考虑用户在淘系的长期行为,从而得到一个多分类任务。如果模型所用的的特征是按月访问频次,那么豆蔻少女很可能网罗非常多的用户,因为女装是淘系行为频次最多的类目。

比如,某用户对耳机发烧友和豆蔻少女一个月内都有4次访问,假设耳机发烧友人均访问次数是3.2次,而豆蔻少女是4.8次,那么可知该用户对耳机发烧友的偏好分应更高。因此,模型特征不仅应该使用用户对该人群的绝对行为频次,还需参照大盘的水位给出相对行为频次。

这时,入选吴军老师《数学之美》的TF-IDF算法就派上用场了。通过引入TF-IDF构建特征,可以显著提高人群标签的模型效果,而TF-IDF则是非常基础的文本分类算法。

在淘系推荐场景,提升feeds流的点击率或转化率往往是一个常见场景。可业务总会给你惊喜:比如商品的库存只有一件(阿里拍卖),比如推荐的商品大多是新品(天猫新品),比如通过小样来吸引用户复购正品,这些用户大多是第一次来(天猫U先),或者是在提升效率的同时还需兼顾类目丰富度(很多场景)。

在上述不同业务约束背景,才是我们真实面对的应用场景。面对这种情况首先是定方向,比如在阿里拍卖中的问题可描述为“如何在浅库存约束下进行个性化推荐”。假如你判断这是一个流量调控问题,就需要列出优化目标和约束条件,并调研如何用拉格朗日乘子法求解。重要的是,最终的结果还需要和个性化推荐系统结合。详见:阿里拍卖全链路导购策略首次揭秘

面对上述应用场景,你需明白你的战略目标是证明浅库存约束下的推荐是一个流量调控问题,并可以快速验证拿到效果。采用一个成熟经典的方法先快速落地实验,后续再逐步迭代是明智的选择。

又比如,K近邻算法似乎能简单到能用一张图或一句话来描述。可它在解决正负样本不均衡问题中就能派上用场。上采样是说通过将少数类(往往是正样本)的数据复制多份,但上采样后存在重复数据集可能会导致过拟合。过拟合的一种原因是在局部范围正负样本比例存在差异,如下图所示:

我们只需对C类局部样本进行上采样,这其中就运用到了K近邻算法。本例中的经典算法虽然只是链路的一部分,甚至只是配角儿,但离了它不行。

朴素贝叶斯虽然简单了点,但贝叶斯理论往后的发展,包括贝叶斯网络、因果图就不那么simple了。比如不论是金融业务中的LR评分卡模型,或是推荐算法精排中的深度模型,交叉特征往往由人工经验配置,这甚至是算法最不自动的一个环节。

使用贝叶斯网络中的structure learning,并结合业务输入的行业知识,构建出贝叶斯概率图,并从中发现相关特征做交叉,会比人工配置带来一定提升,也具有更好的可解释性。这就是把业务领域知识和算法模型结合的一个很好的例子。可如果贝叶斯理论不扎实,很难走到这一步。

不论深度学习怎么火,LR都是大多场景的一个backup。比如在双十一大促投放场景中,如果在0:00~0:30这样的峰值期,所有场景都走深度模型,机器资源肯定不足,这时候就需要做好一个使用LR或FTRL的降级预案。

如何成为业界优秀的算法工程师?

在淘系,算法并不是孤立的存在,算法工程师也不只是一个闭关的剑客。怎么切中业务痛点,快速验证你的idea,怎么进行合适的算法选型,这要求你有很好的算法基本功,以及较广的算法视野。一个模型无论搞的多复杂最终的回答都是业务价值,一个有良好基本功、具备快速学习能力,且善于发掘业务价值的算法工程师,将有很大成长空间。

好吧,假设上面就是我们的职业目标,可怎么实现呢。元(ye)芳(jie),你怎么看?其实只有一个句话,理论和实践相结合

理论

对算法基本思路的了解,查查知乎你可能只需要20分钟,可你忘记它可能也只需要2周。这里的理论是你从内而外对这个算法的感悟,比如说到决策树,你脑海里好像就能模拟出它的信息增益计算、特征选择、节点分裂的过程,知道它的优和劣。最终,都是为了在实践中能快速识别出它就是最适合解决当前问题的模型。

基本功弄扎实是一个慢活儿,方法上可以是看经典原著、学习视频分享或是用高级语言实现。重要的是心态,能平心静气,不设预期,保持热情;另外,如果你有个可以分享的兴趣小组,那么恭喜你。因为学习书籍或看paper的过程其实挺枯燥的,但如果有分享的动力你可以走的更远。

实践

都知道实践出真知,可实践往往也很残酷。因为需求和约束多如牛毛,问题需要你来发现,留给你的时间窗口又很短暂。也就是,算法是一个偏确定性、有适用边界、标准化的事情;而业务则是发散的、多目标的、经验驱动的事情。

你首先是需要有双发现的眼睛,找到那个最值得发力的点,这个需要数据分析配合业务经验,剥丝抽茧留下最主要的优化目标、约束条件,尽量的简化问题;其次是有一张能说会道的嘴,要不然业务怎么愿意给你这个时间窗口让你尝试呢;最后是,在赌上你的人设之后,需要尽快在这个窗口期做出效果,在这个压力之下,考验你算法基本功的时候就到了。

结语

最后,让大家猜个谜语:它是一个贪心的家伙,计算复杂度不大,可以动态的做特征选择,不论特征是离散还是连续;它还可以先剪枝或后剪枝避免过拟合;它融合了信息熵的计算,也可以不讲道理的引入随机因子;它可以孤立的存在,也完全可以做集成学习,还可以配合LR一起来解决特征组合的问题;对小样本、正负样本不均、在线数据流的学习也可以支持;所得到则是有很强可解释性的规则。它,就是决策树。

每一个基础算法,都好比是颗带有智慧的种子,这不是普通的锦上添花,而是石破天惊的原创思维。有时不妨傻傻的回到过去的年代,伴着大师原创的步伐走一走,这种原创的智慧对未来长期的算法之路,大有裨益。神经网络从种下到开花,将近50年;贝叶斯也正在开花结果的路上。下一个,将会是谁呢?


作者:阿里巴巴淘系技术
来源:https://juejin.cn/post/7025964095530598436

收起阅读 »

背包问题_概述(动态规划)

写在前 问题描述 注意:0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。基本思路 第 i 件物品没添加到背包,最大价值:dp[i][j] = dp[i - 1]...
继续阅读 »




写在前

问题描述

有N件物品和一个最多能被重量为W 的背包。一个物品只有两个属性:重量和价值。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

注意:0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。

基本思路

这里有两个可变量体积和价值,我们定义dp[i][j]表示前i件物品体积不超过j能达到的最大价值,设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

  • 第 i 件物品没添加到背包,最大价值:dp[i][j] = dp[i - 1][j]

  • 第 i 件物品添加到背包中:dp[i][j] = dp[i - 1][j - w] + v

第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v)

代码实现

// W 为背包总重量
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values) {
   // dp[i][0]和dp[0][j]没有价值已经初始化0
   int[][] dp = new int[N + 1][W + 1];
   // 从dp[1][1]开始遍历填表
   for (int i = 1; i <= N; ++i) {
       // 第i件物品的重量和价值
       int w = weights[i - 1], v = values[i - 1];
       for (int j = 1; j <= W; ++j) {
           if (j < w) {
               // 超过当前状态能装下的重量j
               dp[i][j] = dp[i - 1][j];
          } else {
               dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
          }
      }
  }
   return dp[N][W];
}

dp[i][j]的值只与dp[i-1][0,...,j-1]有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)。因此,0-1 背包的状态转移方程为: dp[j] = max(dp[j], dp[j - w] + v)

特别注意:为了防止上一层循环的dp[0,...,j-1]被覆盖,循环的时候 j 只能逆向遍历。优化空间复杂度:

ps:滚动数组:即让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。

public int knapsack(int W, int N, int[] weights, int[] values) {
   int[] dp = new int[W + 1];
   for (int i = 1; i <= N; i++) {
       int w = weights[i - 1], v = values[i - 1];
       for (int j = W; j >= 1; --j) {
           if (j >= w) {
               dp[j] = Math.max(dp[j], dp[j - w] + v);
          }
      }
  }
   return dp[W];
}

ps:01背包内循环理解:还原成二维的dp就很好理解,一维的dp是二维dp在空间上进行复用的结果。dp[i]=f(dp[i-num]),等式的右边其实是二维dp上一行的数据,应该是只读的,在被读取前不应该被修改。如果正序的话,靠后的元素在读取前右边的dp有可能被修改了,倒序可以避免读取前被修改的问题。

作者:_code_x
来源:https://www.jianshu.com/p/b789ec845641

收起阅读 »

驳“低代码开发取代程序员”论 为什么专业开发者也需要低代码?

低代码又火了。近几年,腾讯、阿里、百度等互联网大厂纷纷入局,国内外低代码平台融资动辄数千万甚至数亿,以及伴随着热度而来的巨大争议……无不说明“低代码”的火爆。事实上,低代码并非新概念,它可以追溯到上世纪80年代的“第四代编程语言”。2014年,Forreste...
继续阅读 »

低代码又火了。

近几年,腾讯、阿里、百度等互联网大厂纷纷入局,国内外低代码平台融资动辄数千万甚至数亿,以及伴随着热度而来的巨大争议……无不说明“低代码”的火爆。

事实上,低代码并非新概念,它可以追溯到上世纪80年代的“第四代编程语言”。2014年,Forrester正式提出低代码的概念。低代码是一种软件开发技术,衍生于软件开发的高级语言,让使用者通过可视化的方式,以更少的编码,更快速地构建和交付应用软件,全方位降低软件的开发成本。与传统软件开发方式相比,低代码开发平台整合了软件开发和部署所需的 IDE(集成开发环境)、服务器和数据库管理工具,覆盖软件开发的全生命周期,我们可以将其理解为 Visual Studio + IIS + SQL Management Studio(.NET 技 术)或 Eclipse + Tomcat + MySQL Workbench(Java 技术)的组合。

编码更少、交付更快、成本更低,还覆盖软件开发全生命周期,怎么看低代码都可以说是不错的软件开发工具。那么,它又为什么引发争议,甚至被其主要用户群体之一——程序员所诟病呢?“低代码开发会取代程序员” 这一观点大行其是,它说得对吗?

为什么低代码引起专业开发者的反感?

技术浪潮引发巨大变革,也带来了无数“取代论”,比如机器翻译是否取代人类翻译、机器人记者是否取代人类记者,以及低代码开发是否取代程序员。

低代码虽然火爆,但程序员对此抱有不同的心态:

  • 轻视:低代码技术的诸多优势只是炒作,该技术更适合初学者,解决不了复杂的技术问题;

  • 恐惧:担心被低代码取代;

  • 抵触:低代码开发平台能够覆盖所有需求吗;大量封装组件使得低代码开发平台更像一个黑盒子,可能导致难以debug、难以修改和迭代升级等技术问题;低代码开发平台配置有大量组件,简单的拖拉拽动作即可完成大量开发工作,程序员不再需要厉害的技术能力。

那么,上述理由真的站得住脚吗?我们一一来看。

低代码的门槛真的低吗?

低代码开发过程常被比作拼积木:像拼搭积木一样,以可视化的方式,通过拖拉拽组件快速开发出数据填报、流程审批等应用程序,满足企业里比较简单的办公需求。

但这并不意味着低代码开发平台只能做到这些。

Gartner在2020年9月发布的《企业级低代码开发平台的关键能力报告》(Critical Capabilities for Enterprise Low-Code Application Platforms)中,列举了低代码的11项关键能力。

图源:http://www.gartner.com/en/document…

这里我们着重来看其中三项关键能力。

  • 数据建模和管理:该指标就是通常所讲的 “模型驱动” 。相比于表单驱动,模型驱动能够提供满足数据库设计范式的数据模型设计和管理能力。开发的应用复杂度越高,系统集成的要求越高,这个能力就越关键。

  • 流程和业务逻辑:流程应用与业务逻辑开发能力和效率。这个能力有两层,第一层是指使用该低代码开发平台能否开发出复杂的工作流和业务处理逻辑;第二层是开发这些功能时的便利性和易用性程度有多高。

  • 接口和集成:编程接口与系统集成能力。为了避免“数据孤岛”现象,企业级应用通常需要与其他系统进行集成,协同增效。此时,内置的集成能力和编程接口就变得至关重要。除非确认可预期的未来中,项目不涉及系统集成和扩展开发,开发者都应该关注这个能力。

这些关键能力表明低代码平台在建模与逻辑方面具备较强的能力,而接口和集成能力可使专业开发人员完成低代码无法实现的部分,通过低代码与专业代码开发的协作实现复杂应用的开发。 在涉及高价值或复杂的核心业务时,专业开发人员需要理解业务需求,厘清业务逻辑。从这个层面上看,低代码开发的门槛并不低。事实也是如此:海比研究在《2021 年中国低代码/无代码市场研究报告》中提到,截至 2020 年底,技术人员在低代码使用者中的比例超 75%,占主体地位。

低代码什么都能做吗?

程序员的工作围绕开发需求展开。在选择开发工具时,程序员通常考虑的首要问题是:这款工具能否覆盖所有需求?如果需求增加或变更,该工具是否支持相关操作?这些问题同样适用于低代码平台的选型。

在实际项目交付过程中,如果我们仅可以满足99%的需求,另外1%的需求满足不了,那么真实用户大概率是不会买单的。因此,在评估低代码产品的时候,我们一定要保证该平台可以支撑所有系统模块类型的开发,同时也要具备足够的扩展性,确保使用纯代码开发出的模块能够与低代码模块进行无缝集成,而这离不开编程接口。

以国内主流低代码开发平台活字格为例。该平台提供开箱即用的开发组件,同时为系统的各个分层均提供编程扩展能力,以满足企业级应用开发对扩展性的高要求。借助分层编程接口,开发者可以用纯代码的方式实现新增功能,无需受限于低代码开发平台的版本和现有功能。


活字格的编程扩展能力

当然,就具体应用领域而言,低代码开发平台也有其擅长和不擅长的地方。目前,低代码开发更多地被应用于2B企业应用开发,而对于用户量特大的头部互联网应用、对算法和复杂数据结构要求较高的应用,低代码平台则不太适合。

低代码开发不可控?

“低代码开发平台是个黑盒子,内部出问题无法排查和解决。开发过程中发现有问题怎么办?迭代升级难以实现怎么办?”很多程序员会有这种疑惑。

但我们需要注意的是,低代码开发平台本质上仍是软件开发工具,用户模型与软件开发周期支持是其关键能力之一。也就是说,成熟的低代码开发平台具备软件开发全生命周期所需的各项功能,从而大大简化开发者的技术栈,进一步提高开发效率。

具体而言,在面对频繁的需求变更、棘手的问题排查时,低代码开发平台引入了版本管理机制,从而更高效地进行代码审查、版本管理与协调,以及软件的迭代升级。至于debug,日志分析无疑是个好办法。例如,活字格把执行过程及细节以日志方式输出,方便程序员高效debug。

对程序员而言,低代码平台是限制还是助力?

“低代码”意味着更少的代码。代码都不怎么写了,程序员又该怎么成长,怎么获得职业成就感呢?

其实不然。

首先,开发 ≠ 写代码。低代码平台可以减少大量重复工作,提升开发效率,把专业开发人员从简单、重复的开发需求中解放出来,把精力投入到更有价值的事情上,比如精进技术、理清业务逻辑。

其次,低代码平台的组件化和拖拽式配置降低了开发门槛,新手程序员能够借助此类平台快速入门,加速升级打怪;有经验的程序员也有机会参与更多项目,甚至带团队,积累更多经验值,实现快速成长。

宁波聚轩就是一个例子。这家公司自2009年起就专注于智能制造、工业4.0、系统方案集成等领域的探索研究。在接触了低代码之后,项目负责人发现开发效率得到极大提升,采用传统方式需要一个月开发量的项目,现在需要半个月甚至更短的时间就可以完成。此外,其实践经验表明,低代码开发的学习成本较低,毕业新生经过一周学习,两周就可做项目,一个月就能熟练开发。

该公司在2021企业级低代码应用大赛中获得了应用创新奖,获奖作品是一套轴承行业数字化智造系统。这套系统主要集成了ERP、MES、WMS和设备机联网系统,覆盖了销售、采购、仓库、计划、生产、财务等全流程功能,且已经在生产现场投入使用。在开发过程中,宁波聚轩的开发团队利用低代码平台成功解决了定制化要求高、多终端需求等难题,及时完成项目交付。

结语

当迷雾散尽,低代码开发平台重新露出高效率开发工具的本色时,你会选择它吗?


作者:SegmentFault思否
来源:https://juejin.cn/post/7023579572096466974

收起阅读 »

JVM整体结构

JVM结构图类加载子系统加载连接初始化使用卸载运行时数据区域栈帧数据结构动态链接内存管理Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so...
继续阅读 »

Java虚拟机主要负责自动内存管理、类加载与执行、主要包括执行引擎、垃圾回收器、PC寄存器、方法区、堆区、直接内存、Java虚拟机栈、本地方法栈、及类加载子系统几个部分,其中方法区与Java堆区由所有线程共享、Java虚拟机栈、本地方法栈、PC寄存器线程私有,宏观的结构如下图所示:

JVM结构图

类加载子系统

从文件或网络中加载Class信息,类信息存放于方法区,类的加载包括加载->验证->准备->解析->初始化->使用->卸载几个阶段,详细流程后续文章会介绍。

  • 加载

从文件或网络中读取类的二进制数据、将字节流表示的静态存储结构转换为方法区运行时数据结构、并于堆中生成Java对象实例,类加载器既可以使用系统提供的加载器(默认),也可以自定义类加载器。

  • 连接

连接分为验证、准备、解析3个阶段,验证阶段确保类加载的正确性、准备阶段为类的静态变量分配内存,并将其初始化为默认值、解析阶段将类中的符号引用转换为直接引用

  • 初始化

初始化阶段负责类的初始化,Java中类变量初始化的方式有2种,声明类变量时指定初始值、静态代码块指定初始化,只有类被主动使用时才会触发类的初始化,类的初始化会先初始化父类,然后再初始化子类。

  • 使用

类访问方法区内的数据结构的接口,对象是堆区的数据

  • 卸载

程序执行了System.exit()、程序正常执行结束、JVM进程异常终止等

运行时数据区域

程序从静态的源代码到编译成为JVM执行引擎可执行的字节码,会经历类的加载过程,并于内存空间开辟逻辑上的运行时数据区域,便于JVM管理,其中各数据区域如下,其中垃圾回收JVM会自动自行管理

栈帧数据结构

Java中方法的执行在虚拟机栈中执行,为每个线程所私有,每次方法的调用和返回对应栈帧的压栈和出栈,其中栈帧中保存着局部变量表、方法返回地址、操作数栈及动态链接信息。

动态链接

Java中方法执行过程中,栈帧保存方法的符号引用,通过动态链接,将解析为符号引用。

内存管理

  • 内存划分(逻辑上)

Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so、s1,so和s1是大小相同的2块区域,生产环境可以根据具体的场景调整虚拟机内存分配比例参数,达到性能调优的效果。

堆区是JVM管理的最大内存区域,由所有线程共享,采用分代模型,堆区主要用于存放对象实例,堆可以是物理上不连续的空间,逻辑上连续即可,其中堆内存大小可以通过虚拟机参数-Xmx、-Xms指定,当堆无法继续扩展时,将抛出OOM异常。

  • 运行时实例

假设存在如下的SimpleHeap测试类,则SimpleHeap在内存中的堆区、Java栈、方法区对应的映射关系如下图所示:

public class SimpleHeap {
   /**
    * 实例变量
    */
   private int id;

   /**
    * 构造器
    *
    * @param id
    */
   public SimpleHeap(int id) {
       this.id = id;
  }

   /**
    * 实例方法
    */
   private void displayId() {
       System.out.println("id:" + id);
  }

   public static void main(String[]args){
       SimpleHeap heap1=new SimpleHeap(1);
       SimpleHeap heap2=new SimpleHeap(2);
       heap1.displayId();
       heap2.displayId();
  }

}
复制代码


同理,建设存在Person类,则创建对象实例后,内存中堆区、Java方法栈、方法区三者关系如下图:

直接内存

Java NIO库允许Java程序使用直接内存,直接内存是独立于Java堆区的一块内存区域,访问内存的速度优于堆区,出于性能考虑,针对读写频繁的场景,可以直接操作直接内存,它的大小不受Xmx参数的限制,但堆区内存和直接内存总和必须小于系统内存。

PC寄存器

线程私有空间,又称之为程序计数器,任意时刻,Java线程总在执行一个方法,正在执行的方法,我们称之为:"当前方法”,如果当前方法不是本地方法,则PC寄存器指向当前正在被执行的指令;若当前方法是本地方法,则PC寄存器的值是undefined。

垃圾回收系统

垃圾回收系统是Java虚拟机中的重要组成部分,其主要对方法区、堆区、直接内存空间进行回收,与C/C++不同,Java中所有对象的空间回收是隐式进行的,其中垃圾回收会根据GC算法自动完成内存管理。


作者:洞幺幺洞
来源:https://juejin.cn/post/7040742081236566029

收起阅读 »

Apache Log4j 漏洞(JNDI注入 CVE-2021-44228)

本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力...
继续阅读 »



本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力”,而特推 1 则是一个漏洞检测工具,能预防类似漏洞的发生。

除了安全相关的 2 个特推项目之外,本周 GitHub 热门项目还有高性能的 Rust 运行时项目,在你不知道用何词时给你参考词的反向词典 WantWords,还有可玩性贼高的终端模拟器 Tabby。

以下内容摘录自微博@HelloGitHub 的 GitHub Trending 及 Hacker News 热帖(简称 HN 热帖),选项标准:新发布 | 实用 | 有趣,根据项目 release 时间分类,发布时间不超过 14 day 的项目会标注 New,无该标志则说明项目 release 超过半月。由于本文篇幅有限,还有部分项目未能在本文展示,望周知 🌝

  • 本文目录

      1. 本周特推

      • 1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

      • 1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

      1. GitHub Trending 周榜

      • 2.1 塞尔达传说·时之笛反编译:oot

      • 2.2 终端模拟器:Tabby

      • 2.3 反向词典:WantWords

      • 2.4 CPU 性能分析和调优:perf-book

      • 2.5 高性能 Rust Runtime:Monoio

      1. 往期回顾

1. 本周特推

1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

本周 star 增长数: 200+

JNDI-Injection-Exploit 并非是一个新项目,它是一个可用于 Fastjson、Jackson 等相关漏洞的验证的工具,作为 JNDI 注入利用工具,它能生成 JNDI 链接并启动后端相关服务进而检测系统。

GitHub 地址→github.com/welk1n/JNDI…

1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

本周 star 增长数: 1,500+

New CVE-2021-44228-Apache-Log4j-Rce 是 Apache Log4j 远程代码执行,受影响的版本 < 2.15.0。项目开源 1 天便标星 1.5k+ 可见本次 Log4j 漏洞受关注程度。

GitHub 地址→github.com/tangxiaofen…

2. GitHub Trending 周榜

2.1 塞尔达传说·时之笛反编译:oot

本周 star 增长数:900+

oot 是一个反编译游戏塞尔达传说·时之笛的项目,目前项目处于半成品状态,会有较大的代码变更,项目从 scratch 中重新构建代码,并用游戏中发现的信息以及静态、动态分析。如果你想通过这个项目了解反编译知识,建议先保存好个人的塞尔代资产。

GitHub 地址→github.com/zeldaret/oo…

2.2 终端模拟器:Tabby

本周 star 增长数:1,800+

Tabby(曾叫 Terminus)是一个可配置、自定义程度高的终端模拟器、SSH 和串行客户端,适用于 Windows、macOS 和 Linux。它是一种替代 Windows 标准终端 conhost、PowerShell ISE、PuTTY、macOS terminal 的存在,但它不是新的 shell 也不是 MinGW 和 Cygwin 的替代品。此外,它并非一个轻量级工具,如果你注重内存,可以考虑 ConemuAlacritty

GitHub 地址→github.com/Eugeny/tabb…

2.3 反向词典:WantWords

本周 star 增长数:1,000+

WantWords 是清华大学计算机系自然语言处理实验室(THUNLP)开源的反向字词查询工具,反向词典并非是查询反义词的词典,而是基于目前网络词官广泛使用导致部分场景下,我们表达某个意思未能找到精准的用词,所以它可以让你通过想要表达的意思来找寻符合语境的词汇。你可以在线体验反向词典:wantwords.thunlp.org/ 。下图分别为项目 workflow 以及查询结果。

GitHub 地址→github.com/thunlp/Want…

2.4 CPU 性能分析和调优:perf-book

本周 star 增长数:1,300+

perf-book 是书籍《现代 CPU 的性能分析和调优》开源版本,你可以通过 python.exe export_book.py && pdflatex book.tex && bibtex book && pdflatex book.tex && pdflatex book.tex 命令导出 pdf。

GitHub 地址→github.com/dendibakh/p…

2.5 高性能 Rust Runtime:Monoio

本周 star 增长数:1,250+

New Monoio 是字节开源基于 io-uring 的 thread-per-core 模型高性能 Rust Runtime,旨在为高性能网络中间件等场景提供必要的运行时。详细项目背景可以阅读团队的文章Monoio:基于 io-uring 的高性能 Rust Runtime

GitHub 地址→github.com/bytedance/m…

3. 往期回顾

以上为 2021 年第 50 个工作周的 GitHub Trending 🎉如果你 Pick 其他好玩、实用的 GitHub 项目,记得来 HelloGitHub issue 区和我们分享下哟 🌝

最后,记得你在本文留言区留下你想看的主题 Repo(限公众号),例如:AI 换头。👀 和之前的送书活动类似,留言点赞 Top5 的小伙伴(),小鱼干会努力去找 Repo 的^^

作者:HelloGitHub
来源:https://juejin.cn/post/7040980646423953439

收起阅读 »

Redis分布式锁

需求分布式应⽤进⾏逻辑处理时经常会遇到并发问题。互斥访问某个网络上的资源,需要有一个存在于网络上的锁服务器,负责锁的申请与回收。Redis 可以充当锁服务器的角色。首先,Redis 是单进程单线程的工作模式,所有前来申请锁资源的请求都被排队处理,能保证锁资源的...
继续阅读 »



需求

分布式应⽤进⾏逻辑处理时经常会遇到并发问题。

互斥访问某个网络上的资源,需要有一个存在于网络上的锁服务器,负责锁的申请与回收。Redis 可以充当锁服务器的角色。首先,Redis 是单进程单线程的工作模式,所有前来申请锁资源的请求都被排队处理,能保证锁资源的同步访问。

适用原因:

  • Redis 可以被多个客户端共享访问,是·共享存储系统,可以用来保存分布 式锁

  • Redis 的读写性能高,可以应对高并发的锁操作场景。

实现

在分布式场景下,锁变量需要由一个共享存储系统来维护,这样,多个客户端可以通过访问共享存储系统来访问锁变量。

简单实现

模仿单机上的锁,使用锁变量即可在Redis上实现分布式锁。

我们可以在 Redis 服务器设置一个键值对,用以表示一把互斥锁,当申请锁的时候,要求申请方设置(SET)这个键值对,当释放锁的时候,要求释放方删除(DEL)这个键值对。

但最基本需要保证加锁解锁操作的原子性。同时为了保证锁在异常情况下能被释放,必须设置超时时间。

Redis 2.8 版本中作者加⼊了 set 指令的扩展参数,使得 setnx 和 expire 指令可以⼀起执⾏

加锁原子操作

加锁包含了三个操作(读取锁变量、判断锁变量值以及把锁变量值设置为 1),而这 三个操作在执行时需要保证原子性。

首先是 SETNX 命令,它用于设置键值对的值。具体来说,就是这个命令在执行时会判断键 值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。

超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻 辑执⾏的太长,超出了锁的超时限制,就无法保证互斥。

解决方案

最简单的就是避免Redis 分布式锁⽤于较⻓时间的任务。如果真的偶尔出现了,数据出现的⼩波错乱可能需要⼈⼯介⼊解决。

判断拥有者

为了防止锁变量被拥有者之外的客户端进程删除,需要能区分来自不同客户端的锁操作

set 指令的 value 参数设置为⼀个 随机数,释放锁时先匹配随机数是否⼀致,然后再删除 key,这是为 了确保当前线程占有的锁不会被其它线程释放,除⾮这个锁是过期了被服务器⾃动释放的。

Redis 给 SET 命令提供 了类似的选项 NX,用来实现“不存在即设置”。如果使用了 NX 选项,SET 命令只有在键 值对不存在时,才会进行设置,否则不做赋值操作。此外,SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间。

可重入问题

可重⼊性是指线程在持有锁的情况下再次请求加锁,如果⼀个锁⽀持 同⼀个线程的多次加锁,那么这个锁就是可重⼊的。Redis 分布式锁如果要⽀持 可重⼊,需要对客户端的 set ⽅法进⾏包装,使⽤线程的 Threadlocal 变量存储当前持有锁的计数。

分布式拓展

单Redis实例并不能满足我们的高可用要求,一旦实例崩溃,就无法对外分布式锁服务。

但在集群环境下,这种只对主Redis实例使用上述方案是有缺陷 的,它不是绝对安全的。

一旦主节点挂掉,但锁变量没有及时同步,就会导致互斥被破坏。

Redlock

为了解决这个问题,Antirez 发明了 Redlock 算法

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户 端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布 式锁了,否则加锁失败。

执行步骤:

  • 获取当前时间

  • 客户端按顺序依次向 N 个 Redis 实例执行加锁操作

  • 一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过 程的总耗时。客户端只有在满足下面的这两个条件时,才能认为是加锁成功

    • 客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;

    • 客户端获取锁的总耗时没有超过锁的有效时间。


作者:不是二向箔
来源:https://juejin.cn/post/7038155529566289957

收起阅读 »

短信跳小程序

方案:使用微信提供的url link方法 生成短链接 发送给用户 用户点击短链接会跳转到微信提供默认的默认页面 进而打开小程序场景假设:经理人发布一条运输任务 司机收到短信点击打开小程序接单经理人发布时点击发布按钮 h5调用服务端接口 传参服务端要跳转的小程序...
继续阅读 »

方案:使用微信提供的url link方法 生成短链接 发送给用户 用户点击短链接会跳转到微信提供默认的默认页面 进而打开小程序
场景假设:经理人发布一条运输任务 司机收到短信点击打开小程序接单

实现:

  • 经理人发布时点击发布按钮 h5调用服务端接口 传参服务端要跳转的小程序页面 所需要参数

  • 服务端拿access_token和前端传的参数 加链接失效时间 调用微信api

api.weixin.qq.com/wxa/generat… 得到链接 如wxaurl.cn/ow7ctZP4n8v 将此链接发送短信给司机

  • 司机点击此链接 效果如下图所示:打开小程序 h5写逻辑跳转指定页面

自己调postman调微信api post方式 接口: api.weixin.qq.com/wxa/generat…

传参

{ 
    "path": "pages/index/index",\
    "query": "?fromType=4&transportBulkLineId=111&isLinkUrlCome=1&SCANFROMTYPE=143&lineAssignRelId=111",\
     "env_version": "trial",\
     "is_expire": true,\
    "expire_time": "1638855772"\
}

返参

{
     "errcode": 0,
    "errmsg": "ok",
    "url_link": "https://wxaurl.cn/GAxGcil2Bbp"
}

url link说明文档: developers.weixin.qq.com/miniprogram…
url link方法需要服务端调用 调用接口方式参考:
developers.weixin.qq.com/miniprogram…

作者:懿小诺
来源:https://juejin.cn/post/7037356611031007239

收起阅读 »

前端自动化部署:借助Gitlab CI/CD实现

🛫 前端自动化部署:借助Gitlab CI/CD实现🌏 概论传统的前端部署往往都要经历:本地代码更新 => 本地打包项目 => 清空服务器相应目录 => 上传项目包至相应目录几个阶段,这些都是机械重复的步骤。对于这一过程我们往往可以通过CI/...
继续阅读 »

🛫 前端自动化部署:借助Gitlab CI/CD实现

🌏 概论

传统的前端部署往往都要经历:本地代码更新 => 本地打包项目 => 清空服务器相应目录 => 上传项目包至相应目录几个阶段,这些都是机械重复的步骤。对于这一过程我们往往可以通过CI/CD方法进行优化。所谓CI/CD,即持续集成/持续部署,以上我们所说的步骤便可以看作是持续部署的一种形态,其更详细的解释大家可以自行了解。


JenkinsTravis CI这些都是可以完成持续部署的工具。除此之外,Gitlab CI/CD也能很好的完成这一需求。下面就来详细介绍下。


🌏 核心工具

GitLab Runner

GitLab Runner是配合GitLab CI/CD完成工作的核心程序,出于性能考虑,GitLab Runner应该与Gitlab部署在不同的服务器上(Gitlab在单独的仓库服务器上,GitLab Runner在部署web应用的服务器上)。GitLab Runner在与GitLab关联后,可以在服务器上完成诸如项目拉取、文件打包、资源复制等各种命令操作。


Git

web服务器上需要安装Git来进行远程仓库的获取工作。


Node

用于在web服务器上完成打包工作。


NPM or Yarn or pnpm

用于在web服务器上完成依赖下载等工作(用yarn,pnpm亦可)。


web服务器上的所需程序

🌏 流程

这里我自己用的是centOS环境:


1. 在web服务器上安装所需工具

(1)安装Node


# 下载node包
wget https://nodejs.org/dist/v16.13.0/node-v16.13.0-linux-x64.tar.xz

# 解压Node包
tar -xf node-v16.13.0-linux-x64.tar.xz

# 在配置文件(位置多在/etc/profile)末尾添加:
export PATH=$PATH:/root/node-v16.13.0-linux-x64/bin

# 刷新shell环境:
source /etc/profile

# 查看版本(输出版本号则安装成功):
node -v

#后续安装操作,都可通过-v或者--version来查看是否成功

npm已内置在node中,如要使用yarn或,则可通过npm进行全局安装,命令与我们本地环境下的使用命令是一样的:


npm i yarn -g
#or
npm i pnpm -g

(2)安装Git


# 利用yum安装git
yum -y install git

# 查看git版本
git --version

(3)安装Gitlab Runner


# 安装程序
wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# 等待下载完成后分配权限
chmod +x /usr/local/bin/gitlab-runner

# 创建runner用户
useradd --comment 'test' --create-home gitlab-runner --shell /bin/bash

# 安装程序
gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

# 启动程序
gitlab-runner start

# 安装完成后可使用gitlab-runner --version查看是否成功

2. 配置Runner及CI/CD

基本的安装操作完成后,就是最核心的阶段:Runner与CI/CD的配置。


(1)配置Gitlab Runner


首先打开待添加自动部署功能的gitlab仓库,在其中设置 > CI/CD > Runner中找到runner配置信息备用:


image.png

在web服务器中配置runner:


gitlab-runner register

>> Enter the GitLab instance URL (for example, https://gitlab.com/):
# 输入刚才获取到的gitlab仓库地址
>> Enter the registration token:
# 输入刚才获取到的token
>> Enter a description for the runner:
# 自定runner描述
>> Enter tags for the runner (comma-separated):
# 自定runner标签
>> Enter an executor: docker-ssh, docker+machine, docker-ssh+machine, docker, parallels, shell, ssh, virtualbox, kubernetes, custom:
# 选择执行器,此处我们输入shell

完整示例:image.png

(2)配置.gitlab-ci.yml


.gitlab-ci.yml文件是流水线执行的流程文件,Runner会据此完成规定的一系列流程。


我们在项目根目录中创建.gitlab-ci.yml文件,然后在其中编写内容:


# 阶段
stages:
- install
- build
- deploy

cache:
paths:
- node_modules/

# 安装依赖
install:
stage: install
# 此处的tags必须填入之前注册时自定的tag
tags:
- deploy
# 规定仅在package.json提交时才触发此阶段
only:
changes:
- package.json
# 执行脚本
script:
yarn

# 打包项目
build:
stage: build
tags:
- deploy
script:
- yarn build
# 将此阶段产物传递至下一阶段
artifacts:
paths:
- dist/

# 部署项目
deploy:
stage: deploy
tags:
- deploy
script:
# 清空网站根目录,目录请根据服务器实际情况填写
- rm -rf /www/wwwroot/stjerne/salary/*
# 复制打包后的文件至网站根目录,目录请根据服务器实际情况填写
- cp -rf ${CI_PROJECT_DIR}/dist/* /www/wwwroot/stjerne/salary/

保存并推送至gitlab后即可自动开始构建部署。


构建中可在gitlab CI/CD面板查看构建进程:


image.png

待流水线JOB完成后可前往页面查看🛫🛫🛫🛫🛫


作者:星始流年
来源:https://juejin.cn/post/7037022688493338661

收起阅读 »

如何实现跨设备的双向连接? Labo涂鸦鸿蒙亲子版分布式开发技术分享

近期,首届HarmonyOS开发者创新大赛正式落下帷幕。大赛共历时5个月,超过3000支队伍的10000多名选手参赛,25000多位开发者参与了大赛学习,最终23支参赛队伍斩获奖项,产出了多款有创新、有创意、有价值的优秀作品。其中由“Labo Lado儿童艺术...
继续阅读 »

近期,首届HarmonyOS开发者创新大赛正式落下帷幕。大赛共历时5个月,超过3000支队伍的10000多名选手参赛,25000多位开发者参与了大赛学习,最终23支参赛队伍斩获奖项,产出了多款有创新、有创意、有价值的优秀作品。其中由“Labo Lado儿童艺术创想”团队打造的《Labo涂鸦鸿蒙亲子版》就是其中之一,其创造性地通过HarmonyOS分布式技术,实现了多设备下的亲子互动涂鸦功能,最终摘得大赛一等奖。

在很早以前,“Labo Lado儿童艺术创想”团队就做过一款涂鸦游戏的应用,该应用可以让孩子和父母在一个平板或者手机上进行绘画比赛,比赛的方式就是屏幕一分为二,两人各在设备的一边进行涂鸦。这种方式虽然有趣,但是对于绘画而言,屏幕尺寸限制了用户的发挥和操作。因此团队希望这类玩法能通过多个设备完成,于是他们研究了ZeroConf、iOS的Multipeer Connectivity、Google Nearby等近距离互联的技术, 结果发现这些技术在设备发现和应用拉起方面实现的都不理想,尤其是当目标用户是儿童的情况下,操作起来不够简便也不易上手。

HarmonyOS的出现给团队带来了希望。他们发现HarmonyOS的分布式技术有着很大的应用潜力,这项技术让设备的发现和应用拉起变的非常的简单自然,互联的过程也很流畅,很好地解决了单机操作的限制,让跨设备联机功能能够非常容易地实现。此外,HarmonyOS的开发也给团队留下了很深刻的印象,以往繁琐的开发步骤,在 HarmonyOS 中仅需几个配置、几行代码即可完成,无需花费太多精力。在《Labo涂鸦鸿蒙亲子版》里面的5个分布式玩法的开发只用了团队一名开发者不到两个月的时间,其中还包括了学习上手、解决文档不全和各种疑难问题的过程。

以下是“Labo Lado儿童艺术创想”团队基于HarmonyOS的分布式开发关键技术的简单分享:

一、分布式技术实践

HarmonyOS的分布式能力是在系统层面实现的,在设备双方同属一个局域网的情况下,设备都可以快速的发现和进行流畅的通讯。下面将从HarmonyOS设备的发现、应用的拉起、应用通讯和双向通讯几个部分来进行分享。

1、设备的发现

假设设备A想要邀请另外一个设备B加入,AB任何一方都无需启动特别的广播服务,只要发起方设备A在应用内调用设备发现代码,就可以列出附近符合条件可用的的设备。

以下是获取设备列表的示例代码:

public static List<DeviceInfo> getRemoteDevice() {

List<DeviceInfo> deviceInfoList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);

return deviceInfoList;

}

列出设备之后,用户就可以通过设备名选择想要邀请的设备了。

(左侧设备A发现右侧名为“ye”的设备B的界面展示)

2、应用的拉起

设备A邀请了设备B之后,如果设备B上应用没启动,设备A可直接通过调用startAbility方法来拉起设备B上的应用。双方应用都启动了之后,就可以进行RPC通讯了。如果需要事先检查设备B上的应用是否已经启动或者是否在后台,可通过在应用中增加一个PA来实现。在拉起之前,设备A先连接设备B的应用中的PA可以实现更复杂精准的远程应用启动控制。

3、应用通讯

在应用中启动一个PA,专门用作通讯的服务器端。当设备B的应用被拉起之后,设备A就会通过connectAbility与设备B的PA进行连接,通讯采用RPC方式实现,并使用IDL定义通讯接口。

4、双向通讯

RPC的通讯方式使用简单,但是只能支持单向通讯。为了实现双向通讯,可在设备A与设备B发起建立连接成功之后,再让设备B与设备A发起建立一个连接,用两个连接实现了双向通讯。下面是这两个连接建立过程的示意时序图:

在设备A与设备B建立连接的时候,设备A必须将自己的DeviceId发送给设备B,然后设备B才可以主动发起一个与设备A的连接,获取当前设备的DeviceId方法如下:

KvManagerFactory.getInstance().createKvManager(new KvManagerConfig(this)).getLocalDeviceInfo().getId()

应用中,FA主要实现了界面层逻辑,PA部分用做数据通讯的服务端。为了防止拉起应用导致用户当前面的操作被中断,可通过PA来查询当前FA的状态,如果FA已经启动了,就跳过拉起,直接进行下一步操作即可。

二、数据接口与数据结构定义

使用了IDL定义了两个通用的接口,分别用来进行异步和同步调用:

int sendSyncCommand([in] int command, [in] String params);

void sendAsyncCommand([in] int command, [in] String params, [in] byte[] content);

大部分情况下,远程调用大部分都通过同步的方式进行,用户之间的绘画数据通过异步接口传输,数据在用户绘制的时候采集,每50ms左右发送一次,这个频率可以大概保证用户视觉上没有卡顿,而又不至于因为接口过度调用导致卡顿或者耗电量过大。采集的绘画数据的数据结构大致如下:

enum action //动作,表示落笔、移动、提笔等动作

int tagId //多点触摸识别标记

int x //x坐标

int y //y坐标

enum brushType //笔刷类型

int brushSize //笔刷大小

enum brushColor //笔刷颜色

int layer //图层

这款应用是支持多点触摸的,所以每个触摸点在落笔的的时候,都使用了tagId进行标记。这些数据除了通讯外,还会完整地保存在文件中,这样用户就可以通过应用内的播放功能播放该数据,回看绘画的整个过程。

三、教程录制与曲线平滑

1、教程制作

这款产品的特色之一是教程是动态的,用户可以自己拼装或者通过游戏生成教程角色。目前应用内置六种教程。这些教程预先由设计师在photoshop中画好并标记各个部位,然后再通过专门的photoshop脚本导出到教程录制应用中,再由设计师按部位逐个进行临摹绘制,绘制完成,应用会将设计师的绘制过程数据保存为json文件,通过将这些json的文件里的部位互换,我们就实现了用户自己拼装教程的功能了。

2、曲线平滑

绘制过程,为了让用户绘制的曲线更加平滑,采用二次贝塞尔曲线算法进行差值(Quadratic Bezier Curve),该算法简单效率也非常不错:

public Point quadraticBezier(Point p0, Point p1, Point p2, float t) {

Point pFinal = new Point();

pFinal.x = (float) (Math.pow(1 - t, 2) * p0.x + (1 - t) * 2 * t * p1.x + t * t * p2.x);

pFinal.y = (float) (Math.pow(1 - t, 2) * p0.y + (1 - t) * 2 * t * p1.y + t * t * p2.y);

return pFinal;

}

基于HarmonyOS的分布式特性,《Labo涂鸦鸿蒙亲子版》完成了一次已有应用的自我尝试和突破,大大的增加了用户在使用过程中的乐趣,为用户带来了全新的跨设备亲子交互体验,“Labo Lado儿童艺术创想”团队在未来将与更多的HarmonyOS开发者一起,为用户创作出更多更有趣的儿童创造类应用。

近一段时间以来,HarmonyOS 2的发布吸引了广大开发者的关注。作为一款面向万物互联时代的智能终端操作系统,HarmonyOS 2带来了诸多新特性、新功能和新玩法,等待开发者去探索、去学习、去实践。也欢迎广大开发者继续发挥创造力和想象力,基于HarmonyOS开发出更多有创新、有创意、有价值的作品,打造出专属于万物互联时代的创新产品。

收起阅读 »

Uni-app:(不通用)HbuilderX启动别人uni-app项目运行到小程序,提示打开了小程序开发者工具但是不进页面

前段时间下载了环信uni-app Demo,启动HBuilderX,运行到了小程序,但是死活打不开页面不知道什么原因。开发者工具打开了但是页面进不去。 不是什么大问题,但是也很耽误时间,Hello world,连Hello都Hello不出来,最后研究了半天才解...
继续阅读 »
前段时间下载了环信uni-app Demo,启动HBuilderX,运行到了小程序,但是死活打不开页面不知道什么原因。开发者工具打开了但是页面进不去。
不是什么大问题,但是也很耽误时间,Hello world,连Hello都Hello不出来,最后研究了半天才解决了这个问题下面是解决方法:
除了查看对应版本和开启端口外还需注意,因为微信开发者工具是和对应的微信账号绑定的,所以需要设置对应账号的AppID才行,如果使用的是别人的需要用自己的AppID,但是启动别人的项目只用在,mainifest.json下的微信小程序配置,找到微信小程序APPID 删除掉然后再重新启动运行就可以了。




由于大小限制上传的图片是个压缩包,大家不清楚位置的可以下载下来瞜一眼。
  收起阅读 »