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

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


  • 首页

  • 归档

  • 搜索

Android V1及V2签名原理简析 Android的AP

发表于 2019-05-08

Android为了保证系统及应用的安全性,在安装APK的时候需要校验包的完整性,同时,对于覆盖安装的场景还要校验新旧是否匹配,这两者都是通过Android签名机制来进行保证的,本文就简单看下Android的签名与校验原理,分一下几个部分分析下:

  • APK签名是什么
  • APK签名如何保证APK信息完整性
  • 如何为APK签名
  • APK签名怎么校验

Android的APK签名是什么

签名是摘要与非对称密钥加密相相结合的产物,摘要就像内容的一个指纹信息,一旦内容被篡改,摘要就会改变,签名是摘要的加密结果,摘要改变,签名也会失效。Android APK签名也是这个道理,如果APK签名跟内容对应不起来,Android系统就认为APK内容被篡改了,从而拒绝安装,以保证系统的安全性。目前Android有三种签名V1、V2(N)、V3(P),本文只看前两种V1跟V2,对于V3的轮密先不考虑。先看下只有V1签名后APK的样式:

image.png

再看下只有V2签名的APK包样式:

image.png

同时具有V1 V2签名:

image.png

可以看到,如果只有V2签名,那么APK包内容几乎是没有改动的,META_INF中不会有新增文件,按Google官方文档:在使用v2签名方案进行签名时,会在APK文件中插入一个APK签名分块,该分块位于zip中央目录部分之前并紧邻该部分。在APK签名分块内,签名和签名者身份信息会存储在APK签名方案v2分块中,保证整个APK文件不可修改,如下图:

image.png

而V1签名是通过META-INF中的三个文件保证签名及信息的完整性:

image.png

APK签名如何保证APK信息完整性

V1签名是如何保证信息的完整性呢?V1签名主要包含三部分内容,如果狭义上说签名跟公钥的话,仅仅在.rsa文件中,V1签名的三个文件其实是一套机制,不能单单拿一个来说事,

MANIFEST.MF:摘要文件,存储文件名与文件SHA1摘要(Base64格式)键值对,格式如下,其主要作用是保证每个文件的完整性

摘要

如果对APK中的资源文件进行了替换,那么该资源的摘要必定发生改变,如果没有修改MANIFEST.MF中的信息,那么在安装时候V1校验就会失败,无法安装,不过如果篡改文件的同时,也修改其MANIFEST.MF中的摘要值,那么MANIFEST.MF校验就可以绕过。

CERT.SF:二次摘要文件,存储文件名与MANIFEST.MF摘要条目的SHA1摘要(Base64格式)键值对,格式如下

image.png

CERT.SF个人觉得有点像冗余,更像对文件完整性的二次保证,同绕过MANIFEST.MF一样,.SF校验也很容易被绕过。

CERT.RSA 证书(公钥)及签名文件,存储keystore的公钥、发行信息、以及对CERT.SF文件摘要的签名信息(利用keystore的私钥进行加密过)

CERT.RSA与CERT.SF是相互对应的,两者名字前缀必须一致,不知道算不算一个无聊的标准。看下CERT.RSA文件内容:

image.png

CERT.RSA文件里面存储了证书公钥、过期日期、发行人、加密算法等信息,根据公钥及加密算法,Android系统就能计算出CERT.SF的摘要信息,其严格的格式如下:

X.509证书格式

从CERT.RSA中,我们能获的证书的指纹信息,在微信分享、第三方SDK申请的时候经常用到,其实就是公钥+开发者信息的一个签名:

image.png

除了CERT.RSA文件,其余两个签名文件其实跟keystore没什么关系,主要是文件自身的摘要及二次摘要,用不同的keystore进行签名,生成的MANIFEST.MF与CERT.SF都是一样的,不同的只有CERT.RSA签名文件。也就是说前两者主要保证各个文件的完整性,CERT.RSA从整体上保证APK的来源及完整性,不过META_INF中的文件不在校验范围中,这也是V1的一个缺点。V2签名又是如何保证信息的完整性呢?

V2签名块如何保证APK的完整性

前面说过V1签名中文件的完整性很容易被绕过,可以理解单个文件完整性校验的意义并不是很大,安装的时候反而耗时,不如采用更加简单的便捷的校验方式。V2签名就不针对单个文件校验了,而是针对APK进行校验,将APK分成1M的块,对每个块计算值摘要,之后针对所有摘要进行摘要,再利用摘要进行签名。

image.png

也就是说,V2摘要签名分两级,第一级是对APK文件的1、3 、4 部分进行摘要,第二级是对第一级的摘要集合进行摘要,然后利用秘钥进行签名。安装的时候,块摘要可以并行处理,这样可以提高校验速度。

简单的APK签名流程(签名原理)

APK是先摘要,再签名,先看下摘要的定义:Message Digest:摘要是对消息数据执行一个单向Hash,从而生成一个固定长度的Hash值,这个值就是消息摘要,至于常听到的MD5、SHA1都是摘要算法的一种。理论上说,摘要一定会有碰撞,但只要保证有限长度内碰撞率很低就可以,这样就能利用摘要来保证消息的完整性,只要消息被篡改,摘要一定会发生改变。但是,如果消息跟摘要同时被修改,那就无从得知了。

而数字签名是什么呢(公钥数字签名),利用非对称加密技术,通过私钥对摘要进行加密,产生一个字符串,这个字符串+公钥证书就可以看做消息的数字签名,如RSA就是常用的非对称加密算法。在没有私钥的前提下,非对称加密算法能确保别人无法伪造签名,因此数字签名也是对发送者信息真实性的一个有效证明。不过由于Android的keystore证书是自签名的,没有第三方权威机构认证,用户可以自行生成keystore,Android签名方案无法保证APK不被二次签名。

知道了摘要跟签名的概念后,再来看看Android的签名文件怎么来的?如何影响原来APK包?通过sdk中的apksign来对一个APK进行签名的命令如下:

1
复制代码 ./apksigner sign  --ks   keystore.jks  --ks-key-alias keystore  --ks-pass pass:XXX  --key-pass pass:XXX  --out output.apk input.apk

其主要实现在 android/platform/tools/apksig 文件夹中,主体是ApkSigner.java的sign函数,函数比较长,分几步分析

1
2
3
4
5
6
7
8
9
10
11
12
复制代码private void sign(
DataSource inputApk,
DataSink outputApkOut,
DataSource outputApkIn)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
// Step 1. Find input APK's main ZIP sections
ApkUtils.ZipSections inputZipSections;
<!--根据zip包的结构,找到APK中包内容Object-->
try {
inputZipSections = ApkUtils.findZipSections(inputApk);
...

先来看这一步,ApkUtils.findZipSections,这个函数主要是解析APK文件,获得ZIP格式的一些简单信息,并返回一个ZipSections,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码 public static ZipSections findZipSections(DataSource apk)
throws IOException, ZipFormatException {
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
long eocdOffset = eocdAndOffsetInFile.getSecond();
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
...
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
long cdEndOffset = cdStartOffset + cdSizeBytes;
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
return new ZipSections(
cdStartOffset,
cdSizeBytes,
cdRecordCount,
eocdOffset,
eocdBuf);
}

ZipSections包含了ZIP文件格式的一些信息,比如中央目录信息、中央目录结尾信息等,对比到zip文件格式如下:

image.png

获取到 ZipSections之后,就可以进一步解析APK这个ZIP包,继续走后面的签名流程,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码    long inputApkSigningBlockOffset = -1;
DataSource inputApkSigningBlock = null;
<!--检查V2签名是否存在-->
try {
Pair<DataSource, Long> apkSigningBlockAndOffset =
V2SchemeVerifier.findApkSigningBlock(inputApk, inputZipSections);
inputApkSigningBlock = apkSigningBlockAndOffset.getFirst();
inputApkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
} catch (V2SchemeVerifier.SignatureNotFoundException e) {
<!--V2签名不存在也没什么问题,非必须-->
}
<!--获取V2签名以外的信息区域-->
DataSource inputApkLfhSection =
inputApk.slice(
0,
(inputApkSigningBlockOffset != -1)
? inputApkSigningBlockOffset
: inputZipSections.getZipCentralDirectoryOffset());

可以看到先进行了一个V2签名的检验,这里是用来签名,为什么先检验了一次?第一次签名的时候会直接走这个异常逻辑分支,重复签名的时候才能获到取之前的V2签名,怀疑这里获取V2签名的目的应该是为了排除V2签名,并获取V2签名以外的数据块,因为签名本身不能被算入到签名中,之后会解析中央目录区,构建一个DefaultApkSignerEngine用于签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码      <!--解析中央目录区,目的是为了解析AndroidManifest-->
// Step 2. Parse the input APK's ZIP Central Directory
ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
List<CentralDirectoryRecord> inputCdRecords =
parseZipCentralDirectory(inputCd, inputZipSections);

// Step 3. Obtain a signer engine instance
ApkSignerEngine signerEngine;
if (mSignerEngine != null) {
signerEngine = mSignerEngine;
} else {
// Construct a signer engine from the provided parameters
...
List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
new ArrayList<>(mSignerConfigs.size());
<!--一般就一个-->
for (SignerConfig signerConfig : mSignerConfigs) {
engineSignerConfigs.add(
new DefaultApkSignerEngine.SignerConfig.Builder(
signerConfig.getName(),
signerConfig.getPrivateKey(),
signerConfig.getCertificates())
.build());
}
<!--默认V1 V2都启用-->
DefaultApkSignerEngine.Builder signerEngineBuilder =
new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
.setV1SigningEnabled(mV1SigningEnabled)
.setV2SigningEnabled(mV2SigningEnabled)
.setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved);
if (mCreatedBy != null) {
signerEngineBuilder.setCreatedBy(mCreatedBy);
}
signerEngine = signerEngineBuilder.build();
}

先解析中央目录区,获取AndroidManifest文件,获取minSdkVersion(影响签名算法),并构建DefaultApkSignerEngine,默认情况下V1 V2签名都是打开的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码    // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any)
<!--忽略这一步-->
if (inputApkSigningBlock != null) {
signerEngine.inputApkSigningBlock(inputApkSigningBlock);
}

