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

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


  • 首页

  • 归档

  • 搜索

0xA03 Android 10 源码分析:APK 加载流程

发表于 2020-03-14

引言

  • 这是 Android 10 源码分析系列的第 3 篇
  • 分支:android-10.0.0_r14
  • 全文阅读大概 15 分钟

通过这篇文章你将学习到以下内容,文末会给出相应的答案

  • LayoutInflater的inflate 方法的三个参数都代表什么意思?
  • 系统对 merge、include 是如何处理的
  • merge 标签为什么可以起到优化布局的效果?
  • XML 中的 View 是如何被实例化的?
  • 为什么复杂布局会产生卡顿?在 Android 10 上做了那些优化?
  • BlinkLayout 是什么?

前面两篇文章 0xA01 Android 10 源码分析:APK 是如何生成的 和 0xA02 Android 10 源码分析:APK 的安装流程 分析了 APK 大概可以分为代码和资源两部分,那么 APK 的加载也是分为代码和资源两部分,代码的加载涉及了进程的创建、启动、调度,本文主要来分析一下资源的加载,如果没有看过 APK 是如何生成的 和 APK 的安装流程 可以点击下方连接前往:

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  1. Android 资源

Android 资源大概分为两个部分:assets 和 res

assets 资源

assets 资源放在 assets 目录下,它里面保存一些原始的文件,可以以任何方式来进行组织,这些文件最终会原封不动的被打包进 APK 文件中,通过AssetManager 来获取 asset 资源,代码如下

1
2
ini复制代码AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("fileName");

res资源

res 资源放在主工程的 res 目录下,这类资源一般都会在编译阶段生成一个资源 ID 供我们使用,res 目录包括 animator、anim、 color、drawable、layout、menu、raw、values、XML等,通过 getResource() 去获取 Resources 对象

1
ini复制代码Resources res = getContext().getResources();

APK 的生成过程中,会生成资源索引表 resources.arsc 文件和 R.java 文件,前者资源索引表 resources.arsc 记录了所有的应用程序资源目录的信息,包括每一个资源名称、类型、值、ID以及所配置的维度信息,后者定义了各个资源 ID 常量,运行时通过 Resources 和 AssetManger 共同完成资源的加载,如果资源是个文件,Resouces 先根据资源 ID 查找出文件名,AssetManger 再根据文件名查找出具体的资源,关于 resources.arsc,可以查看 0xA01 ASOP应用框架:APK 是如何生成的

  1. 资源的加载和解析到 View 的生成

下面代码一定不会很陌生,在 Activity 常见的几行代码

1
2
3
4
kotlin复制代码override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
}

一起来分析一下调用 setContentView 方法之后做了什么事情,接下来查看一下 Activity 中的 setContentView 方法

frameworks/base/core/java/android/app/Activity.java

1
2
3
4
5
less复制代码public void setContentView(@LayoutRes int layoutResID) {
// 实际上调用的是PhoneWindow.setContentView方法
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

调用 getWindow 方法返回的是 mWindow,mWindow 是 Windowd 对象,实际上是调用它的唯一实现类 PhoneWindow.setContentView 方法

2.1 Activity -> PhoneWindow

PhoneWindow 是 Window 的唯一实现类,它的结构如下:

当调用 Activity.setContentView 方法实际上调用的是 PhoneWindow.setContentView 方法

frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
scss复制代码public void setContentView(int layoutResID) {
// mContentParent是ID为ID_ANDROID_CONTENT的FrameLayout
// 调用setContentView方法,就是给ID为ID_ANDROID_CONTENT的View添加子View
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// FEATURE_CONTENT_TRANSITIONS,则是标记当前内容加载有没有使用过度动画
// 如果内容已经加载过,并且不需要动画,则会调用removeAllViews
mContentParent.removeAllViews();
}

// 检查是否设置了FEATURE_CONTENT_TRANSITIONS
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 解析指定的XML资源文件
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
  • 先判断 mContentParent 是否为空,如果为空则调用 installDecor 方法,生成 mDecor,并将它赋值给 mContentParent
  • 根据 FEATURE_CONTENT_TRANSITIONS 标记来判断是否加载过转场动画
  • 如果设置了 FEATURE_CONTENT_TRANSITIONS 则添加 Scene 来过度启动,否则调用 mLayoutInflater.inflate(layoutResID, mContentParent),解析资源文件,创建 View, 并添加到 mContentParent 视图中

2.2 PhoneWindow -> LayoutInflater

当调用 PhoneWindow.setContentView 方法,之后调用 LayoutInflater.inflate 方法,来解析 XML 资源文件

frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
less复制代码public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

inflate 它有多个重载方法,最后调用的是 inflate(resource, root, root != null) 方法

frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
// 根据XML预编译生成compiled_view.dex, 然后通过反射来生成对应的View,从而减少XmlPullParser解析Xml的时间
// 需要注意的是在目前的release版本中不支持使用
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
// 获取资源解析器 XmlResourceParser
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

这个方法主要做了三件事:

  • 根据 XML 预编译生成 compiled_view.dex, 然后通过反射来生成对应的 View
  • 获取 XmlResourceParser
  • 解析 View

注意:在目前的 release 版本中不支持使用 tryInflatePrecompiled 方法源码如下:

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
java复制代码private void initPrecompiledViews() {
// Precompiled layouts are not supported in this release.
// enabled 是否启动预编译布局,这里始终为false
boolean enabled = false;
initPrecompiledViews(enabled);
}

private void initPrecompiledViews(boolean enablePrecompiledViews) {
mUseCompiledView = enablePrecompiledViews;

if (!mUseCompiledView) {
mPrecompiledClassLoader = null;
return;
}

...
}

View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
boolean attachToRoot) {
// mUseCompiledView始终为false
if (!mUseCompiledView) {
return null;
}

// 获取需要解析的资源文件的 pkg 和 layout
String pkg = res.getResourcePackageName(resource);
String layout = res.getResourceEntryName(resource);

try {
// 根据mPrecompiledClassLoader通过反射获取预编译生成的view对象的Class类
Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
Method inflater = clazz.getMethod(layout, Context.class, int.class);
View view = (View) inflater.invoke(null, mContext, resource);

if (view != null && root != null) {
// 将生成的view 添加根布局中
XmlResourceParser parser = res.getLayout(resource);
try {
AttributeSet attrs = Xml.asAttributeSet(parser);
advanceToRootNode(parser);
ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
// 如果 attachToRoot=true添加到根布局中
if (attachToRoot) {
root.addView(view, params);
} else {
// 否者将获取到的根布局的LayoutParams,设置到生成的view中
view.setLayoutParams(params);
}
} finally {
parser.close();
}
}

return view;
} catch (Throwable e) {

} finally {
}
return null;
}
  • tryInflatePrecompiled 方法是 Android 10 新增的方法,这是一个在编译器运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled 方法根据 XML 预编译生成compiled_view.dex, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,然后根据 attachToRoot 参数来判断是添加到根布局中,还是设置 LayoutParams 参数返回给调用者
  • 用一个全局变量 mUseCompiledView 来控制是否启用 tryInflatePrecompiled 方法,根据源码分析,mUseCompiledView 始终为 false

了解了 tryInflatePrecompiled 方法之后,在来查看一下 inflate 方法中的三个参数都什么意思

  • resource:要解析的 XML 布局文件 ID
  • root:表示根布局
  • attachToRoot:是否要添加到父布局 root 中

resource 其实很好理解就是资源 ID,而 root 和 attachToRoot 分别代表什么意思:

  • 当 attachToRoot == true 且 root != null 时,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回
  • 当 attachToRoot == false 且 root != null 时,新解析的 View 会直接作为结果返回,而且 root 会为新解析的 View 生成 LayoutParams 并设置到该 View 中去
  • 当 attachToRoot == false 且 root == null 时,新解析的 View 会直接作为结果返回

根据源码知道调用 tryInflatePrecompiled 方法返回的 view 为空,继续往下执行调用 Resources 的 getLayout 方法获取资源解析器 XmlResourceParser

2.3 LayoutInflater -> Resources

上面说到 XmlResourceParser 是通过调用 Resources 的 getLayout 方法获取的,getLayout 方法又去调用了 Resources 的loadXmlResourceParser 方法

frameworks/base/core/java/android/content/res/Resources.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
less复制代码public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
// TypedValue 主要用来存储资源
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
// 获取XML资源,保存到 TypedValue
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
// 为指定的XML资源,加载解析器
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}

TypedValue 是动态的数据容器,主要用来存储 Resource 的资源,获取 XML 资源保存到 TypedValue,之后调用 ResourcesImpl 的 loadXmlResourceParser 方法加载对应的解析器

2.4 Resources -> ResourcesImpl

ResourcesImpl 实现了 Resource 的访问,它包含了 AssetManager 和所有的缓存,通过 Resource 的 getValue 方法获取 XML 资源保存到 TypedValue,之后就会调用 ResourcesImpl 的 loadXmlResourceParser 方法对该布局资源进行解析

frameworks/base/core/java/android/content/res/ResourcesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
less复制代码XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException {
if (id != 0) {
try {
synchronized (mCachedXmlBlocks) {
final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
// 首先从缓存中查找XML资源
final int num = cachedXmlBlockFiles.length;
for (int i = 0; i < num; i++) {
if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
&& cachedXmlBlockFiles[i].equals(file)) {
// 调用newParser方法去构建一个XmlResourceParser对象,返回给调用者
return cachedXmlBlocks[i].newParser(id);
}
}

// 如果缓存中没有,则创建XmlBlock,并将它放到缓存中
// XmlBlock是已编译的XML文件的一个包装类
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
if (block != null) {
final int pos = (mLastCachedXmlBlockIndex + 1) % num;
mLastCachedXmlBlockIndex = pos;
final XmlBlock oldBlock = cachedXmlBlocks[pos];
if (oldBlock != null) {
oldBlock.close();
}
cachedXmlBlockCookies[pos] = assetCookie;
cachedXmlBlockFiles[pos] = file;
cachedXmlBlocks[pos] = block;
// 调用newParser方法去构建一个XmlResourceParser对象,返回给调用者
return block.newParser(id);
}
}
} catch (Exception e) {
final NotFoundException rnf = new NotFoundException("File " + file
+ " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
}

throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
+ Integer.toHexString(id));
}

首先从缓存中查找 XML 资源之后调用 newParser 方法,如果缓存中没有,则调用 AssetManger 的 openXmlBlockAsset 方法创建一个 XmlBlock,并将它放到缓存中,XmlBlock 是已编译的 XML 文件的一个包装类

frameworks/base/core/java/android/content/res/AssetManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
Preconditions.checkNotNull(fileName, "fileName");
synchronized (this) {
ensureOpenLocked();
// 调用native方法nativeOpenXmlAsset, 加载指定的XML资源文件,得到ResXMLTree
// xmlBlock是ResXMLTree对象的地址
final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
if (xmlBlock == 0) {
throw new FileNotFoundException("Asset XML file: " + fileName);
}
// 创建XmlBlock,封装xmlBlock,返回给调用者
final XmlBlock block = new XmlBlock(this, xmlBlock);
incRefsLocked(block.hashCode());
return block;
}
}

最终调用 native 方法 nativeOpenXmlAsset 去打开指定的 XML 文件,加载对应的资源,来查看一下 navtive 方法 NativeOpenXmlAsset

frameworks/base/core/jni/android_util_AssetManager.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码// java方法对应的native方法
{"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset}

static jlong NativeOpenXmlAsset(JNIEnv* env, jobject /*clazz*/, jlong ptr, jint jcookie,
jstring asset_path) {
ApkAssetsCookie cookie = JavaCookieToApkAssetsCookie(jcookie);
...

const DynamicRefTable* dynamic_ref_table = assetmanager->GetDynamicRefTableForCookie(cookie);

std::unique_ptr<ResXMLTree> xml_tree = util::make_unique<ResXMLTree>(dynamic_ref_table);
status_t err = xml_tree->setTo(asset->getBuffer(true), asset->getLength(), true);
asset.reset();
...

return reinterpret_cast<jlong>(xml_tree.release());
}
  • C++ 层的 NativeOpenXmlAsset 方法会创建 ResXMLTree 对象,返回的是 ResXMLTree 在 C++ 层的地址
  • Java 层 nativeOpenXmlAsse t方法的返回值 xmlBlock 是 C++ 层的 ResXMLTree 对象的地址,然后将 xmlBlock 封装进 XmlBlock 中返回给调用者

当 xmlBlock 创建之后,会调用 newParser 方法,构建一个 XmlResourceParser 对象,返回给调用者

2.5 ResourcesImpl -> XmlBlock

XmlBlock 是已编译的 XML 文件的一个包装类,XmlResourceParser 负责对 XML 的标签进行遍历解析的,它的真正的实现是 XmlBlock 的内部类 XmlBlock.Parser,而真正完成 XML 的遍历操作的函数都是由 XmlBlock 来实现的,为了提升效率都是通过 JNI 调用 native 的函数来做的,接下来查看一下 newParser 方法

frameworks/base/core/java/android/content/res/XmlBlock.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public XmlResourceParser newParser(@AnyRes int resId) {
synchronized (this) {
// mNative是C++层的ResXMLTree对象的地址
if (mNative != 0) {
// nativeCreateParseState方法根据 mNative 查找到ResXMLTree,
// 在C++层构建一个ResXMLParser对象,
// 构建Parser,封装ResXMLParser,返回给调用者
return new Parser(nativeCreateParseState(mNative, resId), this);
}
return null;
}
}

这个方法做两件事

  • mNative 是 C++ 层的 ResXMLTree 对象的地址,调用 native 方法 nativeCreateParseState,在 C++ 层构建一个 ResXMLParser 对象,返回 ResXMLParser 对象在 C++ 层的地址
  • Java 层拿到 ResXMLParser 在 C++ 层地址,构建 Parser,封装 ResXMLParser,返回给调用者

接下来查看一下 native 方法 nativeCreateParseState

frameworks/base/core/jni/android_util_XmlBlock.cpp

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
scss复制代码// java方法对应的native方法
{ "nativeCreateParseState", "(JI)J",
(void*) android_content_XmlBlock_nativeCreateParseState }


static jlong android_content_XmlBlock_nativeCreateParseState(JNIEnv* env, jobject clazz,
jlong token, jint res_id)
{
ResXMLTree* osb = reinterpret_cast<ResXMLTree*>(token);
if (osb == NULL) {
jniThrowNullPointerException(env, NULL);
return 0;
}

ResXMLParser* st = new ResXMLParser(*osb);
if (st == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
return 0;
}

st->setSourceResourceId(res_id);
st->restart();

return reinterpret_cast<jlong>(st);
}
  • token 对应 Java 层 mNative,是 C++ 层的 ResXMLTree 对象的地址
  • 调用 C++ 层 android_content_XmlBlock_nativeCreateParseState 方法,根据 token找到 ResXMLTree 对象
  • 在 C++ 层构建一个 ResXMLParser 对象,返给 Java 层对应 ResXMLParser 对象在 C++ 层的地址
  • Java 层拿到 ResXMLParser 在 C++ 层地址,封装到 Parser 中

2.6 再次回到 LayoutInflater

经过一系列的跳转,最后调用 XmlBlock.newParser 方法获取资源解析器 XmlResourceParser,之后回到 LayoutInflater 调用处 inflate 方法,然后调用 rInflate 方法解析 View

frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
java复制代码public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// 获取context
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
// 存储根布局
View result = root;

try {
// 处理 START_TA G和 END_TAG
advanceToRootNode(parser);
final String name = parser.getName();

// 解析merge标签,rInflate方法会将merge标签下面的所有子view添加到根布局中
// 这也是为什么merge标签可以简化布局的效果
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 解析merge标签下的所有的View,添加到根布局中
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 如果不是merge标签,调用createViewFromTag方法解析布局视图,这里的temp其实是我们xml里的top view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;

// 如果根布局不为空的话,且attachToRoot为false,为View设置布局参数
if (root != null) {
// 获取根布局的LayoutParams
params = root.generateLayoutParams(attrs);
// attachToRoot为false,为View设置LayoutParams
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}

// 解析当前View下面的所有子View
rInflateChildren(parser, temp, attrs, true);

// 如果 root 不为空且 attachToRoot 为false,将解析出来的View 添加到根布局
if (root != null && attachToRoot) {
root.addView(temp, params);
}

// 如果根布局为空 或者 attachToRoot 为false,返回当前的View
if (root == null || !attachToRoot) {
result = temp;
}
}

} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
throw ie;
} finally {
}
return result;
}
}
  • 解析 merge 标签,使用 merge 标签必须有父布局,且依赖于父布局加载
  • rInflate 方法会将 merge 标签下面的所有 View 添加到根布局中
  • 如果不是 merge 标签,调用 createViewFromTag 解析布局视图,返回 temp, 这里的 temp 其实是我们 XML 里的 Top View
  • 调用 rInflateChildren 方法,传递参数 temp,在 rInflateChildren方 法里内部,会调用 rInflate 方法, 解析当前 View 下面的所有子 View

