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

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


  • 首页

  • 归档

  • 搜索

Node+Express快速搭建后台应用框架

发表于 2020-05-03

1、项目基本框架搭建

1.1、项目初始化

1
复制代码npm init -y

1.2、安装Express框架生产依赖

1
复制代码npm i -S express

1.3、根目录下创建app.js文件作为入口文件,写入入口服务程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码//引入node框架
const express = require("express");
//创建服务对象
const app = express();
//监听get请求下的 '/'路由
app.get("/", function (req, res) {
res.send("hahah");
});

//开始服务对象的监听端口返回服务对象实例
const server = app.listen(5000, function () {
//通过服务对象实例调用address()获得当前服务程序的IP地址和端口号
const { address, port } = server.address();
console.log("服务已启动请访问http://%s:%s", address, port);
});

1.4、中间件函数(就相当于回调函数包含三个参数,回调时注入)必须放在app监听请求之前

1
2
3
4
5
复制代码function mylogger(req,res,next){
next();//必须调用次函数
}

app.use(mylogger);

1.5、路由

应用如何响应请求的一种规则
规则主要分两部分:
请求方法:get、post……
请求的路径:/、/user、/.*fly$/……

1
2
3
4
5
6
7
复制代码app.get('/', function(req, res) {
res.send('hello node')
})

app.post('/', function(req, res) {
res.send('hello node')
})

1.6、异常处理

通过自定义异常处理中间件处理请求中产生的异常

1
2
3
4
5
6
7
8
9
10
11
复制代码app.get('/', function(req, res) {
throw new Error('something has error...')
})

const errorHandler = function (err, req, res, next) {
console.log('errorHandler...')
res.status(500)
res.send('down...')
}

app.use(errorHandler)

使用时需要注意两点:

第一: 参数一个不能少,否则会视为普通的中间件
第二:异常处理中间件需要在请求之后引用

2、项目框架优化

2.1、自定义路由中间件

  • (1)在根目录下创建router文件夹并创建index.js文件
  • (2)在app.js文件中导入该自定义路由中间件,再通过app.use()使用中间件
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
复制代码router/index.js

/* 自定义路由中间件 */

//引入express框架
const express = require("express");
//引入boom依赖快速生成浏览器错误信息
const boom = require("boom");
//导入二级路由
const userRouter = require("./map/user");
// 通过express框架创建路由实例来处理路由监听
const router = express.Router();

//监听get请求
router.get("/", (req, res) => {
res.send("中研在线");
});
//监听到一级路由是/user是使用自定义二级路由userRouter中间件
router.use("/user", userRouter);

//路由匹配是从上到下的,所以当前面所有的路由地址都不匹配时加载请求404错误中间件
router.use((req, res, next) => {
next(boom.notFound("接口不存在"));
});
//定义一个错误处理中间件 向客户端响应json格式的错误数据
router.use((err, req, res, next) => {
//获取错误信息
const msg = (err && err.message) || "系统错误";
//获取错误状态码
const statusCode = (err.output && err.output.statusCode) || 500;
//获取errorMsg
const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message;
//返回状态码,并返回json格式错误异常信息
res.status(statusCode).json({
code: -1,
msg,
error: statusCode,
errorMsg,
});
});

module.exports = router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码app.js

//引入node框架
const express = require("express");
//引入自定义路由中间件函数
const router = require("./router");
//创建服务对象
const app = express();
//使用路由中间件
app.use("/", router);

//开始服务对象的监听端口返回服务对象实例
const server = app.listen(5000, function () {
//通过服务对象实例调用address()获得当前服务程序的IP地址和端口号
const { address, port } = server.address();
console.log("服务已启动请访问http://%s:%s", address, port);
});

本文转载自: 掘金

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

0xA06 Android 10 源码分析:WindowMa

发表于 2020-05-02

引言

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

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

  • Acivity 和 Dialog 视图解析绑定过程?
  • Activity 的视图如何与 Window 关联的?
  • Window 如何与 WindowManager 关联?
  • Dialog 的视图如何与 Window 关联?

本文主要分析 Activity、Window、PhoneWindow、WindowManager 之间的关系,为我们后面的文章 「如何在 Andorid 系统里添加自定义View」 等等文章奠定基础,先来了解一下它们的基本概念

windo

  • WindowManager:它是一个接口类,继承自接口 ViewManager,对 Window 进行管理
  • Window:它是一个抽象类,它作为一个顶级视图添加到 WindowManager 中,对 View 进行管理
  • PhoneWindow:Window唯一实现类,Window是一个抽象概念,添加到WindowManager的根容器
  • DecorView: 它是 PhoneWindow 内部的一个成员变量,继承自 FrameLayout,FrameLayout 继承自 ViewGroup

在分析他们之前的关系之前,我们先来回顾一下 Acivity 和 Dialog 视图解析绑定的过程

Acivity 和 Dialog 视图解析绑定的过程

Acivity 和 Dialog 相关的文章:

  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用

在之前的文章 分别介绍了 Acivity 和 自定义 Dialog 视图的解析和绑定,总的来说分为三步

    1. 调用 LayoutInflater 的 inflate 方法,深度优先遍历解析 View
    1. 调用 ViewGroup 的 addView 方法将子 View 添加到根布局中
    1. 调用 WindowManager 的 addView 方法添加根布局

LayoutInflater 的 inflate 方法有多个重载的方法,常用的是下面三个参数的方法

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);
}
  • 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 会直接作为结果返回

当 View 解析完成之后,最后会调用 WindowManager 的 addView 方法,WindowManager 是一个接口类,继承自接口 ViewManager,用来管理 Window,它的实现类为 WindowManagerImpl,所以调用 WindowManager 的 addView 方法,实际上调用的是 WindowManagerImpl 的 addView 方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
// 单例的设计模式
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
private final Context mContext;
private final Window mParentWindow;
......

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// mGlobal 是 WindowManagerGlobal 的实例
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
......

}

mGlobal 是 WindowManagerGlobal 的实例,使用的单例设计模式,参数 mParentWindow 是 Window 的实例,实际上是委托给 WindowManagerGlobal 去实现的

到这里我们关于 Acivity 和 Dialog 视图的解析和添加过程大概介绍完了,关于 Dialog 的视图如何与 Window 绑定在 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用 文章中介绍了,接下来分析一下 Activity、Window、WindowManager 的关系

Activity、Window、WindowManager 的关系

在 Activity 内部维护着一个 Window 的实例变量 mWindow

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

1
2
3
scala复制代码public class Activity extends ContextThemeWrappe{
private Window mWindow;
}

Window 是一个抽象类,它的具体实现类为 PhoneWindow,在 Activity 的 attach 方法中给 Window 的实例变量 mWindow 赋值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arduino复制代码final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
......

mWindow = new PhoneWindow(this, window, activityConfigCallback);
......

mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
......

}
  • 创建了 PhoneWindow 并赋值给 mWindow
  • 调用 PhoneWindow 的 setWindowManager 方法,这个方法的具体实现发生在 Window 中,最终调用的是 Window 的 setWindowManager 方法

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

1
2
3
4
5
6
typescript复制代码public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
......
// mWindowManager 是 WindowManagerImpl的实例变量
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

将 WindowManager 转换为 WindowManagerImpl,之后调用 createLocalWindowManager 方法,并传递当前的 Window 对象,构建 WindowManagerImpl 对象,之后赋值给 mWindowManager

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

1
2
3
typescript复制代码public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}

其实在 createLocalWindowManager 方法中,就做了一件事,将 Window 作为参数构建了一个 WindowManagerImpl 对象返还给调用处

总的来说,其实就是在 Activity 的 attach 方法中,通过调用 Window 的 setWindowManager 方法将 Window 和 WindowManager 关联在了一起

PhoneWindow 是 Window 的实现类,它是一个窗口,本身并不具备 View 相关的能力,实际上在 PhoneWindow 内部维护这一个变量 mDecor

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
28
29
30
31
32
33
34
35
36
scala复制代码public class PhoneWindow extends Window{
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 完成DecorView的实例化
mDecor = generateDecor(-1);
......
}