// Step 5. Iterate over input APK's entries and output the Local File Header + data of those
// entries which need to be output. Entries are iterated in the order in which their Local
// File Header records are stored in the file. This is to achieve better data locality in
// case Central Directory entries are in the wrong order.
List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
new ArrayList<>(inputCdRecords);
Collections.sort(
inputCdRecordsSortedByLfhOffset,
CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
int lastModifiedDateForNewEntries = -1;
int lastModifiedTimeForNewEntries = -1;
long inputOffset = 0;
long outputOffset = 0;
Map<String, CentralDirectoryRecord> outputCdRecordsByName =
new HashMap<>(inputCdRecords.size());
...

// Step 6. Sort output APK's Central Directory records in the order in which they should
// appear in the output
List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
String entryName = inputCdRecord.getName();
CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
if (outputCdRecord != null) {
outputCdRecords.add(outputCdRecord);
}
}

第五步与第六步的主要工作是:apk的预处理,包括目录的一些排序之类的工作,应该是为了更高效处理签名,预处理结束后,就开始签名流程,首先做的是V1签名(默认存在,除非主动关闭):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
复制代码    // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File
// Header + data entries and add to the list of output Central Directory records.
ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
signerEngine.outputJarEntries();
if (outputJarSignatureRequest != null) {
if (lastModifiedDateForNewEntries == -1) {
lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
lastModifiedTimeForNewEntries = 0;
}
for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
outputJarSignatureRequest.getAdditionalJarEntries()) {
String entryName = entry.getName();
byte[] uncompressedData = entry.getData();
ZipUtils.DeflateResult deflateResult =
ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
byte[] compressedData = deflateResult.output;
long uncompressedDataCrc32 = deflateResult.inputCrc32;

ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
signerEngine.outputJarEntry(entryName);
if (inspectEntryRequest != null) {
inspectEntryRequest.getDataSink().consume(
uncompressedData, 0, uncompressedData.length);
inspectEntryRequest.done();
}

long localFileHeaderOffset = outputOffset;
outputOffset +=
LocalFileRecord.outputRecordWithDeflateCompressedData(
entryName,
lastModifiedTimeForNewEntries,
lastModifiedDateForNewEntries,
compressedData,
uncompressedDataCrc32,
uncompressedData.length,
outputApkOut);


outputCdRecords.add(
CentralDirectoryRecord.createWithDeflateCompressedData(
entryName,
lastModifiedTimeForNewEntries,
lastModifiedDateForNewEntries,
uncompressedDataCrc32,
compressedData.length,
uncompressedData.length,
localFileHeaderOffset));
}
outputJarSignatureRequest.done();
}

// Step 8. Construct output ZIP Central Directory in an in-memory buffer
long outputCentralDirSizeBytes = 0;
for (CentralDirectoryRecord record : outputCdRecords) {
outputCentralDirSizeBytes += record.getSize();
}
if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
throw new IOException(
"Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
+ " bytes");
}
ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
for (CentralDirectoryRecord record : outputCdRecords) {
record.copyTo(outputCentralDir);
}
outputCentralDir.flip();
DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
long outputCentralDirStartOffset = outputOffset;
int outputCentralDirRecordCount = outputCdRecords.size();

// Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer
ByteBuffer outputEocd =
EocdRecord.createWithModifiedCentralDirectoryInfo(
inputZipSections.getZipEndOfCentralDirectory(),
outputCentralDirRecordCount,
outputCentralDirDataSource.size(),
outputCentralDirStartOffset);

步骤7、8、9都可以看做是V1签名的处理逻辑,主要在V1SchemeSigner中处理,其中包括创建META-INFO文件夹下的一些签名文件,更新中央目录、更新中央目录结尾等,流程不复杂,不在赘述,简单流程就是:

image.png

这里特殊提一下重复签名的问题:对一个已经V1签名的APK再次V1签名不会有任何问题,原理就是:再次签名的时候,会排除之前的签名文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码  public static boolean isJarEntryDigestNeededInManifest(String entryName) {
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File

// Entries which represent directories sould not be listed in the manifest.
if (entryName.endsWith("/")) {
return false;
}

// Entries outside of META-INF must be listed in the manifest.
if (!entryName.startsWith("META-INF/")) {
return true;
}
// Entries in subdirectories of META-INF must be listed in the manifest.
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
return true;
}

// Ignored file names (case-insensitive) in META-INF directory:
// MANIFEST.MF
// *.SF
// *.RSA
// *.DSA
// *.EC
// SIG-*
String fileNameLowerCase =
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
if (("manifest.mf".equals(fileNameLowerCase))
|| (fileNameLowerCase.endsWith(".sf"))
|| (fileNameLowerCase.endsWith(".rsa"))
|| (fileNameLowerCase.endsWith(".dsa"))
|| (fileNameLowerCase.endsWith(".ec"))
|| (fileNameLowerCase.startsWith("sig-"))) {
return false;
}
return true;
}

可以看到目录、META-INF文件夹下的文件、sf、rsa等结尾的文件都不会被V1签名进行处理,所以这里不用担心多次签名的问题。接下来就是处理V2签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码    // Step 10. Generate and output APK Signature Scheme v2 signatures, if necessary. This may
// insert an APK Signing Block just before the output's ZIP Central Directory
ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest =
signerEngine.outputZipSections(
outputApkIn,
outputCentralDirDataSource,
DataSources.asDataSource(outputEocd));
if (outputApkSigingBlockRequest != null) {
byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock();
outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
ZipUtils.setZipEocdCentralDirectoryOffset(
outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length);
outputApkSigingBlockRequest.done();
}

// Step 11. Output ZIP Central Directory and ZIP End of Central Directory
outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
outputApkOut.consume(outputEocd);
signerEngine.outputDone();
}

V2SchemeSigner处理V2签名,逻辑比较清晰,直接对V1签名过的APK进行分块摘要,再集合签名,V2签名不会改变之前V1签名后的任何信息,签名后,在中央目录前添加V2签名块,并更新中央目录结尾信息,因为V2签名后,中央目录的偏移会再次改变:

image.png

APK签名怎么校验

签名校验的过程可以看做签名的逆向,只不过覆盖安装可能还要校验公钥及证书信息一致,否则覆盖安装会失败。签名校验的入口在PackageManagerService的install里,安装官方文档,7.0以上的手机优先检测V2签名,如果V2签名不存在,再校验V1签名,对于7.0以下的手机,不存在V2签名校验机制,只会校验V1,所以,如果你的App的miniSdkVersion<24(N),那么你的签名方式必须内含V1签名:

签名校验流程

校验流程就是签名的逆向,了解签名流程即可,本文不求甚解,有兴趣自己去分析,只是额外提下覆盖安装,覆盖安装除了检验APK自己的完整性以外,还要校验证书是否一致只有证书一致(同一个keystore签名),才有可能覆盖升级。覆盖安装同全新安装相比较多了几个校验

  • 包名一致
  • 证书一致
  • versioncode不能降低

这里只关心证书部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码    // Verify: if target already has an installer package, it must
// be signed with the same cert as the caller.
if (targetPackageSetting.installerPackageName != null) {
PackageSetting setting = mSettings.mPackages.get(
targetPackageSetting.installerPackageName);
// If the currently set package isn't valid, then it's always
// okay to change it.
if (setting != null) {
if (compareSignatures(callerSignature,
setting.signatures.mSignatures)
!= PackageManager.SIGNATURE_MATCH) {
throw new SecurityException(
"Caller does not have same cert as old installer package "
+ targetPackageSetting.installerPackageName);
}
}
}

V1、V2签名下美团多渠道打包的切入点

  • V1签名:META_INFO文件夹下增加文件不会对校验有任何影响,则是美团V1多渠道打包方案的切入点
  • V2签名:V2签名块中可以添加一些附属信息,不会对签名又任何影响,这是V2多渠道打包的切入点。

总结

  • V1签名靠META_INFO文件夹下的签名文件
  • V2签名依靠中央目录前的V2签名快,ZIP的目录结构不会改变,当然结尾偏移要改。
  • V1 V2签名可以同时存在(miniSdkVersion 7.0以下如果没有V1签名是不可以的)
  • 多去到打包的切入点原则:附加信息不影响签名验证

作者:看书的小蜗牛

Android V1及V2签名签名原理简析

仅供参考,欢迎指正

本文转载自: 掘金

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

Wizard 开源文档管理系统10发布啦

发表于 2019-05-06

Wizard 是一款开源文档管理系统,项目地址为 github.com/mylxsw/wiza…。这个项目是 我 在2017年就开始开发的,起初只是想做一款能够在公司内部把Swagger文档管理起来的工具,但在这近两年的时间里,一直断断续续的为其添加各种功能,现在终于下决心发布1.0版本了,目前支持三种类型的文档管理

  • Markdown:也是Wizard最主要的文档类型,研发团队日常工作中交流所采用的最常用文档类型,在 Wizard 中,对 Editor.md 项目进行了功能扩展,增加了文档模板,Json 转表格,图片粘贴上传等功能

-w590

  • Swagger:支持 OpenAPI 3.0 规范,嵌入了 Swagger 官方的编辑器,通过定制开发,使其融入到 Wizard 项目当中,支持文档模板,全屏编辑,文档自动同步功能

-w594

  • Table:这种文档类型是类似于 Excel 电子表格,采用了 x-spreadsheet 项目,将该项目嵌入到了 Wizard 中,目前还不是很完善

-w592

目前主要包含以下功能

  • Swagger,Markdown,Table 类型的文档管理
  • 文档修改历史管理
  • 文档修改差异对比
  • 用户权限管理
  • 项目分组管理
  • LDAP 统一身份认证
  • 文档搜索,标签搜索
  • 阅读模式
  • 文档评论
  • 消息通知
  • 文档分享
  • 统计功能

如果想快速体验一下Wizard的功能,有两种方式

  • 在线体验请访问 wizard.aicode.cc/ ,目前只提供部分功能的体验,功能预览和使用说明请参考 Wiki。
  • 使用Docker来创建一个完整的Wizard服务

进入项目的根目录,执行 docker-compose up,就可以快速创建一个Wizard服务了,访问地址 http://localhost:8080 。

起源

为了鼓励大家写开发文档,最开始我们选择了 ShowDoc 项目来作为文档管理工具,当时团队规模也非常的小,大家都是直接用 Markdown 写一些简单的开发文档。后来随着团队的壮大,前后端分离,团队分工的细化,仅仅采用 Markdown 开始变得捉襟见肘,这时候,我们首先想到了使用开源界比较流行的 Swagger 来创建开发文档。但是 Swagger 文档多了,总得有个地方维护起来吧?