通过分析源码知道了attachToRoot 和 root的参数代表什么意思,这里总结一下:*\

  • 当 attachToRoot == true 且 root != null 时,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回
  • 当 attachToRoot == false 且 root != null 时,新解析的 View 会直接作为结果返回,而且 root 会为新解析的View生成 LayoutParams并设置到该 View 中去
  • 当 attachToRoot == false 且 root == null 时,新解析的 View 会直接作为结果返回

无论是不是 merge 标签,最后都会调用 rInflate 方法进行 View 树的解析,他们的区别在于,如果是 merge 标签传递的参数 finishInflate 是 false,如果不是 merge 标签传递的参数 finishInflate 是 true

frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
scss复制代码void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

// 获取数的深度
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
// 逐个 View 解析
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

if (type != XmlPullParser.START_TAG) {
continue;
}

final String name = parser.getName();

if (TAG_REQUEST_FOCUS.equals(name)) {
// 解析android:focusable="true", 获取View的焦点
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
// 解析android:tag标签
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
// 解析include标签,include标签不能作为根布局
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
// merge标签必须作为根布局
throw new InflateException("<merge /> must be the root element");
} else {
// 根据元素名解析,生成View
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// rInflateChildren方法内部调用的rInflate方法,深度优先遍历解析所有的子View
rInflateChildren(parser, view, attrs, true);
// 添加解析的View
viewGroup.addView(view, params);
}
}

if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}

// 如果finishInflate为true,则调用onFinishInflate方法
if (finishInflate) {
parent.onFinishInflate();
}
}

整个 View 树的解析过程如下:

  • 获取 View 树的深度
  • 逐个 View 解析
  • 解析 android:focusable=”true”, 获取 View 的焦点
  • 解析 android:tag 标签
  • 解析 include 标签,并且 include 标签不能作为根布局
  • 解析 merge 标签,并且 merge 标签必须作为根布局
  • 根据元素名解析,生成对应的 View
  • rInflateChildren 方法内部调用的 rInflate 方法,深度优先遍历解析所有的子 View
  • 添加解析的 View

注意:通过分析源码, 以下几点需要特别注意

  • include 标签不能作为根元素,需要放在 ViewGroup中
  • merge 标签必须为根元素,使用 merge 标签必须有父布局,且依赖于父布局加载
  • 当 XmlResourseParser 对 XML 的遍历,随着布局越复杂,层级嵌套越多,所花费的时间也越长,所以对布局的优化,可以使用 meger 标签减少层级的嵌套

在解析过程中调用 createViewFromTag 方法,根据元素名解析,生成对应的 View,接下来查看一下 createViewFromTag 方法

frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
ini复制代码private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

// 如果设置了theme, 构建一个ContextThemeWrapper
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}

try {
// 如果name是blink,则创建BlinkLayout
// 如果设置factory,根据factory进行解析, 这是系统留给我们的Hook入口
View view = tryCreateView(parent, name, context, attrs);

// 如果 tryCreateView方法返回的View为空,则判断是内置View还是自定义View
// 如果是内置的View则调用onCreateView方法,如果是自定义View 则调用createView方法
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
// 如果使用自定义View,需要在XML指定全路径的,
// 例如:com.hi.dhl.CustomView,那么这里就有个.了
// 可以利用这一点判定是内置的View,还是自定义View
if (-1 == name.indexOf('.')) {
// 解析内置View
view = onCreateView(context, parent, name, attrs);
} else {
// 解析自定义View
view = createView(context, name, null, attrs);
}
/**
* onCreateView方法与createView方法的区别
* onCreateView方法:会给内置的View前面加一个前缀,例如:android.widget,最终会调用createView方法
* createView方法: 据完整的类的路径名利用反射机制构建View对象
*/
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;
} catch (InflateException e) {
throw e;

} catch (ClassNotFoundException e) {
throw ie;

} catch (Exception e) {
throw ie;
}
}
  • 解析 View 标签,如果设置了 theme, 构建一个 ContextThemeWrapper
  • 调用 tryCreateView 方法,如果 name 是 blink,则创建 BlinkLayout,如果设置 factory,根据 factory 进行解析,这是系统留给我们的 Hook 入口,我们可以人为的干涉系统创建 View,添加更多的功能
  • 如果 tryCreateView 方法返回的 View 为空,则分别调用 onCreateView 方法和 createView 方法,onCreateView 方法解析内置 View,createView 方法解析自定义 View

在解析过程中,会先调用 tryCreateView 方法,来看一下 tryCreateView 方法内部做了什么

frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
less复制代码public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
// BlinkLayout它是FrameLayout的子类,是LayoutInflater中的一个内部类,
// 如果当前标签为TAG_1995,则创建一个隔500毫秒闪烁一次的BlinkLayout来承载它的布局内容
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
// 源码注释也很有意思,写了Let's party like it's 1995!, 据说是为了庆祝1995年的复活节
return new BlinkLayout(context, attrs);
}

// 如果设置factory,根据factory进行解析, 这是系统留给我们的Hook入口,我们可以人为的干涉系统创建View,添加更多的功能
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

return view;
}
  • 如果 name 是 blink,则创建 BlinkLayout,返给调用者
  • 如果设置 factory,根据 factory 进行解析, 这是系统留给我们的 Hook 入口,我们可以人为的干涉系统创建 View,添加更多的功能,例如夜间模式,将 View 返给调用者

根据刚才的分析,会先调用 tryCreateView 方法,如果这个方法返回的 View 为空,然后会调用 onCreateView 方法对内置 View 进行解析,createView 方法对自定义 View 进行解析

onCreateView 方法与 createView 方法的有什么区别

  • onCreateView 方法:会给内置的 View 前面加一个前缀,例如: android.widget,最终会调用 createView 方法
  • createView 方法: 根据完整的类的路径名利用反射机制构建 View 对象

来看一下这两个方法的实现,LayoutInflater 是一个抽象类,我们实际使用的是 PhoneLayoutInflater,它的结构如下

PhoneLayoutInflater 重写了 LayoutInflater 的 onCreatView 方法,这个方法就是给内置的 View 前面加一个前缀

frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};

protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {

}
}

return super.onCreateView(name, attrs);
}

onCreateView 方法会给内置的 View 前面加一个前缀,之后调用 createView 方法,真正的 View 构建还是在 LayoutInflater 的 createView 方法里完成的,createView 方法根据完整的类的路径名利用反射机制构建 View 对象

frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
less复制代码public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...

try {

if (constructor == null) {
// 如果在缓存中没有找到构造函数,则根据完整的类的路径名利用反射机制构建View对象
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);

if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
// 利用反射机制构建clazz, 将它的构造函数存入sConstructorMap中,下次可以直接从缓存中查找
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// 如果从缓存中找到了缓存的构造函数
if (mFilter != null) {
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// 根据完整的类的路径名利用反射机制构建View对象
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);

...
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}

...

try {
// 利用构造函数,创建View
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 如果是ViewStub,则设置LayoutInflater
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
} catch (NoSuchMethodException e) {
throw ie;

} catch (ClassCastException e) {

throw ie;
} catch (ClassNotFoundException e) {
throw e;
} catch (Exception e) {

throw ie;
} finally {
}
}
  • 先从缓存中寻找构造函数,如果存在直接使用
  • 如果没有找到根据完整的类的路径名利用反射机制构建 View 对象

到了这里关于 APK 的布局 XML 资源文件的查找和解析 -> View 的生成流程到这里就结束了

总结

那我们就来依次来回答上面提出的几个问题

LayoutInflater 的 inflate 的三个参数都代表什么意思?

  • resource:要解析的 XML 布局文件 ID
  • root:表示根布局
  • attachToRoot:是否要添加到父布局 root 中

resource 其实很好理解就是资源 ID,而 root 和 attachToRoot 分别代表什么意思:

  • 当 attachToRoot == true 且 root != null 时,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回
  • 当 attachToRoot == false 且 root != null 时,新解析的 View 会直接作为结果返回,而且 root 会为新解析的 View 生成 LayoutParams 并设置到该 View 中去
  • 当 attachToRoot == false 且 root == null 时,新解析的 View 会直接作为结果返回

系统对 merge、include 是如何处理的

  • 使用 merge 标签必须有父布局,且依赖于父布局加载
  • merge 并不是一个 ViewGroup,也不是一个 View,它相当于声明了一些视图,等待被添加,解析过程中遇到 merge 标签会将 merge 标签下面的所有子 view 添加到根布局中
  • merge 标签在 XML 中必须是根元素
  • 相反的 include 不能作为根元素,需要放在一个 ViewGroup 中
  • 使用 include 标签必须指定有效的 layout 属性
  • 使用 include 标签不写宽高是没有关系的,会去解析被 include 的 layout

merge 标签为什么可以起到优化布局的效果?

解析过程中遇到 merge 标签,会调用 rInflate 方法,部分代码如下

1
2
3
4
5
6
7
8
ini复制代码// 根据元素名解析,生成对应的View
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// rInflateChildren方法内部调用的rInflate方法,深度优先遍历解析所有的子View
rInflateChildren(parser, view, attrs, true);
// 添加解析的View
viewGroup.addView(view, params);

解析 merge 标签下面的所有子 View,然后添加到根布局中

View 是如何被实例化的?

View 分为系统 View 和自定义 View, 通过调用 onCreateView 与createView 方法进行不同的处理

  • onCreateView 方法:会给内置的 View 前面加一个前缀,例如:android.widget,最终会调用 createView 方法
  • createView 方法:根据完整的类的路径名利用反射机制构建 View 对象

为什么复杂布局会产生卡顿?在 Android 10 上做了那些优化?

  • XmlResourseParser 对 XML 的遍历,随着布局越复杂,层级嵌套越多,所花费的时间也越长
  • 调用 onCreateView 与 createView 方法是通过反射创建 View 对象导致的耗时
  • 在 Android 10上,新增 tryInflatePrecompiled 方法是为了减少 XmlPullParser 解析 XML 的时间,但是用一个全局变量 mUseCompiledView 来控制是否启用 tryInflatePrecompiled 方法,根据源码分析,mUseCompiledView 始终为 false,所以 tryInflatePrecompiled 方法目前在 release 版本中不可使用

BlinkLayout 是什么?

BlinkLayout 继承 FrameLayout,是一种会闪烁的布局,被包裹的内容会一直闪烁,根据源码注释 Let’s party like it’s 1995!,BlinkLayout 是为了庆祝 1995 年的复活节, 有兴趣可以看看 reddit 上的讨论,来查看一下它的源码是如何实现的

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
java复制代码private static class BlinkLayout extends FrameLayout {
private static final int MESSAGE_BLINK = 0x42;
private static final int BLINK_DELAY = 500;

private boolean mBlink;
private boolean mBlinkState;
private final Handler mHandler;

public BlinkLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MESSAGE_BLINK) {
if (mBlink) {
mBlinkState = !mBlinkState;
// 每隔500ms循环调用
makeBlink();
}
// 触发dispatchDraw
invalidate();
return true;
}
return false;
}
});
}

private void makeBlink() {
// 发送延迟消息
Message message = mHandler.obtainMessage(MESSAGE_BLINK);
mHandler.sendMessageDelayed(message, BLINK_DELAY);
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mBlink = true;
mBlinkState = true;
makeBlink();
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mBlink = false;
mBlinkState = true;
// 移除消息,避免内存泄露
mHandler.removeMessages(MESSAGE_BLINK);
}

@Override
protected void dispatchDraw(Canvas canvas) {
if (mBlinkState) {
super.dispatchDraw(canvas);
}
}
}

通过源码分析可以看出,BlinkLayout 通过 Handler 每隔 500ms 发送消息,在 handleMessage 中循环调用 invalidate 方法,通过调用 invalidate 方法,来触发 dispatchDraw 方法,做到一闪一闪的效果