if (mContentParent == null) {
// 调用 generateLayout 方法 要负责了DecorView的初始设置,诸如主题相关的feature、DecorView的背景
mContentParent = generateLayout(mDecor);
}
......
}

// 完成DecorView的实例化
protected DecorView generateDecor(int featureId) {
......
return new DecorView(context, featureId, this, getAttributes());
}

// 调用 generateLayout 方法 要负责了DecorView的初始设置,
// 诸如主题相关的feature、DecorView的背景,同时也初始化 contentParent
protected ViewGroup generateLayout(DecorView decor) {
......

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

......
}

}
  • mDecor 是 window 的顶级视图,它继承自 FrameLayout,它的创建过程由 installDecor 完成,然后在 installDecor 方法中通过 generateDecor 方法来完成DecorView的实例化
  • 调用 generateLayout 方法 要负责了DecorView的初始设置,诸如主题相关的feature、DecorView的背景,同时也初始化 contentParent
  • mDecor 它实际上是一个 ViewGroup,当在 Activity 中调用 setContentView 方法,通过调用 inflater 方法把布局资源转换为一个 View,然后添加到 DecorView 的 mContenParnent 中

当 View 初始化完成之后,最后会进入 ActivityThread 的 handlerResumeActivity 方法,执行 执行了r.activity.makeVisible()方法

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

1
2
3
4
5
6
7
8
9
typescript复制代码public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
......

if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
......
}

最终调用 Activity 的 makeVisible 方法,把 decorView 添加到 WindowManage 中

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

1
2
3
4
5
6
7
8
ini复制代码void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

到这里他们之间的关系明确了:

  • 一个 Activity 持有一个 PhoneWindow 的对象,而一个 PhoneWindow 对象持有一个 DecorView 的实例
  • PhoneWindow 继承自 Window,一个 Window 对象内部持有 mWindowManager 的实例,通过调用 setWindowManager 方法与 WindowManager 关联在一起
  • WindowManager 继承自 ViewManager,WindowManagerImpl 是 WindowManager 接口的实现类,但是具体的功能都会委托给 WindowManagerGlobal 来实现
  • 调用 WindowManager 的 addView 方法,实际上调用的是 WindowManagerImpl 的 addView 方法

总结

Acivity 和 Dialog 视图解析绑定过程?

    1. 调用 LayoutInflater 的 inflate 方法,深度优先遍历解析 View
    1. 调用 ViewGroup 的 addView 方法将子 View 添加到根布局中
    1. 调用 WindowManager 的 addView 方法添加根布局

Activity 的视图如何与 Window 关联的?

  • 在 Activity 内部维护着一个 Window 的实例变量 mWindow

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

1
2
3
scala复制代码public class Activity extends ContextThemeWrappe{
private Window mWindow;
}
  • 最后调用 Activity 的 makeVisible 方法,把 decorView 添加到 WindowManage 中

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

1
2
3
4
5
6
7
8
ini复制代码void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

Window 如何与 WindowManager 关联?

在 Activity 的 attach 方法中,调用 PhoneWindow 的 setWindowManager 方法,这个方法的具体实现发生在 Window 中,最终调用的是 Window 的 setWindowManager 方法,将 Window 和 WindowManager 关联在了一起

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

1
2
3
4
5
6
typescript复制代码public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
......
// mWindowManager 是 WindowManagerImpl的实例变量
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

Dialog 的视图如何与 Window 关联?

  • 在 Dialog 的构造方法中初始化了 Window 对象

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

1
2
3
4
5
6
7
8
9
10
less复制代码Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
// 获取WindowManager对象
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// 构建PhoneWindow
final Window w = new PhoneWindow(mContext);
// mWindow 是PhoneWindow实例
mWindow = w;
...
}
  • 调用 Dialog 的 show 方法,完成 view 的绘制和 Dialog 的显示

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

1
2
3
4
5
6
7
8
scss复制代码public void show() {
// 获取DecorView
mDecor = mWindow.getDecorView();
// 获取布局参数
WindowManager.LayoutParams l = mWindow.getAttributes();
// 将DecorView和布局参数添加到WindowManager中
mWindowManager.addView(mDecor, l);
}

参考文献

  • liuwangshu.cn/framework/w…
  • gudong.site/2017/05/08/…

结语

致力于分享一系列 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 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

工具系列

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

逆向系列

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

本文转载自: 掘金

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

大厂Java项目如何进行Maven多模块管理

发表于 2020-05-02

什么是多模块管理

多模块管理简单地理解就是一个 Java 工程项目中不止有一个 pom.xml 文件,会在不同的目录中有多个这样的文件,进而实现 Maven 的多模块管理

为什么要使用多模块管理

随着业务的增长,代码会出现以下问题:

  • 不同业务之间的代码互相耦合,难以区分且快速定位问题
  • 增加开发成本,入手难度增高
  • 开发界线模糊,不易定位到具体负责人
  • 对于有特殊需求的模块无法拆解,比如:上传 maven 仓库只需要部分代码即可,但由于只有 1 个模块,不得不全部上传

故而拆分模块之后,可以避免上述问题

模块拆分方案

通常拆分有 2 种方案

按照结构拆分

1
2
3
4
复制代码- project
- project-service
- project-controller
- project-dao

按照业务拆分

1
2
3
4
复制代码- project
- project-order
- project-account
- project-pay

实际项目结构

以一个普通 Spring Boot 项目为例,首先放一张图,看一下整体项目完成后的结构


其中目录结构为

1
2
3
4
复制代码- detail-page
- detail-client
- detail-service
- detail-start
  • detail-client 用于放需要打包传到 maven 库的代码
  • detail-service 用于放置主要的业务逻辑代码
  • detail-start 用于放启动代码

其中需要注意的是 pom.xml 的文件的配置,该配置决定了父子模块之间的关系

1、detail-page 的 pom.xml

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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<groupId>com.drawcode</groupId>
<artifactId>detail-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging> <!-- 此处必须为pom -->
<name>detail-page</name>

<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<!-- modules即为父子关系 -->
<modules>
<module>detail-client</module>
<module>detail-service</module>
<module>detail-start</module>
</modules>

<!-- dependencyManagement非常重要,决定了子pom.xml是否可以直接引用父pom.xml的包 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<!--注意这个包就是项目本身的模块-->
<dependency>
<groupId>com.drawcode</groupId>
<artifactId>detail-service</artifactId>
<version>${project.version}</version>
<!-- 这个版本就表示0.0.1-SNAPSHOT -->
</dependency>

<!--注意这个包就是项目本身的模块-->
<dependency>
<groupId>com.drawcode</groupId>
<artifactId>detail-client</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<!-- 注意此处为空 -->
</plugins>
</build>

</project>

2、detail-start 的 pom.xml

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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<!--parent使用的即为父pom.xml的信息-->
<parent>
<groupId>com.drawcode</groupId>
<artifactId>detail-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>detail-start</artifactId>
<packaging>jar</packaging> <!-- 注意此处要配置为jar -->
<name>detail-start</name>

<!--子pom.xml不必添加dependencyManagement-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<!--这里可以看到因为父pom.xml已经引用了自身项目的包模块,所以这里可以不加version直接使用-->
<dependency>
<groupId>com.drawcode</groupId>
<artifactId>detail-service</artifactId>
</dependency>

</dependencies>

<build>
<plugins>
<!--因为启动类在detail-start中,所以此处必须添加该plugin-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

3、detail-service 的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<parent>
<groupId>com.drawcode</groupId>
<artifactId>detail-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>detail-service</artifactId>
<packaging>jar</packaging>
<name>detail-service</name>

<!--detail-service依赖于detail-client-->
<dependencies>
<dependency>
<groupId>com.drawcode</groupId>
<artifactId>detail-client</artifactId>
</dependency>
</dependencies>
</project>

4、detail-start 的 pom.xml

因为 detail-start 没有任何依赖所以比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<parent>
<groupId>com.drawcode</groupId>
<artifactId>detail-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>detail-client</artifactId>
<packaging>jar</packaging>
<name>detail-client</name>

<dependencies>
</dependencies>

<build>
</build>

</project>

通过上述文件我们可以分析出以下关系:

1
2
3
4
复制代码- detail-page:父模块
- detail-client:子模块,无依赖
- detail-service:子模块,依赖detail-client
- detail-start:子模块,依赖detail-service

注意:在依赖引用过程中,千万不可以出现循环依赖,比如 client 引用了 service,service 也引用了 client,如果出现这种情况 maven 在打包的时候会直接报错

其中建议除了各个子模块单独使用的包之外,其他的都要在父模块下的 pom.xml 中配置包信息,这样便于包的版本控制



项目内部存在了包的依赖之后,不同模块之间的代码即可进行使用,比如 detail-service 依赖 detail-client,那么 detail-client 中的 Test2 就可以被 detail-service 使用了



但是反过来 detail-client 不可以使用 detail-service 中的类,因为依赖是单向的关系

如何启动

启动指令如下

1
复制代码$ mvn clean install && mvn spring-boot:run -pl detail-start

其中 spring-boot:run 可以使用就是因为 spring-boot-maven-plugin 的存在

-pl detail-start 则代表的是有 application 启动类的子模块目录


参考代码


https://github.com/guanpengchn/detail-page/tree/demo1


本文使用 mdnice 排版

本文转载自: 掘金

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

摸着良心说,你是领导青睐的“技术好手”么?

发表于 2020-04-30

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

公司里有很多好手,代码写的漂亮,工具玩的贼6,知识面也很广。可惜事务缠身,只能在虚拟机里左捣鼓一点,右调试一通,没有用武之地。

能碰到一个好手,是非常难得的。一些正在茁壮成长的苗子,由于我们一直在喂他屎吃,可能就被熏跑了—-大多数宁可换个地方去吃屎,也不会蹲在一个茅坑里。

青楼的老妈都知道除了让顾客满意,队伍的建设是重中之重。客户无止境,花魁不常有。一颗耀眼的新星,能够照亮半个苍穹。当然我现在说的是技术类需求,所以就看能不能出点技术类的建设指导。

我们把他叫做《花魁培养计划》v1.0.0。

花魁的竞争是采用自我意识的管理方式。他们的诉求可能有下面几个:

  1. 工作中像做点拿得出手的“技术活”;
  2. 提高自己的技术水平,增加自己的竞争力;
  3. 做点东西,为跳槽做准备;
  4. 发奋图强大展宏图;
  5. 个人素养如此,这种情况少的可怜;

激发个人的潜力,只能靠奖励,而不是惩罚。要想调动利益不相关之人,所以不外乎:激发需求->拿出成果->给予奖励->进入循环,缺一不可。如果公司不出钱,也只能从个人需求上做文章,这又将路弄窄了许多。

为了支持这种发展,就需要出具一个基本的指导意见。一个技术组件的建设要想达到完善的水平,大体上要考虑以下几点。

  1. 功能实现:要求能够满足大多数业务需求
  2. 高可用方案:能够适应恶劣的主机环境
  3. 运维实现:运维成本和权限问题
  4. 流程建设:如何更加顺畅融入公司流程
  5. 分享:浅显易懂的将其推广和布道
  6. 监控方案&服务集成:方便的监控接口(pull/push),监控指标。如何发现问题
  7. 开发成本和展望:后续的演进路径

可以看到,在其中,编码作为一种基本素养,是默认的技能,我们更加看中的是额外的东西,决定了这个人能够在技术的路上走多远。

做技术组件一定要抛弃只管写代码的这种观念,它通常无法能漂亮的完成工作。要警惕那些代码写的快,但是晦涩,只管生不管养的同学,他们通常会让系统、或者你的管理陷入窘境。

更要警惕那些只说不做的同学,你可能被他高大上的规划忽悠了一年,到最后连个屁都没有。这种经常发生在频繁跳槽的同学身上,因为这种玩法一般在一年之后就无法继续。

有条件的公司会采用多个团队竞争的模式去发掘闪光点。比如多个团队同时启动一个功能相似的组件,最后失败的灰溜溜退出舞台。这种模式有个显而易见的缺陷,那就是推广落地能力强的团队,可能会碾压技术实力强的团队。

但是一般的企业并没有这种条件。说的好听点这叫竞争;说的难听点,就是内耗。所以我们更希望挖掘自带光环的个体,来补充团队的营养。

通过实践,我发现这种方式并不能总是有效。有些诉求可能确实存在,但无法使用广而告之的方式进行宣贯。

收到这些信息的同学,也总是心存疑虑。而在某些公司,更多的人根本没有这样的需求—他们只想要平平安安拿一份工资而已。哪怕多做一点,都觉得吃亏。

要想顺利的进行下去,你首先需要是一个合格的伯乐。对大部分好手应该有一个比较密切的接触,然后,在合适的时候进行初步邀请。

要知道,很多时候,技术建设都是无功而返。在这种情况下,会严重伤害到参与者的积极性。

如果公司环境实在是差,而要做的事情又多的让人无法忍受,那么尝试说服老板扩大招聘吧;或者压迫一下自己的野心,静观其变。

xjjdog曾在两家公司实施这样的策略,用来发掘思维敏捷、思考全面、技能突出的同学,然后帮他们获取更多的利益。这个过程虽然缓慢,但总是在快要遗忘的时候收获一些惊喜。

无法形容发现一个志同道合的人时,那种喜悦的感觉。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

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

为了资料不被白嫖,我学会了做网站的防盗链

发表于 2020-04-30

下午摸鱼的时候遇到了一件有意思的事,在网上找到一个资源站,将资源站的 url 放到自己的博客里,想白嫖一波,结果在我自己的博客里链接失效了,折腾半天忽然想起来,这个网站应该是做了防盗链处理。

什么是盗链

盗链是个什么操作,看一下百度给出的解释:盗链是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终用户的浏览和点击率。受益者不提供资源或提供很少的资源,而真正的服务提供商却得不到任何的收益。
在这里插入图片描述
术语听得有点迷糊?那我们简单的举个栗子:

平时我们在TX网看新闻,里边有很多劲爆的图片、视频资源,每天吸引上亿的用户活跃浏览,赚着大把的广告费。
在这里插入图片描述
有一天一个穷比程序员小富突发奇想,也想建一个自己的网站吸引用户赚广告费,但苦于自己没有资源,他灵光一闪盯上了TX网,心想:要是把它的资源为我所用,这样就能借助TX的资源为自己赚钱。

于是他通过爬虫等一些列技术手段,把TX网资源拉取到自己的小富网,绕过了TX网的展示页面直接呈现给用户,达到了自己不提供资源又能赚钱的目的。

而如此做法却严重的损害了TX网的利益,不仅分流了大量用户,而且由于小富网的大量间接资源请求,大大增加TX网服务器及带宽的压力。

TX网蛋糕被动,忍无可忍决定封杀小富网这类空手套白狼的站点,终于祭出防盗链系统,对除了在TX网本站以外发起的资源请求全部封杀,小富网没法再拉取资源,小富一下子又成了穷比,嘤嘤嘤~
在这里插入图片描述
上边我们简单的举例说了什么是网站的盗链,再总结的简单点就是小站点盗取大站点资源以此来获利的一种行为。

既然有人盗就会有人防盗,接下来在看看怎么防止盗链。

如何防盗链

防盗链在google,新浪,网易,天涯等,内容为主的网站应用的比较多,毕竟主要靠资源内容赚钱的嘛。

在这里插入图片描述

在这里插入图片描述

提到防盗链的实现原理得从HTTP协议说起,上边我们说过设置防盗链以后,会对 “除了在TX网本站以外发起的资源请求全部封杀”,那么问题来了,如何识别一个请求URL是从哪个站点发出的呢?

熟悉HTTP协议的小伙伴应该知道,在HTTP协议头里有一个叫referer的字段,通过referer 告诉服务器该网页是从哪个页面链接过来的,知道这个就好办了,只要获取 referer 字段,一旦检测到来源不是本站即进行阻止或者返回指定的页面。

在这里插入图片描述
防盗链的核心理念:尽量做到不让外站获取到我的资源,即便能通过一些手段获取到资源,也让你的获取过程异常繁琐复杂,无法实现自动化处理,或者干脆就给你有问题的资源恶心死你。