项目中的文档仅仅用Swagger也是不够的,它只适应于API文档的管理,还有很多其它文档,比如设计文档,流程图,架构文档,技术方案,数据库变更等各种文档需要一起维护起来。因此,我决定利用业余时间开发一款 支持 Markdown 和 Swagger 的文档管理工具,也就是 Wizard 项目了。

起初打算用 Go 语言来开发,但是没过几天发现使用 Golang 来做 Web 项目开发效率太低(快速开发效率,并非指性能,Golang做API接口开发还是很不错的),很多常用的功能都需要自己去实现,遂放弃使用 Golang,转而使用 PHP 的 Laravel 框架来开发。所以虽然项目创建的时间为 2017年7月27日,但是实际上真正开始的时间应该算是 2017年7月31日。

-w986

起初Wizard项目的想法比较简单,只是用来将 Markdown 文档和 Swagger 文档放在一起,提供一个简单的管理界面就足够了,但是随着在团队中展开使用后,发现在企业中作为一款文档管理工具来说,只提供简单的文档管理功能是不够的,比如说权限控制,文档修改历史,文档搜索,文档分类等功能需求不断的被提出来,因此也促成了 Wizard 项目的功能越来越完善。

  • 用户权限管理 参考了 Gitlab 的权限管理方式,在用户的身份上只区分了 管理员 和 普通用户,通过创建用户组来对用户的权限进行细致的管理,同时每个项目都支持单独的为用户赋予读写权限。
  • 项目分组 在 Wizard 中,文档是以项目为单位进行组织的,刚开始的时候发现这样是OK的,后来项目越来越多,项目分组功能应运而生,以目录的形式来组织项目结构。
  • 文档修改历史 每次对文档的修改,Wizard 都会记录一个快照,避免错误的修改了文档而造成损失,可以通过文档历史快速的恢复文档,对文档的修改,新增,删除等关键操作都会记录审计日志,以最近活动的形式展示出来。
  • 文档差异对比 在团队协助中,经常会出现很多人修改同一份文档,为了避免冲突,文档修改后,其它人在提交旧的历史版本时,系统会提示用户文档内容发生了变更,用户可以通过文档比对功能找出文档中有哪些内容发生了修改。
  • 阅读模式 当使用投影仪展示文档来过技术方案的时候,为了减少不必要的干扰,使用阅读模式,只展示文档内容部分,提供更好的展示体验。
  • 文档搜索 通过搜索功能快速查找需要的文档,目前支持通过文档标题来搜素文档,后续会增加全文检索功能。
  • LDAP支持 很多公司都会使用 LDAP 来统一的管理公司员工的账号,员工的在公司内部的所有系统中都是用同一套帐号来登录各种系统比如 Jira,Wiki,Gitlab 等,Wizard 也提供了对 LDAP 的支持,只需要简单的几个配置,就可以快速的接入公司的统一帐号体系。
  • 文档附件,文档分享,统计,文档排序,模板管理,文档评论 …

关于代码

项目采用了 Laravel 框架开发,目前版本已经升级到最新的 5.8(最开始为5.4,一路升级过来)。为了提高开发效率,保持架构的简洁,在开发过程中,一直避免引入过多的外部组件,尽可能的利用 Laravel 提供的各种组件,比如 Authentication,Authorization,Events,Mail,Notifications 等,非常适合用来学习 Laravel 框架。

总结

如果你在为公司寻找一款开源免费的 开发文档/API文档管理 工具,不妨考虑一下 Wizard 项目,一定不会让你失望的。如果你是一名 PHP 或者 Laravel 新手,想找个项目学习一下如何用 Laravel 做 Web 开发,这个项目更加不能错过!

最后,也是本文最核心的部分,赶紧去 Star 一下,顺便给我个 Star 啊 !如果再能贡献点 Issues 或者P R,那就更好啦 😄!

本文转载自: 掘金

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

关于performSelector看我就够了

发表于 2019-05-06

1、performSelector简单使用

performSelector(方法执行器),iOS中提供了如下几种常用的调用方式

1
2
3
4
5
6
7
复制代码[self performSelector:@selector(sureTestMethod)];
[self performSelector:@selector(sureTestMethod)
withObject:params];
[self performSelector:@selector(sureTestMethod)
withObject:params
withObject:params2];
......

performSelector响应Objective-C动态性,将方法的绑定延迟到运行时,因此编译阶段不会检测方法有效性,即方法不存在也不会提示报错。反之因为此特性,performSelector也广泛用于动态化和组件化的模块中。

如果方法名称也是动态不确定的,会提示如下警告:

1
2
复制代码SEL selector = @selector(dynamicMethod);
[self performSelector:selector];
1
复制代码⚠️ PerformSelector may cause a leak because its selector is unknown

意为因为当前方法名未知可能会引起内存泄露相关问题。
可以通过如下代码忽略此警告

1
2
3
4
复制代码#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:selector];
#pragma clang diagnostic pop

performSelector默认最多只可传递两个参数,若需多参可将参数封装为NSArray、NSDictionary、NSInvocation进行传递。另外方法调用本质都是消息机制,也可以通过msg_send实现。

1
2
3
4
5
6
7
8
9
10
复制代码id params;
id params2;
id params3;

SEL selector = NSSelectorFromString(@"sureTestMethod:params2:params3:");
objc_msgSend(self, selector,params,params2,params3);

- (void)sureTestMethod:(id)params params2:(id)params2 params3:(id)params3 {
NSLog(@"sureTestMethod-multi-parameter");
}

2、performSelector延迟调用

1
2
3
复制代码[self performSelector:@selector(sureTestMethod:)
withObject:params
afterDelay:3];

此方法意为在当前Runloop中延迟3秒后执行selector中方法。
使用该方法需要注意以下事项:
在子线程中调用performSelector: withObject: afterDelay:默认无效,如下代码并不会打印sureTestMethodCall

1
2
3
4
5
6
7
8
复制代码dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(sureTestMethod:)
withObject:params
afterDelay:3];
});
- (void)sureTestMethod:(id)objcet {
NSLog(@"sureTestMethodCall");
}

这是因为performSelector: withObject: afterDelay:是在当前Runloop中延时执行的,而子线程的Runloop默认不开启,因此无法响应方法。

所以我们尝试在GCD Block中添加 [[NSRunLoop currentRunLoop]run];

1
2
3
4
5
6
复制代码dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(sureTestMethod:)
withObject:params
afterDelay:3];
[[NSRunLoop currentRunLoop]run];
});

运行代码发现可以正常打印sureTestMethodCall。

这里有个坑需要注意,曾经尝试将 [[NSRunLoop currentRunLoop]run]添加在performSelector: withObject: afterDelay:方法前,但发现延迟方法仍然不调用,这是因为若想开启某线程的Runloop,必须具有timer、source、observer任一事件才能触发开启。

简言之如下代码在执行 [[NSRunLoop currentRunLoop]run]前没有任何事件添加到当前Runloop,因此该线程的Runloop是不会开启的,从而延迟事件不执行。

1
2
3
4
5
6
复制代码dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSRunLoop currentRunLoop]run];
[self performSelector:@selector(sureTestMethod:)
withObject:params
afterDelay:3];
});

关于Runloop,可详见:深入理解RunLoop

3、performSelector取消延迟

我们在View上放置一个Button,预期需求是防止暴力点击,只响应最后一次点击时的事件。

此需求我们可以通过cancelPreviousPerformRequestsWithTarget来进行实现。cancelPreviousPerformRequestsWithTarget的作用为取消当前延时任务。在执行延迟事件前取消当前存在的延迟任务即可实现如上效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码- (IBAction)buttonClick:(id)sender {
id params;
[[self class]cancelPreviousPerformRequestsWithTarget:self
selector:@selector(sureTestMethod:)
object:params];
[self performSelector:@selector(sureTestMethod:)
withObject:params
afterDelay:3];
}

- (void)sureTestMethod:(id)objcet {
NSLog(@"sureTestMethodCall");
}

重复点击后,打印结果如下,只响应了一次点击

1
复制代码2019-05-06 11:29:50.352157+0800 performSelector[14342:457353] sureTestMethodCall

4、performSelector模拟多线程

我们可以通过performSelectorInBackground将某selector任务放在子线程中

1
2
3
4
5
复制代码[self performSelectorInBackground:@selector(sureTestMethod:)
withObject:params];
- (void)sureTestMethod:(id)objcet {
NSLog(@"%@",[NSThread currentThread]);
}
1
复制代码//<NSThread: 0x600003854080>{number = 3, name = (null)}

打印结果可见当前方法运行在子线程中。

回到主线程执行我们可以通过方法

1
2
3
复制代码[self performSelectorOnMainThread:@selector(sureTestMethod)
withObject:params
waitUntilDone:NO];

waitUntilDone表示是否等待当前selector任务完成后再执行后续任务。示例如下,waitUntilDone为YES时,打印1,2,3。为NO时打印1,3,2。

1
2
3
复制代码    NSLog(@"1");
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:NO];
NSLog(@"3");
1
2
3
4
复制代码- (void)test {
sleep(3);
NSLog(@"2");
}

另外performSelector还提供了将任务执行在某个指定线程的操作

1
2
3
4
复制代码[self performSelector:@selector(sureTestMethod:)
onThread:thread
withObject:params
waitUntilDone:NO];

使用该方法一定要注意所在线程生命周期是否正常,若thread已销毁不存在,而performSelector强行执行任务在该线程,会导致崩溃:

1
2
3
4
5
6
7
8
复制代码NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"do thread event");
}];
[thread start];
[self performSelector:@selector(sureTestMethod:)
onThread:thread
withObject:params
waitUntilDone:YES];

上述代码会导致崩溃,崩溃信息为:

1
2
3
复制代码*** Terminating app due to uncaught exception 'NSDestinationInvalidException',
reason: '*** -[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]:
target thread exited while waiting for the perform'

因为thread开启执行do thread event完毕后即退出销毁,所以在等待执行任务时Thread已不存在导致崩溃。

好了,关于performSelector的内容暂时写到这里了,有其他补充欢迎评论~

iOS执行器performSelector详解