参考

  • www.reddit.com/r/androidde…
  • github.com/RTFSC-Andro…
  • www.yuque.com/beesx/beesa…

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,文章中有什么没有写明白的地方,或者有什么更好的建议欢迎留言,欢迎一起来学习,在技术的道路上一起前进。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 如何高效获取视频截图
  • 10分钟入门 Shell 脚本编程
  • 如何在项目中封装 Kotlin + Android Databinding

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

看完这篇 HTTPS,和面试官扯皮就没问题了

发表于 2020-03-14

下面我们来一起学习一下 HTTPS ,首先问你一个问题,为什么有了 HTTP 之后,还需要有 HTTPS ?我突然有个想法,为什么我们面试的时候需要回答标准答案呢?为什么我们不说出我们自己的想法和见解,却要记住一些所谓的标准回答呢?技术还有正确与否吗?

HTTPS 为什么会出现

一个新技术的出现必定是为了解决某种问题的,那么 HTTPS 解决了 HTTP 的什么问题呢?

HTTPS 解决了什么问题

一个简单的回答可能会是 HTTP 它不安全。由于 HTTP 天生明文传输的特性,在 HTTP 的传输过程中,任何人都有可能从中截获、修改或者伪造请求发送,所以可以认为 HTTP 是不安全的;在 HTTP 的传输过程中不会验证通信方的身份,因此 HTTP 信息交换的双方可能会遭到伪装,也就是没有用户验证;在 HTTP 的传输过程中,接收方和发送方并不会验证报文的完整性,综上,为了结局上述问题,HTTPS 应用而生。

什么是 HTTPS

你还记得 HTTP 是怎么定义的吗?HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol) 协议,它 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范,那么我们看一下 HTTPS 是如何定义的

HTTPS 的全称是 Hypertext Transfer Protocol Secure,它用来在计算机网络上的两个端系统之间进行安全的交换信息(secure communication),它相当于在 HTTP 的基础上加了一个 Secure 安全的词眼,那么我们可以给出一个 HTTPS 的定义:HTTPS 是一个在计算机世界里专门在两点之间安全的传输文字、图片、音频、视频等超文本数据的约定和规范。 HTTPS 是 HTTP 协议的一种扩展,它本身并不保传输的证安全性,那么谁来保证安全性呢?在 HTTPS 中,使用传输层安全性(TLS)或安全套接字层(SSL)对通信协议进行加密。也就是 HTTP + SSL(TLS) = HTTPS。

HTTPS 做了什么

HTTPS 协议提供了三个关键的指标

  • 加密(Encryption), HTTPS 通过对数据加密来使其免受窃听者对数据的监听,这就意味着当用户在浏览网站时,没有人能够监听他和网站之间的信息交换,或者跟踪用户的活动,访问记录等,从而窃取用户信息。
  • 数据一致性(Data integrity),数据在传输的过程中不会被窃听者所修改,用户发送的数据会完整的传输到服务端,保证用户发的是什么,服务器接收的就是什么。
  • 身份认证(Authentication),是指确认对方的真实身份,也就是证明你是你(可以比作人脸识别),它可以防止中间人攻击并建立用户信任。

有了上面三个关键指标的保证,用户就可以和服务器进行安全的交换信息了。那么,既然你说了 HTTPS 的种种好处,那么我怎么知道网站是用 HTTPS 的还是 HTTP 的呢?给你两幅图应该就可以解释了。

HTTPS 协议其实非常简单,RFC 文档很小,只有短短的 7 页,里面规定了新的协议名,默认端口号443,至于其他的应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。

也就是说,除了协议名称和默认端口号外(HTTP 默认端口 80),HTTPS 协议在语法、语义上和 HTTP 一样,HTTP 有的,HTTPS 也照单全收。那么,HTTPS 如何做到 HTTP 所不能做到的安全性呢?关键在于这个 S 也就是 SSL/TLS 。

什么是 SSL/TLS

认识 SSL/TLS

TLS(Transport Layer Security) 是 SSL(Secure Socket Layer) 的后续版本,它们是用于在互联网两台计算机之间用于身份验证和加密的一种协议。

注意:在互联网中,很多名称都可以进行互换。

我们都知道一些在线业务(比如在线支付)最重要的一个步骤是创建一个值得信赖的交易环境,能够让客户安心的进行交易,SSL/TLS 就保证了这一点,SSL/TLS 通过将称为 X.509 证书的数字文档将网站和公司的实体信息绑定到加密密钥来进行工作。每一个密钥对(key pairs) 都有一个 私有密钥(private key) 和 公有密钥(public key),私有密钥是独有的,一般位于服务器上,用于解密由公共密钥加密过的信息;公有密钥是公有的,与服务器进行交互的每个人都可以持有公有密钥,用公钥加密的信息只能由私有密钥来解密。

什么是 X.509:X.509 是公开密钥证书的标准格式,这个文档将加密密钥与(个人或组织)进行安全的关联。

X.509 主要应用如下

  • SSL/TLS 和 HTTPS 用于经过身份验证和加密的 Web 浏览
  • 通过 S/MIME 协议签名和加密的电子邮件
  • 代码签名:它指的是使用数字证书对软件应用程序进行签名以安全分发和安装的过程。