做防盗链的方法比较多,基于HTTP协议头的referer属性也只是其中一种,下边我们来分析几种实现防盗链的方法,如果你有更好的实现方法欢迎留言哦。

基于 HTTP 协议的 referer

基于HTTP协议中的 referer做防盗链,可以从网关层或者利用AOP、Filter拦截器实现。

使用Nginx在网关层做防盗链,目前是最简单的方式之一。通过拦截访问资源的请求,valid_referers 关键字定义了白名单,校验请求头中referer地址是否为本站,如不是本站请求,rewrite 转发请求到指定的警告页面。

在 server 或者 location 配置模块中加入:valid_referers none blocked,其中 none : 允许没有http_refer的请求访问资源(比如:直接在浏览器输入图片网址);blocked : 允许不是http://开头的,不带协议的请求访问资源。

「注意」:这种实现可以限制大多数普通的非法请求,但不能限制有目的的请求,因为可以通过伪造referer信息来绕过。

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
复制代码[root@server1 nginx]# vim conf/nginx.conf  

      location / {
            root /web;
             index index.html;
      }
      location ~* \.(gif|jpg|png|jpeg)$ {
            root /web;
            valid_referers none blocked www.chengxy-nds.top;
            if ($invalid_referer){
                #return 403;
                rewrite ^/ https://img-blog.csdnimg.cn/20200429152123372.png;
         }
     }

     server {
         listen 80;
         server_name www.chengxy-nds.top;
         location / {
                 root /bbs;
                 index index.html;
         }
    }
    
[root@server1 nginx]# systemctl restart nginx

Filter拦截器的实现方式更加简单,拦截指定请求URL,拿到HttpServletRequest 中 referer值比对是否为本站。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class MyFilter implements Filter {  
    @Override
    public void doFilter(HttpServletRequest request, HttpServletResponse response,
            FilterChain chain) throws IOException, ServletException {
            
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        String referer = req.getHeader("referer");
        
        if (referer == null || !referer.contains(req.getServerName())) {
            req.getRequestDispatcher("XXX.jpg").forward(req, res);
        } else {
            chain.doFilter(request, response);
        }
    }
}

登录验证,禁止游客访问

登录验证这种就属于一刀切的方式,一般在论坛、社区类网站使用比较多,不管你发起请求的站点是什么,到我这先登录,没登录请求直接拒绝,简单又粗暴。

图形验证码

图形验证码是一种比较常规的限制办法,比如:下载资源时,必须手动操作验证码,使爬虫工具无法绕过校验,起到保护资源的目的。

在这里插入图片描述
实现防盗链的方式还有很多,这里就不一一列举了(别问,问就是还有很多)。

总结

本来没想写这篇文章,下午搭建自己的博客整理资料,白嫖别人资源没成功有感而发,哈哈哈~ 正好借此机会简单的介绍一下防盗链的概念,提醒 everyone 在开发中要提高安全意识。其实盗链与防盗链就是像是矛与盾一样,说不好是矛更锋利还是盾更坚固,做不到绝对的防盗。道高一尺魔高一丈,盗链的手段越高,相应的防盗技术也会越成熟。

小福利:

整理了几百本各类技术电子书相送 ,嘘~,「免费」 送给小伙伴们。关注公号回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步。

本文转载自: 掘金

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

8 Go 语言流程控制:if-else

发表于 2020-04-30

Hi,大家好,我是明哥。

在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。

我的在线博客:golang.iswbm.com
我的 Github:github.com/iswbm/GolangCodingTime


  1. 条件语句模型

Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:

  • if - else 条件语句
  • switch - case 选择语句
  • for - range 循环语句
  • goto 无条件跳转语句
  • defer 延迟执行

今天先来讲讲 if-else 条件语句

Go 里的条件语句模型是这样的

1
2
3
4
5
6
7
8
9
go复制代码if 条件 1 {
分支 1
} else if 条件 2 {
分支 2
} else if 条件 ... {
分支 ...
} else {
分支 else
}

Go编译器,对于 { 和 } 的位置有严格的要求,它要求 else if (或 else)和 两边的花括号,必须在同一行。

由于 Go是 强类型,所以要求你条件表达式必须严格返回布尔型的数据(nil 和 0 和 1 都不行,具体可查看《详解数据类型:字典与布尔类型》)。

对于这个模型,分别举几个例子来看一下。

  1. 单分支判断

只有一个 if ,没有 else

1
2
3
4
5
6
7
8
go复制代码import "fmt"

func main() {
age := 20
if age > 18 {
fmt.Println("已经成年了")
}
}

如果条件里需要满足多个条件,可以使用 && 和 ||

  • &&:表示且,左右都需要为true,最终结果才能为 true,否则为 false
  • ||:表示或,左右只要有一个为true,最终结果即为true,否则 为 false
1
2
3
4
5
6
7
8
9
go复制代码import "fmt"

func main() {
age := 20
gender := "male"
if (age > 18 && gender == "male") {
fmt.Println("是成年男性")
}
}
  1. 多分支判断

if - else

1
2
3
4
5
6
7
8
9
10
go复制代码import "fmt"

func main() {
age := 20
if age > 18 {
fmt.Println("已经成年了")
} else {
fmt.Println("还未成年")
}
}

if - else if - else

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码import "fmt"

func main() {
age := 20
if age > 18 {
fmt.Println("已经成年了")
} else if age >12 {
fmt.Println("已经是青少年了")
} else {
fmt.Println("还不是青少年")
}
}
  1. 高级写法

在 if 里可以允许先运行一个表达式,取得变量后,再对其进行判断,比如第一个例子里代码也可以写成这样

1
2
3
4
5
6
7
go复制代码import "fmt"

func main() {
if age := 20;age > 18 {
fmt.Println("已经成年了")
}
}

系列导读

01. 开发环境的搭建(Goland & VS Code)

02. 学习五种变量创建的方法

03. 详解数据类型:**整形与浮点型**

04. 详解数据类型:byte、rune与string

05. 详解数据类型:数组与切片

06. 详解数据类型:字典与布尔类型

07. 详解数据类型:指针

08. 面向对象编程:结构体与继承

09. 一篇文章理解 Go 里的函数

10. Go语言流程控制:if-else 条件语句

11. Go语言流程控制:switch-case 选择语句

12. Go语言流程控制:for 循环语句

13. Go语言流程控制:goto 无条件跳转

14. Go语言流程控制:defer 延迟调用

15. 面向对象编程:接口与多态

16. 关键字:make 和 new 的区别?

17. 一篇文章理解 Go 里的语句块与作用域

18. 学习 Go 协程:goroutine

19. 学习 Go 协程:详解信道/通道

20. 几个信道死锁经典错误案例详解

21. 学习 Go 协程:WaitGroup

22. 学习 Go 协程:互斥锁和读写锁

23. Go 里的异常处理:panic 和 recover

24. 超详细解读 Go Modules 前世今生及入门使用

25. Go 语言中关于包导入必学的 8 个知识点

26. 如何开源自己写的模块给别人用?

27. 说说 Go 语言中的类型断言?

28. 这五点带你理解Go语言的select用法


本文转载自: 掘金

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

Spring Boot集成MyBatis报错:Invalid

发表于 2020-04-28

问题描述

自己用Spring Boot集成MyBatis搭建好项目并运行时,出现了如下错误。

分析

看报错信息很明显,没找到mapper的xml配置文件,于是检查了xml文件和应用配置文件application.yml。

反复检查了很久,没发现什么不对,mapper-locations的路径和UserMapper.xml文件的路径是一样的。既然找的是类路径下的地址,那么去classpath看看编译后的UserMapper.xml的位置。

com.stormth.malldemo是一个文件夹的名字,而不是com/stormth/malldemo这种树形结构,于是问题找到了。