本文转载自: 掘金

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

【译】 使用 VS Code 调试 Nodejs 的超简单

发表于 2019-05-05
  • 原文地址:The Absolute Easiest Way to Debug Node.js — with VSCode
  • 原文作者:Paige Niedringhaus
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:iceytea
  • 校对者:fireairforce, cyz980908

让我们面对现实吧…调试 Node.js 一直是我们心中的痛。

触达调试 Node.js 的痛点

如果你曾经有幸为 Node.js 项目编写代码,那么当我说调试它以找到出错的地方并不是最简单的事情时,你就知道我在谈论什么。

不像浏览器中的 JavaScript,也不像有类似 IntelliJ 这样强大的 IDE 的 Java,你无法到处设置断点,刷新页面或者重启编译器,也无法慢慢审阅代码、检查对象、评估函数、查找变异或者遗漏的变量等。你无法那样去做,这简直太糟糕了。

但 Node.js 也是可以被调试的,只是需要多费些体力。让我们认真讨论这些可选方法,我会展示给你在我开发经历中遇到的最简单调试方法。

调试 Node.js 的一些可选方法

有一些方式能调试有问题的 Node.js 程序。我把这些方法(包含详细链接)都列在了下面。如果你感兴趣,可以去了解下。

  • Console.log() — 如果你曾经编写过 JavaScript 代码,那么这个可靠的备用程序真的不需要进一步解释。它被内置在 Node.js 并在终端中打印,就像内置到 JavaScript,并在浏览器控制台中打印一样。

在 Java 语言下,它是 System.out.println()。在 Python 语言下,它是 print()。你明白我的意思了吧。这是最容易实现的方法,也是用额外的行信息来“弄脏”干净代码的最快方法 —— 但它(有时)也可以帮助你发现和修复错误。

  • Node.js 文档 —-inspect — Node.js 文档撰写者本身明白调试不大简单,所以他们做了一些方便的参考帮助人们开始调试。

这很有用,但是老实说,除非你已经编写了一段时间的程序,否则它并不是最容易破译的。它们很快就进入了 UUIDs、WebSockets 和安全隐患的陷阱,我开始感到无所适从。我心里想:一定有一种不那么复杂的方法来做这件事。

  • Chrome DevTools — Paul Irish 在 2016 年撰写了一篇有关使用 Chrome 开发者工具调试 Node.js 的博文(并在 2018 年更新)。它看起来相当简单,对于调试来说是一个很大的进步。

半个小时之后,我仍然没有成功地将 DevTools 窗口连接到我的简单 Node 程序上,我不再那么肯定了。也许我只是不能按照说明去做,但是 Chrome DevTools 似乎让调试变得比它应该的更复杂。

  • JetBrains — JetBrains 是我最喜欢的软件开发公司之一,也是 IntelliJ 和 WebStorm 的开发商之一。他们的工具有一个奇妙的插件生态系统,直到最近,他们还是我的首选 IDE。

有了这样一个专业用户基础,就出现了许多有用的文章,比如这一篇,它们调试 Node,但与 Node 文档和 Chrome DevTools 选项类似,这并不容易。你必须创建调试配置,附加正在运行的进程,并在 WebStorm 准备就绪之前在首选项中进行大量配置。

  • Visual Studio Code — 这是我新的 Node 调试黄金标准。我从来没有想过我会这么说,但是我完全投入到 VS Code 中,并且团队所做的每一个新特性的发布,都使我更加喜爱这个 IDE。

VS Code 做了其他所有选项在调试 Node.js 都没能做到的事情,这让它变得傻瓜式简单。如果你想让你的调试变得更高级,这当然也是可以的,但是他们把它分解得足够简单,任何人都可以快速上手并运行,不论你对 IDE、Node 和编程的熟练度如何。这太棒了。

配置 VS Code 来调试 Node.js

好吧,让我们来配置 VS Code 来调试 Node。我假设你已经从这里下载了 VS Code,开始配置它吧。

打开 Preferences > Settings,在搜索框中输入 node debug。在 Extensions 选项卡下应该会有一个叫 Node debug 的扩展。在这里点击第一个方框: Debug > Node: Auto Attach,然后设置下拉框的选项为 on。你现在几乎已经配置完成了。是的,这相当的简单。

这是当你点击 Settings 选项卡,你应该能看到的内容。设置第一个下拉框 **Debug > Node: Auto Attach** 选项为 `on`。

现在进入项目文件,然后通过点击文件的左侧边栏,在你想要看到代码暂停的地方设置一些断点。在终端内输入 node --inspect <FILE NAME>。现在看,神奇的事情发生了…

看到红色断点了吗?看到终端中的 `node — inspect readFileStream.js` 了吗?就像这样。

VS Code 正在进行的代码调试

如果你需要一个 Node.js 项目来测试它,可以在这里下载我的 repo。它是用来测试使用 Node 传输大量数据的不同形式的,但是它在这个演示中非常好用。如果你想了解更多关于流数据节点和性能优化的内容,你可以点击这里和这里。

当你敲击 Enter 键时,你的 VS Code 终端底部会变成橙色,表示你处于调试模式,你的控制台会打印一些类似于 Debugger Attached 的信息。

橙色的工具栏和 `Debugger attached` 消息会告诉你 VS Code 正常运行在调试模式。

当你看到这一幕发生时,恭喜你,你已经让 Node.js 运行在调试模式下啦!

至此,你可以在屏幕的左下角看到你设置的断点(而且你可以通过复选框切换这些断点的启用状态),而且,你可以像在浏览器中那样去调试。在 IDE 的顶部中心有小小的继续、步出、步入、重新运行等按钮,从而逐步完成代码。VS Code 甚至用黄色突出显示了你已经停止的断点和行,使其更容易被跟踪。

单击顶部的继续按钮,从一个断点跳转到代码中的下一个断点。

当你从一个断点切换到另一个断点时,你可以看到程序在 VS Code 底部的调试控制台中打印出一堆 console.log,黄色的高亮显示也会随之一起移动。

如你所见,当我们暂停在断点上时,我们可以在 VS Code 的左上角看到可以在控制台中探索到的所有局部作用域信息。

正如你所看到的,随着程序的运行,调试控制台输出的内容越多,断点就越多,在此过程中,我可以使用 VS Code 左上角的工具在本地范围内探索对象和函数,就像我可以在浏览器中探索范围和对象一样。不错!

这很简单,对吧?

总结

Node.js 的调试不需要像过去那样麻烦,也不需要在代码库中包含 500 多个 console.log 来找出 bug 的位置。

Visual Studio Code 的 Debug > Node: Auto Attach 设置使之成为过去,我对此非常感激。

再过几周,我将会写一些关于端到端测试的文章,使用 Puppeteer 和 headless Chrome,或者使用 Nodemailer 在 MERN 应用程序中重置密码,所以请关注我,以免错过。

感谢阅读,希望这篇文章能让你了解如何在 VS Code 的帮助下更容易、更有效地调试 Node.js 程序。非常感谢你给我的掌声和对我文章的分享!

如果你喜欢阅读这篇文章,你可能也会喜欢我的其他文章:

  • 使用 Node.js 读取超大数据集和文件(第一部分)
  • Sequelize: Node.js SQL ORM 框架
  • 流的胜利:用于读取大型数据集的 Node.js 方法的性能比较(第二部分)

参考资料和进阶资源:

  • Github, Node 读取文件 Repo:github.com/paigen11/fi…
  • Node.js 文档 — 调试部分:nodejs.org/en/docs/gui…
  • Paul Irish’s:使用 Chrome DevTools 调试 Node.js:medium.com/@paul_irish…
  • JetBrains 提供的文档 — 《运行和调试 Node.js》 — www.jetbrains.com/help/websto…
  • Visual Studio Code 下载链接:code.visualstudio.com/download
  • VS Code 调试 Node.js 文档:code.visualstudio.com/docs/nodejs…

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文转载自: 掘金

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

就业寒冬,从拉勾招聘看Python就业前景

发表于 2019-05-05

1.数据采集

事情的起源是这样的,某个风和日丽的下午… 习惯性的打开知乎准备划下水,看到一个问题刚好邀请回答

于是就萌生了采集下某招聘网站Python岗位招聘的信息,看一下目前的薪水和岗位分布,说干就干。

先说下数据采集过程中遇到的问题,首先请求头是一定要伪装的,否则第一步就会给你弹出你的请求太频繁,请稍后再试,其次网站具有多重反爬策略,解决方案是每次先获取session然后更新我们的session进行抓取,最后拿到了想要的数据。

Chrome浏览器右键检查查看network,找到链接https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false

可以看到返回的数据正是页面的Python招聘详情,于是我直接打开发现直接提示{"status":false,"msg":"您操作太频繁,请稍后再访问","clientIp":"124.77.161.207","state":2402},机智的我察觉到事情并没有那么简单

真正的较量才刚刚开始,我们先来分析下请求的报文,

可以看到请求是以post的方式传递的,同时传递了参数

1
2
3
4
5
复制代码datas = {
'first': 'false',
'pn': x,
'kd': 'python',
}

同时不难发现每次点击下一页都会同时发送一条get请求

这里我点了两次,出现两条get请求

经过探索,发现这个get请求和我们post请求是一致的,那么问题就简单许多,整理一下思路

关键词:python
**搜索范围:**全国
**数据时效:**2019.05.05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
复制代码#!/usr/bin/env python3.4
# encoding: utf-8
"""
Created on 19-5-05
@title: ''
@author: Xusl
"""
import json
import requests
import xlwt
import time