通过使用由知名公共证书颁发机构(例如SSL.com)颁发的证书对软件进行数字签名,开发人员可以向最终用户保证他们希望安装的软件是由已知且受信任的开发人员发布;并且签名后未被篡改或损害。

  • 还可用于文档签名
  • 还可用于客户端认证
  • 政府签发的电子身份证(详见 www.ssl.com/article/pki…

我们后面还会讨论。

HTTPS 的内核是 HTTP

HTTPS 并不是一项新的应用层协议,只是 HTTP 通信接口部分由 SSL 和 TLS 替代而已。通常情况下,HTTP 会先直接和 TCP 进行通信。在使用 SSL 的 HTTPS 后,则会先演变为和 SSL 进行通信,然后再由 SSL 和 TCP 进行通信。也就是说,HTTPS 就是身披了一层 SSL 的 HTTP。(我都喜欢把骚粉留在最后。。。)

SSL 是一个独立的协议,不只有 HTTP 可以使用,其他应用层协议也可以使用,比如 SMTP(电子邮件协议)、Telnet(远程登录协议) 等都可以使用。

探究 HTTPS

我说,你起这么牛逼的名字干嘛,还想吹牛批?你 HTTPS 不就抱上了 TLS/SSL 的大腿么,咋这么牛批哄哄的,还想探究 HTTPS,瞎胡闹,赶紧改成 TLS 是我主,赞美我主。

SSL 即安全套接字层,它在 OSI 七层网络模型中处于第五层,SSL 在 1999 年被 IETF(互联网工程组)更名为 TLS ,即传输安全层,直到现在,TLS 一共出现过三个版本,1.1、1.2 和 1.3 ,目前最广泛使用的是 1.2,所以接下来的探讨都是基于 TLS 1.2 的版本上的。

TLS 用于两个通信应用程序之间提供保密性和数据完整性。TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术(如果你觉得一项技术很简单,那你只是没有学到位,任何技术都是有美感的,牛逼的人只是欣赏,并不是贬低)。

说了这么半天,我们还没有看到 TLS 的命名规范呢,下面举一个 TLS 例子来看一下 TLS 的结构(可以参考 www.iana.org/assignments…

1
复制代码ECDHE-ECDSA-AES256-GCM-SHA384

这是啥意思呢?我刚开始看也有点懵啊,但其实是有套路的,因为 TLS 的密码套件比较规范,基本格式就是 密钥交换算法 - 签名算法 - 对称加密算法 - 摘要算法 组成的一个密码串,有时候还有分组模式,我们先来看一下刚刚是什么意思

使用 ECDHE 进行密钥交换,使用 ECDSA 进行签名和认证,然后使用 AES 作为对称加密算法,密钥的长度是 256 位,使用 GCM 作为分组模式,最后使用 SHA384 作为摘要算法。

TLS 在根本上使用对称加密和 非对称加密 两种形式。

对称加密

在了解对称加密前,我们先来了解一下密码学的东西,在密码学中,有几个概念:明文、密文、加密、解密

  • 明文(Plaintext),一般认为明文是有意义的字符或者比特集,或者是通过某种公开编码就能获得的消息。明文通常用 m 或 p 表示
  • 密文(Ciphertext),对明文进行某种加密后就变成了密文
  • 加密(Encrypt),把原始的信息(明文)转换为密文的信息变换过程
  • 解密(Decrypt),把已经加密的信息恢复成明文的过程。

对称加密(Symmetrical Encryption)顾名思义就是指加密和解密时使用的密钥都是同样的密钥。只要保证了密钥的安全性,那么整个通信过程也就是具有了机密性。

TLS 里面有比较多的加密算法可供使用,比如 DES、3DES、AES、ChaCha20、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK 等。目前最常用的是 AES-128, AES-192、AES-256 和 ChaCha20。

DES 的全称是 Data Encryption Standard(数据加密标准) ,它是用于数字数据加密的对称密钥算法。尽管其 56 位的短密钥长度使它对于现代应用程序来说太不安全了,但它在加密技术的发展中具有很大的影响力。

3DES 是从原始数据加密标准(DES)衍生过来的加密算法,它在 90 年代后变得很重要,但是后面由于更加高级的算法出现,3DES 变得不再重要。

AES-128, AES-192 和 AES-256 都是属于 AES ,AES 的全称是Advanced Encryption Standard(高级加密标准),它是 DES 算法的替代者,安全强度很高,性能也很好,是应用最广泛的对称加密算法。

ChaCha20 是 Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错算法。

(其他可自行搜索)

加密分组

对称加密算法还有一个分组模式 的概念,对于 GCM 分组模式,只有和 AES,CAMELLIA 和 ARIA 搭配使用,而 AES 显然是最受欢迎和部署最广泛的选择,它可以让算法用固定长度的密钥加密任意长度的明文。

最早有 ECB、CBC、CFB、OFB 等几种分组模式,但都陆续被发现有安全漏洞,所以现在基本都不怎么用了。最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data),在加密的同时增加了认证的功能,常用的是 GCM、CCM 和 Poly1305。

比如 ECDHE_ECDSA_AES128_GCM_SHA256 ,表示的是具有 128 位密钥, AES256 将表示 256 位密钥。GCM 表示具有 128 位块的分组密码的现代认证的关联数据加密(AEAD)操作模式。

我们上面谈到了对称加密,对称加密的加密方和解密方都使用同一个密钥,也就是说,加密方必须对原始数据进行加密,然后再把密钥交给解密方进行解密,然后才能解密数据,这就会造成什么问题?这就好比《小兵张嘎》去送信(信已经被加密过),但是嘎子还拿着解密的密码,那嘎子要是在途中被鬼子发现了,那这信可就是被完全的暴露了。所以,对称加密存在风险。

非对称加密

非对称加密(Asymmetrical Encryption) 也被称为公钥加密,相对于对称加密来说,非对称加密是一种新的改良加密方式。密钥通过网络传输交换,它能够确保及时密钥被拦截,也不会暴露数据信息。非对称加密中有两个密钥,一个是公钥,一个是私钥,公钥进行加密,私钥进行解密。公开密钥可供任何人使用,私钥只有你自己能够知道。

使用公钥加密的文本只能使用私钥解密,同时,使用私钥加密的文本也可以使用公钥解密。公钥不需要具有安全性,因为公钥需要在网络间进行传输,非对称加密可以解决密钥交换的问题。网站保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。

非对称加密算法的设计要比对称算法难得多(我们不会探讨具体的加密方式),常见的比如 DH、DSA、RSA、ECC 等。

其中 RSA 加密算法是最重要的、最出名的一个了。例如 DHE_RSA_CAMELLIA128_GCM_SHA256。它的安全性基于 整数分解,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。

ECC(Elliptic Curve Cryptography)也是非对称加密算法的一种,它基于椭圆曲线离散对数的数学难题,使用特定的曲线方程和基点生成公钥和私钥, ECDHE 用于密钥交换,ECDSA 用于数字签名。

TLS 是使用对称加密和非对称加密 的混合加密方式来实现机密性。

混合加密

RSA 的运算速度非常慢,而 AES 的加密速度比较快,而 TLS 正是使用了这种混合加密方式。在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE ,首先解决密钥交换的问题。然后用随机数产生对称算法使用的会话密钥(session key),再用公钥加密。对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换。

现在我们使用混合加密的方式实现了机密性,是不是就能够安全的传输数据了呢?还不够,在机密性的基础上还要加上完整性、身份认证的特性,才能实现真正的安全。而实现完整性的主要手段是 摘要算法(Digest Algorithm)

摘要算法

如何实现完整性呢?在 TLS 中,实现完整性的手段主要是 摘要算法(Digest Algorithm)。摘要算法你不清楚的话,MD5 你应该清楚,MD5 的全称是 Message Digest Algorithm 5,它是属于密码哈希算法(cryptographic hash algorithm)的一种,MD5 可用于从任意长度的字符串创建 128 位字符串值。尽管 MD5 存在不安全因素,但是仍然沿用至今。MD5 最常用于验证文件的完整性。但是,它还用于其他安全协议和应用程序中,例如 SSH、SSL 和 IPSec。一些应用程序通过向明文加盐值或多次应用哈希函数来增强 MD5 算法。

什么是加盐?在密码学中,盐就是一项随机数据,用作哈希数据,密码或密码的单向函数的附加输入。盐用于保护存储中的密码。例如

什么是单向?就是在说这种算法没有密钥可以进行解密,只能进行单向加密,加密后的数据无法解密,不能逆推出原文。

我们再回到摘要算法的讨论上来,其实你可以把摘要算法理解成一种特殊的压缩算法,它能够把任意长度的数据压缩成一种固定长度的字符串,这就好像是给数据加了一把锁。

除了常用的 MD5 是加密算法外,SHA-1(Secure Hash Algorithm 1) 也是一种常用的加密算法,不过 SHA-1 也是不安全的加密算法,在 TLS 里面被禁止使用。目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2。

SHA-2 的全称是Secure Hash Algorithm 2 ,它在 2001 年被推出,它在 SHA-1 的基础上做了重大的修改,SHA-2 系列包含六个哈希函数,其摘要(哈希值)分别为 224、256、384 或 512 位:SHA-224, SHA-256, SHA-384, SHA-512。分别能够生成 28 字节、32 字节、48 字节、64 字节的摘要。

有了 SHA-2 的保护,就能够实现数据的完整性,哪怕你在文件中改变一个标点符号,增加一个空格,生成的文件摘要也会完全不同,不过 SHA-2 是基于明文的加密方式,还是不够安全,那应该用什么呢?

安全性更高的加密方式是使用 HMAC,在理解什么是 HMAC 前,你需要先知道一下什么是 MAC。

MAC 的全称是message authentication code,它通过 MAC 算法从消息和密钥生成,MAC 值允许验证者(也拥有秘密密钥)检测到消息内容的任何更改,从而保护了消息的数据完整性。

HMAC 是 MAC 更进一步的拓展,它是使用 MAC 值 + Hash 值的组合方式,HMAC 的计算中可以使用任何加密哈希函数,例如 SHA-256 等。

现在我们又解决了完整性的问题,那么就只剩下一个问题了,那就是认证,认证怎么做的呢?我们再向服务器发送数据的过程中,黑客(攻击者)有可能伪装成任何一方来窃取信息。它可以伪装成你,来向服务器发送信息,也可以伪装称为服务器,接受你发送的信息。那么怎么解决这个问题呢?

认证

如何确定你自己的唯一性呢?我们在上面的叙述过程中出现过公钥加密,私钥解密的这个概念。提到的私钥只有你一个人所有,能够辨别唯一性,所以我们可以把顺序调换一下,变成私钥加密,公钥解密。使用私钥再加上摘要算法,就能够实现数字签名,从而实现认证。

到现在,综合使用对称加密、非对称加密和摘要算法,我们已经实现了加密、数据认证、认证,那么是不是就安全了呢?非也,这里还存在一个数字签名的认证问题。因为私钥是是自己的,公钥是谁都可以发布,所以必须发布经过认证的公钥,才能解决公钥的信任问题。

所以引入了 CA,CA 的全称是 Certificate Authority,证书认证机构,你必须让 CA 颁布具有认证过的公钥,才能解决公钥的信任问题。

全世界具有认证的 CA 就几家,分别颁布了 DV、OV、EV 三种,区别在于可信程度。DV 是最低的,只是域名级别的可信,EV 是最高的,经过了法律和审计的严格核查,可以证明网站拥有者的身份(在浏览器地址栏会显示出公司的名字,例如 Apple、GitHub 的网站)。不同的信任等级的机构一起形成了层级关系。

通常情况下,数字证书的申请人将生成由私钥和公钥以及证书签名请求(CSR)组成的密钥对。CSR是一个编码的文本文件,其中包含公钥和其他将包含在证书中的信息(例如域名,组织,电子邮件地址等)。密钥对和 CSR生成通常在将要安装证书的服务器上完成,并且 CSR 中包含的信息类型取决于证书的验证级别。与公钥不同,申请人的私钥是安全的,永远不要向 CA(或其他任何人)展示。

生成 CSR 后,申请人将其发送给 CA,CA 会验证其包含的信息是否正确,如果正确,则使用颁发的私钥对证书进行数字签名,然后将其发送给申请人。

总结

本篇文章我们主要讲述了 HTTPS 为什么会出现 ,HTTPS 解决了 HTTP 的什么问题,HTTPS 和 HTTP 的关系是什么,TLS 和 SSL 是什么,TLS 和 SSL 解决了什么问题?如何实现一个真正安全的数据传输?

文章参考:

www.ssl.com/faqs/what-i…

www.ibm.com/support/kno…

en.wikipedia.org/wiki/Messag…

en.wikipedia.org/wiki/HMAC

www.quora.com/What-does-i…

hpbn.co/transport-l…

www.ssl2buy.com/wiki/symmet…

crypto.stackexchange.com/questions/2…

en.wikipedia.org/wiki/Advanc…

www.comparitech.com/blog/inform…

《极客时间-透析 HTTP 协议》

www.tutorialsteacher.com/https/how-s…

baike.baidu.com/item/密码系统/5…

baike.baidu.com/item/对称加密/2…

www.ssl.com/faqs/faq-wh…

en.wikipedia.org/wiki/HTTPS

support.google.com/webmasters/…

www.cloudflare.com/learning/ss…

www.cisco.com/c/en/us/pro…

www.freecodecamp.org/news/web-se…

本文转载自: 掘金

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

腾讯面试官这样问我二叉树,我刚好都会 前记 面试问题 面试官

发表于 2020-03-13

前记

上周我投递出了简历,岗位是后端开发工程师。这周腾讯面试官给我进行了视频面试。面试过程中他问了二叉树的问题。二叉树相关算法题,在面试中出现的次数非常非常多,所以我面试之前也有所准备。今天结合面试问题详细讲一讲二叉树,结合实例分析二叉树的存储结构的建立方法和遍历过程。

面试问题

面试官大佬:看你的简历上写熟悉数据结构,谈谈二叉树遍历的方式?

我:(这可难不倒我)

先序遍历

先访问根节点,后依次访问左孩子和右孩子

递归算法

1
2
3
4
5
6
7
8
9
10
11
12
复制代码void PreOrder1(BTREE bt) //递归先根遍历 
{
if (bt)
{
if (bt->data != '#')
{
printf(" %c", bt->data);//结点不空 ,打印结点值
}
PreOrder1(bt->lchild);//依次访问左右节点
PreOrder1(bt->rchild);
}
}

非递归算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码void PreOrder2(BTREE p)//非递归先根遍历 ,先访问根节点,后依次访问左孩子和右孩子 
{
int top = -1;
node *Q[N];
while (p != NULL || top != -1)
{
while (p != NULL)
{
if (p->data != '#')
{
printf(" %c", p->data);
}
Q[++top] = p;
p = p->lchild;
}
if (top != -1)
{
p = Q[top--];
p = p->rchild;
}
}
}

中序遍历

先访问左孩子,后依次访问根节点和右孩子

递归算法

1
2
3
4
5
6
7
8
9
10
11
12
复制代码void InOrder1(BTREE bt)//递归中序遍历
{
if (bt)
{
InOrder1(bt->lchild);//先访问左节点
if (bt->data != '#')
{
printf(" %c", bt->data);//结点不空 ,打印结点值
}
InOrder1(bt->rchild);//先访问右节点
}
}

非递归算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码void InOrder2(BTREE p)//非递归中序遍历,先访问左孩子,然后访问根节点,后访问右孩子
{
int top = -1;
node *Q[N];
while (p != NULL || top != -1)
{
while (p != NULL)
{
Q[++top] = p;
p = p->lchild;
}
if (top != -1)
{
p = Q[top--];
if (p->data != '#')
{
printf(" %c",p->data);
}
p = p->rchild;
}
}
}

后序遍历

先访问左孩子孩子,后依次访问右孩子和根节点

递归算法

1
2
3
4
5
6
7
8
9
10
11
12
复制代码void PostOrder1(BTREE bt)//后序遍历 
{
if (bt)
{
PostOrder1(bt->lchild);//先访问左,右孩子节点
PostOrder1(bt->rchild);
if (bt->data != '#')
{
printf(" %c", bt->data);//后访问根节点
}
}
}

非递归算法

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
复制代码void PostOrder2(BTREE p)//非递归后序遍历 ,先访问左孩子,然后访问右孩子,后访问根节点 
{
int top = -1;
node *Q[N];
int flag[N] = { 0 };
while (p != NULL || top != -1)
{
while (p != NULL)
{
top++;
Q[top] = p;
flag[top] = 1;
p = p->lchild;
}
while (top != -1 && flag[top] == 2)
{
if (Q[top]->data != '#')
{
printf(" %c", Q[top]->data);
top--;
}
}
if (top != -1)
{
flag[top] = 2;
p = Q[top]->rchild;
}
}
}

面试官大佬:你回答得很好,还有其他遍历方式吗

我:……

沉默了几秒,我(这可难不倒我):还有一种层序遍历

层序遍历

从根开始,依次向下,对于每一层从左向右遍历

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
复制代码//层序遍历 
void Sequense(BTREE bt)//建立栈,依次将根节点,左孩子,右孩子压栈 ,并打印栈顶元素
{
node *Q[N];
node *p;
int front = 0, top = 0;
if (bt != NULL)
{
Q[++top] = bt;//将根节点压栈
while (front < top) //遍历栈
{
p = Q[++front];
if (p->data != '#')
{
printf(" %c", p->data);//打印栈顶元素
}
if (p->lchild)
{
Q[++top] = p->lchild;//将左孩子压栈
}
if (p->rchild)
{
Q[++top] = p->rchild;//将右孩子压栈
}
}
}
}

遍历算法总结

在这里插入图片描述

面试官大佬:如何判断是否完全二叉树呢

我:(这可难不倒我)

判断完全二叉树

  1. 按层遍历二叉树, 从每层从左向右遍历所有的结点
  2. 如果当前结点有右孩子, 但没有左孩子, 那么不是完全二叉树
  3. 如果当前结点有左孩子但无右孩子, 那么它之后的所有结点都必须为叶子结点,否则不是完全二叉树
  4. 如果当前结点有左孩子和右孩子, 继续遍历
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
复制代码int Compnode(BTREE G)//判断是否是完全二叉树 
{
node *D[N], *p; //建立一个队列D[N]
int front = 0, last = 0; //front是队头指针,last是队尾指针
int tree_signal = 1;//tree_signal是判断是否为完全二叉树的标志
int odd_signal = 1;//odd_signal是判断是否存在无左孩子的节点的标志
if (G != NULL)
{
last++;
D[last] = G; //将根节点压入队尾
while (front != last)
{
front++;
p = D[front];
if (p->lchild == NULL ||(p->lchild)->data == '#') //*p节点没有左孩子
{
odd_signal = 0;
if (p->rchild != NULL && (p->rchild)->data != '#') //没有左孩子但有右孩子,不是完全二叉树
tree_signal = 0;
}
else //*p节点有左子树
{
if (odd_signal == 1) //目前不存在无左孩子的节点
{
last++; //左孩子进队
D[last] = p->lchild;
if (p->rchild == NULL || (p->rchild)->data == '#') //*p有左孩子但没有右孩子
{
odd_signal = 0;
}
else
{
last++; //右孩子进队
D[last] = p->rchild;
}
}
else //目前存在有左孩子的节点,不是完全二叉树
{
tree_signal = 0;
}
}
}
}
else
{
tree_signal = 0;//假设空树不是完全二叉树
}
return tree_signal;
}

总结

咱们玩归玩,闹归闹,别拿面试开玩笑。

二叉树的遍历虽然简单,但遍历方式多样,也有递归算法和非递归算法之分。一旦问到了,大家一定要回答全面,不要丢三落四,回答到点上。二叉树相关算法题,在面试中出现的次数非常非常多,大家面试前要把二叉树等数据结构的基础打牢。

如果有收获?希望老铁们来个双连击,给更多的人看到这篇文章

1、老铁们,关注我的原创微信公众号「程序猿的进阶」,主要是IT与竞赛

2、创作不易,顺便点个赞呗,可以让更多的人看到这篇文章,激励一下我这个小白。

本文转载自: 掘金

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

你必须会的--Dijkstra算法--单源最短路径问题

发表于 2020-03-12

一.算法原理

1.基本原理

Dijkstra算法是利用广度优先搜索思想(BFS)的一种**单源最短路径算法**,相对于简单粗暴时间复杂度为O(n^3)的Floyed算法,(详情见我另一篇博客 [只有五行的Floyed算法及全路径输出](https://blog.csdn.net/include_IT_dog/article/details/88938561)),Dijkstra算法的时间复杂度则有好的多,为O(n^2)。 该算法以起点为中心,将相邻边加入到优先队列中,每次弹出队列中距起点最近的点,利用伸缩操作(relaxation),更新该点各相邻节点到源点的最近距离(这里用到了贪心算法原理), 存入到一个集合disTo中,该集合中记录每一个点到源点的最近距离。 伪代码: undefined ### 2.如何保存最短路径? 在pathTo集合中,设置此节点的上一节点。如果这点没有被访问过,就加入到优先队列中,就这样重复操作层层向外遍历,最后就可以生成一个最短路径树,对于从该源点到某一点的最短路径问题,只要看该点是否被访问过,被访问过的点说明存在最短路径,回溯pathTo集合,如pathTo(A) = B, B是使A到源点距离最近的相邻点(由贪心算法可知),pathTo(B) = C , C是使B到源点距离最近的相邻点,反复操作,直到pathTo(X) = 源点。即可得到最短路径 二.算法实现 ------ undefined ### 1.测试 ![](https://gitee.com/songjianzaina/juejin_p15/raw/master/img/814c14c3935154f84de9af55c9a4312802b9591fea0114b75d523d1b3e1a6c3f) 输入: | edges: | n: | k: | pathTo | | --- | --- | --- | --- | | {{0, 1, 15},{0, 3, 5}, | 6 | 0 | 4 | | {1, 5, 6}, {3, 5, 20}, | | | | | {1, 2, 15}, {3, 2, 30}, | | | | | {2, 4, 10}, {5, 4, 9}}; | | | | 预计输出:最短距离:30 路径:0-->1-->5-->4 环境:windows10,java11 ### 2.结果 ![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p15/raw/master/img/44b8dd070bdce9a05de36c6c47a74513b4128709be382c41c42a83844e478407)

会了Dijkstra,不会DFS那更不得行,帮你安排了DFS的递归实现与堆栈实现

本文转载自: 掘金

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

学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原

发表于 2020-03-12

前言

你好,我是若川。这是学习源码整体架构系列第七篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库

2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库

3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库

4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

5.学习 vuex 源码整体架构,打造属于自己的状态管理库

6.学习 axios 源码整体架构,打造属于自己的请求库

7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理

感兴趣的读者可以点击阅读。

其他源码计划中的有:express、vue-router、react-redux 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。

所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

如果你简历上一不小心写了熟悉koa,面试官大概率会问:

1、koa洋葱模型怎么实现的。

2、如果中间件中的next()方法报错了怎么办。

3、co的原理是怎样的。

等等问题

导读

文章通过例子调试koa,梳理koa的主流程,来理解koa-compose洋葱模型原理和co库的原理,相信看完一定会有所收获。

本文目录

本文目录

本文学习的koa版本是v2.11.0。克隆的官方仓库的master分支。
截至目前(2020年3月11日),最新一次commit是2020-01-04 07:41 Olle Jonsson eda27608,build: Drop unused Travis sudo: false directive (#1416)。

本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis。求个star呀。

本文阅读最佳方式

先star一下我的仓库,再把它git clone https://github.com/lxchuan12/koa-analysis.git克隆下来。不用管你是否用过nodejs。会一点点promise、generator、async、await等知识即可看懂。如果一点点也不会,可以边看阮一峰老师的《ES6标准入门》相关章节。跟着文章节奏调试和示例代码调试,动手调试(用vscode或者chrome)印象更加深刻。文章长段代码不用细看,可以调试时再细看。看这类源码文章百遍,可能不如自己多调试几遍。也欢迎加我微信交流lxchuan12。

1
2
3
4
5
6
7
8
9
10
复制代码# 克隆我的这个仓库  
git clone https://github.com/lxchuan12/koa-analysis.git
# chrome 调试:
# 全局安装 http-server
npm i -g http-server
hs koa/examples/
# 可以指定端口 -p 3001
# hs -p 3001 koa/examples/
# 浏览器中打开
# 然后在浏览器中打开localhost:8080,开心的把代码调试起来

这里把这个examples文件夹做个简单介绍。

  • middleware文件夹是用来vscode调试整体流程的。
  • simpleKoa 文件夹是koa简化版,为了调试koa-compose洋葱模型如何串联起来各个中间件的。
  • koa-convert文件夹是用来调试koa-convert和co源码的。
  • co-generator文件夹是模拟实现co的示例代码。

vscode 调试 koa 源码方法

之前,我在知乎回答了一个问题一年内的前端看不懂前端框架源码怎么办?
推荐了一些资料,阅读量还不错,大家有兴趣可以看看。主要有四点:

1.借助调试

2.搜索查阅相关高赞文章

3.把不懂的地方记录下来,查阅相关文档

4.总结

看源码,调试很重要,所以我详细写下 koa 源码调试方法,帮助一些可能不知道如何调试的读者。

1
2
复制代码# 我已经克隆到我的koa-analysis仓库了  
git clone https://github.com/koajs/koa.git
1
2
3
4
5
6
7
复制代码// package.json  
{
"name": "koa",
"version": "2.11.0",
"description": "Koa web app framework",
"main": "lib/application.js",
}

克隆源码后,看package.json找到main,就知道入口文件是lib/application.js了。

大概看完项目结构后发现没有examples文件夹(一般项目都会有这个文件夹,告知用户如何使用该项目),这时仔细看README.md。
如果看英文README.md有些吃力,会发现在Community标题下有一个中文文档 v2.x。同时也有一个examples仓库。

1
2
复制代码# 我已经克隆下来到我的仓库了  
git clone https://github.com/koajs/examples.git

这时再开心的把examples克隆到自己电脑。可以安装好依赖,逐个研究学习下这里的例子,然后可能就一不小心掌握了koa的基本用法。当然,我这里不详细写这一块了,我是自己手写一些例子来调试。

继续看文档会发现使用指南讲述编写中间件。

使用文档中的中间件koa-compose例子来调试

学习 koa-compose 前,先看两张图。

洋葱模型示意图

洋葱模型示意图

洋葱模型中间件示意图

洋葱模型中间件示意图

在koa中,请求响应都放在中间件的第一个参数context对象中了。

再引用Koa中文文档中的一段:

如果您是前端开发人员,您可以将 next(); 之前的任意代码视为“捕获”阶段,这个简易的 gif 说明了 async 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:

中间件gif图

中间件gif图

  1. 创建一个跟踪响应时间的日期
  2. 等待下一个中间件的控制
  3. 创建另一个日期跟踪持续时间
  4. 等待下一个中间件的控制
  5. 将响应主体设置为“Hello World”
  6. 计算持续时间
  7. 输出日志行
  8. 计算响应时间
  9. 设置 X-Response-Time 头字段
  10. 交给 Koa 处理响应

读者们看完这个gif图,也可以思考下如何实现的。根据表现,可以猜测是next是一个函数,而且返回的可能是一个promise,被await调用。

看到这个gif图,我把之前写的examples/koa-compose的调试方法含泪删除了。默默写上gif图上的这些代码,想着这个读者们更容易读懂。
我把这段代码写在这里 koa/examples/middleware/app.js便于调试。

在项目路径下配置新建.vscode/launch.json文件,program配置为自己写的koa/examples/middleware/app.js文件。

.vscode/launch.json 代码,点击这里展开/收缩,可以复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码{  
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/koa/examples/middleware/app.js"
}
]
}

按F5键开始调试,调试时先走主流程,必要的地方打上断点,不用一开始就关心细枝末节。

断点调试要领:

赋值语句可以一步跳过,看返回值即可,后续详细再看。

函数执行需要断点跟着看,也可以结合注释和上下文倒推这个函数做了什么。

上述比较啰嗦的写了一堆调试方法。主要是想着授人予鱼不如授人予渔,这样换成其他源码也会调试了。

简单说下chrome调试nodejs,chrome浏览器打开chrome://inspect,点击配置**configure…**配置127.0.0.1:端口号(端口号在Vscode 调试控制台显示了)。

更多可以查看English Debugging Guide

中文调试指南

喜欢看视频的读者也可以看慕课网这个视频node.js调试入门,讲得还是比较详细的。

不过我感觉在chrome调试nodejs项目体验不是很好(可能是我方式不对),所以我大部分具体的代码时都放在html文件script形式,在chrome调试了。

先看看 new Koa() 结果app是什么

看源码我习惯性看它的实例对象结构,一般所有属性和方法都放在实例对象上了,而且会通过原型链查找形式查找最顶端的属性和方法。

用koa/examples/middleware/app.js文件调试时,先看下执行new Koa()之后,app是什么,有个初步印象。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// 文件 koa/examples/middleware/app.js  
const Koa = require('../../lib/application');

// const Koa = require('koa');
// 这里打个断点
const app = new Koa();
// x-response-time

// 这里打个断点
app.use(async (ctx, next) => {

});

在调试控制台ctrl + 反引号键(一般在Tab上方的按键)唤起,输入app,按enter键打印app。会有一张这样的图。

koa 实例对象调试图

koa 实例对象调试图

VScode也有一个代码调试神器插件Debug Visualizer。

安装好后插件后,按ctrl + shift + p,输入Open a new Debug Visualizer View,来使用,输入app,显示是这样的。

koa 实例对象可视化简版

koa 实例对象可视化简版

不过目前体验来看,相对还比较鸡肋,只能显示一级,而且只能显示对象,相信以后会更好。更多玩法可以查看它的文档。

我把koa实例对象比较完整的用xmind画出来了,大概看看就好,有个初步印象。

koa 实例对象

koa 实例对象

接着,我们可以看下app 实例、context、request、request的官方文档。

app 实例、context、request、request 官方API文档

  • index API | context API | request API | response API

可以真正使用的时候再去仔细看文档。

koa 主流程梳理简化

通过F5启动调试(直接跳到下一个断点处)、F10单步跳过、F11单步调试等,配合重要的地方断点,调试完整体代码,其实比较容易整理出如下主流程的代码。

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
复制代码class Emitter{  
// node 内置模块
constructor(){
}
}
class Koa extends Emitter{
constructor(options){
super();
options = options || {};
this.middleware = [];
this.context = {
method: 'GET',
url: '/url',
body: undefined,
set: function(key, val){
console.log('context.set', key, val);
},
};
}
use(fn){
this.middleware.push(fn);
return this;
}
listen(){
const fnMiddleware = compose(this.middleware);
const ctx = this.context;
const handleResponse = () => respond(ctx);
const onerror = function(){
console.log('onerror');
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
function respond(ctx){
console.log('handleResponse');
console.log('response.end', ctx.body);
}

重点就在listen函数里的compose这个函数,接下来我们就详细来欣赏下这个函数。

koa-compose 源码(洋葱模型实现)

通过app.use() 添加了若干函数,但是要把它们串起来执行呀。像上文的gif图一样。

compose函数,传入一个数组,返回一个函数。对入参是不是数组和校验数组每一项是不是函数。

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
复制代码function compose (middleware) {  
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

// 传入对象 context 返回Promise
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

把简化的代码和koa-compose代码写在了一个文件中。koa/examples/simpleKoa/koa-compose.js

1
2
复制代码hs koa/examples/  
# 然后可以打开localhost:8080/simpleKoa,开心的把代码调试起来

不过这样好像还是有点麻烦,我还把这些代码放在codepen https://codepen.io/lxchuan12/pen/wvarPEb中,直接可以在线调试啦。是不是觉得很贴心^_^,自己多调试几遍便于消化理解。

你会发现compose就是类似这样的结构(移除一些判断)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码// 这样就可能更好理解了。  
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);

也就是说koa-compose返回的是一个Promise,Promise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。

第一个next函数里也是返回的是一个Promise,Promise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。

第二个next函数里也是返回的是一个Promise,Promise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。

第三个…

以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。
这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。

不得不说非常惊艳,“玩还是大神会玩”。

这种把函数存储下来的方式,在很多源码中都有看到。比如lodash源码的惰性求值,vuex也是把action等函数存储下,最后才去调用。

搞懂了koa-compose 洋葱模型实现的代码,其他代码就不在话下了。

错误处理

中文文档 错误处理

仔细看文档,文档中写了三种捕获错误的方式。

  • ctx.onerror 中间件中的错误捕获
  • app.on('error', (err) => {}) 最外层实例事件监听形式
    也可以看看例子koajs/examples/errors/app.js 文件
  • app.onerror = (err) => {} 重写onerror自定义形式
    也可以看测试用例 onerror
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// application.js 文件  
class Application extends Emitter {
// 代码有简化组合
listen(){
const fnMiddleware = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const onerror = err => ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
onerror(err) {
// 代码省略
// ...
}
}

ctx.onerror

lib/context.js文件中,有一个函数onerror,而且有这么一行代码this.app.emit('error', err, this)。

1
2
3
4
5
6
7
复制代码module.exports = {  
onerror(){
// delegate
// app 是在new Koa() 实例
this.app.emit('error', err, this);
}
}
1
2
3
4
5
6
7
8
复制代码app.use(async (ctx, next) => {  
try {
await next();
} catch (err) {
err.status = err.statusCode || err.status || 500;
throw err;
}
});

try catch 错误或被fnMiddleware(ctx).then(handleResponse).catch(onerror);,这里的onerror是ctx.onerror

而ctx.onerror函数中又调用了this.app.emit('error', err, this),所以在最外围app.on('error',err => {})可以捕获中间件链中的错误。
因为koa继承自events模块,所以有’emit’和on等方法)

koa2 和 koa1 的简单对比

中文文档中描述了 koa2 和 koa1 的区别

koa1中主要是generator函数。koa2中会自动转换generator函数。

1
2
3
4
5
6
7
复制代码// Koa 将转换  
app.use(function *(next) {
const start = Date.now();
yield next;
const ms = Date.now() - start;
console.log(`${this.method} ${this.url} - ${ms}ms`);
});

koa-convert 源码

在vscode/launch.json文件,找到这个program字段,修改为"program": "${workspaceFolder}/koa/examples/koa-convert/app.js"。

通过F5启动调试(直接跳到下一个断点处)、F10单步跳过、F11单步调试调试走一遍流程。重要地方断点调试。

app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码class Koa extends Emitter{  
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
}

koa-convert源码挺多,核心代码其实是这样的。

1
2
3
4
5
6
7
8
复制代码function convert(){  
return function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
function * createGenerator (next) {
return yield next()
}
}

最后还是通过co来转换的。所以接下来看co的源码。

co 源码

tj大神写的co 仓库

本小节的示例代码都在这个文件夹koa/examples/co-generator中,hs koa/example,可以自行打开https://localhost:8080/co-generator调试查看。

看co源码前,先看几段简单代码。

1
2
3
4
5
6
7
8
复制代码// 写一个请求简版请求  
function request(ms= 1000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({name: '若川'});
}, ms);
});
}
1
2
3
4
5
6
复制代码// 获取generator的值  
function* generatorFunc(){
const res = yield request();
console.log(res, 'generatorFunc-res');
}
generatorFunc(); // 报告,我不会输出你想要的结果的

简单来说co,就是把generator自动执行,再返回一个promise。
generator函数这玩意它不自动执行呀,还要一步步调用next(),也就是叫它走一步才走一步。

所以有了async、await函数。

1
2
3
4
5
6
复制代码// await 函数 自动执行  
async function asyncFunc(){
const res = await request();
console.log(res, 'asyncFunc-res await 函数 自动执行');
}
asyncFunc(); // 输出结果

也就是说co需要做的事情,是让generator向async、await函数一样自动执行。

模拟实现简版 co(第一版)

这时,我们来模拟实现第一版的co。根据generator的特性,其实容易写出如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码// 获取generator的值  
function* generatorFunc(){
const res = yield request();
console.log(res, 'generatorFunc-res');
}

function coSimple(gen){
gen = gen();
console.log(gen, 'gen');

const ret = gen.next();
const promise = ret.value;
promise.then(res => {
gen.next(res);
});
}
coSimple(generatorFunc);
// 输出了想要的结果
// {name: "若川"}"generatorFunc-res"

模拟实现简版 co(第二版)

但是实际上,不会上面那么简单的。有可能是多个yield和传参数的情况。
传参可以通过这如下两行代码来解决。

1
2
复制代码const args = Array.prototype.slice.call(arguments, 1);  
gen = gen.apply(ctx, args);

两个yield,我大不了重新调用一下promise.then,搞定。

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
复制代码// 多个yeild,传参情况  
function* generatorFunc(suffix = ''){
const res = yield request();
console.log(res, 'generatorFunc-res' + suffix);

const res2 = yield request();
console.log(res2, 'generatorFunc-res-2' + suffix);
}

function coSimple(gen){
const ctx = this;
const args = Array.prototype.slice.call(arguments, 1);
gen = gen.apply(ctx, args);
console.log(gen, 'gen');

const ret = gen.next();
const promise = ret.value;
promise.then(res => {
const ret = gen.next(res);
const promise = ret.value;
promise.then(res => {
gen.next(res);
});
});
}

coSimple(generatorFunc, ' 哎呀,我真的是后缀');

模拟实现简版 co(第三版)

问题是肯定不止两次,无限次的yield的呢,这时肯定要把重复的封装起来。而且返回是promise,这就实现了如下版本的代码。

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
复制代码function* generatorFunc(suffix = ''){  
const res = yield request();
console.log(res, 'generatorFunc-res' + suffix);

const res2 = yield request();
console.log(res2, 'generatorFunc-res-2' + suffix);

const res3 = yield request();
console.log(res3, 'generatorFunc-res-3' + suffix);

const res4 = yield request();
console.log(res4, 'generatorFunc-res-4' + suffix);
}

function coSimple(gen){
const ctx = this;
const args = Array.prototype.slice.call(arguments, 1);
gen = gen.apply(ctx, args);
console.log(gen, 'gen');

return new Promise((resolve, reject) => {

onFulfilled();

function onFulfilled(res){
const ret = gen.next(res);
next(ret);
}

function next(ret) {
const promise = ret.value;
promise && promise.then(onFulfilled);
}

});
}

coSimple(generatorFunc, ' 哎呀,我真的是后缀');

但第三版的模拟实现简版co中,还没有考虑报错和一些参数合法的情况。

最终来看下co源码

这时来看看co的源码,报错和错误的情况,错误时调用reject,是不是就好理解了一些呢。

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
复制代码function co(gen) {  
var ctx = this;
var args = slice.call(arguments, 1)

// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
// 把参数传递给gen函数并执行
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 如果不是函数 直接返回
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/

function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/

// 反复执行调用自己
function next(ret) {
// 检查当前是否为 Generator 函数的最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
// 确保返回值是promise对象。
var value = toPromise.call(ctx, ret.value);
// 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

koa 和 express 简单对比

中文文档 koa 和 express 对比

文档里写的挺全面的。简单来说koa2语法更先进,更容易深度定制(egg.js、think.js、底层框架都是koa)。

总结

文章通过授人予鱼不如授人予鱼的方式,告知如何调试源码,看完了koa-compose洋葱模型实现,koa-convert和co等源码。

koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise。

koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。

co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。

koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文(context)、http请求对象、http响应对象。

本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis。求个star呀。

1
复制代码git clone https://github.com/lxchuan12/koa-analysis.git

再强烈建议下按照本文阅读最佳方式,克隆代码下来,动手调试代码学习更加深刻。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出,也欢迎加我微信交流lxchuan12。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,万分感谢。

解答下开头的提问

仅供参考

1、koa洋葱模型怎么实现的。

可以参考上文整理的简版koa-compose作答。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码// 这样就可能更好理解了。  
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);

答:app.use() 把中间件函数存储在middleware数组中,最终会调用koa-compose导出的函数compose返回一个promise,中间函数的第一个参数ctx是包含响应和请求的一个对象,会不断传递给下一个中间件。next是一个函数,返回的是一个promise。

2、如果中间件中的next()方法报错了怎么办。

可参考上文整理的错误处理作答。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码ctx.onerror = function {  
this.app.emit('error', err, this);
};
listen(){
const fnMiddleware = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const onerror = err => ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
onerror(err) {
// 代码省略
// ...
}

答:中间件链错误会由ctx.onerror捕获,该函数中会调用this.app.emit('error', err, this)(因为koa继承自events模块,所以有’emit’和on等方法),可以使用app.on('error', (err) => {}),或者app.onerror = (err) => {}进行捕获。

3、co的原理是怎样的。

答:co的原理是通过不断调用generator函数的next方法来达到自动执行generator函数的,类似async、await函数自动执行。

答完,面试官可能觉得小伙子还是蛮懂koa的啊。当然也可能继续追问,直到答不出…

还能做些什么 ?

学完了整体流程,koa-compose、koa-convert和co的源码。

还能仔细看看看http请求上下文(context)、http请求对象、http响应对象的具体实现。

还能根据我文章说的调试方式调试koa 组织中的各种中间件,比如koa-bodyparser, koa-router,koa-jwt,koa-session、koa-cors等等。

还能把examples仓库克隆下来,我的这个仓库已经克隆了,挨个调试学习下源码。

web框架有很多,比如Express.js,Koa.js、Egg.js、Nest.js、Next.js、Fastify.js、Hapi.js、Restify.js、Loopback.io、Sails.js、Midway.js等等。

还能把这些框架的优势劣势、设计思想等学习下。

还能继续学习HTTP协议、TCP/IP协议网络相关,虽然不属于koa的知识,但需深入学习掌握。

学无止境~

推荐阅读

koa 官网 | koa 仓库 | koa 组织 | koa2 中文文档 | co 仓库

知乎@姚大帅:可能是目前市面上比较有诚意的Koa2源码解读

知乎@零小白:十分钟带你看完 KOA 源码

微信开放社区@小丹の:可能是目前最全的koa源码解析指南

IVWEB官方账号: KOA2框架原理解析和实现

深入浅出vue.js 作者 berwin: 深入浅出 Koa2 原理

阮一峰老师:co 函数库的含义和用法

另一个系列

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

面试官问:能否模拟实现JS的new操作符

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

若川的博客,使用vuepress重构了,阅读体验可能更好些

掘金专栏,欢迎关注~

segmentfault前端视野专栏,欢迎关注~

知乎前端视野专栏,欢迎关注~

语雀前端视野专栏,新增语雀专栏,欢迎关注~

github blog,相关源码和资源都放在这里,求个star^_^~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注。欢迎加我微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野

若川视野

本文转载自: 掘金

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

这样学习Servlet,会事半功倍!!

发表于 2020-03-12

前言

工作已经有一段时间了,如果让我重新学Servlet,我会怎么学呢?下面抛出两个常见的问题,我分开来解答

  • 2020年了,还需要学Servlet吗?
  • Servlet的学习路线(学习重点)

一、2020年了,还需要学Servlet吗?

老实说,Servlet放在现在肯定算是一个古老的技术了。现在你去任何的一家公司,应该都不是直接用Servlet来写项目的。现在的项目一般来说还是以SpringMVC-Spring-Mybatis / SpringBoot居多。面试也几乎不会问Servlet的知识(无论是校招还是社招)

既然Servlet已经是一个这么古老的技术了,那我还需要学吗?这是一个非常常见的问题。我初学的时候也非常喜欢搜相关的问题:“SWING/AWT这种技术还需要学吗”。

无论是我在知乎回答Java学习路线,还是读者问到的这个问题,我都会给予肯定的回答:“需要学Servlet,不要跳过Servlet去学框架”

我因为好好学了Servlet,在学Struts2(没错,我还学过Struts2)和SpringMVC的都非常容易上手,几天就基本会用了。

如果了解Struts2或SpringMVC的同学就会知道,其实他俩的底层都离不开Servlet。Struts2的核心用的是Filter(过滤器),而SpringMVC的核心用的就是Servlet。

学过Servlet好处是什么:

  • 打下坚实的基础,学习框架就得心应手了。

二、Servlet的学习路径

下面我来讲讲Servlet的重点有哪些,其实就是学习Servlet的路线。还是要重复一句话:“在学习一项技术之前,首先要知道为什么要学习它”

2.1 Tomcat

学Servlet之前,首先我们要学学Tomcat。Tomcat是一个Web服务器(同时也是Servlet容器),通过它我们可以很方便地接收和返回到请求(如果不用Tomcat,那我们需要自己写Socket来接收和返回请求)。

Tomcat其实我们并不需要学太多的知识,只要学会安装和启动以及了解一下各个目录的含义就差不多了。

Tomcat各个目录的含义:

2.2 Servlet版“Hello world“

首先,我们需要认清一个JavaWeb的标准目录结构:

随后,我们编写一个最简单的Servlet程序和配置web.xml来完成一次交互。

在写Servlet的时候,我们顺便了解一下Servlet的继承体系和生命周期

2.3 HTTP简单学一下

HTTP协议是客户端和服务器交互的一种通迅的格式。

例如:在浏览器点击一个链接,浏览器就为我打开这个链接的网页。

原理:当在浏览器中点击这个链接的时候,浏览器会向服务器发送一段文本,**告诉服务器请求打开的是哪一个网页。服务器收到请求后,就返回一段文本给浏览器,浏览器会将该文本解析,然后显示出来。**这段「文本」就是遵循HTTP协议规范的。

在初学的时候,我们只要记住一些常用的头信息(请求头和响应头)就足够了。

2.4 ServletConfig和ServerContext对象

ServletConfig:通过此对象可以读取web.xml中配置的初始化参数,不写硬编码,将配置写在配置文件中。

ServletContext:这个对象是在Tomcat启动的时候就会创建,代表着当前整个应用。我们一般用来获取整个应用的配置信息(ServletConfig是单个的,而ServletContext是整个应用的),还可以用这个对象来读取资源文件。

这几个最基本的Servlet对象学完了以后,我们就可以关注一下Servlet的一些小细节了,比如说:

  • Servlet是单例的
  • 配置通配符的时候可以用各种的通配符*.和一个Servlet可以被多个配置映射
  • 访问任何资源其实都是在访问Servlet(即便是访问图片资源,Tomcat都有默认Servlet处理)
  • ….

2.5request和response对象

Servlet的重点需要学习request和response对象。当我们学完HTTP的请求头和响应头以后,再看到这两个对象。我们就应该知道:request其实就是封装了HTTP的请求头,而response就是封装了HTTP响应头。

这两个对象是Servlet中最重要的,因为我们跟外接的交互都是通过request和response对象来进行的。

通过response对象,我们可以尝试写一些Demo,比如:

  • 给浏览器输出一些简单的内容
  • 实现文件下载的功能
  • 实现页面自动刷新的功能
  • 实现对数据的压缩
  • 生成验证码图片
  • 重定向跳转
  • …..

通过request对象,我们也可以尝试做些Demo,比如:

  • 得到浏览器的传递过来的各类信息(请求参数、请求头等)
  • 实现防盗链
  • 通过request对象来转发
  • 解决请求参数中文乱码的问题
  • ….

一句话总结:request对象主要用于接收请求各种的信息,response对象主要用户返回给请求各种的信息。围绕着请求、响应我们分别有request和response对象供我们操作。

2.6 Cookie和session会话机制

前面我们已经学到了Servlet的几个对象了,分别是Config(获取配置信息)、Context(代表整个Web应用)、Request(HTTP请求)、Response(HTTP响应)。

每个网站都会有登录注册的功能,那它是怎么实现的呢?上这上面的几个对象,好像都不是实现登录注册的。于是我们该来学学会话机制 Cookie和Session啦。

首先我们了解一下Cookie是存储在哪的,以及Cookie的基本API使用,包括:

  • Cookie的有效期如何设置
  • Cookie如何保存中文
  • Cookie的不可跨域性是什么意思
  • 使用Cookie来显示用户上次访问的时间
  • 使用Cookie来显示上次浏览过的商品

Cookie的API使用基本会了以后,我们就可以学习Session了,学Session的时候我们需要解决:

  • 有了Cookie,为什么需要Session(因为他俩都是会话机制)
  • Session的API基本使用
  • Session的生命周期和有效期
  • Session的实现原理,如果禁用Cookie,还能使用Session吗
  • 尝试完成Session的几个小Demo
    • 使用Session完成购物的功能
    • 使用Session完成简单的登录注册
    • 使用Session完成防止表单重复提交
    • 使用Session完成一次性校验码

完了以后,我们可以对比一下Cookie和Session的区别主要有哪些。

一句话总结:Cookie是检查用户身上的”通行证“来确认用户的身份,Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”。

img

2.7 Servlet知识总结

其实纵观Servlet,无非就是学几个对象,但这几个对象对我们后面的学习都非常重要,我之前画过一张思维导图概括了这几个对象,希望对大家有帮助:

三、发干货!

如果了解我的同学,应该知道我已经写过不少的文章了,GitHub的原创列表文章需要拖动很久才能拖到底。

Servlet是我最开始写系列级文章的开始,我在各大博客发表的第一篇文章就叫做《Tomcat 就是这么简单》。

现在已经工作有一段时间了,为什么还来写Servlet呢,原因有以下几个:

  • 我是一个对排版有追求的人,如果早期关注我的同学可能会发现,我的GitHub、文章导航的read.me会经常更换。现在的GitHub导航也不合我心意了(太长了),并且早期的Servlet文章,说实话排版也不太行,我决定重新搞一波。
  • 我的文章会分发好几个平台,但文章发完了可能就没人看了,并且图床很可能因为平台的防盗链就挂掉了。又因为有很多的读者问我:”你能不能把你的文章转成PDF啊?“
  • 我写过很多系列级的文章,这些文章就几乎不会有太大的改动了,就非常适合把它们给”持久化“。

基于上面的原因,我决定把我的Servlet汇总成一个PDF/HTML/WORD文档。说实话,打造这么一个文档花了我不少的时间。为了防止白嫖,关注我的公众号回复「888」即可获取。

文档的内容均为手打,有任何的不懂都可以直接来问我(公众号有我的联系方式)。

如果点赞超过500,那下周再肝一个系列出来。想要看什么,可以留言告诉我

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号「Java3y」。

  • 🔥Java精美脑图
  • 🔥Java学习路线
  • 🔥开发常用工具
  • 🔥精美文档电子书

在公众号下回复「888」即可获取!!

本已收录至我的GitHub精选文章,欢迎Star:github.com/ZhongFuChen…

求点赞 求关注️ 求分享👥 求留言💬 对我来说真的 非常有用!!!

,

本文转载自: 掘金

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

面试被问分布式事务(2PC、3PC、TCC),这样解释没毛病

发表于 2020-03-11

整理了一些Java方面的架构、面试资料(微服务、集群、分布式、中间件等),有需要的小伙伴可以关注公众号【程序员内点事】,无套路自行领取

更多优选

  • 一口气说出 9种 分布式ID生成方式,面试官有点懵了
  • 面试总被问分库分表怎么办?你可以这样怼他
  • 3万字总结,Mysql优化之精髓
  • 技术部突然宣布:JAVA开发人员全部要会接口自动化测试框架
  • 9种分布式ID生成之美团(Leaf)实战

絮絮叨叨

还记得刚入行开始写Java时,接触的第一个项目是国家电网的一个业务系统,这个系统据说投资了5亿人民币进行研发,鼎盛时期研发人员一度达到过500人。项目采用当时最流行的ssh(Struts+Spring+Hibernate)框架,典型的三层架构(controller - > service -> dao)简单又粗暴,所有人写的代码都放在一个大工程里,项目文件大小达到几百M,解决代码冲突是当时最大的工作量。

然而戏剧性的是,交测当天五人同时上线,项目崩 崩 崩溃了。。。 哎!你永远想象不到甲方愤怒的样子,项目组每个人的祖宗都被问候到了。

在这里插入图片描述

说了一些没用的,脑子里总想起这个事,不说不痛快,大家姑且就当笑话听吧,下边我们进入正题


引言

前两天有个学弟公众号留言,说让讲讲分布式事务,面试就挂在这个问题上。时下随着微服务架构体系的流行,面试的题目也都慢慢开始升级,不再是早些年单纯的问点SSH框架知识、数据结构了。高并发、高可用、分布式服务治理、分布式文件系统、分布式xxx,反正和分布式沾边的都会问点, 项目实际用不用不要紧,关键你得了解,是不是总有一种学不动了的感觉?


什么是分布式事务?

我们看看百度上对于分布式事务的定义:分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

在这里插入图片描述

额~ 看了反而更懵逼了,简单的画个图好让大家理解一下,拿下单减库存来说举例:当系统的业务量很小时,“一站式”的系统完全可以满足现有业务需求,所有的业务都共用一个数据库,整个下单流程或许只用在一个方法里同一个事务下操作数据库即可。
此时所有操作都在一个事务里,要么全部提交,要么全部回滚 。

图糙理不糙

但随着业务量不断增长,“一站式”系统渐渐扛不住巨大的流量,就需要对数据库进行分库分表,将业务服务化拆分(SOA),就会分离出了订单中心、用户中心、库存中心。而这样就造成业务间相互隔离,每个业务都维护着自己的数据库,数据的交换只能进行RPC调用。

用户再下单时,创建订单和扣减库存,需要同时对订单DB和库存DB进行操作。两步操作必须同时成功,否则就会造成业务混乱,可此时我们只能保证自己服务的数据一致性,无法保证调用其他服务的操作是否成功,所以为了保证整个下单流程的数据一致性,就需要分布式事务介入。

图糙理不糙

在说分布式事务之前,先回忆一下事务的基本概念:事务是一个程序执行单元,里面的所有操作要么全部执行成功,要么全部执行失败。

一个事务有四个基本特性,也就是我们常说的(ACID)。

Atomicity(原子性) :事务是一个不可分割的整体,事务内所有操作要么全做成功,要么全失败。

Consistency(一致性) :务执行前后,数据从一个状态到另一个状态必须是一致的(A向B转账,不能出现A扣了钱,B却没收到)。

Isolation(隔离性): 多个并发事务之间相互隔离,不能互相干扰。

Durablity(持久性) :事务完成后,对数据库的更改是永久保存的,不能回滚。

上面这些知识点都是反反复复念叨的概念,面试必背的东西。

分布式事务解决方案

有困难就一定会有解决问题的办法,什么都难不倒聪明的程序员。

XA协议是一个基于数据库的分布式事务协议,其分为两部分:事务管理器和本地资源管理器。事务管理器作为一个全局的调度者,负责对各个本地资源管理器统一号令提交或者回滚。二阶提交协议(2PC)和三阶提交协议(3PC)就是根据此协议衍生出来而来。如今Oracle、Mysql等数据库均已实现了XA接口。

1、两段提交(2PC)

两段提交顾名思义就是要进行两个阶段的提交:第一阶段,准备阶段(投票阶段) ; 第二阶段,提交阶段(执行阶段)。

在这里插入图片描述

上边图片源自网络,如有侵权联系删除

下面还拿下单扣库存举例子,简单描述一下两段提交(2PC)的原理:

之前说过业务服务化(SOA)以后,一个下单流程就会用到多个服务,各个服务都无法保证调用的其他服务的成功与否,这个时候就需要一个全局的角色(协调者)对各个服务(参与者)进行协调。

在这里插入图片描述

一个下单请求过来通过协调者,给每一个参与者发送Prepare消息,执行本地数据脚本但不提交事务。

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中被占用的资源,显然2PC做到了所有操作要么全部成功、要么全部失败。

两段提交(2PC)的缺点

二阶段提交看似能够提供原子性的操作,但它存在着严重的缺陷

  • 网络抖动导致的数据不一致: 第二阶段中协调者向参与者发送commit命令之后,一旦此时发生网络抖动,导致一部分参与者接收到了commit请求并执行,可其他未接到commit请求的参与者无法执行事务提交。进而导致整个分布式系统出现了数据不一致。
  • 超时导致的同步阻塞问题: 2PC中的所有的参与者节点都为事务阻塞型,当某一个参与者节点出现通信超时,其余参与者都会被动阻塞占用资源不能释放。
  • 单点故障的风险: 由于严重的依赖协调者,一旦协调者发生故障,而此时参与者还都处于锁定资源的状态,无法完成事务commit操作。虽然协调者出现故障后,会重新选举一个协调者,可无法解决因前一个协调者宕机导致的参与者处于阻塞状态的问题。
2、三段提交(3PC)

三段提交(3PC)是对两段提交(2PC)的一种升级优化,3PC在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前,各参与者节点的状态都一致。同时在协调者和参与者中都引入超时机制,当参与者各种原因未收到协调者的commit请求后,会对本地事务进行commit,不会一直阻塞等待,解决了2PC的单点故障问题,但3PC 还是没能从根本上解决数据一致性的问题。

在这里插入图片描述

上边图片源自网络,如有侵权联系删除

3PC 的三个阶段分别是CanCommit、PreCommit、DoCommit

CanCommit:协调者向所有参与者发送CanCommit命令,询问是否可以执行事务提交操作。如果全部响应YES则进入下一个阶段。

PreCommit:协调者向所有参与者发送PreCommit命令,询问是否可以进行事务的预提交操作,参与者接收到PreCommit请求后,如参与者成功的执行了事务操作,则返回Yes响应,进入最终commit阶段。一旦参与者中有向协调者发送了No响应,或因网络造成超时,协调者没有接到参与者的响应,协调者向所有参与者发送abort请求,参与者接受abort命令执行事务的中断。

DoCommit: 在前两个阶段中所有参与者的响应反馈均是YES后,协调者向参与者发送DoCommit命令正式提交事务,如协调者没有接收到参与者发送的ACK响应,会向所有参与者发送abort请求命令,执行事务的中断。

3、补偿事务(TCC)

很多初学者总是被TCC、2PC、3PC这几个概念搞混淆,傻傻分不清,实际上 TCC与 2PC、3PC一样,都只是实现分布式事务的一种方案而已。

TCC(Try-Confirm-Cancel)又被称补偿事务,TCC与2PC的思想很相似,事务处理流程也很相似,但2PC 是应用于在DB层面,TCC则可以理解为在应用层面的2PC,是需要我们编写业务逻辑来实现。

TCC它的核心思想是:”针对每个操作都要注册一个与其对应的确认(Try)和补偿(Cancel)”。

还拿下单扣库存解释下它的三个操作:

Try阶段:

下单时通过Try操作去扣除库存预留资源。

Confirm阶段:

确认执行业务操作,在只预留的资源基础上,发起购买请求。

Cancel阶段:

只要涉及到的相关业务中,有一个业务方预留资源未成功,则取消所有业务资源的预留请求。

在这里插入图片描述

上边图片源自网络,如有侵权联系删除

TCC的缺点:

  • 应用侵入性强:TCC由于基于在业务层面,至使每个操作都需要有 try、confirm、cancel三个接口。
  • 开发难度大:代码开发量很大,要保证数据一致性 confirm 和 cancel 接口还必须实现幂等性。

总结

很浅显的介绍了一下2PC、3PC、TCC的概念,如有错误还望温柔指正,分布式事务一直都是面试中比较热点的问题,也是进阶高级Java工程师必备的知识点。


整理了一些Java方面的架构、面试资料(微服务、集群、分布式、中间件等),有需要的小伙伴可以关注公众号【程序员内点事】,无套路自行领取

本文转载自: 掘金

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

springboot分布式事务atomikos

发表于 2020-03-11

atomikos应用场景:单应用多数据源

引入依赖

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
复制代码<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<!-- 这里不使用自动配置,所以不引入starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.14</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

修改配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码spring:  
datasource:
#使用druid连接池
druid:
#数据源1的名称
one:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.128:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
#数据源2的名称
two:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.129:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root

配置数据源

只需要配置数据源即可,全局事务管理器(JtaTransactionManager)由spring自动配置

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
复制代码@Configuration  
public class DataSourceConfig {

/**
* 创建Druid的XA连接池
* @return
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.one")
public XADataSource druidXADataSource1(){
return new DruidXADataSource();
}

/**
* 创建Atomikos数据源
* 注解@DependsOn("druidXADataSource1"),在名为druidXADataSource1的bean实例化后加载当前bean
* @param xaDataSource
* @return
*/
@Bean
@DependsOn("druidXADataSource1")
@Primary
public DataSource dataSource1(@Qualifier("druidXADataSource1") XADataSource xaDataSource) {
//这里的AtomikosDataSourceBean使用的是spring提供的
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setXaDataSource(xaDataSource);
return dataSource;
}


@Bean
@ConfigurationProperties("spring.datasource.druid.two")
public XADataSource druidXADataSource2(){
return new DruidXADataSource();
}

@Bean
@DependsOn("druidXADataSource2")
public DataSource dataSource2(@Qualifier("druidXADataSource2") XADataSource xaDataSource) {
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setXaDataSource(xaDataSource);
return dataSource;
}
}

配置mybatis

如果使用其他的orm框架,自行配置。

数据源2的mybatis配置和下面代码相似,去除@Primary注解,修改配置属性即可。

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
复制代码@Configuration  
//指定扫描的dao包和SqlSession实例
@MapperScan(basePackages = "demo.springboot.atomikos.dao1", sqlSessionTemplateRef = "sessionTemplate1")
public class Mybatis1Config {

/**
* SqlSessionFactory
*
* @param dataSource
* @return
* @throws Exception
*/
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory1(@Qualifier("dataSource1") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//mapper文件位置
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/one/*.xml"));
return bean.getObject();
}

/**
* SqlSession实例
*
* @param sqlSessionFactory
* @return
*/
@Bean
@Primary
public SqlSessionTemplate sessionTemplate1(@Qualifier("sqlSessionFactory1") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}

DAO

1
2
3
4
5
6
7
8
9
10
11
复制代码public interface UserDAO1 {  

@Update("update user set name = #{name} where id = #{id}")
int updateById(@Param("name")String name, @Param("id")Long id);
}

public interface UserDAO2 {

@Update("update user set name = #{name} where id = #{id}")
int updateById(@Param("name")String name, @Param("id")Long id);
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码@Service  
public class TestService {
@Autowired
private UserDAO1 userDAO1;
@Autowired
private UserDAO2 userDAO2;

/**
* 在需要事务的方法加上@Transactional注解即可
*/
@Transactional
public void test(){
userDAO1.updateById("haha", 1L);
userDAO2.updateById("hehe", 2L);
//模拟异常
int a = 1/0;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@RunWith(SpringRunner.class)  
@SpringBootTest
public class AtomikosApplicationTests {

@Autowired
private TestService testService;

@Test
public void test() {
testService.test();
}

}

数据源1中的User{“id”:1,”name”:”张三”}

数据源2中的User{“id”:2,name:”李四”}

运行测试出现异常后,两个数据库都回滚了,数据未改变

项目路径


作者博客

作者公众号
在这里插入图片描述

本文转载自: 掘金

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

java并发中ExecutorService的使用

发表于 2020-03-10

java并发中ExecutorService的使用

ExecutorService是java中的一个异步执行的框架,通过使用ExecutorService可以方便的创建多线程执行环境。

本文将会详细的讲解ExecutorService的具体使用。

创建ExecutorService

通常来说有两种方法来创建ExecutorService。

第一种方式是使用Executors中的工厂类方法,例如:

1
复制代码ExecutorService executor = Executors.newFixedThreadPool(10);

除了newFixedThreadPool方法之外,Executors还包含了很多创建ExecutorService的方法。

第二种方法是直接创建一个ExecutorService, 因为ExecutorService是一个interface,我们需要实例化ExecutorService的一个实现。

这里我们使用ThreadPoolExecutor来举例:

1
2
3
复制代码ExecutorService executorService =
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());

为ExecutorService分配Tasks

ExecutorService可以执行Runnable和Callable的task。其中Runnable是没有返回值的,而Callable是有返回值的。我们分别看一下两种情况的使用:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码Runnable runnableTask = () -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
};

Callable<String> callableTask = () -> {
TimeUnit.MILLISECONDS.sleep(300);
return "Task's execution";
};

将task分配给ExecutorService,可以通过调用xecute(), submit(), invokeAny(), invokeAll()这几个方法来实现。

execute() 返回值是void,他用来提交一个Runnable task。

1
复制代码executorService.execute(runnableTask);

submit() 返回值是Future,它可以提交Runnable task, 也可以提交Callable task。 提交Runnable的有两个方法:

1
2
3
复制代码<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

第一个方法在返回传入的result。第二个方法返回null。

再看一下callable的使用:

1
2
复制代码Future<String> future = 
executorService.submit(callableTask);

invokeAny() 将一个task列表传递给executorService,并返回其中的一个成功返回的结果。

1
复制代码String result = executorService.invokeAny(callableTasks);

invokeAll() 将一个task列表传递给executorService,并返回所有成功执行的结果:

1
复制代码List<Future<String>> futures = executorService.invokeAll(callableTasks);

关闭ExecutorService

如果ExecutorService中的任务运行完毕之后,ExecutorService不会自动关闭。它会等待接收新的任务。如果需要关闭ExecutorService, 我们需要调用shutdown() 或者 shutdownNow() 方法。

shutdown() 会立即销毁ExecutorService,它会让ExecutorServic停止接收新的任务,并等待现有任务全部执行完毕再销毁。

1
复制代码executorService.shutdown();

shutdownNow()并不保证所有的任务都被执行完毕,它会返回一个未执行任务的列表:

1
复制代码List<Runnable> notExecutedTasks = executorService.shutdownNow();

oracle推荐的最佳关闭方法是和awaitTermination一起使用:

1
2
3
4
5
6
7
8
复制代码executorService.shutdown();
try {
if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}

先停止接收任务,然后再等待一定的时间让所有的任务都执行完毕,如果超过了给定的时间,则立刻结束任务。

Future

submit() 和 invokeAll() 都会返回Future对象。之前的文章我们已经详细讲过了Future。 这里就只列举一下怎么使用:

1
2
3
4
5
6
7
复制代码Future<String> future = executorService.submit(callableTask);
String result = null;
try {
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}

ScheduledExecutorService

ScheduledExecutorService为我们提供了定时执行任务的机制。

我们这样创建ScheduledExecutorService:

1
2
复制代码ScheduledExecutorService executorService
= Executors.newSingleThreadScheduledExecutor();

executorService的schedule方法,可以传入Runnable也可以传入Callable:

1
2
3
4
5
6
7
8
复制代码Future<String> future = executorService.schedule(() -> {
// ...
return "Hello world";
}, 1, TimeUnit.SECONDS);

ScheduledFuture<?> scheduledFuture = executorService.schedule(() -> {
// ...
}, 1, TimeUnit.SECONDS);

还有两个比较相近的方法:

1
2
3
复制代码scheduleAtFixedRate( Runnable command, long initialDelay, long period, TimeUnit unit )

scheduleWithFixedDelay( Runnable command, long initialDelay, long delay, TimeUnit unit )

两者的区别是前者的period是以任务开始时间来计算的,后者是以任务结束时间来计算。

ExecutorService和 Fork/Join

java 7 引入了Fork/Join框架。 那么两者的区别是什么呢?

ExecutorService可以由用户来自己控制生成的线程,提供了对线程更加细粒度的控制。而Fork/Join则是为了让任务更加快速的执行完毕。

本文的代码请参考github.com/ddean2009/l…

更多教程请参考 flydean的博客

本文转载自: 掘金

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

SpringBoot中处理校验逻辑的两种方式,真的很机智!

发表于 2020-03-10

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

摘要

平时在开发接口的时候,常常会需要对参数进行校验,这里提供两种处理校验逻辑的方式。一种是使用Hibernate Validator来处理,另一种是使用全局异常来处理,下面我们讲下这两种方式的用法。

Hibernate Validator

Hibernate Validator是SpringBoot内置的校验框架,只要集成了SpringBoot就自动集成了它,我们可以通过在对象上面使用它提供的注解来完成参数校验。

常用注解

我们先来了解下常用的注解,对Hibernate Validator所提供的校验功能有个印象。

  • @Null:被注释的属性必须为null;
  • @NotNull:被注释的属性不能为null;
  • @AssertTrue:被注释的属性必须为true;
  • @AssertFalse:被注释的属性必须为false;
  • @Min:被注释的属性必须大于等于其value值;
  • @Max:被注释的属性必须小于等于其value值;
  • @Size:被注释的属性必须在其min和max值之间;
  • @Pattern:被注释的属性必须符合其regexp所定义的正则表达式;
  • @NotBlank:被注释的字符串不能为空字符串;
  • @NotEmpty:被注释的属性不能为空;
  • @Email:被注释的属性必须符合邮箱格式。

使用方式

接下来我们以添加品牌接口的参数校验为例来讲解下Hibernate Validator的使用方法,其中涉及到一些AOP的知识,不了解的朋友可以参考下《SpringBoot应用中使用AOP记录接口访问日志》。

  • 首先我们需要在添加品牌接口的参数PmsBrandParam中添加校验注解,用于确定属性的校验规则及校验失败后需要返回的信息;
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
复制代码/**
* 品牌传递参数
* Created by macro on 2018/4/26.
*/
public class PmsBrandParam {
@ApiModelProperty(value = "品牌名称",required = true)
@NotEmpty(message = "名称不能为空")
private String name;
@ApiModelProperty(value = "品牌首字母")
private String firstLetter;
@ApiModelProperty(value = "排序字段")
@Min(value = 0, message = "排序最小为0")
private Integer sort;
@ApiModelProperty(value = "是否为厂家制造商")
@FlagValidator(value = {"0","1"}, message = "厂家状态不正确")
private Integer factoryStatus;
@ApiModelProperty(value = "是否进行显示")
@FlagValidator(value = {"0","1"}, message = "显示状态不正确")
private Integer showStatus;
@ApiModelProperty(value = "品牌logo",required = true)
@NotEmpty(message = "品牌logo不能为空")
private String logo;
@ApiModelProperty(value = "品牌大图")
private String bigPic;
@ApiModelProperty(value = "品牌故事")
private String brandStory;

//省略若干Getter和Setter方法...
}
  • 然后在添加品牌的接口中添加@Validated注解,并注入一个BindingResult参数;
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
复制代码/**
* 品牌功能Controller
* Created by macro on 2018/4/26.
*/
@Controller
@Api(tags = "PmsBrandController", description = "商品品牌管理")
@RequestMapping("/brand")
public class PmsBrandController {
@Autowired
private PmsBrandService brandService;

@ApiOperation(value = "添加品牌")
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
public CommonResult create(@Validated @RequestBody PmsBrandParam pmsBrand, BindingResult result) {
CommonResult commonResult;
int count = brandService.createBrand(pmsBrand);
if (count == 1) {
commonResult = CommonResult.success(count);
} else {
commonResult = CommonResult.failed();
}
return commonResult;
}
}
  • 然后在整个Controller层创建一个切面,在其环绕通知中获取到注入的BindingResult对象,通过hasErrors方法判断校验是否通过,如果有错误信息直接返回错误信息,验证通过则放行;
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
复制代码/**
* HibernateValidator错误结果处理切面
* Created by macro on 2018/4/26.
*/
@Aspect
@Component
@Order(2)
public class BindingResultAspect {
@Pointcut("execution(public * com.macro.mall.controller.*.*(..))")
public void BindingResult() {
}

@Around("BindingResult()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
BindingResult result = (BindingResult) arg;
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
if(fieldError!=null){
return CommonResult.validateFailed(fieldError.getDefaultMessage());
}else{
return CommonResult.validateFailed();
}
}
}
}
return joinPoint.proceed();
}
}
  • 此时我们访问添加品牌的接口,不传入name字段,就会返回名称不能为空的错误信息;

自定义注解

有时候框架提供的校验注解并不能满足我们的需要,此时我们就需要自定义校验注解。比如还是上面的添加品牌,此时有个参数showStatus,我们希望它只能是0或者1,不能是其他数字,此时可以使用自定义注解来实现该功能。

  • 首先自定义一个校验注解类FlagValidator,然后添加@Constraint注解,使用它的validatedBy属性指定校验逻辑的具体实现类;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码/**
* 用户验证状态是否在指定范围内的注解
* Created by macro on 2018/4/26.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Constraint(validatedBy = FlagValidatorClass.class)
public @interface FlagValidator {
String[] value() default {};

String message() default "flag is not found";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
  • 然后创建FlagValidatorClass作为校验逻辑的具体实现类,实现ConstraintValidator接口,这里需要指定两个泛型参数,第一个需要指定为你自定义的校验注解类,第二个指定为你要校验属性的类型,isValid方法中就是具体的校验逻辑。
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
复制代码/**
* 状态标记校验器
* Created by macro on 2018/4/26.
*/
public class FlagValidatorClass implements ConstraintValidator<FlagValidator,Integer> {
private String[] values;
@Override
public void initialize(FlagValidator flagValidator) {
this.values = flagValidator.value();
}

@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
boolean isValid = false;
if(value==null){
//当状态为空时使用默认值
return true;
}
for(int i=0;i<values.length;i++){
if(values[i].equals(String.valueOf(value))){
isValid = true;
break;
}
}
return isValid;
}
}
  • 接下来我们就可以在传参对象中使用该注解了;
1
2
3
4
5
6
7
8
9
10
11
12
复制代码/**
* 品牌传递参数
* Created by macro on 2018/4/26.
*/
public class PmsBrandParam {

@ApiModelProperty(value = "是否进行显示")
@FlagValidator(value = {"0","1"}, message = "显示状态不正确")
private Integer showStatus;

//省略若干Getter和Setter方法...
}
  • 最后我们测试下该注解,调用接口是传入showStatus=3,会返回显示状态不正确的错误信息。

优缺点

这种方式的优点是可以使用注解来实现参数校验,不需要一些重复的校验逻辑,但是也有一些缺点,比如需要在Controller的方法中额外注入一个BindingResult对象,只支持一些简单的校验,涉及到要查询数据库的校验就无法满足了。

全局异常处理

使用全局异常处理来处理校验逻辑的思路很简单,首先我们需要通过@ControllerAdvice注解定义一个全局异常的处理类,然后自定义一个校验异常,当我们在Controller中校验失败时,直接抛出该异常,这样就可以达到校验失败返回错误信息的目的了。

使用到的注解

@ControllerAdvice:类似于@Component注解,可以指定一个组件,这个组件主要用于增强@Controller注解修饰的类的功能,比如说进行全局异常处理。

@ExceptionHandler:用来修饰全局异常处理的方法,可以指定异常的类型。

使用方式

  • 首先我们需要自定义一个异常类ApiException,当我们校验失败时抛出该异常:
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
复制代码/**
* 自定义API异常
* Created by macro on 2020/2/27.
*/
public class ApiException extends RuntimeException {
private IErrorCode errorCode;

public ApiException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public ApiException(String message) {
super(message);
}

public ApiException(Throwable cause) {
super(cause);
}

public ApiException(String message, Throwable cause) {
super(message, cause);
}

public IErrorCode getErrorCode() {
return errorCode;
}
}
  • 然后创建一个断言处理类Asserts,用于抛出各种ApiException;
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码/**
* 断言处理类,用于抛出各种API异常
* Created by macro on 2020/2/27.
*/
public class Asserts {
public static void fail(String message) {
throw new ApiException(message);
}

public static void fail(IErrorCode errorCode) {
throw new ApiException(errorCode);
}
}
  • 然后再创建我们的全局异常处理类GlobalExceptionHandler,用于处理全局异常,并返回封装好的CommonResult对象;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码/**
* 全局异常处理
* Created by macro on 2020/2/27.
*/
@ControllerAdvice
public class GlobalExceptionHandler {

@ResponseBody
@ExceptionHandler(value = ApiException.class)
public CommonResult handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
  • 这里拿用户领取优惠券的代码为例,我们先对比下改进前后的代码,首先看Controller层代码。改进后只要Service中的方法执行成功就表示领取优惠券成功,因为领取不成功的话会直接抛出ApiException从而返回错误信息;
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
复制代码/**
* 用户优惠券管理Controller
* Created by macro on 2018/8/29.
*/
@Controller
@Api(tags = "UmsMemberCouponController", description = "用户优惠券管理")
@RequestMapping("/member/coupon")
public class UmsMemberCouponController {
@Autowired
private UmsMemberCouponService memberCouponService;

//改进前
@ApiOperation("领取指定优惠券")
@RequestMapping(value = "/add/{couponId}", method = RequestMethod.POST)
@ResponseBody
public CommonResult add(@PathVariable Long couponId) {
return memberCouponService.add(couponId);
}

//改进后
@ApiOperation("领取指定优惠券")
@RequestMapping(value = "/add/{couponId}", method = RequestMethod.POST)
@ResponseBody
public CommonResult add(@PathVariable Long couponId) {
memberCouponService.add(couponId);
return CommonResult.success(null,"领取成功");
}
}
  • 再看下Service接口中的代码,区别在于返回结果,改进后返回的是void。其实CommonResult的作用本来就是为了把Service中获取到的数据封装成统一返回结果,改进前的做法违背了这个原则,改进后的做法解决了这个问题;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码/**
* 用户优惠券管理Service
* Created by macro on 2018/8/29.
*/
public interface UmsMemberCouponService {
/**
* 会员添加优惠券(改进前)
*/
@Transactional
CommonResult add(Long couponId);

/**
* 会员添加优惠券(改进后)
*/
@Transactional
void add(Long couponId);
}
  • 再看下Service实现类中的代码,可以看到原先校验逻辑中返回CommonResult的逻辑都改成了调用Asserts的fail方法来实现;
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
复制代码/**
* 会员优惠券管理Service实现类
* Created by macro on 2018/8/29.
*/
@Service
public class UmsMemberCouponServiceImpl implements UmsMemberCouponService {
@Autowired
private UmsMemberService memberService;
@Autowired
private SmsCouponMapper couponMapper;
@Autowired
private SmsCouponHistoryMapper couponHistoryMapper;
@Autowired
private SmsCouponHistoryDao couponHistoryDao;

//改进前
@Override
public CommonResult add(Long couponId) {
UmsMember currentMember = memberService.getCurrentMember();
//获取优惠券信息,判断数量
SmsCoupon coupon = couponMapper.selectByPrimaryKey(couponId);
if(coupon==null){
return CommonResult.failed("优惠券不存在");
}
if(coupon.getCount()<=0){
return CommonResult.failed("优惠券已经领完了");
}
Date now = new Date();
if(now.before(coupon.getEnableTime())){
return CommonResult.failed("优惠券还没到领取时间");
}
//判断用户领取的优惠券数量是否超过限制
SmsCouponHistoryExample couponHistoryExample = new SmsCouponHistoryExample();
couponHistoryExample.createCriteria().andCouponIdEqualTo(couponId).andMemberIdEqualTo(currentMember.getId());
long count = couponHistoryMapper.countByExample(couponHistoryExample);
if(count>=coupon.getPerLimit()){
return CommonResult.failed("您已经领取过该优惠券");
}
//省略领取优惠券逻辑...
return CommonResult.success(null,"领取成功");
}

//改进后
@Override
public void add(Long couponId) {
UmsMember currentMember = memberService.getCurrentMember();
//获取优惠券信息,判断数量
SmsCoupon coupon = couponMapper.selectByPrimaryKey(couponId);
if(coupon==null){
Asserts.fail("优惠券不存在");
}
if(coupon.getCount()<=0){
Asserts.fail("优惠券已经领完了");
}
Date now = new Date();
if(now.before(coupon.getEnableTime())){
Asserts.fail("优惠券还没到领取时间");
}
//判断用户领取的优惠券数量是否超过限制
SmsCouponHistoryExample couponHistoryExample = new SmsCouponHistoryExample();
couponHistoryExample.createCriteria().andCouponIdEqualTo(couponId).andMemberIdEqualTo(currentMember.getId());
long count = couponHistoryMapper.countByExample(couponHistoryExample);
if(count>=coupon.getPerLimit()){
Asserts.fail("您已经领取过该优惠券");
}
//省略领取优惠券逻辑...
}
}
  • 这里我们输入一个没有的优惠券ID来测试下该功能,会返回优惠券不存在的错误信息。

优缺点

使用全局异常来处理校验逻辑的优点是比较灵活,可以处理复杂的校验逻辑。缺点是我们需要重复编写校验代码,不像使用Hibernate Validator那样只要使用注解就可以了。不过我们可以在上面的Asserts类中添加一些工具方法来增强它的功能,比如判断是否为空和判断长度等都可以自己实现。

总结

我们可以两种方法一起结合使用,比如简单的参数校验使用Hibernate Validator来实现,而一些涉及到数据库操作的复杂校验使用全局异常处理的方式来实现。

项目源码地址

github.com/macrozheng/…

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

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

1…828829830…956

开发者博客

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