当我在resources下创建存储UserMapper.xml的文件夹时,直接新建了一个名为
com.stormth.malldemo的文件夹。跟在java目录下新建包不同,IDEA创建文件夹并不会识别小数点进行分层,而是作为文件夹的全名。
解决方案
====

  • mapper-locations重新配置为classpath:com.stormth.malldemo/mapper/*.xml
  • 新建文件夹时逐层创建

推荐第二种方案,第一种方案的目录命名方式不太规范,容易造成误解。

总结

源代码存放在java目录的包下,包是存储代码的基本单位。新建包aa.bb.xx时,编译器会识别小数点进行目录的分层创建。

配置文件存放在resources目录下,文件夹是存储配置文件的基本单位。新建aa.bb.xx的文件夹,编译器不会识别小数点,而仅仅是创建一个名为aa.bb.xx的文件夹。

本文转载自: 掘金

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

有了这篇文章, Python 中的编码不再是噩梦

发表于 2020-04-27

首发于个人公众号:《Python编程时光》

我的博客原文:python.iswbm.com/en/latest/c…

我的 Github:github.com/iswbm/Pytho…


Python 中编码问题,一直是很多 Python 开发者的噩梦,尽管你是工作多年的 Python 开发者,也肯定会经常遇到令人神烦的编码问题,好不容易花了半天搞明白了。

一段时间后,又全都忘光光了,一脸懵逼的你又开始你找各种博客、帖子,从头搞清楚什么是编码?什么是 unicode?它和 ASCII 有什么区别?为什么 decode encode 老是报错?python2 里和 python3 的字符串类型怎么都不一样,怎么对应起来?如何检测编码格式?

反反复复,这个过程真是太痛苦了。

今天我把大家在 Python 上会遇到的一些编码问题都讲清楚了,以后你可以不用再 Google,收藏这篇文章就行。

  1. Python 3 中 str 与 bytes

在 Python3中,字符串有两种类型 ,str 和 bytes。

今天就来说一说这二者的区别:

  • unicode string(str 类型):以 Unicode code points 形式存储,人类认识的形式
  • byte string(bytes 类型):以 byte 形式存储,机器认识的形式

在 Python 3 中你定义的所有字符串,都是 unicode string类型,使用 type 和 isinstance 可以判别

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码# python3

>>> str_obj = "你好"
>>>
>>> type(str_obj)
<class 'str'>
>>>
>>> isinstance("你好", str)
True
>>>
>>> isinstance("你好", bytes)
False
>>>

而 bytes 是一个二进制序列对象,你只要你在定义字符串时前面加一个 b,就表示你要定义一个 bytes 类型的字符串对象。

1
2
3
4
5
6
7
8
9
10
11
复制代码# python3
>>> byte_obj = b"Hello World!"
>>> type(byte_obj)
<class 'bytes'>
>>>
>>> isinstance(byte_obj, str)
False
>>>
>>> isinstance(byte_obj, bytes)
True
>>>

但是在定义中文字符串时,你就不能直接在前面加 b 了,而应该使用 encode 转一下。

1
2
3
4
5
6
7
8
9
复制代码>>> byte_obj=b"你好"
File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.
>>>
>>> str_obj="你好"
>>>
>>> str_obj.encode("utf-8")
b'\xe4\xbd\xa0\xe5\xa5\xbd'
>>>
  1. Python 2 中 str 与 unicode

而在 Python2 中,字符串的类型又与 Python3 不一样,需要仔细区分。

在 Python2 里,字符串也只有两种类型,unicode 和 str 。

只有 unicode object 和 非unicode object(其实应该叫 str object) 的区别:

  • unicode string(unicode类型):以 Unicode code points 形式存储,人类认识的形式
  • byte string(str 类型):以 byte 形式存储,机器认识的形式

当我们直接使用双引号或单引号包含字符的方式来定义字符串时,就是 str 字符串对象,比如这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码# python2

>>> str_obj="你好"
>>>
>>> type(str_obj)
<type 'str'>
>>>
>>> str_obj
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>>
>>> isinstance(str_obj, bytes)
True
>>> isinstance(str_obj, str)
True
>>> isinstance(str_obj, unicode)
False
>>>
>>> str is bytes
True

而当我们在双引号或单引号前面加个 u,就表明我们定义的是 unicode 字符串对象,比如这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码# python2

>>> unicode_obj = u"你好"
>>>
>>> unicode_obj
u'\u4f60\u597d'
>>>
>>> type(unicode_obj)
<type 'unicode'>
>>>
>>> isinstance(unicode_obj, bytes)
False
>>> isinstance(unicode_obj, str)
False
>>>
>>> isinstance(unicode_obj, unicode)
True
  1. 如何检测对象的编码

所有的字符,在 unicode 字符集中都有对应的编码值(英文叫做:code point)

而把这些编码值按照一定的规则保存成二进制字节码,就是我们说的编码方式,常见的有:UTF-8,GB2312 等。

也就是说,当我们要将内存中的字符串持久化到硬盘中的时候,都要指定编码方法,而反过来,读取的时候,也要指定正确的编码方法(这个过程叫解码),不然会出现乱码。

那问题就来了,当我们知道了其对应的编码方法,我们就可以正常解码,但并不是所有时候我们都能知道应该用什么编码方式去解码?

这时候就要介绍到一个 python 的库 – chardet ,使用它之前 需要先安装

1
复制代码python3 -m pip install chardet

chardet 有一个 detect 方法,可以 预测其其编码格式

1
2
3
复制代码>>> import chardet
>>> chardet.detect('微信公众号:Python编程时光'.encode('gbk'))
{'encoding': 'GB2312', 'confidence': 0.99, 'language': 'Chinese'}

为什么说是预测呢,通过上面的输出来看,你会看到有一个 confidence 字段,其表示预测的可信度,或者说成功率。

但是使用它时,若你的字符数较少,就有可能 “误诊”),比如只有 中文 两个字,就像下面这样,我们是 使用 gbk 编码的,使用 chardet 却识别成 KOI8-R 编码。

1
2
3
4
5
6
7
8
9
复制代码>>> str_obj = "中文"
>>> byte_obj = bytes(a, encoding='gbk') # 先得到一个 gbk 编码的 bytes
>>>
>>> chardet.detect(byte_obj)
{'encoding': 'KOI8-R', 'confidence': 0.682639754276994, 'language': 'Russian'}
>>>
>>> str_obj2 = str(byte_obj, encoding='KOI8-R')
>>> str_obj2
'жпнд'

所以为了编码诊断的准确,要尽量使用足够多的字符。

chardet 支持多国的语言,从官方文档中可以看到支持如下这些语言(chardet.readthedocs.io/en/latest/s…)

  1. 编码与解码的区别

编码和解码,其实就是 str 与 bytes 的相互转化的过程(Python 2 已经远去,这里以及后面都只用 Python 3 举例)

  • 编码:encode 方法,把字符串对象转化为二进制字节序列
  • 解码:decode 方法,把二进制字节序列转化为字符串对象

那么假如我们真知道了其编码格式,如何来转成 unicode 呢?

有两种方法

第一种是,直接使用 decode 方法

1
2
3
复制代码>>> byte_obj.decode('gbk')
'中文'
>>>

第二种是,使用 str 类来转

1
2
3
4
复制代码>>> str_obj = str(byte_obj, encoding='gbk')
>>> str_obj
'中文'
>>>
  1. 如何设置文件编码

在 Python 2 中,默认使用的是 ASCII 编码来读取的,因此,我们在使用 Python 2 的时候,如果你的 python 文件里有中文,运行是会报错的。

1
复制代码SyntaxError: Non-ASCII character '\xe4' in file demo.py

原因就是 ASCII 编码表太小,无法解释中文。

而在 Python 3 中,默认使用的是 uft-8 来读取,所以省了不少的事。

对于这个问题,通常解决方法有两种:

第一种方法

在 python2 中,可以使用在头部指定

可以这样写,虽然很好看

1
复制代码# -*- coding: utf-8 -*-

但这样写太麻烦了,我通常使用下面两种写法

1
2
复制代码# coding:utf-8
# coding=utf-8

第二种方法

1
2
3
4
复制代码import sys 

reload(sys)
sys.setdefaultencoding('utf-8')

这里在调用sys.setdefaultencoding(‘utf-8’) 设置默认的解码方式之前,执行了reload(sys),这是必须的,因为python在加载完sys之后,会删除 sys.setdefaultencoding 这个方法,我们需要重新载入sys,才能调用 sys.setdefaultencoding 这个方法。


关注公众号,获取最新干货!

本文转载自: 掘金

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

聊聊BinaryLogFileReader

发表于 2020-04-26

序

本文主要研究一下BinaryLogFileReader

BinaryLogFileReader

mysql-binlog-connector-java-0.20.1/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogFileReader.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
复制代码public class BinaryLogFileReader implements Closeable {

public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};

private final ByteArrayInputStream inputStream;
private final EventDeserializer eventDeserializer;

public BinaryLogFileReader(File file) throws IOException {
this(file, new EventDeserializer());
}

public BinaryLogFileReader(File file, EventDeserializer eventDeserializer) throws IOException {
this(file != null ? new BufferedInputStream(new FileInputStream(file)) : null, eventDeserializer);
}

public BinaryLogFileReader(InputStream inputStream) throws IOException {
this(inputStream, new EventDeserializer());
}

public BinaryLogFileReader(InputStream inputStream, EventDeserializer eventDeserializer) throws IOException {
if (inputStream == null) {
throw new IllegalArgumentException("Input stream cannot be NULL");
}
if (eventDeserializer == null) {
throw new IllegalArgumentException("Event deserializer cannot be NULL");
}
this.inputStream = new ByteArrayInputStream(inputStream);
try {
byte[] magicHeader = this.inputStream.read(MAGIC_HEADER.length);
if (!Arrays.equals(magicHeader, MAGIC_HEADER)) {
throw new IOException("Not a valid binary log");
}
} catch (IOException e) {
try {
this.inputStream.close();
} catch (IOException ex) {
// ignore
}
throw e;
}
this.eventDeserializer = eventDeserializer;
}

/**
* @return deserialized event or null in case of end-of-stream
*/
public Event readEvent() throws IOException {
return eventDeserializer.nextEvent(inputStream);
}