# 获取存储职位信息的json对象,遍历获得公司名、福利待遇、工作地点、学历要求、工作类型、发布时间、职位名称、薪资、工作年限
def get_json(url, datas):
my_headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
"Referer": "https://www.lagou.com/jobs/list_Python?city=%E5%85%A8%E5%9B%BD&cl=false&fromSearch=true&labelWords=&suginput=",
"Content-Type": "application/x-www-form-urlencoded;charset = UTF-8"
}
time.sleep(5)
ses = requests.session() # 获取session
ses.headers.update(my_headers) # 更新
ses.get("https://www.lagou.com/jobs/list_python?city=%E5%85%A8%E5%9B%BD&cl=false&fromSearch=true&labelWords=&suginput=")
content = ses.post(url=url, data=datas)
result = content.json()
info = result['content']['positionResult']['result']
info_list = []
for job in info:
information = []
information.append(job['positionId']) # 岗位对应ID
information.append(job['city']) # 岗位对应城市
information.append(job['companyFullName']) # 公司全名
information.append(job['companyLabelList']) # 福利待遇
information.append(job['district']) # 工作地点
information.append(job['education']) # 学历要求
information.append(job['firstType']) # 工作类型
information.append(job['formatCreateTime']) # 发布时间
information.append(job['positionName']) # 职位名称
information.append(job['salary']) # 薪资
information.append(job['workYear']) # 工作年限
info_list.append(information)
# 将列表对象进行json格式的编码转换,其中indent参数设置缩进值为2
# print(json.dumps(info_list, ensure_ascii=False, indent=2))
# print(info_list)
return info_list


def main():
page = int(input('请输入你要抓取的页码总数:'))
# kd = input('请输入你要抓取的职位关键字:')
# city = input('请输入你要抓取的城市:')

info_result = []
title = ['岗位id', '城市', '公司全名', '福利待遇', '工作地点', '学历要求', '工作类型', '发布时间', '职位名称', '薪资', '工作年限']
info_result.append(title)
for x in range(1, page+1):
url = 'https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false'
datas = {
'first': 'false',
'pn': x,
'kd': 'python',
}
try:
info = get_json(url, datas)
info_result = info_result + info
print("第%s页正常采集" % x)
except Exception as msg:
print("第%s页出现问题" % x)

# 创建workbook,即excel
workbook = xlwt.Workbook(encoding='utf-8')
# 创建表,第二参数用于确认同一个cell单元是否可以重设值
worksheet = workbook.add_sheet('lagouzp', cell_overwrite_ok=True)
for i, row in enumerate(info_result):
# print(row)
for j, col in enumerate(row):
# print(col)
worksheet.write(i, j, col)
workbook.save('lagouzp.xls')


if __name__ == '__main__':
main()

日志记录

当然存储于excel当然是不够的,之前一直用matplotlib做数据可视化,这次换个新东西pyecharts。

2.了解pyecharts

pyecharts是一款将python与echarts结合的强大的数据可视化工具,包含多种图表

  • Bar(柱状图/条形图)
  • Bar3D(3D 柱状图)
  • Boxplot(箱形图)
  • EffectScatter(带有涟漪特效动画的散点图)
  • Funnel(漏斗图)
  • Gauge(仪表盘)
  • Geo(地理坐标系)
  • Graph(关系图)
  • HeatMap(热力图)
  • Kline(K线图)
  • Line(折线/面积图)
  • Line3D(3D 折线图)
  • Liquid(水球图)
  • Map(地图)
  • Parallel(平行坐标系)
  • Pie(饼图)
  • Polar(极坐标系)
  • Radar(雷达图)
  • Sankey(桑基图)
  • Scatter(散点图)
  • Scatter3D(3D 散点图)
  • ThemeRiver(主题河流图)
  • WordCloud(词云图)

用户自定义

  • Grid 类:并行显示多张图
  • Overlap 类:结合不同类型图表叠加画在同张图上
  • Page 类:同一网页按顺序展示多图
  • Timeline 类:提供时间线轮播多张图

另外需要注意的是从版本0.3.2 开始,为了缩减项目本身的体积以及维持 pyecharts 项目的轻量化运行,pyecharts 将不再自带地图 js 文件。如用户需要用到地图图表(Geo、Map),可自行安装对应的地图文件包。

  1. 全球国家地图: echarts-countries-pypkg (1.9MB): 世界地图和 213 个国家,包括中国地图
  2. 中国省级地图: echarts-china-provinces-pypkg (730KB):23 个省,5 个自治区
  3. 中国市级地图: echarts-china-cities-pypkg (3.8MB):370 个中国城市

也可以使用命令进行安装

1
2
3
复制代码pip install echarts-countries-pypkg
pip install echarts-china-provinces-pypkg
pip install echarts-china-cities-pypkg

3.数据可视化(代码+展示)

  • 各城市招聘数量
1
2
3
4
5
6
7
8
9
10
复制代码from pyecharts import Bar

city_nms_top10 = ['北京', '上海', '深圳', '成都', '杭州', '广州', '武汉', '南京', '苏州', '郑州', '天津', '西安', '东莞', '珠海', '合肥', '厦门', '宁波',
'南宁', '重庆', '佛山', '大连', '哈尔滨', '长沙', '福州', '中山']
city_nums_top10 = [149, 95, 77, 22, 17, 17, 16, 13, 7, 5, 4, 4, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]

bar = Bar("Python岗位", "各城市数量")
bar.add("数量", city_nms, city_nums, is_more_utils=True)
# bar.print_echarts_options() # 该行只为了打印配置项,方便调试时使用
bar.render('Python岗位各城市数量.html') # 生成本地 HTML 文件

  • 地图分布展示(这个场景意义不大,不过多分析)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码from pyecharts import Geo

city_datas = [('北京', 149), ('上海', 95), ('深圳', 77), ('成都', 22), ('杭州', 17), ('广州', 17), ('武汉', 16), ('南京', 13), ('苏州', 7),
('郑州', 5), ('天津', 4), ('西安', 4), ('东莞', 3), ('珠海', 2), ('合肥', 2), ('厦门', 2), ('宁波', 1), ('南宁', 1), ('重庆', 1),
('佛山', 1), ('大连', 1), ('哈尔滨', 1), ('长沙', 1), ('福州', 1), ('中山', 1)]
geo = Geo("Python岗位城市分布地图", "数据来源拉勾", title_color="#fff",
title_pos="center", width=1200,
height=600, background_color='#404a59')
attr, value = geo.cast(city_datas)
geo.add("", attr, value, visual_range=[0, 200], visual_text_color="#fff",
symbol_size=15, is_visualmap=True)
geo.render("Python岗位城市分布地图_scatter.html")


geo = Geo("Python岗位城市分布地图", "数据来源拉勾", title_color="#fff",
title_pos="center", width=1200,
height=600, background_color='#404a59')
attr, value = geo.cast(city_datas)
geo.add("", attr, value, type="heatmap", visual_range=[0, 10], visual_text_color="#fff",
symbol_size=15, is_visualmap=True)
geo.render("Python岗位城市分布地图_heatmap.html")

  • 各个城市招聘情况
1
2
3
4
5
6
7
8
复制代码from pyecharts import Pie

city_nms_top10 = ['北京', '上海', '深圳', '成都', '广州', '杭州', '武汉', '南京', '苏州', '郑州']
city_nums_top10 = [149, 95, 77, 22, 17, 17, 16, 13, 7, 5]
pie = Pie()
pie.add("", city_nms_top10, city_nums_top10, is_label_show=True)
# pie.show_config()
pie.render('Python岗位各城市分布饼图.html')

北上深的岗位明显碾压其它城市,这也反映出为什么越来越多的it从业人员毕业以后相继奔赴一线城市,除了一线城市的薪资高于二三线这个因素外,还有一个最重要的原因供需关系,因为一线岗位多,可选择性也就比较高,反观二三线的局面,很有可能你跳个几次槽,发现同行业能呆的公司都待过了…

  • 薪资范围

由此可见,python的岗位薪资多数在10k~20k,想从事Python行业的可以把工作年限和薪资结合起来参考一下。

  • 学历要求 + 工作年限

从工作年限来看,1-3年或者3-5年工作经验的招聘比较多,而应届生和一年以下的寥寥无几,对实习生实在不太友好,学历也普遍要求本科,多数公司都很重视入职人员学历这点毋容置疑,虽然学历不代表一切,但是对于一个企业来说,想要短时间内判断一个人的能力,最快速有效的方法无疑是从学历入手。学历第一关,面试第二关。

但是,这不代表学历不高的人就没有好的出路,现在的大学生越来越多,找工作也越来越难,竞争越来越激烈,即使具备高学历,也不能保证你一定可以找到满意的工作,天道酬勤,特别是it这个行业,知识的迭代,比其他行业来的更频密。不断学习,拓展自己学习的广度和深度,才是最正确的决定。

就业寒冬来临,我们需要的是理性客观的看待,而不是盲目地悲观或乐观。从以上数据分析,如果爱好Python,仍旧可以入坑,不过要注意一个标签有工作经验,就算没有工作经验,自己在学习Python的过程中一定要尝试独立去做一个完整的项目,爬虫也好,数据分析也好,亦或者是开发,都要尝试独立去做一套系统,在这个过程中培养自己思考和解决问题的能力。持续不断的学习,才是对自己未来最好的投资,也是度过寒冬最正确的姿势。

招聘数据获取可在公众号:Python攻城狮 后台回复 招聘数据 即可。

本文转载自: 掘金

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

关于三次握手与四次挥手面试官想考我们什么?--- 不看后悔系

发表于 2019-05-04

在面试中,三次握手和四次挥手可以说是问的最频繁的一个知识点了,我相信大家也都看过很多关于三次握手与四次挥手的文章,今天的这篇文章,重点是围绕着面试,我们应该掌握哪些比较重要的点,哪些是比较被面试官给问到的,我觉得如果你能把我下面列举的一些点都记住、理解,我想就差不多了。

三次握手

由于在面试中,三次握手是被问的最频繁的面试题,所以本次我们从面试的角度来讲解三次握手

当面试官问你为什么需要有三次握手、三次握手的作用、讲讲三次三次握手的时候,我想很多人会这样回答:

首先很多人会先讲下握手的过程:

1、第一次握手:客户端给服务器发送一个 SYN 报文。

2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。

3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。

4、服务器收到 ACK 报文之后,三次握手建立完成。

作用是为了确认双方的接收与发送能力是否正常。

这里我顺便解释一下为啥只有三次握手才能确认双方的接受与发送能力是否正常,而两次却不可以:

第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。

第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。

第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

这样回答其实也是可以的,但我觉得,这个过程的我们应该要描述的更详细一点,因为三次握手的过程中,双方是由很多状态的改变的,而这些状态,也是面试官可能会问的点。所以我觉得在回答三次握手的时候,我们应该要描述的详细一点,而且描述的详细一点意味着可以扯久一点。加分的描述我觉得应该是这样:

刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后

1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 SN(c)。此时客户端处于 SYN_Send 状态。

2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 \SYN_REVD** 的状态。

3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。

4、服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。

三次握手的作用

三次握手的作用也是有好多的,多记住几个,保证不亏。例如:

1、确认双方的接受能力、发送能力是否正常。

2、指定自己的初始化序列号,为后面的可靠传送做准备。

单单这样还不足以应付三次握手,面试官可能还会问一些其他的问题,例如:

1、(ISN)是固定的吗

三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。

如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

2、什么是半连接队列

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, ….

3、三次握手过程中可以携带数据吗

很多人可能会认为三次握手都不能携带数据,其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。

而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。

四次挥手

由于在面试中,三次握手是被问的最频繁的面试题,所以本次我们从面试的角度来讲解三次握手

四次挥手也一样,千万不要对方一个 FIN 报文,我方一个 ACK 报文,再我方一个 FIN 报文,我方一个 ACK 报文。然后结束,最好是说的详细一点,例如想下面这样就差不多了,要把每个阶段的状态记好,我上次面试就被问了几个了,呵呵。我答错了,还以为自己答对了,当时还解释的头头是道,呵呵。

刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:

1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。

2、第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。

3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态

5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

这里特别需要主要的就是****TIME_WAIT****这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。

至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。

这里我给出每个状态所包含的含义,有兴趣的可以看看。

LISTEN - 侦听来自远方TCP端口的连接请求;

SYN-SENT -在发送连接请求后等待匹配的连接请求;

SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认;

ESTABLISHED- 代表一个打开的连接,数据可以传送给用户;

FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;

FIN-WAIT-2 - 从远程TCP等待连接中断请求;

CLOSE-WAIT - 等待从本地用户发来的连接中断请求;

CLOSING -等待远程TCP对连接中断的确认;

LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;

TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认;

CLOSED - 没有任何连接状态;

最后,在放在三次握手与四次挥手的图

另外,计算机网网络和操作系统被问的概率还是很高的,推荐大家看这份笔记,通俗易懂,看完基本就稳了

图解操作系统、网络、计算机组成 PDF 下载!

这里也有一些写的不错的文章,给大家找来了

1. 计算机网络五层模型入门

2. 通信双方如何保证消息不丢失?

3. 集线器、交换机与路由器有什么区别?

4. 什么是 TCP 拥塞控制?

5. 什么是 TCP 流量控制

6. 什么是 TCP 三次握手?

7. 什么是 TCP 四次挥手?

8. 什么是 HTTP?

9. 什么是 HTTPS?

10. 什么是 SSL/TLS 协议?

11. 什么是 DNS?

12. 什么是 DHCP ?

13. 什么是广播路由算法?

14. 什么是数字签名?

15. 什么是 SQL 注入攻击?

16. 什么是 XSS 攻击?

喜欢看视频的,也可以看我整理的这个计算机基础视频
[计算机基础三门课视频](

本文转载自: 掘金

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

从Hessian RPC 注解方式看Spring依赖注入

发表于 2019-05-03

前言

Hessian 是一个binary-rpc协议轻量级RPC调用框架,相对于我们常见的Dubbo,Spring Cloud 使用起来方便简洁。

基于Spring IOC 实现Hessian注解形式服务发布与服务消费。从实现过程深入了解Spring 依赖注入的原理。

Hessian 使用

HessianServiceProxyExporter

1
2
3
4
5
6
7
8
9
复制代码public class HessianServiceProxyExporter extends HessianServiceExporter {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//此处可做统一数据验签
log.info("HessianServiceProxyExporter get request at {}" , LocalDateTime.now());
super.handleRequest(request ,response);
}
}

hessian 服务暴露

1
2
3
4
5
6
7
复制代码@Bean(name = "/userService")
public HessianServiceExporter initHessian(){
HessianServiceExporter exporter = new HessianServiceProxyExporter();
exporter.setService(new UserServiceImpl());
exporter.setServiceInterface(IUserService.class);
return exporter;
}

hessian 服务消费

1
2
3
4
5
6
7
复制代码@Bean
public HessianReferenceProxyFactoryBean helloClient() {
HessianReferenceProxyFactoryBean factory = new HessianReferenceProxyFactoryBean();
factory.setServiceUrl("http://127.0.0.1:8080/userService");
factory.setServiceInterface(IUserService.class);
return factory;
}

将hessian 服务初始化到Spring IOC容器中后与普通Service使用一样。

1
2
复制代码@Autowired
private IUserService userService;

我们从上述的使用Hessian的服务开发过程中会发现每一个Hessian服务的开发都会有一一对应的服务暴露,服务引用。当系统比较庞大的时候就会增加服务管理的难度与大量的重复代码

Hessian 注解实现

随着Spring Boot普及,越来越多人习惯于注解开发模式。Hessian 也可以实现注解模式的开发与使用。

@HessianService

我们先来分析下Hessian服务暴露的过程:

  • 实例化一个Service Bean
  • 实例化一个HessianServiceExporter bean
  • 将HessianServiceExporter 注册到IOC容器中进行统一管理
    这样我们就可以定义一个组合注解@HessianService
1
2
3
4
5
6
7
8
9
10
11
复制代码Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface HessianService {
/**
* hessian服务名称
* @return
*/
String name() default "";
}

Service Bean的实例交由Spring IOC来处理

通过实现InitializingBean , ApplicationContextAware相应的接口处理@HessianService服务暴露

@HessianReference

consumer 消费服务,借鉴@Autowired实现思路:继承InstantiationAwareBeanPostProcessorAdapter 重载postProcessPropertyValues方法

  • InstantiationAwareBeanPostProcessorAdapter Bean实例化后但在设值显示属性之前回调(实现额外injection策略)
  • postProcessPropertyValues 实现依赖注入

以上便实现了Hessian的注解模式

1
2
3
复制代码@HessianService
public class StudentServiceImpl implements IStudentService{
}
1
2
复制代码@HessianReference
private IUserService userService;

小结

上述实现过程中,我们可明显看出:基于Spring framework的开发中 一切的Bean操作都围绕IOC容器进行;并体现了Spring framework一个很重要的核心思想面向扩展开放 ,Spring framework 提供了多种可供扩展的接口,通过实现接口中的方法可实现个性化业务(如上述中的@ HessianReference依赖注入)

此篇文章的重点不是实现Hessian注解模式,而是通过Hessian注解模式的实现过程学习Spring良好的设计分层(面向扩展开放):通过分层的设计使得我们的代码具高可扩展性

源码在这里

本文转载自: 掘金

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

面试官:说说你知道的几种负载均衡分类

发表于 2019-05-02

负载均衡其实就是任务的分发,使得任务能按照你的预想分配到各个计算单元上,它能提高服务对外的性能,避免单点失效场景。这里要注意的一点是虽说叫负载均衡,但是有时候我们的分配算法就是不是均衡的。
比如配个nginx,做两台服务器的负载均衡,一台机子比较老是以前的配置比较低,一台是新机子配置高,那我们的分配权重可能就是3-7分,而不是五五开。所以是预想分配。但是业界还是习惯按照负载均衡来表达这个任务分配机制。

负载均衡分类

负载均衡常见的有:软件负载均衡、硬件负载均衡、DNS负载均衡。

软件负载均衡

软件负载均衡是最常见的,大小公司都需要用到它。

软件负载均衡是通过负载均衡功能的软件来实现负载均衡,常见的软件有LVS、Nginx、HAProxy。

软件负载负载均衡又分四层和七层负载均衡,四层负载均衡就是在网络层利用IP地址端口进行请求的转发,基本上就是起个转发分配作用。而七层负载均衡就是可以根据访问用户的HTTP请求头、URL信息将请求转发到特定的主机。
LVS为四层负载均衡。Nginx、HAProxy可四可七。

Nginx是万级别的,通常只用它来做七层负载,LVS来做四层负载。LVS是十万级别的,所以如果顶不住常见的也有这样的搭配。

软件负载均衡的优点在于便宜而且简单灵活,就买个主机,装下软件,配置一下就能用了,配置也很简单对于一般小型企业,或者并发量不高的企业来说就够用了。而且在高峰期时容易扩容。
缺点在于(和硬件负载均衡比)性能一般,流量很大的企业就用软件负载均衡顶不住,没防火墙或者防DDos攻击等安全性功能。

硬件负载均衡

硬件负载均衡就是用一个硬件一个基础网络设备,类似我们的交换机啊这样的硬件,来实现负载均衡。
常见的硬件有F5、A10。

优点就是:

1.功能强大,支持全局负载均衡提供全面的复杂均衡算法。

2.性能强悍,支持百万以上的并发。

3.提供安全功能,例如防火墙,防DDos攻击等。

这么一听我靠这么吊谁不用啊赶紧买个。别急我们下面个图片。
这网上找的,价格升序了最低也得15万,高的我看到有90万的。

缺点:
1.贵!这算是它最大的缺点了。为了安全通常还得一主一备,啧啧。

2.扩展能力差,当访问量突增的时候超过限度不能动态扩容。

DNS负载均衡

这个负载均衡时通过DNS来的,因为DNS解析同一个域名可以返回不同的ip。所以例如哈尔滨的人访问百度就返回距离他近的那个机房的IP,海南的人访问百度就返回距离他近的那个机房的IP。所以主要是用来实现地理级别的负载均衡。

优点就是:

1.简单,交给DNS服务器处理咱们都不用干活

2.因为是就近访问可以减少响应的时间,提升访问速度

缺点:

1.DNS有缓存而且缓存时间较长,所以当机房迁移等需要修改DNS配置的时候,用户可能还会访问之前的IP,导致访问失败。

2.扩展能力差,因为运营商管理控制的,由不得我们定制或者扩展。

3.比较笨,不能区分服务器之间的差异,也不能反映服务器的当前运行状态

使用套路

DNS负载均衡是地理级别的,硬件负载均衡对应的是集群级别的,软件负载均衡对应的是机器级别的。

不过一般而言像小公司或者流量不大的公司都是只需要软件负载均衡,也可能LVS都不需要上所以是按实际情况删减上图的一些东西。真正公司发展起来用户量激增才会考虑多机房和上硬件,毕竟是需求的驱使和不差钱了。


如有错误欢迎指正!

个人公众号:yes的练级攻略

有相关面试进阶(分布式、性能调优、经典书籍pdf)资料等待领取

本文转载自: 掘金

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