@Override
public void close() throws IOException {
inputStream.close();
}

}
  • BinaryLogFileReader实现了Closeable接口,其close方法会关闭inputStream;它定义了inputStream、eventDeserializer两个属性,其构造器接收binlog的文件或者inputStream,也允许指定eventDeserializer;其readEvent方法通过eventDeserializer.nextEvent(inputStream)来返回Event,读完的时候返回null

EventDeserializer

mysql-binlog-connector-java-0.20.1/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.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
复制代码public class EventDeserializer {

private final EventHeaderDeserializer eventHeaderDeserializer;
private final EventDataDeserializer defaultEventDataDeserializer;
private final Map<EventType, EventDataDeserializer> eventDataDeserializers;

private EnumSet<CompatibilityMode> compatibilitySet = EnumSet.noneOf(CompatibilityMode.class);
private int checksumLength;

private final Map<Long, TableMapEventData> tableMapEventByTableId;

private EventDataDeserializer tableMapEventDataDeserializer;
private EventDataDeserializer formatDescEventDataDeserializer;

//......

public Event nextEvent(ByteArrayInputStream inputStream) throws IOException {
if (inputStream.peek() == -1) {
return null;
}
EventHeader eventHeader = eventHeaderDeserializer.deserialize(inputStream);
EventData eventData;
switch (eventHeader.getEventType()) {
case FORMAT_DESCRIPTION:
eventData = deserializeFormatDescriptionEventData(inputStream, eventHeader);
break;
case TABLE_MAP:
eventData = deserializeTableMapEventData(inputStream, eventHeader);
break;
default:
EventDataDeserializer eventDataDeserializer = getEventDataDeserializer(eventHeader.getEventType());
eventData = deserializeEventData(inputStream, eventHeader, eventDataDeserializer);
}
return new Event(eventHeader, eventData);
}

//......

}
  • EventDeserializer定义了eventHeaderDeserializer、defaultEventDataDeserializer等属性;其nextEvent方法先通过eventHeaderDeserializer.deserialize(inputStream)解析eventHeader,之后根据eventHeader.getEventType()来解析不同的eventData,最后将header及data组装为Event返回

EventHeader

mysql-binlog-connector-java-0.20.1/src/main/java/com/github/shyiko/mysql/binlog/event/EventHeader.java

1
2
3
4
5
6
7
8
复制代码public interface EventHeader extends Serializable {

long getTimestamp();
EventType getEventType();
long getServerId();
long getHeaderLength();
long getDataLength();
}
  • EventHeader定义了getTimestamp、getEventType、getServerId、getHeaderLength、getDataLength方法

EventData

mysql-binlog-connector-java-0.20.1/src/main/java/com/github/shyiko/mysql/binlog/event/EventData.java

1
2
复制代码public interface EventData extends Serializable {
}
  • EventData接口继承了Serializable接口

小结

BinaryLogFileReader实现了Closeable接口,其close方法会关闭inputStream;它定义了inputStream、eventDeserializer两个属性,其构造器接收binlog的文件或者inputStream,也允许指定eventDeserializer;其readEvent方法通过eventDeserializer.nextEvent(inputStream)来返回Event,读完的时候返回null

doc

  • BinaryLogFileReader

本文转载自: 掘金

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

每天都在用,但你知道 Tomcat 的线程池有多努力吗? 荒

发表于 2020-04-26

这是why的第 45 篇原创文章。说点不一样的线程池执行策略和线程拒绝策略,探讨怎么让线程池先用完最大线程池再把任务放到队列中。


荒腔走板
====

大家好,我是 why,一个四川程序猿,成都好男人。

先是本号的特色,技术分享之前先简短的荒腔走板聊聊生活。让文章的温度更多一点点。

上面的图是我在一次跑步的过程中拍的。活动之前赛事方搞了个留言活动,收集每公里路牌的一个宣传语。

我的留言有幸被选中了:

每人知道你在坚持什么,但你自己心里应该清楚。

是在说跑马拉松,也是在说其他的事情。

我记得那天的太阳,骄阳似火,路上的树荫也非常的少。苦就苦在我还报的是超级马拉松(说是超级马拉松,其实就是一个全马 42 km加最后 3 km纯上坡的马拉松)

到底有多晒,我给你看一下对比:


酷暑难耐,以至于 30 公里左右的地方我的心里出现了两个小人:

一个说:我好累啊,我跑不动了,我要退赛。

一个说:好呀好呀,我也好晒啊,退赛退赛。

我说:呸,看你们两个不争气的东西,让我带你去终点

于是在 36 公里的地方碰到了我提交的标语,非常开心,停下来拍了几张照片。给自己说:坚持不住的时候再坚持一下。

最后的 3 公里上坡,抽筋了不知道多少次。远远看见终点拱门的时候我突然想到了在敦煌的时候悟出的一句话:自己给自己的辛苦,不是辛苦,是幸福。

好了,说回文章。

违背直觉的JDK线程池

先用 JDK 线程池来开个题。

还是用我之前这个文章《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》“先劝退一波”这一小节里面的例题:


问:这是一个自定义线程池,假设这个时候来了 100 个比较耗时的任务,请问有多少个线程在运行?

正确回答在之前的文章中回答了,这里不在赘述。

但是我面试的时候曾经遇到过很多对于 JDK 线程池不了解的朋友。

而这些人当中大多数都有一个通病,那就是遇到不太会的问题,那就去猜。

面试者遇到这个不会的题的时候,表面上微微一笑,实际上我都已经揣摩出他们的内心活动了:

MD,这题我没背过呀,但是刚刚听面试官说核心线程数是 10,最大线程数是 30。看题也知道答案不是 10 就是 30。

选择题,百分之 50 的命中率,难道不赌一把?

等等,30 是最大线程数?最大?我感觉就是它了。

于是在电光火石一瞬间的思考后,和我对视起来,自信的说:


于是我也是微微一笑,告诉他:下去再了解一下吧,我们聊聊别的。


确实,如果完全不了解 JDK 线程池运行规则,按照直觉来说,我也会觉得应该是,不管是核心还是最大线程数,有任务来了应该先把线程池里面可用的线程用完了,然后再把任务提交到队列里面去排队。

可惜 JDK 的线程池,就是反直觉的。

那有符合我们直觉的线程池吗?

有的,你经常用的的 Tomcat ,它里面的线程池的运行过程就是先把最大线程数用完,然后再提交任务到队列里面去的。