【最短路径Floyd算法详解推导过程】看完这篇,你还能不懂F

发表于 2019-04-30

简介

1
复制代码 Floyd-Warshall算法(Floyd-Warshall algorithm),是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。

简单的说就是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。Floyd-Warshall算法的时间复杂度为O(N3),空间复杂度为O(N2)。

解决最短路径问题有几个出名的算法:

  • 1.dijkstra算法,最经典的单源最短路径算法 上篇文章已经讲到
  • 2.bellman-ford算法,允许负权边的单源最短路径算法
  • 3.spfa,其实是bellman-ford+队列优化,其实和bfs的关系更密一点
  • 4.floyd算法,经典的多源最短路径算法

今天先说说Floyd

Floyd算法详解

描述

a)如图:存在【0,1,2,3】 4个点,两点之间的距离就是边上的数字,如果两点之间,没有边相连,则无法到达,为无穷大。
b)要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。那么这个中转的顶点k是0~n中的哪个点呢?

image.png

算法过程

准备

1)如图 0->1距离为5,0->2不可达,距离为∞,0->3距离为7……依次可将图转化为邻接矩阵(主对角线,也就是自身到自身,我们规定距离为0,不可达为无穷大),如图矩阵 用于存放任意一对顶点之间的最短路径权值。

image.png

2)再创建一个二维数组Path路径数组,用于存放任意一对顶点之间的最短路径。每个单元格的内容表示从i点到j点途经的顶点。(初始还未开始查找,默认-1)
image.png

image.png

开始查找

1)列举所有的路径(自己到自己不算)

image.png

即为:
0 -> 1 , 0 -> 2 , 0 -> 3 ,

1 -> 0 , 1 -> 2 , 1 -> 3 ,
2 -> 0 , 1 -> 1 , 1 -> 3
转化成二元数组即为:
{0,1},{0,2},{0,3},{1,0},{1,2},{1,3},{2,0},{2,1},{2,3},{3,0},{3,1},{3,2}

2)选择编号为0的点为中间点

{0,1},{0,2},{0,3},{1,0},{1,2},{1,3},{2,0},{2,1},{2,3},{3,0},{3,1},{3,2}
从上面中二元组集合的第一个元素开始,循环执行以下过程:

1
2
3
复制代码1. 用i,j两个变量分别指向二元组里的两个元素,比如{0,1}这个二元组,i指向0;j指向1
2. 判断 (A[ i ][ 0 ]+A[ 0 ][ j ] ) < A[ i ][ j ] (即判断 i -> j,i点到j点的距离是否小于从0点中转的距离),如果false,则判断下一组二元数组。
3. 如果表达式为真,更新A[ i ] [ j ]的值为A[ i ] [ 0 ] + A[ 0 ] [ j ],Path[ i ] [ j ]的值为点0(即设置i到j要经过0点中转)

{0,1}按照此过程执行之后,

image.png

0->0 + 0->1的距离不小于0->1 ,下一组{0,2},{0,3}, {1,0},{2,0},{3,0}也同理。
{1,2}按照此过程执行,A[1,0] 无穷大, A[0,2]也是无穷大,而A[1,4] = 4,则1点到2点肯定不会从0点中转。

A[1][0]无穷大同理下一组{1,2}, {1,3}也同理。

{2,1}按照此过程执行,A[2][0] = 3 ,A[0][1]=5 ,A[2][1] = 3那么 A[2][0]+ ,A[0][1] > A[2][1]
…………
依次类推,遍历二元组集合,没有0点适合做中转的

3)选择编号为1的点为中间点

4)选择编号为2的点为中间点

依次类推,遍历二元组集合{0,1},{0,2},{0,3},{1,0},{1,2},{1,3},{2,0},{2,1},{2,3},{3,0},{3,1},{3,2}
,当遍历{3,0}时,A[3][2] = 1 ,A[2][0]=3 ,A[3][0] = 不可达,那么 2点适合做从3点到0点之间的中转点。
设置距离矩阵A[3][0] = 1+3 =4 ,Path矩阵Path[3][0] = 2点,表示从3到0在2点中转,距离最近。

image.png

如图表示(红色单元格),从3到0,最近距离为4,在2点中转 。
依次类推,遍历完二元组集合

image.png

5)选择编号为3的点为中间点,最终结果

依次类推,遍历二元组集合,直到所有的顶点都做过一次中间点为止。

image.png

6)根据最终结果,就可以知道任意2点的最短距离和路径

比如1点到2点怎么走?根据路径Path矩阵,Path[1][2] = 3,表示从点3中转,即 1-> 3 ->2

image.png

6)如果中转点不止1个呢?

有时候不只通过一个点,而是经过两个点或者更多点中转会更短,即a->k1->k2b->或者a->k1->k2…->k->i…->b。
比如顶点1到顶点0,我们看数组Path
Path[1][0] = 3,说明顶点3是中转点,那么再从3到0
Path[3][0] = 2,说明从3到0,顶点2是中转点,然后在从2到0
Path[2][0] = -1,说明顶点2到顶点0没有途径顶点,也就是说,可以由顶点2直接到顶点0,即它们有边连接。

最终,最短路径为1->3->2->0,距离为 A[1][0] = 6 。
显然,这是一个逐层递进,递归的过程。

算法实现

基本定义

1
2
3
4
5
6
复制代码    //    表示无穷大 即不可达
public static int MAX = Integer.MAX_VALUE;
// 距离矩阵
public int[][] dist;
// 路径Path矩阵
public int[][] path;

核心算法

1
2
3
4
5
6
7
8
9
10
11
12
复制代码//        核心算法
for(int k = 0 ; k < size ; k++){
for(int i = 0;i < size;i++){
for(int j = 0 ;j < size;j++){
// 判断如果 ik距离可达且 kj距离可达 且 i和j的距离是否大于 i-> k 与 k->j的距离和
if( dist[i][k] != MAX && dist[k][j] != MAX && dist[i][j] > (dist[i][k] + dist[k][j]) ){
path[i][j]= k;
dist[i][j]= dist[i][k] + dist[k][j];
}
}
}
}

运行结果

image.png

源码下载

Floyd算法java实现-下载

Floyd算法java实现

看完这篇文章如果你还不会Floyd,请留言评论。

image.png

本文转载自: 掘金

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

混乱的java日志体系

发表于 2019-04-29

日志组件是开发中最常用到的组件,但也是最容易被忽视的一个组件,我自己就遇到过很多次由于Log4j报错导致的应用无法启动的问题,以下做一个梳理,参考和借鉴了一些前辈的经验,并加入了一些自己的理解,相对容易看懂一些~

一、常见日志框架

目前常见的Java日志框架和facades(中文似乎不太好翻译)有一下几种:

  • 1、log4j
  • 2、logback
  • 3、SLF4J
  • 4、commons-logging
  • 5、j.u.l (即java.util.logging)

1-3是同一个作者(Ceki)所写。4被很多开源项目所用,5是Java原生库(以下用j.u.l简写来代替),但是在Java 1.4中才被引入。这么多日志库,了解他们的优劣和关系才能找到一款更加适配自己项目的框架。

二、框架间关系

如下图,common-logging与slf4j同属于日志的门面 (facade),下层可以对接具体的日志框架层。

common-logging:开发者可以使用它兼容j.u.l和log4j,相当于一个中间层,需要注意的是common-logging对j.u.l和log4j的配置兼容性并没有理想中那么好,更糟糕的是,在common-logging发布初期,使用common-logging可能会遇到类加载问题,导致

NoClassDefFoundError的错误出现;

slf4j:能够更加灵活的对接多个底层框架;

三、log4j简介

1、Logger(限定日志级别)

级别顺序为:DEBUG < INFO < WARN < ERROR < FATAL (制定当前日志的重要程度);

log4j的级别规则:只输出级别不低于设定级别的日志信息,例:loggers级别为INFO,则INFO、WARN、ERROR、FATAL级别的日志信息都会输出,而级别比INFO低的DEBUG则不会输出;

2、Appender(日志输出目的地)

Appender允许通过配置将日志输出到不同的地方,如控制台(Console)、文件(Files)等,可以根据天数或者文件大小产生新的文件,可以以流的形式发送到其他地方;

常用配置类:

org.apache.log4j.ConsoleAppender(控制台)

org.apache.log4j.FileAppender(文件)

org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)

org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

3、Logout(日志输出格式)

用户可以根据需求格式化日志输出,log4j可以在Appenders后面附加Layouts来完成;

Layouts提供四种日志输出格式:根据HTML样式、自由指定样式、包含日志级别与信息的样式、包含日志时间/线程/类别等信息的样式;

org.apache.log4j.HTMLLayout(以HTML表格形式布局)

org.apache.log4j.PatternLayout(可以灵活地指定布局模式)

org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)

org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)

四、logger的组织结构

在这三个类中都通过Logger.getLogger(XXX.class);获取logger:

logger组织结构如下,父子关系通过 “.” 实现:

Loggers是被命名的实体,Logger的名称是大小写敏感的,并且遵循层次命名规则。命名为“com.mobile”的logger是命名为“com.mobile.log”的logger的父亲。同理,命名为“java”的logger是命名为“java.util”的logger的父亲,是命名为“java.util.Vector”的祖先。
root logger位于整个logger继承体系的最顶端,相比于普通logger它有两个特别之处:

1)、root logger总是存在。

2)、root logger不能通过名称获取。

可以通过调用Logger类的静态方法getRootLogger获取root logger对象。其它普通logger的实例可以通过Logger类的另一个静态方法getLogger获取。getLogger方法接受一个参数作为logger的名字。

五、level继承关系

logger节点的继承关系体现在level上,可以参见下面几个例子:

Example 1:这个例子中,只有root logger被分配了一个level值Proot,Proot会被其他的子logger继承:x、x.y、x.y.z

Example 2:这个例子中,所有的logger都被分配了一个level值,就不需要继承level了

Example 3:这个例子中,root、x和x.y.z三个logger分别被分配了Proot、Px和Pxyz三个level值,x.y这个logger从它的父亲那里继承level值;

Example 4:这个例子中,root和x两个logger分别被分配了Proot和Px这两个level值。x.y和x.y.z两个logger则从离自己最近的祖先x继承level值;

六、Appender继承关系

logger的additivity表示:子logger是否继承父logger的输出源 (Appender),即默认情况下子logger会继承父logger的Appender (子logger会在父logger的Appender里输出),当手动设置additivity flag为false,子logger只会在自己的Appender里输出,不会在父Appender里输出。

如下图展示:

七、slf4j两种使用方式

slf4j的使用有两种方式,一种是混合绑定(concrete-bindings), 另一种是桥接遗产(bridging-legacy).

1、混合绑定(concrete-bindings)

concrete-bindings模式指在新项目中即开发者直接使用sl4j的api来打印日志, 而底层绑定任意一种日志框架,如logback, log4j, j.u.l等.混合绑定根据实现原理,基本上有两种形式, 分别为有适配器(adapter)的绑定和无适配器的绑定.

有适配器的混合绑定是指底层没有实现slf4j的接口,而是通过适配器直接调用底层日志框架的Logger, 无适配器的绑定不需要调用其它日志框架的Logger, 其本身就实现了slf4j的全部接口.

几个混合绑定的包分别是:

  • slf4j-log4j12-1.7.21.jar(适配器, 绑定log4j, Logger由log4j-1.2.17.jar提供)
  • slf4j-jdk14-1.7.21.jar(适配器, 绑定l.u.l, Logger由JVM runtime, 即j.u.l库提供)
  • logback-classic-1.0.13.jar(无适配器, slf4j的一个native实现)
  • slf4j-simple-1.7.21.jar(无适配器,slf4j的简单实现, 仅打印INFO及更高级别的消息, 所有输出全部重定向到System.err, 适合小应用)

以上几种绑定可以无缝切换, 不需要改动内部代码. 无论哪种绑定,均依赖slf4j-api.jar.

此外, 适配器绑定需要一种具体的日志框架, 如log4j绑定slf4j-log4j12-1.7.21.jar依赖log4j.jar, j.u.l绑定slf4j-jdk14-1.7.21.jar依赖j.u.l(java runtime提供); 无适配器的直接实现, logback-classic依赖logback-core提供底层功能, slf4j-simple则不依赖其它库.

以上四种绑定的示例图如下:

关于适配器,正常使用slf4j从LoggerFactory.getLogger获取logger开始,在getLogger内部会先通过StaticLoggerBinder获取ILoggerFactory,StaticLoggerBinder则是存在具体的适配器包中的,我了解的一种实现是通过在适配器中的StaticLoggerBinder来绑定,举个例子,引用这四个slf4j-api.jar, log4j-core-2.3.jar, log4j-api-2.3.jar, log4j-slf4j-impl.jar(将slf4j转发到log4j2):

下面来分析两个典型绑定log4j (有适配器) 和logback (无适配器) 的用法.

1)log4j适配器绑定(slf4j-log4j12)
1
2
3
4
5
6
复制代码<!--pom.xml-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12.version}</version>
</dependency>

注意: 添加上述适配器绑定配置后会自动拉下来两个依赖库, 分别是slf4j-api-1.7.21.jar和log4j-1.2.17.jar

基本逻辑: 用户层 <- 中间层 <- 底层基础日志框架层

2)slf4j绑定到logback-classic上
1
2
3
4
5
6
复制代码<!--pom.xml-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
</dependency>

注意: 添加上述适配器绑定配置后会自动拉下来两个依赖库, 分别是slf4j-api-1.7.21.jar和logback-core-1.0.13.jar

logback-classic没有适配器层, 而是在logback-classic-1.0.13.jar的ch.qos.logback.classic.Logger直接实现了slf4j的org.slf4j.Logger, 并强依赖ch.qos.logback.core中的大量基础类:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.spi.LocationAwareLogger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.util.LoggerNameUtil;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.spi.AppenderAttachable;
import ch.qos.logback.core.spi.AppenderAttachableImpl;
import ch.qos.logback.core.spi.FilterReply;

public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {}

绑定图:

2、桥接遗产(bridging-legacy)

桥接遗产用法主要针对历史遗留项目, 不论是用log4j写的, j.c.l写的,还是j.u.l写的, 都可以在不改动代码的情况下具有另外一种日志框架的能力。比如,你的项目使用java提供的原生日志库j.u.l写的, 使用slf4j的bridging-legacy模式,便可在不改动一行代码的情况下瞬间具有log4j的全部特性。说得更直白一些,就是你的项目代码可能是5年前写的, 当时由于没得选择, 用了一个比较垃圾的日志框架, 有各种缺陷和问题, 如不能按天存储, 不能控制大小, 支持的appender很少, 无法存入数据库等. 你很想对这个已完工并在线上运行的项目进行改造, 显然, 直接改代码, 把旧的日志框架替换掉是不现实的, 因为很有可能引入不可预期的bug。那么,如何在不修改代码的前提下, 替换掉旧的日志框架,引入更优秀且成熟的日志框架如如log4j和logback呢? slf4j的bridging-legacy模式便是为了解决这个痛点。

slf4j以slf4j-api为中间层, 将上层旧日志框架的消息转发到底层绑定的新日志框架上。

举例说明上述facade的使用, 以便理解。假如我有一个已完成的使用了旧日志框架commons-loggings的项目,现在想把它替换成log4j以获得更多更好的特性.
项目的maven旧配置如下:

1
2
3
4
5
复制代码<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons-logging.version}</version>
</dependency>

项目代码:

1
2
3
4
5
6
7
8
9
10
复制代码import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class LogTest {
private static Log logger = LogFactory.getLog(LogTest.class);

public static void main(String[] args) throws InterruptedException {
logger.info("hello,world");
}
}

项目打印的基于commons-logging的日志显示在console上,具体如下:

1
2
复制代码十一月 20, 2017 5:52:00 下午 LogTest main
信息: hello,world

下面我们对项目改造, 将commongs-logging框架的日志转发到log4j上. 改造很简单, 我们将commongs-logging依赖删除, 替换为相应的facade(此处为jcl-over-slf4j.jar), 并在facade下面挂一个5.1的混合绑定即可.

具体来讲, 将commons-logging.jar替换成jcl-over-slf4j.jar, 并加入适配器slf4j-log412.jar(注意, 加入slf4j-log412.jar后会自动pull下来另外两个jar包), 所以实际最终只需添加facadejcl-over-slf4j.jar和混合绑定中相同的jar包slf4j-log412.jar即可.

改造后的maven配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<!--facade-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${jcl-over-slf4j.version}</version>
</dependency>

<!--binding-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12.version}</version>
</dependency>

现在, 我们的旧项目在没有改一行代码的情况下具有了log4j的全部特性, 下面进行测试.
在resources/下新建一个log4j.properties文件, 对commongs-logging库的日志输出进行定制化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码# Root logger option
log4j.rootLogger=INFO, stdout, fout

# Redirect log messages to console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.Threshold = INFO
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

# add a FileAppender to the logger fout
log4j.appender.fout=org.apache.log4j.FileAppender
# create a log file
log4j.appender.fout.File=log-testing.log
log4j.appender.fout.layout=org.apache.log4j.PatternLayout
# use a more detailed message pattern
log4j.appender.fout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

重新编译运行, console输出变为:

1
复制代码2017-11-20 19:26:15 INFO LogTest:11 - hello,world

同时在当前目录生成了一个日志文件:

1
2
复制代码% cat log-testing.log
INFO 2017-11-20 19:26:15,341 0 LogTest [main] hello,world

可见, 基于facade的日志框架桥接已经生效, 我们再不改动代码的前提下,让commons-logging日志框架具有了log4j12的全部特性.

八、log4j与log4j2比较

配置文件方式内容比较多,用得到时候可以详细查阅一下相关文档,还有log4j2相比log4j来讲,性能、代码可读性、支持日志参数化打印等方面都表现了很高的优越性。

九、日志组件可能遇到的问题

1、死循环

下图同一个颜色的两行表示不可共存的包,不要在工程中引入会形成循环的这两个包(可能不全,欢迎补充~):

2、日志重复输出

当log4j.xml中Appender:additivity设置为true 且 appender-ref配置了对应的appender 时,会出现重复打印的问题,光说不好理解,举个例子:

log4j.xml配置如下:

testlog.java测试类中:

结果是:在root.log和logtest.log里面分别打印了相同的日志进去

当additivity置为false:root.log就没打日志了

3、slf4j的warning、error提示信息含义

1)、SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”.

SLF4J: See www.slf4j.org/codes.html#… for further details.

报出错误的地方主要是slf4j的jar包,而故障码中“Failed to load ‘org.slf4j.impl.StaticLoggerBinder’的意思则是“加载类文件org.slf4j.impl.StaticLoggerBinder时失败””,

Apache官方给出的解决方案是:

This error is reported when the org.slf4j.impl.StaticLoggerBinder class could not be loaded into memory. This happens when no appropriate SLF4J binding could be found on the class path. Placing one (and only one) of slf4j-nop.jar, slf4j-simple.jar, slf4j-log4j12.jar, slf4j-jdk14.jar or logback-classic.jar on the class path should solve the problem.

翻译来就是:这个错误当org.slf4j.impl.StaticLoggerBinder找不到的时候会报出,当类路径中没有找到合适的slf4j绑定时就会发生。可以放置如下jar包中的一个且有且一个jar包即可:slf4j-nop.jar、slf4j-simple.jar、slf4j-log4j12.jar、slf4j-jdk14.jar 或者 logback-classic.jar。

2)、multiple bindings were found on the class path.

slf4j是一个日志门面框架,它只能同时绑定有且一个底层日志框架,如果绑定了多个,slf4j就会有这个提示,并且会列出这些绑定的具体位置,当有多个绑定的时候,选择你想去绑定的那个,然后删掉其他的,比如:你绑定了

slf4j-simple-1.8.0-beta0.jar和slf4j-nop-1.8.0-beta0.jar,你最终想用的是slf4j-nop-1.8.0-beta0.jar,那么就删掉另外那个就好了。我测试了一下,同时在slf4j-api.jar绑定了两个适配器:

真实的绑定是slf4j-log4j12.jar这个包里面的实现:

所以当绑定了两个包的时候,最后选择了哪个包里的实现方式,应该是按照classpath里面的顺序,这个顺序应该与classLoader的加载规则有关。

其他具体的信息可查阅:www.slf4j.org/codes.html#… for an explanation

十、参考

segmentfault.com/a/119000001…

本文转载自: 掘金

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

1…873874875…956

开发者博客

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