我带你剖析一下。

Tomcat线程池

先打开 Tomcat 的 server.xml 看一下:


眼熟吧?哪一个学过 java web 的人没有配置过这个文件?哪一个配置过这个文件的人没有留意过 Executor 配置?

具体的可配置项可以查看官方文档:

http://tomcat.apache.org/tomcat-9.0-doc/config/executor.html

同时我找到一个可配置的参数的中文说明如下:


注意其中的第一个参数是 className,图片中少了个字母 c。

然后还有两个参数没有介绍,我补充一下:

1.prestartminSpareThreads:boolean 类型,当服务器启动时,是否要创建出最小空闲线程(核心线程)数量的线程,默认值为 false 。

2.threadRenewalDelay:long 类型,当我们配置了 ThreadLocalLeakPreventionListener 的时候,它会监听一个请求是否停止。当线程停止后,如果有需要,会进行重建,为了避免多个线程,该设置可以检测是否有 2 个线程同时被创建,如果是,则会按照该参数,延迟指定时间创建。 如果拒绝,则线程不会被重建。默认为 1000 ms,设定为负值表示不更新。

我们主要关注 className 参数,如果不配置,默认实现是:

org.apache.catalina.core.StandardThreadExecutor

我们先解读一下这个方法(注意,本文中 Tomcat 源码版本号为:10.0.0-M4):

org.apache.catalina.core.StandardThreadExecutor#startInternal


从 123 行到 130 行,就是构建 Tomcat 线程池的地方,很关键,我解读一下:

123行

taskqueue = new TaskQueue(maxQueueSize);

创建一个 TaskQueue 队列,这个队列是继承自 LinkedBlockingQueue 的:


该队列上的注释值得关注一下:

主要是说这是一个专门为线程池设计的一个任务队列。配合线程池使用的时候和普通队列有不一样的地方。

同时传递了一个队列长度,默认为 Integer.MAX_VALUE:


124行


TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());

构建一个 ThreadFactory,其三个入参分为如下:


namePrefix:名称前缀。可以指定,其默认是“tomcat-exec-”。

daemon:是否以守护线程模式启动。默认是 true。

priority:线程优先级。是一个 1 到 10 之前的数,默认是 5。

125行

executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);

构建线程池,其 6 个入参分别如下:


这个具体含义我就不解释了,和 JDK 线程池是一样的。

只是给大家看一下默认参数。


另外还需要十分注意的一点是,这里的 ThreadPoolExecuteor 是 Tomcat 的,不是 JDK 的,虽然名字一样。


看一下 Tomcat 的 ThreadPoolExecuteor注释,里面提到了两个点,一是已提交总数,二是拒绝策略。后面都会讲到。

126行

executor.setThreadRenewalDelay(threadRenewalDelay);

设置 threadRenewalDelay 参数。不是本文重点,可以先不关心。

127 - 129行

1
2
3
复制代码if (prestartminSpareThreads) {
executor.prestartAllCoreThreads();
}

设置是否预启动所有的核心线程池,这个参数在之前文章中也有讲到过。

prestartminSpareThreads 参数默认是 false。但是我觉得这个地方你设置为 true 也是多次一举。完全没有必要。

为什么呢?

因为在 125 行构建线程池的时候已经调用过这个方法了:


从源码可以看出,不管你调用哪一个线程池构造方法,都会去调用 prestartAllCoreThreads 方法。

所以,这算不算 Tomcat 的一个小 Bug 呢?快拿起你的键盘给它提 pr 吧。

130行

taskqueue.setParent(executor);

这行代码非常关键。没有这行代码,Tomcat 的线程池则会表现的和 JDK 的线程池一样。

拿下面的程序举例:


自定义线程池最多可以容纳 150+300 个任务。

当 24 行注释的时候,Tomcat 线程池运行的过程和 JDK 线程池的运行过程一样,运行的线程数只会是核心程序数 5。

当 24 行取消注释的时候,Tomcat 线程池就会一直创建线程个数到 150 个,然后把剩下的任务提交到自定义的 TaskQueue 队列里面去。

我再提供一个复制粘贴直接运行版本,你分别运行一下,试一试,看看结果:

1
复制代码public class TomcatThreadPoolExecutorTest {

public static void main(String[] args) throws InterruptedException {

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

String namePrefix = "why不止技术-exec-";
boolean daemon = true;
TaskQueue taskqueue = new TaskQueue(300);
TaskThreadFactory tf = new TaskThreadFactory(namePrefix, daemon, Thread.NORM_PRIORITY);
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
150, 60000, TimeUnit.MILLISECONDS, taskqueue, tf);
//taskqueue.setParent(executor);
for (int i = 0; i &lt; 300; i++) {
try {
executor.execute(() -&gt; {
logStatus(executor, "创建任务");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
Thread.currentThread().join();
}

private static void logStatus(ThreadPoolExecutor executor, String name) {
TaskQueue queue = (TaskQueue) executor.getQueue();
System.out.println(Thread.currentThread().getName() + "-" + name + "-:" +
"核心线程数:" + executor.getCorePoolSize() +
"\t活动线程数:" + executor.getActiveCount() +
"\t最大线程数:" + executor.getMaximumPoolSize() +
"\t总任务数:" + executor.getTaskCount() +
"\t当前排队线程数:" + queue.size() +
"\t队列剩余大小:" + queue.remainingCapacity());
}

`}`

接着就去分析这行代码的用途,看看这一行代码,怎么就反转了 JDK 线程池的运行过程。

源码之下无秘密

如果你对 JDK 线程池的源码熟悉一点的话,你大概能猜到 Tomcat 肯定是在控制新建线程的地方做了手脚,也就是下面这个地方:


PS:需要说明一下的是,上面的截图是 JDK 线程池的 execute 方法。因为 Tomcat 线程池的提交也是复用的这个方法。但是 workQueue 不是同一个队列。


那你先把工作流程和各个参数都摸熟了,然后写个 Demo ,接着去疯狂的 Debug 吧。然后你总会找到这个地方的,而且你会发现,不难找。

好了,上面主要关注我圈起来的部分。

在截图的 1371 行,如果没有把任务成功放到队列里面(前提是线程池是运行状态),则会执行 1378 行的逻辑,而这个逻辑,就是创建非核心线程的逻辑。

所以,经过上面的推导之后,一切都清晰了,Tomcat 只需要在自定义队列的 offer 方法中做文章即可。

所以,我们重点关注一下该方法:

org.apache.tomcat.util.threads.TaskQueue#offer


为了更加直观的看出来其运行流畅,我在第 80 行打了个断点运行程序如下:


可以看到里面的几个参数,下面的讲解会用到这里面的参数:


第一个 if 判断


首先第一个 if,判断 parent 是否为空:


从断点运行参数截图可以看出,这里的 parent 就是 Tomcat 的 ThreadPoolExecutor 类。

当 parent 为 null 时,直接调用原始的 offer 方法。

所以,还记得我前面说的吗?


现在你知道为什么了吧?

源码,就是这个源码。道理,就是这么个道理。

所以,这里不为空,不满足条件,进入下一个 if 判断。

第二个 if 判断


首先,需要明确的是,能进入到第二个判断的时候,当前运行中的线程数肯定是大于等于核心线程数(因为已经在执行往队列里面放的逻辑了,说明核心线程数肯定是满了),小于最大线程数的。


其中 getPoolSize 方法是获取线程池中当前运行的线程数量:


所以,第二个 if 判断的是运行中的线程数是否等于最大线程数。如果等于,说明所有线程都在工作了,把任务扔到队列里面去。

从断点运行参数截图可以看到, 当前运行数为 5 ,最大线程数为 150。不满足条件,进入下一个 if 判断。

第三个 if 判断


首先我们看看 getSubmittedCount 获取的是个什么玩意:


getSubmittedCount 获取的是当前已经提交但是还未完成的任务的数量,其值是队列中的数量加上正在运行的任务的数量。

从断点运行参数截图可以看到,当前情况下该数据为 6。

而 parent.getPoolSize() 为 5。

不满足条件,进入下一个 if 判断。

但是这个地方需要多说一句的是,如果当已经提交但是还未完成的任务的数量小于线程池中运行线程的数量时,Tomcat 的做法是把任务放到队列里面去,而不是立即执行。

其实这样想来也是很符合逻辑且简单的做法的。

反正有空闲的线程嘛,扔到队列里面去就被空闲的线程消费了。又何必立即执行呢?破坏流程不说,还需要额外实现。

出力不讨好。没必要。

第四个 if 判断


这个判断就很关键了。

如果当前运行中的线程数量小于最大线程数,返回 false。

注意哦,前面的几个 if 判断都是不满足条件就放入队列哦。而这里是不满足条件,就返回 false。

返回 false 意味着什么?


意味着要执行 1378 行代码,去创建线程了呀。

所以,整个流程图大概就是这样:


再聊聊拒绝策略
=======

拒绝策略需要看这个方法:

org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable, long, java.util.concurrent.TimeUnit)

看一下该方法上的注释:


如果队列满了,则会等待指定时间后再次放入队列。

如果再次放入队列的时候还是满的,则抛出拒绝异常。

这个逻辑就类似于你去上厕所,发现坑位全都被人占着。这个时候你的身体告诉你,你括弧肌最多还能在忍一分钟。

于是,你掐着表在门口,深呼吸,闭眼冥想,等了一分钟。

运气好的,再去一看:哎,有个空的坑位了,赶紧占着。

运气不好,再去一看:哎,还是没有位置,怎么办呢?抛异常吧。具体怎么抛就不说了,自行想象。


所以我们看看这个地方,Tomcat 的代码是怎么实现的:


catch 部分首先判断队列是不是 Tomcat 的自定义队列。如果是,则进入这个 if 分支。

关键的逻辑就在这个 if 判断里面了。

可以看到 172 行:

if (!queue.force(command, timeout, unit))

调用了队列的 force 方法。我们知道 BlockingQueue 是没有 force 方法的。

所以这个force 是 Tomcat 自定义队列特有的方法:

1
2
3
4
5
6
复制代码public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
if (parent == null || parent.isShutdown())
throw new RejectedExecutionException(sm.getString("taskQueue.notRunning"));
//forces the item onto the queue, to be used if the task is rejected
return super.offer(o,timeout,unit);
}

进去一看发现:害,也不过如此嘛。就是对原生 offer 方法的一层包装而已。

如果成功加入队列则万事大吉,啥事没有。

如果没有成功加入队列,则抛出异常,并维护 submittedCount 参数。


前面说过:submittedCount 参数 = 队列中的任务个数 + 正在运行的任务数。

所以,这里需要进行减一操作。

拒绝策略就说完了。

但是这个地方的源码是我带你找到的。如果你想自己找到应该怎么操作呢?

你想啊,你是想测试拒绝策略。那只要触发其拒绝策略就行了。

比如下面这样:


给一个只能容纳 450 个任务的线程池提交 500 个任务。

然后就会抛出这个异常:


就会找到 Tomcat 线程池的 174 行:


然后你打上一个断点,玩去吧。

那我们刚刚说的,可以在门口等一分钟再进坑是怎么回事呢?

我们把参数告诉线程池就可以了,比如下面这样:


然后再去运行,因为队列满了后,触发拒绝异常,然后等 3 秒再去提交任务。而我们提交的一个任务 2 秒就能被执行完。

所以,这个场景下,所有的任务都会被正常执行。

现在你知道为了把你给它的任务尽量快速、全部的执行完成,Tomcat有多努力了吗?


小彩蛋
===

在看 Tomcat 自定义队列的时候我发现了作者这样的注释:


这个地方作用是把 forcedRemainingCapacity 参数设置为 0。

这个参数是在什么时候设置的呢?

就是下面这个关闭方法的时候:

org.apache.tomcat.util.threads.ThreadPoolExecutor#contextStopping


可以看到,调用 setCorePoolSize 方法之前,作者直接把 forcedRemainingCapacity 参数设置为了 0。

注释上面写的原因是JDK ThreadPoolExecutor.setCorePoolSize 方法会去检查 remainingCapacity 是否为 0。

至于为什么会去做这样的检查,Tomcat 的作者两次表示:I don’t see why。I did not understand why。

so,他 fake 了 condition。


总之就是他说他也不明白为什么JDK 线程池 setCorePoolSize 方法调小核心线程池的时候要的限制队列剩余长度为 0 ,反正这样写就对了。

别问,问就是规定。

于是我去看了 JDK 线程池的 setCorePoolSize 方法,发现这个限制是在 jdk 1.6 里有,1.6 之后的版本对线程池进行了大规模的重构,取消了这个限制:


那 Tomcat 直接设置为 0 会带来什么问题呢?

正常的逻辑是队列剩余大小 = 队列长度 - 队列里排队的任务数。


而当你对其线程池(队列长度为300)进行监控的时候正常情况应该是这样:


但是当你调用 contextStopping 方法后可能会出现这样的问题:


很明显不符合上面的算法了。

好了,如果你们以后需要对 Tomcat 的线程池进行监控,且 JDK 版本在 1.6版本以上。那你可以去掉这个限制,以免误报警。

好了,恭喜你,朋友。又学到了一个基本用不上的知识点,奇怪的知识又增加了一点点。


Dubbo 线程池
=========

这里再扩展一个 Dubbo 的线程池实现。


org.apache.dubbo.common.threadpool.support.eager.EagerThreadPoolExecutor

你可以看一下,思想还是这个思想:


但是 execute 方法有点不一样:


从代码上看,这里放入失败之后又立马调了一次 offer 方法,且没有等待时间。

也就是说两次 offer 的间隔是非常的短的。

其实我不太明白为什么这样去写,可能是作者留着口子好扩展吧?

因为如果这样写,为什么不直接调用这个方法呢?

java.util.concurrent.LinkedBlockingQueue#offer(E)

也是作者是想在极短的时间能赌一下吧?谁知道呢?

然后可以发现该线程池在拒绝策略上也做了很大的文章:


可以看到日志打印的非常详尽,warn 级别:


dumpJStack 方法,看名字也知道它是要去 Dump 线程了,保留现场:


在这个方法里面,他用了 JDK 默认的线程池,去异常 Dump 线程。

等等,阿里开发规范不是说了不建议用默认线程池吗?

其实这个规范看你怎么去拿捏。在这个场景下,用自带的线程池就能满足需求了。

而且你看第二个红框:提交之后就执行了 shutdown 方法,上面还给了个贴心警告。

必须要 shutdown 线程池,不然会导致 OOM。

这就是细节呀,朋友们。魔鬼都在细节里!

这里为什么用的 shutdown 不是 shutdownNow ?他们的区别是什么?为什么不调用 shutdown 方法会 OOM?

知识点呀,朋友们,都是知识点啊!


好了,到这里本文的分享也到了尾声。

以后当面试官问你 JDK 线程池的运行流程的时候,你答完之后画风一转,再来一个:

其实我们也可以先把最大线程数用完,然后再让任务进入队列。通过自定义队列,重写其 offer 方法就可以实现。目前我知道的 Tomcat 和 Dubbo 都提供了这样策略的线程池。

轻描淡写之间又装了一个漂亮逼。让面试官能进入下一个知识点,让你能更多的展现自己。


最后说一句(求关注)
==========

本文主要介绍了 Tomcat 线程池的运行流程,和 JDK 线程池的流程比起来,它确实不一样。

而 Tomcat 线程池为什么要这样做呢?

其实就是因为 Tomcat 处理的多是 IO 密集型任务,用户在前面等着响应呢,结果你明明还能处理,却让用户的请求入队等待?

这样不好,不好。

说到底,又回到了任务类型是 IO 密集型还是 CPU 密集型这个话题上来。

有兴趣的可以看看我的这篇文章:《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》


点个赞吧,周更很累的,不要白嫖我,需要一点正反馈。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是why技术,一个不是大佬,但是喜欢分享,又暖又有料的四川好男人。

欢迎关注公众号【why不止技术】,坚持输出原创。分享技术、品味生活,愿你我共同进步。

本文转载自: 掘金

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

1…817818819…956

开发者博客

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