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

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


  • 首页

  • 归档

  • 搜索

从22行有趣的源码库中,我学到了 callback prom

发表于 2021-11-10
  1. 前言

大家好,我是若川。欢迎关注我的公众号若川视野,最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,已进行三个月,大家一起交流学习,共同进步,很多人都表示收获颇丰。

想学源码,极力推荐之前我写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite等10余篇源码文章。

本文仓库 remote-git-tags-analysis,求个star^_^

最近组织了源码共读活动,大家一起学习源码。于是搜寻各种值得我们学习,且代码行数不多的源码。

我们经常会在本地git仓库切换tags,或者git仓库切换tags。那么我们是否想过如果获取tags呢。本文就是学习 remote-git-tags 这个22行代码的源码库。源码不多,但非常值得我们学习。

阅读本文,你将学到:

1
2
3
4
5
js复制代码1. Node 加载采用什么模块
2. 获取 git 仓库所有 tags 的原理
3. 学会调试看源码
4. 学会面试高频考点 promisify 的原理和实现
5. 等等

刚开始先不急着看上千行、上万行的源码。源码长度越长越不容易坚持下来。看源码讲究循序渐进。比如先从自己会用上的百来行的开始看。

我之前在知乎上回答过类似问题。

一年内的前端看不懂前端框架源码怎么办?

简而言之,看源码

1
2
3
4
5
bash复制代码循序渐进
借助调试
理清主线
查阅资料
总结记录
  1. 使用

1
2
3
4
js复制代码import remoteGitTags from 'remote-git-tags';

console.log(await remoteGitTags('https://github.com/lxchuan12/blog.git'));
//=> Map {'3.0.5' => 'c39343e7e81d898150191d744efbdfe6df395119', …}
  1. 源码

Get tags from a remote Git repo

这个库的作用是:从远程仓库获取所有标签。

原理:通过执行 git ls-remote --tags repoUrl (仓库路径)获取 tags

应用场景:可以看有哪些包依赖的这个包。
npm 包描述信息

其中一个比较熟悉的是npm-check-updates

npm-check-updates 将您的 package.json 依赖项升级到最新版本,忽略指定的版本。

还有场景可能是 github 中获取所有 tags 信息,切换 tags 或者选定 tags 发布版本等,比如微信小程序版本。

看源码前先看 package.json 文件。

3.1 package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// package.json
{
// 指定 Node 以什么模块加载,缺省时默认是 commonjs
"type": "module",
"exports": "./index.js",
// 指定 nodejs 的版本
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "xo && ava"
}
}

众所周知,Node 之前一直是 CommonJS 模块机制。 Node 13 添加了对标准 ES6 模块的支持。

告诉 Node 它要加载的是什么模块的最简单的方式,就是将信息编码到不同的扩展名中。
如果是 .mjs 结尾的文件,则 Node 始终会将它作为 ES6 模块来加载。
如果是 .cjs 结尾的文件,则 Node 始终会将它作为 CommonJS 模块来加载。

对于以 .js 结尾的文件,默认是 CommonJS 模块。如果同级目录及所有目录有 package.json 文件,且 type 属性为module 则使用 ES6 模块。type 值为 commonjs 或者为空或者没有 package.json 文件,都是默认 commonjs 模块加载。

关于 Node 模块加载方式,在《JavaScript权威指南第7版》16.1.4 Node 模块 小节,有更加详细的讲述。此书第16章都是讲述Node,感兴趣的读者可以进行查阅。

3.2 调试源码

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码# 推荐克隆我的项目,保证与文章同步,同时测试文件齐全
git clone https://github.com/lxchuan12/remote-git-tags-analysis.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

# 或者克隆官方项目
git clone https://github.com/sindresorhus/remote-git-tags.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

用最新的VSCode 打开项目,找到 package.json 的 scripts 属性中的 test 命令。鼠标停留在test命令上,会出现 运行命令 和 调试命令 的选项,选择 调试命令 即可。

调试如图所示:

调试如图所示

VSCode 调试 Node.js 说明如下图所示:

VSCode 调试 Node.js 说明

跟着调试,我们来看主文件。

3.3 主文件仅有22行源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码// index.js
import {promisify} from 'node:util';
import childProcess from 'node:child_process';

const execFile = promisify(childProcess.execFile);

export default async function remoteGitTags(repoUrl) {
const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
const tags = new Map();

for (const line of stdout.trim().split('\n')) {
const [hash, tagReference] = line.split('\t');

// Strip off the indicator of dereferenced tags so we can override the
// previous entry which points at the tag hash and not the commit hash
// `refs/tags/v9.6.0^{}` → `v9.6.0`
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');

tags.set(tagName, hash);
}

return tags;
}

源码其实一眼看下来就很容易懂。

3.4 git ls-remote –tags

支持远程仓库链接。

git ls-remote 文档

如下图所示:

ls-remote

获取所有tags git ls-remote --tags https://github.com/vuejs/vue-next.git

把所有 tags 和对应的 hash值 存在 Map 对象中。

3.5 node:util

Node 文档

Core modules can also be identified using the node: prefix, in which case it bypasses the require cache. For instance, require(‘node:http’) will always return the built in HTTP module, even if there is require.cache entry by that name.

也就是说引用 node 原生库可以加 node: 前缀,比如 import util from 'node:util'

看到这,其实原理就明白了。毕竟只有22行代码。接着讲述 promisify。

  1. promisify

源码中有一段:

1
js复制代码const execFile = promisify(childProcess.execFile);

promisify 可能有的读者不是很了解。

接下来重点讲述下这个函数的实现。

promisify函数是把 callback 形式转成 promise 形式。

我们知道 Node.js 天生异步,错误回调的形式书写代码。回调函数的第一个参数是错误信息。也就是错误优先。

我们换个简单的场景来看。

4.1 简单实现

假设我们有个用JS加载图片的需求。我们从 这个网站 找来图片。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码examples
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';

function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src;
image.alt = '公众号若川视野专用图?';
image.style = 'width: 200px;height: 200px';
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}

我们很容易写出上面的代码,也很容易写出回调函数的代码。需求搞定。

1
2
3
4
5
6
7
js复制代码loadImage(imageSrc, function(err, content){
if(err){
console.log(err);
return;
}
console.log(content);
});

但是回调函数有回调地狱等问题,我们接着用 promise 来优化下。

4.2 promise 初步优化

我们也很容易写出如下代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码const loadImagePromise = function(src){
return new Promise(function(resolve, reject){
loadImage(src, function (err, image) {
if(err){
reject(err);
return;
}
resolve(image);
});
});
};
loadImagePromise(imageSrc).then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});

但这个不通用。我们需要封装一个比较通用的 promisify 函数。

4.3 通用 promisify 函数

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
js复制代码function promisify(original){
function fn(...args){
return new Promise((resolve, reject) => {
args.push((err, ...values) => {
if(err){
return reject(err);
}
resolve(values);
});
// original.apply(this, args);
Reflect.apply(original, this, args);
});
}
return fn;
}

const loadImagePromise = promisify(loadImage);
async function load(){
try{
const res = await loadImagePromise(imageSrc);
console.log(res);
}
catch(err){
console.log(err);
}
}
load();

需求搞定。这时就比较通用了。

这些例子在我的仓库存放在 examples 文件夹中。可以克隆下来,npx http-server .跑服务,运行试试。

examples

跑失败的结果可以把 imageSrc 改成不存在的图片即可。

promisify 可以说是面试高频考点。很多面试官喜欢考此题。

接着我们来看 Node.js 源码中 promisify 的实现。

4.4 Node utils promisify 源码

github1s node utils 源码

源码就暂时不做过多解释,可以查阅文档。结合前面的例子,其实也容易理解。

utils promisify 文档

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
js复制代码const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');

let validateFunction;

function promisify(original) {
// Lazy-load to avoid a circular dependency.
if (validateFunction === undefined)
({ validateFunction } = require('internal/validators'));

validateFunction(original, 'original');

if (original[kCustomPromisifiedSymbol]) {
const fn = original[kCustomPromisifiedSymbol];

validateFunction(fn, 'util.promisify.custom');

return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
}

// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
const argumentNames = original[kCustomPromisifyArgsSymbol];

function fn(...args) {
return new Promise((resolve, reject) => {
ArrayPrototypePush(args, (err, ...values) => {
if (err) {
return reject(err);
}
if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
resolve(obj);
} else {
resolve(values[0]);
}
});
ReflectApply(original, this, args);
});
}

ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));

ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
return ObjectDefineProperties(
fn,
ObjectGetOwnPropertyDescriptors(original)
);
}

promisify.custom = kCustomPromisifiedSymbol;
  1. ES6+ 等知识

文中涉及到了Map、for of、正则、解构赋值。

还有涉及封装的 ReflectApply、ObjectSetPrototypeOf、ObjectDefineProperty、ObjectGetOwnPropertyDescriptors 等函数都是基础知识。

这些知识可以查看esma规范,或者阮一峰老师的《ES6 入门教程》 等书籍。

  1. 总结

一句话简述 remote-git-tags 原理:使用Node.js的子进程 child_process 模块的execFile方法执行 git ls-remote --tags repoUrl 获取所有 tags 和 tags 对应 hash 值 存放在 Map 对象中。

文中讲述了我们可以循序渐进,借助调试、理清主线、查阅资料、总结记录的流程看源码。

通过 remote-git-tags 这个22行代码的仓库,学会了 Node 加载采用什么模块,知道了原来 git ls-remote --tags支持远程仓库,学到了面试高频考点 promisify 函数原理和源码实现,巩固了一些 ES6+ 等基础知识。

建议读者克隆我的仓库动手实践调试源码学习。

后续也可以看看 es6-promisify 这个库的实现。

最后可以持续关注我@若川。欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,大家一起学习源码,共同进步。


关于 && 交流群

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

Java 并发基础(五):面试实战之多线程顺序打印

发表于 2021-11-10

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

本文被《从小工到专家的 Java 进阶之旅》收录。

你好,我是看山。

来个面试题,让大家练练手。这个题在阿里和小米都被问过,所以放在这个抛砖引玉,期望能够得到一个更佳的答案。

实现 3 个线程 A、B、C,A 线程持续打印“A”,B 线程持续打印“B”,C 线程持续打印“C”,启动顺序是线程 C、线程 B、线程 A,打印的结果是:ABC。

解法一:状态位变量控制

这个问题考察的是多线程协同顺序执行。也就是第一个线程最先达到执行条件,开始执行,执行完之后,第二个线程达到执行条件,开始执行,以此类推。可以想到的是,通过状态位来表示线程执行的条件,多个线程自旋等待状态位变化。

线上代码:

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复制代码import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ABCThread {
    private static final Lock lock = new ReentrantLock();
    private static volatile int state = 0;

    private static final Thread threadA = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                if (state % 3 == 0) {
                    System.out.println("A");
                    state++;
                    break;
                } else {
                    System.out.println("A thread & state = " + state);
                }
            } finally {
                lock.unlock();
            }
        }
    });

    private static final Thread threadB = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                if (state % 3 == 1) {
                    System.out.println("B");
                    state++;
                    break;
                } else {
                    System.out.println("B thread & state = " + state);
                }
            } finally {
                lock.unlock();
            }
        }
    });

    private static final Thread threadC = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                if (state % 3 == 2) {
                    System.out.println("C");
                    state++;
                    break;
                } else {
                    System.out.println("C thread & state = " + state);
                }
            } finally {
                lock.unlock();
            }
        }
    });

    public static void main(String[] args) {
        threadC.start();
        threadB.start();
        threadA.start();
    }
}

可以看到,状态位state使用volatile修饰,是希望一个线程修改状态位值之后,其他线程可以读取到刚修改的数据,这个属于 Java 内存模型的范围,后续会有单独的章节描述。

这个可以解题,但是却有很多性能上的损耗。因为每个进程都在自旋检查状态值state是否符合条件,而且自旋过程中会有获取锁的过程,代码中在不符合条件时打印了一些内容,比如:System.out.println("A thread & state = " + state);,我们可以运行一下看看结果:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码C thread & state = 0
...67行
C thread & state = 0
B thread & state = 0
...43行
B thread & state = 0
A
C thread & state = 1
...53行
C thread & state = 1
B
C

可以看到,在A线程获取到锁之前,C线程和B线程自旋了100多次,然后A线程才获取机会获取锁和打印。然后在B线程获取锁之前,C线程又自旋了53次。性能损耗可见一斑。

解法二:Condition实现条件判断

既然无条件自旋浪费性能,那就加上条件自旋。

代码如下:

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
java复制代码import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ABCThread2 {
    private static final Lock lock = new ReentrantLock();
    private static volatile int state = 0;
    private static final Condition conditionA = lock.newCondition();
    private static final Condition conditionB = lock.newCondition();
    private static final Condition conditionC = lock.newCondition();

    private static final Thread threadA = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                while(state % 3 != 0) {
                    System.out.println("A await start");
                    conditionA.await();
                    System.out.println("A await end");
                }
                System.out.println("A");
                state++;
                conditionB.signal();
                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });

    private static final Thread threadB = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                while(state % 3 != 1) {
                    System.out.println("B await start");
                    conditionB.await();
                    System.out.println("B await end");
                }
                System.out.println("B");
                state++;
                conditionC.signal();
                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });

    private static final Thread threadC = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                while(state % 3 != 2) {
                    System.out.println("C await start");
                    conditionC.await();
                    System.out.println("C await end");
                }
                System.out.println("C");
                state++;
                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });

    public static void main(String[] args) {
        threadC.start();
        threadB.start();
        threadA.start();
    }
}

通过Lock锁的Condition实现有条件自旋,运行结果如下:

1
2
3
4
5
6
7
vbnet复制代码C await start
B await start
A
B await end
B
C await end
C

可以从运行结果看到,C线程发现自己不符合要求,就通过conditionC.await();释放锁,然后等待条件被唤醒后重新获得锁。然后是B线程,最后是A线程开始执行,发现符合条件,直接运行,然后唤醒B线程的锁条件,依次类推。这种方式其实和信号量很类似。

解法三:信号量

先上代码:

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
java复制代码import java.util.concurrent.Semaphore;

class ABCThread3 {
    private static Semaphore semaphoreA = new Semaphore(1);
    private static Semaphore semaphoreB = new Semaphore(1);
    private static Semaphore semaphoreC = new Semaphore(1);

    private static final Thread threadA = new Thread(() -> {
        try {
            semaphoreA.acquire();
            System.out.println("A");
            semaphoreB.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    private static final Thread threadB = new Thread(() -> {
        try {
            semaphoreB.acquire();
            System.out.println("B");
            semaphoreC.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    private static final Thread threadC = new Thread(() -> {
        try {
            semaphoreC.acquire();
            System.out.println("C");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    public static void main(String[] args) throws InterruptedException {
        semaphoreB.acquire();
        semaphoreC.acquire();
        threadC.start();
        threadB.start();
        threadA.start();
    }
}

代码中执行前先执行了semaphoreB.acquire();和semaphoreC.acquire();,是为了将B和C的信号释放,这个时候,就能够阻塞B线程、C线程中信号量的获取,直到顺序获取了信号值。

文末总结

这个题是考察大家对线程执行顺序和线程之间协同的理解,文中所实现的三种方式,都能解题,只不过代码复杂度和性能有差异。因为其中涉及很多多线程的内容,后续会单独开文说明每个知识点。

推荐阅读

  • Java 并发基础(一):synchronized 锁同步
  • Java 并发基础(二):主线程等待子线程结束
  • Java 并发基础(三):再谈 CountDownLatch
  • Java 并发基础(四):再谈 CyclicBarrier
  • Java 并发基础(五):面试实战之多线程顺序打印

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

从单体架构到微服务架构 to be or not to be

发表于 2021-11-10

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

你好,我是看山。

微服务的优势众多,在现在如果有谁没有听过微服务架构,可以从什么是微服务了解一下。本文主要聊一聊是否值得花时间将单体架构重构为微服务架构?

微服务架构是一种架构风格,专注于软件研发效能,主要包括单位时间内实现更多功能,或者软件从想法到上线的整个持续交付的过程。在当前的互联网环境中,业务变化迅速,也促使了微服务架构的普及。这种架构迫使团队迅速反应,快速实施,在方案没有过期之前已经上线运行,经受市场考察和考验。

目前国内大多数公司正在运行的系统都是单体架构系统,不可否认,这些系统在公司发展过程中,发挥了不可替代的作用,保障了公司正常运行,创造了很多价值。但是,随着系统的日渐膨胀,集成的功能越来越多,开发效率变得越来越低,一个功能从想法到实现,需要花费越来越长的时间。更严重的是,由于代码模块纠结在一起,很多已经老化的架构或者废弃的功能,已经成为新功能的阻碍。

众所周知,单体架构随着功能增多,不可避免的是研发效能的降低:研发周期变长、研发资源占用增多。从而引发的情况是:新员工培训时间增多、员工加班时间变长、员工要求涨薪或者跳槽。到了这种情况就说明,单体架构已经不能够满足企业发展需要,这个时候,需要升级架构来提升研发效能,比如微服务架构。

想要说明微服务架构的好处,可以来一个比喻。我们建了一个空间站,为此,我们需要将人、货物和设备运输到空间站中,这个时候,运载火箭是比较好的选择,尽管运载火箭造价也比较高,但是几个月发射一次,也能够满足需求。随着空间站的扩大,火箭发射的间隔变短,运输成本高的离谱,而且越来越没法满足空间站运转需求。这个时候,可以尝试另外一种方式,比如,太空电梯。当然太空电梯的造价成本高于一次飞行的费用,但是只要建成,以后的成本就降低了很多。

这个比喻也是说明了微服务带来的美好期望,同时也说明一个问题,实施微服务架构会带来巨大的投资。所以,我们在建造太空电梯之前需要想好,我们真的需要这种投入,否则只能是一种浪费。

to be or not to be

决定从单体架构升级为微服务架构时,先问问自己下面几个问题:

  • 产品或系统是否经过市场考验
  • 是否需要超过一个团队来保证产品发布
  • 系统是否对可靠性、可伸缩性有较高要求

微服务架构

什么是微服务架构呢?Sam Newman认为是:“一组围绕业务领域建模的、小而自治的、彼此协同工作的服务。”

微服务架构中的服务,是根据业务能力抽取的业务模块,独立开发和部署,但是需要彼此配合完成整个业务功能。服务不是单纯的数据存储组件,那是数据库。也不是单纯的逻辑函单元,那是函数。只有同时包括数据+逻辑,才是真正意义上的服务。

服务边界

服务拆解过程中,DDD(领域驱动设计)可以作为微服务架构的指导方针。因为微服务是围绕业务功能定义服务,根据服务定义团队,这与DDD将业务域拆解为业务子域、定义限定上下文的方法论如出一辙,于是DDD作为微服务的指导方针,快速定义各个服务组件,完成从单体架构到微服务架构的迁移。

Alberto Brandolini提出识别服务上下文的方式叫做“Event Storming”。第一步是识别业务域中发生的事件,也就是说,我们的关注点是行为,不是数据结构。这样做的好处是,系统中不同服务之间是松散耦合关系,而且单个服务能够自治。

定义好了服务边界,还需要定义事务边界。过去,我们的服务在一个进程中,后面挂着一个数据库,事务可以选择强一致性事务,也就是ACID。当服务增多,彼此配合,这个时候可以使用最终一致性事务,也就是BASE。不同于ACID,BASE更加灵活,只要数据在最终能够保持一致就可以了。这个最终的时间范围,根据不同的业务场景不同,可能是分钟、小时、天,甚至是周或者月。

准备工作

微服务架构愿景美好,属于重型武器,优点众多,缺点也很明显。服务增多,运维难度增大,错误调试难度增大。所以需要自动化构建、配置、测试和部署,需要日志收集、指标监控、调用链监控等工具,也就是需要DevOps实践。实现DevOps的三步工作法中说明了实现DevOps文化的三个步骤。

除了上面提到的基础,还需要在早期确定服务之间如何集成和彼此调用方式,还需要确定数据体系,包括事务一致性和数据可靠性方法。随着服务增多,还需要配置管理、服务发现等众多组件。具体需要的基础组件可以参考微服务的基建工作。

这些基础的服务和设计,最好在早期定义,否则,后期需要花费更多的资源才能够完善架构。如果前期缺失,后期也没有补足,造成的后果就是微服务架构迁移失败,最后的系统也只是披着微服务外衣的单体架构。

进化还是革命?

定义好服务边界之后,还有一个问题需要解决:是逐步进化更新系统、还是破釜沉舟重构整个系统。

第二种方式很诱人,比较符合大多数程序猿的思维,系统不行,推倒重来,名为重构。但是在大多数情况下,这种方式不能被允许,因为市场变化迅速、竞争激烈,大多数公司不会停止业务,去等待重构一个能够运行、只是有些缺点的系统。所以,逐步提取更新系统才是王道,大多数公司也能接受。这种方式又被称为绞杀模式。

Transformation

该如何逐步过渡到微服务架构?下面一步步进行展示:

图片

第一步,将用户视图层与服务层部分逻辑进行分离。业务逻辑委托给服务层,支持页面展示的查询定向到数据库。这个阶段,我们不修改数据库本身。

图片

第二步,用户视图层与数据库完全分离,依赖于服务层操作数据库。

图片

第三步,将用户视图层与服务层拆分为不同服务,并在服务层创建一个API层,用户视图层与服务层之间通信。

图片

第四步,拆分数据库,将不同业务数据拆分到不同的数据库中,同时对应业务服务层拆分到不同的服务。用户视图层通过API网关与不同业务服务层的API组件通信。这个时候需要注意,如果团队没有微服务开发经验,可以首先抽取简单业务域服务,因为业务简单,实现简单,可以练手,积累经验。

图片

最后一步,拆分用户视图层。

图片

绞杀模式的优势就在于,我们可以随着业务变化随时调整方案,不会造成整个业务进化过程的停摆。

成功标准

当我们完成了整个升级过程,就需要检查一下我们是否达到了预期的结果。引入微服务的目的首先是改善开发流程,我们可以通过简单的指标来衡量:

  • 开发周期:从概念到上线持续的时间
  • 开发效能:单位时间内团队或个人完成的功能或用户故事
  • 系统可伸缩性
  • 平均维修时间:查找和排除故障所需时间

通过对比老架构和新架构的这些特性值,可以评估升级过程取得的效果。当然,升级过程中也要有这些指标的监控。

最重要的事

作为攻城狮,我们为能够解决或改善周围世界而自豪,着迷于提供解决方案。同时,我们也要意识到,我们付出的每一份努力,都要有回报。如果不能带来任何回报的重构升级,都是浪费时间。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

springboot基础入门之返回json数据 一、启动类

发表于 2021-11-10

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

​​​​​一、启动类

在做如下操作之前,我们对之前的Hello进行简单的修改,我们新建一个包com.hpit.test.web 然后新建一个类HelloControoler, 然后修改App.java类,主要是的这个类就是一个单纯的启动类。

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码App.java

iackage com.hpit;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

/**

 * Hello world!

 */

其中@SpringBootApplication申明让spring boot自动给程序进行必要的配置,等价于以默认属性使用@Configuration,@EnableAutoConfiguration和@ComponentScan

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码//@SpringBootApplication

public class App {

              public static void main(String[] args) {

                 SpringApplication.run(App.class, args);

       }

}

运行代码和之前是一样的效果的。

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
kotlin复制代码
com.hpit.test.web.HelloController :

package com.hpit.test.web;

 

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

 

@RestController// 标记为:restful

public class HelloController {

   

    @RequestMapping("/")

    public String hello(){

       return"Hello world!";

}

}

我们在编写接口的时候,时常会有需求返回json数据,那么在spring boot应该怎么操作呢?主要是在class中加入注解@RestController,。

返回 JSON 之步骤:

(1)编写一个实体类Demo

(2)编写DemoController;

(3)在DemoController加上@RestController和@RequestMapping注解;

(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
typescript复制代码com.hpit.test.bean.Demo :

package com.hpit.test.bean;

/**

 * 测试实体类.

 * @author Administrator

 *

 */

public class Demo {

    private longid;//主键.

    private String name;//测试名称.

    public long getId() {

       returnid;

    }

    public void setId(longid) {

       this.id = id;

    }

    public String getName() {

       returnname;

    }

    publicvoid setName(String name) {

       this.name = name;

    }

}

那么在浏览器访问地址:http://127.0.0.1:8080/demo/getDemo 返回如下数据:

{

id: 1,

name: “Zjs”

}

是不是很神奇呢,其实Spring Boot也是引用了JSON解析包Jackson,那么自然我们就可以在Demo对象上使用Jackson提供的json属性的注解,对时间进行格式化,对一些字段进行忽略等等。

二、Spring boot热部署

在编写代码的时候,你会发现我们只是简单把打印信息改变了下,就需要重新部署,如果是这样的编码方式,那么我们估计一天下来之后就真的是打几个Hello World之后就下班了。那么如何解决热部署的问题呢?那就是springloaded,加入如下配置:

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
xml复制代码<plugin>

       <groupId>org.springframework.boot</groupId>

       <artifactId>spring-boot-maven-plugin</artifactId>

       <!-- 配置热部署 -->

       <dependencies>

              <dependency>

                     <groupId>org.springframework</groupId>

                     <artifactId>springloaded</artifactId>

                     <version>1.2.4.RELEASE</version>

              </dependency>

       </dependencies>

       <executions>

              <execution>

                     <goals>

                            <goal>repackage</goal>

                     </goals>

                     <configuration>

                            <classifier>exec</classifier>

                     </configuration>

              </execution>

       </executions>

</plugin>

如果使用的run as – java application的话,那么还需要做一些处理哦:如果是使用spring-boot:run的话,那么到此配置结束,现在你就可以体验coding…coding的爽了。

把spring-loader-1.2.4.RELEASE.jar下载下来,放到项目的lib目录中,然后把IDEA的run参数里VM参数设置为:

-javaagent:.\lib\springloaded-1.2.4.RELEASE.jar -noverify

然后启动就可以了,这样在run as的时候,也能进行热部署了。

​

本文转载自: 掘金

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

C++11 新增的判断算法 正文

发表于 2021-11-10

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

参加该活动的第 16 篇文章

参考原文地址:C++11 新增的一些便利的算法

我对文章的格式和错别字进行了调整,并补充标注出了重要的部分,根据我自己的理解把重点部分进一步解释完善(原作者的代码注释甚少,有的地方甚至有错误)。以下是正文

正文

C++11 新增加了一些便利的算法 —— 比如三个用于判断的算法 all_of、any_of 和 one_of ,使我们的代码写起来更简洁方便,更多的新增算法读者可以参考 en.cppreference.com/w/cpp/algor…

对应的定义如下

1
2
3
4
5
6
7
8
cpp复制代码template <class InputIt, class UnaryPredicate>
bool all_of(InputIt first, InputIt last, UnaryPredicate p);

template <class InputIt, class UnaryPredicate>
bool any_of(InputIt first, InputIt last, UnaryPredicate p);

template <class InputIt, class UnaryPredicate>
bool none_of(InputIt first, InputIt last, UnaryPredicate p);
  • all_of: 检查区间 [first, last) 中是否所有的元素都满足一元判断式 p,所有的元素都满足条件返回 true ,否则返回 false 。
  • any_of:检查区间 [first, last) 中是否至少有一个元素都满足一元判断式 p,只要有一个元素满足条件就返回 true,否则返回 true。
  • none_of:检查区间 [first, last) 中是否所有的元素都不满足一元判断式 p,所有的元素都不满足条件返回 true,否则返回 false 。

下面是这几个算法的示例【原作者的代码有误】:

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
cpp复制代码#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main() {
vector<int> v = { 1, 3, 5, 7, 9 };

auto isOdd = [](int i) {
return i % 2 != 0;
};

auto isEven = [](int i) {
return i % 2 == 0;
};

/// @note 所有都是奇数才返回 true
bool isallOdd = std::all_of(v.begin(), v.end(), isOdd);
if (isallOdd)
cout << "all is odd" << endl;

/// @note 没有一个是偶数才返回 true
bool isNoneEven = std::none_of(v.begin(), v.end(), isEven);
if (isNoneEven)
cout << "none is even" << endl;

/// @note 至少有一个是偶数才返回 true
vector<int> v1 = { 1, 3, 5, 7, 8, 9 };
bool anyof = std::any_of(v1.begin(), v1.end(), isEven);
if (anyof)
cout << "at least one is even" << endl;
}

输出结果为

image.png

(完)

本文转载自: 掘金

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

用队列实现栈 <难度系数⭐>

发表于 2021-11-10

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

📝 题述:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

1️⃣ void push(int x) 将元素 x 压入栈顶。

2️⃣ int pop() 移除并返回栈顶元素。

3️⃣ int top() 返回栈顶元素。

4️⃣ boolean empty() 如果栈是空的,返回 true ;否则,返回 false

⚠ 注意

1️⃣ 你只能使用队列的基本操作——也就是push to back、peek/pop from front、size和is empty这些操作。

2️⃣ 你所使用的语言也许不支持队列。你可以使用list (列表) 或者deque (双端队列) 来模拟一个队列,只要是标准的队列操作即可。

💨 示例:

输入:
[“MyStack”, “push”, “push”, “top”, “pop”, “empty”] - 调用函数接口简称
[[], [1], [2], [], [], []] - 参数

输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

🍳 提示

1️⃣ 1 <= x <= 9

2️⃣ 最多调用100 次 push、pop、top 和 empty

3️⃣ 每次调用 pop 和 top 都保证栈不为空

⚜ 进阶
你能否实现每种操作的均摊时间复杂度为 O(1) 的栈?换句话说,执行 n 个操作的总时间复杂度 O(n) ,尽管其中某个操作可能需要比其他操作更长的时间。你可以使用两个以上的队列。

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:队列的数据是不能从队尾出的,只能从队头出,而这里要实现的是队列实现栈,所以只能遵循栈的特性——后进先出。这里就需要另外一个队列,具体步骤如下:

1、一个队列有数据,一个队列没数据

2、入数据时向不为空的那个入

3、出数据时,就将不为空的队列的前 size-1 个拷贝至另一个队列,然后再Pop掉剩下的一个数据

leetcode原题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
c复制代码//声明
//结构体
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next; //指向下一个节点
QDataType data; //存储整型数据
}QueueNode;

typedef struct Queue
{
QueueNode* phead;//头指针
QueueNode* ptail;//尾指针
}Queue;

//函数
void QueueInit(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
bool QueueEmpty(Queue* pq);
void QueuePop(Queue* pq);
QDataType QueueSize(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
void QueueDestory(Queue* pq);
//函数实现
void QueueInit(Queue* pq)
{
assert(pq);
//把2个指针置空
pq->phead = pq->ptail = NULL;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//malloc空间,如果需要频繁的开辟空间建议再实现一个BuyQueueNode用于malloc
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
//第一次插入
if (pq->phead == NULL)
{
pq->phead = pq->ptail = newnode;
}
//非第一次插入
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
//空链表返回true,非空链表返回false
return pq->phead == NULL;
}
void QueuePop(Queue* pq)
{
assert(pq);
//链表为空时不能删除
assert(!QueueEmpty(pq));
//只有一个节点的情况
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
//多个节点的情况
else
{
QueueNode* next = pq->phead->next;
free(pq->phead) ;
pq->phead = next;
}
}
QDataType QueueSize(Queue* pq)
{
assert(pq);
//如果需要频繁的调用QueueSize这个接口,可以在Queue这个结构体中增加一个成员用于记录长度
int sz = 0;
QueueNode* cur = pq->phead;
while (cur)
{
sz++;
cur = cur->next;
}
return sz;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
//链表为空时不能取头
assert(!QueueEmpty(pq));

return pq->phead->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
//链表为空时不能取尾
assert(!QueueEmpty(pq));

return pq->ptail->data;
}
void QueueDestory(Queue* pq)
{
assert(pq);

QueueNode* cur = pq->phead;
//遍历链表
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
}

typedef struct
{
Queue q1;
Queue q2;
} MyStack;

/** Initialize your data structure here. */

MyStack* myStackCreate()
{
//malloc空间
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
//Init两个队列
QueueInit(&pst->q1);
QueueInit(&pst->q2);
return pst;
}

/** Push element x onto stack. */
void myStackPush(MyStack* obj, int x)
{
assert(obj);
//QueueEmpty为空时返回true,不为空时返回false
//往不为空的那个队列里插入数据(q1不为空往q1插入,q2不为空往q2插入)
if(!QueueEmpty(&obj->q1))
{
QueuePush(&obj->q1, x);
}
else
{
QueuePush(&obj->q2, x);
}
}

/** Removes the element on top of the stack and returns that element. */
int myStackPop(MyStack* obj)
{
assert(obj);
//先默认q1为空,q2不为空
Queue* emptyQ = &obj->q1;
Queue* nonemptyQ = &obj->q2;
//q1不为空,重新赋值
if(!QueueEmpty(&obj->q1))
{
emptyQ = &obj->q2;
nonemptyQ = &obj->q1;
}
//拷贝 - 将非空队列的size-1个数据拷贝
while(QueueSize(nonemptyQ) > 1)
{
//将非空的队列拷贝至空的队列
QueuePush(emptyQ, QueueFront(nonemptyQ));
//删除迭代
QueuePop(nonemptyQ);
}
//top记录非空队列的剩下一个值
int top = QueueFront(nonemptyQ);
//删除非空队列的剩下一个数据,这个数据就是栈顶的数据
QueuePop(nonemptyQ);
//返回
return top;
}

/** Get the top element. */
int myStackTop(MyStack* obj)
{
assert(obj);
//q1不为空,取q1的数据
if(!QueueEmpty(&obj->q1))
{
return QueueBack(&obj->q1);
}
//q2不为空,取q2的数据
else
{
return QueueBack(&obj->q2);
}
}

/** Returns whether the stack is empty. */
bool myStackEmpty(MyStack* obj)
{
assert(obj);
//q1和q2一定有一个为空
//q1为空返回真,q2为空返回真,mystackEmpty为空返回真;
//q1为空返回真,q2为非空返回假,myStackEmpty为非空,返回假
//q1为非空返回假,q2为空返回真,myStackEmpty为非空,返回假
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}

void myStackFree(MyStack* obj)
{
assert(obj);
//结合上面的代码分析我们需要回收两个队列和myStackcreate里malloc的空间
QueueDestory(&obj->q1);
QueueDestory(&obj->q2);

free(obj);
}

/**
* Your MyStack struct will be instantiated and called as such:
* MyStack* obj = myStackCreate();
* myStackPush(obj, x);

* int param_2 = myStackPop(obj);

* int param_3 = myStackTop(obj);

* bool param_4 = myStackEmpty(obj);

* myStackFree(obj);
*/

本文转载自: 掘金

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

【转载】C++11 实现一个超级对象池 正文

发表于 2021-11-10

参考原文地址:(原创) 一个超级对象池的实现

我对文章的格式和错别字进行了调整,并补充标注出了重要的部分,代码也增加了详细注释。以下是正文。

正文

对象池对于创建开销比较大的对象来说很有意义,为了避免重复创建开销比较大的对象,我们可以通过对象池来优化。

对象池的思路比较简单,事先创建好一批对象,放到一个集合中,以后每当程序需要新的对象时候,都从对象池里获取,每当程序用完该对象后,都把该对象归还给对象池。这样会避免重复的对象创建,提高程序性能。

先来看看对象池的简单实现:

简单对象池的实现

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
cpp复制代码#include <list>

template <typename Object>
class ObjectPool
{
public:
ObjectPool(size_t unSize) : m_unSize(unSize)
{
for (size_t unIdx = 0; unIdx < m_unSize; ++unIdx)
{
m_oPool.push_back(new Object());
}
}

~ObjectPool()
{
typename std::list<Object *>::iterator oIt = m_oPool.begin();
while (oIt != m_oPool.end())
{
delete (*oIt);
++oIt;
}
m_unSize = 0;
}

Object *GetObject()
{
Object *pObj = NULL;
if (0 == m_unSize)
{
pObj = new Object();
}
else
{
pObj = m_oPool.front();
m_oPool.pop_front();
--m_unSize;
}

return pObj;
}

void ReturnObject(Object *pObj)
{
m_oPool.push_back(pObj);
++m_unSize;
}

private:
size_t m_unSize;
std::list<object *> m_oPool;
};

这个 object pool 的实现很典型,初始创建一定数量的对象,取的时候就直接从池子中取,用完之后再回收到池子。一般的对象池的实现思路和这个类似,这种实现方式虽然能达到目的,但是存在以下不足:

  1. 对象池 ObjectPool<T> 只能容纳特定类型的对象,不能容纳所有类型的对象,无法支持重载的和参数不同的构造函数;
  2. 对象用完之后需要手动回收,用起来不够方便,更大的问题是存在忘记回收的风险;

我希望能有一个更强大的对象池,这个对象池能容纳所有的对象,还能自动回收用完了对象,不需要手动回收,用起来更方便。要实现这样的对象池需要解决前面提到的两个问题,通过 C++11 就可以解决这两个问题。

  • 对于问题 1 :容纳所有的对象。本质上需要将对象池中的对象类型擦除,这里用 Any 类型就可以解决。
  • 对于问题 2 :自动回收用完的对象。这里用智能指针就可以解决,在创建智能指针时可以指定删除器,在删除器中不删除对象,而是回收到对象池中,而这个过程对外界来说是看不见的,由智能指针自己完成。

关于 Any 的实现见我前面的博客内容:C++11 打造好用的 Any 。

下面来看看超级对象池的具体实现吧。

超级对象池的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
cpp复制代码#include <string>
#include <functional>
#include <tuple>
#include <map>

#include "Any.hpp"

const int MaxObjectNum = 10;

class ObjectPool
{
template <typename T, typename... Args>
using Constructor = std::function<std::shared_ptr<T>(Args...)>;

public:
ObjectPool() : needClear(false)
{
}

~ObjectPool()
{
needClear = true;
}

//默认创建多少个对象
template <typename T, typename... Args>
void Create(int num)
{
if (num <= 0 || num > MaxObjectNum)
throw std::logic_error("object num errer");

auto constructName = typeid(Constructor<T, Args...>).name();

/// @note 用函数对象封装
Constructor<T, Args...> f = [constructName, this](Args... args)
{
return createPtr<T>(string(constructName), args...);
};

m_map.emplace(typeid(T).name(), f); ///< 存储函数对象

m_counter.emplace(constructName, num);
}

/// @note 返回智能指针
template <typename T, typename... Args>
std::shared_ptr<T> createPtr(std::string &constructName, Args... args)
{
/// 调用构造函数创建指针
return std::shared_ptr<T>(new T(args...), [constructName, this](T *t)
{///< Deleter 回收器
if (needClear)
delete[] t;
else
m_object_map.emplace(constructName, std::shared_ptr<T>(t)); ///< 放回对象池(存储对象指针)
});
}

template <typename T, typename... Args>
std::shared_ptr<T> Get(Args... args)
{
using ConstructType = Constructor<T, Args...>;

std::string constructName = typeid(ConstructType).name();
auto range = m_map.equal_range(typeid(T).name()); ///< 取得满足类型名的函数对象范围

for (auto it = range.first; it != range.second; ++it)
{
/// @note 取得范围中满足类型条件的函数对象
/// 继而利用它获取(或创建)对象指针
if (it->second.Is<ConstructType>())
{
auto ptr = GetInstance<T>(constructName, args...);

if (ptr != nullptr)
return ptr;

return CreateInstance<T, Args...>(it->second, constructName, args...);
}
}

return nullptr;
}

private:
template <typename T, typename... Args>
std::shared_ptr<T> CreateInstance(Any &any,
std::string &constructName, Args... args)
{
using ConstructType = Constructor<T, Args...>;
ConstructType f = any.AnyCast<ConstructType>();
/// @note 返回智能指针
return createPtr<T, Args...>(constructName, args...);
}

/// @note 初始化对象池
template <typename T, typename... Args>
void InitPool(T &f, std::string &constructName, Args... args)
{
int num = m_counter[constructName]; ///< 获取该类型需创建的对象个数

if (num != 0)
{
for (int i = 0; i < num - 1; i++)
{
m_object_map.emplace(constructName, f(args...)); ///< 直接构造对象并存储
}
m_counter[constructName] = 0;
}
}

/// @note 从对象池中获取对象
template <typename T, typename... Args>
std::shared_ptr<T> GetInstance(std::string &constructName, Args... args)
{
/// @note 寻找对象池中是否已经存有该对象
auto it = m_object_map.find(constructName);
if (it == m_object_map.end())
return nullptr;

/// @note 取出并转型该指针
auto ptr = it->second.AnyCast<std::shared_ptr<T>>();
if (sizeof...(Args) > 0)
*ptr.get() = std::move(T(args...));

m_object_map.erase(it); ///< 从对象池中除名该对象
return ptr;
}

private:
std::multimap<std::string, Any> m_map;
std::multimap<std::string, Any> m_object_map;
std::map<std::string, int> m_counter;
bool needClear;
};

测试代码:

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
cpp复制代码struct AT
{
AT() {}

AT(int a, int b) : m_a(a), m_b(b) {}

void Fun()
{
cout << m_a << " " << m_b << endl;
}

int m_a = 0;
int m_b = 0;
};

struct BT
{
void Fun()
{
cout << "from object b " << endl;
}
};

void TestObjectPool()
{
ObjectPool pool;
pool.Create<AT>(2);
pool.Create<BT>(2);
pool.Create<AT, int, int>(2);

{
auto p = pool.Get<AT>();
p->Fun();
}

auto pb = pool.Get<BT>();
pb->Fun();

auto p = pool.Get<AT>();
p->Fun();

int a = 5, b = 6;
auto p2 = pool.Get<AT>(a, b);
p2->Fun();

auto p3 = pool.Get<AT>(3, 4);
p3->Fun();

{
auto p4 = pool.Get<AT>(3, 4);
p4->Fun();
}
auto p4 = pool.Get<AT>(7, 8);
p4->Fun();
}

本文转载自: 掘金

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

C语言详解:函数

发表于 2021-11-10

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

函数

定义

程序里的函数又被叫做子程序,他作为一个大型程序的部分代码,有一或多个语句项组成。函数负责完成某项特定任务,提供了对过程的封装和对细节的隐藏,这样的代码通常会被集成为软件库。

特点:

  1. 具备相对的独立性
  2. 一般有输入值和返回值
  3. 功能单一且灵活

函数的分类有:库函数和自定义函数。

库函数

定义

库函数,顾名思义,放在库里供他人使用的函数。如打印输出这样的基础功能,他不是业务性的代码,在开发过程中使用率高且可移植性强,故C语言的基础库里提供了这样的一系列基础功能的代码。

一般库函数有:

  • IO函数(input&output)—— printf scanf getchar putchar …
  • 字符串操作函数 —— strlen strcmp strcat strcpy …
  • 字符操作函数 —— tolower toupper …
  • 内存操作函数 —— memcpy menset memmove memcmp …
  • 时间/日期操作函数 —— time …
  • 数学函数 —— sqrt abs fabs pow …
  • 其他库函数
介绍

为了掌握库函数的使用方法的学习,我们可以参照权威网站 cplusplus 的解析为样本,一般在不同的平台上也是大同小异。一般都是按照这样的顺序对函数进行解析。

  1. 函数的基本信息
  2. 功能描述
  3. 函数参数
  4. 返回值
  5. 例子
  6. 拓展
Example 1 strcpy
1
C复制代码char * strcpy ( char * destination, const char * source);

库函数解析示例

strcpy监视检查尾0

1
2
3
4
5
6
7
8
C复制代码当然这里 strcpy 函数的返回值是目标空间的首地址,故接收是也可以使用函数的返回值。
char arr1[20] = { 0 };
char arr2[] = "damn it!";
//1.
char* ret = strcpy(arr1, arr2);
printf("%s\n", ret);
//2.
printf("%s\n",strcpy(arr1, arr2));
Example 2 memset
1
C复制代码void * ( void * ptr, int value, size_t num );

memset函数解析

1
2
3
4
5
6
C复制代码	char arr[20] = "damn it!";
memset(arr, 'x', 2);
//1.
printf("%s\n", arr);
//2.
printf("%s\n", (char*)memset(arr, 'x', 2));
  • memset函数是以字节为单位,去修改我们的地址中的内容。
1
2
C复制代码	int arr[30] = { 0 };
memset(arr, 1, 5 * sizeof(int));

memset遇整型数组

这样的话只能把整型变量中每一个字节都变成1,而若想用此法置零则是可行的。

就按照这样的方式去读网站上对函数的解析内容。

注意
  1. 每次使用库函数都有引用#include头文件

自定义函数

定义

库函数虽好,但不可贪杯哦~ 库函数虽多,但是毕竟不能实现所有功能,所以还是需要自定义函数来满足我们的各种各样的需求。自定义函数和库函数一样,有函数名、返回类型和函数参属,但不同的是这些都由我们自己来设计。

形式
1
2
3
4
5
6
7
C复制代码ret_type fun_name(para1,...)
{
statment;//语句项
}
ret_type//返回类型
fun_name//函数名
para//参数

有了这样的形式模板,我们就可以照葫芦画瓢了。

Example 1
  1. 找出两个数的最大值

函数形式示例

如图所示,写函数,函数名、参数、返回类型都要对应。

Example 2 两数交换

先看再程序设计中如何进行两数交换,用酱油、醋和空瓶举例。

两数交换示例

  1. 先把a赋值给t,那么现在t里面存有a的值
  2. 现在再把b赋值给a,这样a还在t里不会被覆盖
  3. 最后把t(里的a)赋值给b,这样就完成了a和b的互换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
C复制代码void Swap1(int x, int y) {
int t = 0;
t = x;
x = y;
y = t;
}
void Swap2(int* px, int* py){
int t = 0;
t = *px;
*px = *py;
*py = t;
}
int main(){
int a = 10;
int b = 20;
Swap1(a,b);
printf("Swap1:a=%d,b=%d\n", a, b);
Swap2(&a, &b);
printf("Swap2:a=%d,b=%d\n", a, b);
return 0;
}

Swap1和Swap2那个函数能够实现这样的功能呢?

Swap1仅仅是把a和b传值给x和y,此时去修改x和y是影响不到a和b的。

Swap2是把a,b的地址传给指针变量px和py,这样的话,再函数内去将px和py解引用再修改,就可以指向a,b的内容了。简而言之,通过指针指向实参的地址使得形参与实参同时发生变化。

传值传址示例

由图可知,px和py内部存储的是变量a和b的地址,这样对px和py解引用就可以修改a和b的值。

由右图的监视可看出,Swap1函数x和y确实发生了交换,但并没有影响到a和b,Swap2函数的px、py和&a、&b是一个意思。

参数

函数参数分为实际参数和形式参数两种,

  1. 实际参数又叫实参,实参可以是任意有确定值的形式,以便在进行函数调用时,将其传给形参。
  2. 形式参数又叫形参,只有当函数调用时,他们才被分配确定值以及内存单元,调前不存在,调后销毁,所以形参只是形式上存在。
函数调用
  1. 传值调用

形参实例化之后相当于实参的一份临时拷贝,并且形参和实参占用不同的内存单元,本质上是两个不同的变量,形参的修改不影响实参。

  1. 传址调用

将外部变量的地址传给函数参数,这样的调用可使函数内外建立真正的联系,即形参实参建立联系。

练习
  1. 写一个函数能够判断素数
1
2
3
4
5
6
7
8
9
10
11
12
C复制代码#include <math.h>
int is_prime(int n)
{
//试除法
int j = 0;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
return 0;
}
return 1;
}

函数的功能要单一且灵活,判断素数就是判断素数,打印的操作留给其他函数,这样的话,写出来的代码才能够很好的互相配合。

  1. 写一个函数判断一年是否为闰年
1
2
3
4
5
C复制代码//是闰年返回1,不是闰年返回0
int is_leap_year(int y)
{
return (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0));
}

不可以将两个或者的条件分开成if..else..的形式,这样的话1200,1600,2000这样可整除400,也可整除100的数据就在第一轮判断就淘汰了,进入不了第二个条件的判断。

  1. 写一个函数实现整型有序数组的二分查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
C复制代码int binary_search(char arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left+right)/2;
if (arr[mid] > k)
{
right = mid - 1;
}
else if (arr[mid] < k)
{
left = mid + 1;
}
else
return mid;
}
return -1;
}

而在主程序中是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C复制代码int main()
{
char arr[20] = { 1,2,3,4,5,6,7,8,9,10 };
int key = 7;
int sz = sizeof(arr) / sizeof(arr[0]);
//计算数组元素个数

int ret = binary_search(arr, key, sz);//TDD - 测试驱动开发
//找到返回下标0~9
//找不到返回-1
if (ret == -1)
printf("找不到\n");
else
printf("找到了,下标为%d", ret);
return 0;
}

在主程序编写代码时,把binary_search函数当成库函数一样写,并将函数的实现逻辑实现规定好,最后再去写函数实现。

这样的方法叫TDD(test drive develop)—测试驱动开发。

  1. 写一个函数每调用一次,就将num的值加1
1
2
3
4
5
6
7
8
9
10
C复制代码int Add(int num)
{
num++;
}
int main()
{
int num = 0;
num = Add(num);
return 0;
}

讲到这里基本内容就讲完了,下面开始进一步的深入。

嵌套调用

函数可不可以嵌套定义?

当然是不可以的,函数与函数是平等的,是并列关系,不可以在任意函数(包括主函数)中定义其他函数。

但是函数是可以互相调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
C复制代码void fun1()
{
printf("hanpidiaoyong\n");
}
void fun2()
{
fun1();
}
int main()
{
fun2();
return 0;
}

如代码所示,main函数调用fun2函数,fun2函数又调用fun1函数,最终在屏幕上打印憨批调用字样/[doge]。

链式访问

链式访问(chain access),顾名思义,把一个函数的返回值作为另一个函数的参数。像是用链子把函数首尾相连拴起来。

如:

1
2
3
4
5
C复制代码int main()
{
printf("%d\n",strlen("abcde")); //把strlen的返回值作为printf的参数
return 0;
}
1
2
3
4
5
6
7
8
C复制代码int main()
{
char arr1[20] = "xxxxxxx";
char arr2[20] = "abcde";
//strcpy(arr1,arr2);
printf("%s\n", strcpy(arr1, arr2));//strcpy函数的返回值是目标空间首元素地址
return 0;
}
Example 1

如果觉得掌握了的话,可以看看这个经典例子。

1
C复制代码	printf("%d", printf("%d", printf("%d", 43)));

请问这条语句输出什么?

想要知道这个,那必然要先了解 printf 函数的返回值是什么,通过MSDN或者cplusplus.com网站去查找。

可以看到 printf 函数的返回值是打印字符的个数,如果发生错误,则返回负值。(ps:scanf的返回值是输出字符的个数)

首先可以看出第三个printf打印了43;

然后第二个printf打印了第三个printf的返回值为2;

最后第一个printf打印第二个printf的返回值1;

所以屏幕上打印了4321。

笔者良心说一句,如果学校里有人说自己C语言不错,那么请拿这题考考他。

函数声明

代码是从前往后执行的,如果函数定义在后面的话,调用时便会发出警告:函数未定义,若想消除警告,我们便需要在前面声明一下。

1
2
3
4
5
6
C复制代码void test();
int main(){
test();
}
void test()
{}

定义:声明就是把函数定义在加个;,目的是告诉编译器函数的返回类型、函数名、参数这些具体信息。

特点

函数的声明一般出现在函数使用之前。

函数的声明一般放在头文件中。

在工作的时候,一般是把函数的声明、定义和使用放在三个不同的文件内,方便所有人协作。如:

函数跨文件示例

链接:两个.c的源文件编译之后,会分别生成.obj的目标文件,然后再链接起来,最后生成.exe的可执行文件。

在C语言,不提供源码,也可以使用文件的内容,怎么做的呢?

请移步至我的其他博客:关于vs2019的各种使用问题及解决方法(随即更新)

头文件引用

#include的预编译指令是在预编译阶段将头文件内的所有内容拷贝到源文件内。

  • 所以,头文件中的内容若是内容重复包含则会造成效率降低。那么,怎么解决这件事呢?
  1. 头文件中包含语句#pragma once使得头文件中不会重复包含其他头文件;
  2. 添加这样的代码,将语句包含起来。
1
2
3
4
5
C复制代码#ifndef __ADD_H__// if not define 
#define __ADD_H__//define
//Add函数声明
extern int Add(int x, int y);
#endif//end if

早期都是用第二种方法的,这两种方法是完全等价的。

函数递归

什么叫函数递归呢?

程序自身调用自身的编程技巧叫递归。

特点
  1. 大型复杂问题层层转化为小规模的问题
  2. 少量程序描述除多次运算

递归的思维方法在于:大事化小。

Example 1
  • 接收一个无符号整型值,按照顺序打印其每一位。如输入:1234,输出:1 2 3 4 .

那我们创建一个函数叫print,若print(1234),则剥离一位变成print(123)+4,再剥离一位成print(12)+3+4,再来一位就是print(1)+2+3+4,最后只有一位了,那就全部用printf 函数打印。如:

函数调用解析

我们发现只要将数字1234模10就可以得到4,除10便可以得到123,如此模10除10循环往复,可以将1 2 3 4全部剥离出来。

1
2
3
4
5
6
7
8
9
c复制代码//函数递归
void print(size_t n)
{
if (n > 9)//只有1位便不再往下进行
{
print(n / 10);
}
printf("%d ", n%10);
}

具体流程可参考下面这张图。

函数递归调用示例

红线部分即在返回的时候,n是本次函数n,而不是前一次调用的n。

递归递归,就是递推加回归。

现在有两个问题

  1. if(n > 9)这个条件没有行不行?没有会怎么样?

自然是不行的,我们在上面的推到中发现,最后1<9条件不成立,就结束了递归,否则会永远递归下去,造成死循环且耗干了栈区。

  1. 或者是我们不论代码的正确性,将print(n / 10)改成print(n)会怎么样?

改成print(n)的话每次递归都是相同的值,递归也会无止境的延续下去。

这样便引出了我们递归的两个重要的必要条件:

必要条件
  1. 必须存在限制条件,满足条件时,递归不在继续
  2. 每次递归调用后必须越来越接近限制条件
函数栈帧

在第一个问题中,如果我们要去试验的话,编译器会报出这样的错误:递归栈溢出示例)Stackoverflow(栈溢出)。内存划分

内存粗略的划分为栈区,堆区,静态区。

  1. 栈区主要存放:局部变量,形参(形参和局部变量差不多)
  2. 动态内存分配:malloc calloc等函数开辟空间
  3. 静态区主要存放:全局变量,static修饰的静态变量

若是把栈区放大细看的话,如图所示,有为main函数开辟的空间和print函数开辟的空间,为函数开辟的空间叫函数栈帧也可以叫运行时堆栈。

  • 程序开始执行时开辟空间,程序结束时销毁空间
  • 函数每调用一次就在栈上开辟一次空间,递归返回时,空间会被回收
Example 2

不创建临时变量,实现Strlen函数

1
2
3
4
5
6
7
8
9
c复制代码int my_strlen1(char* pa)
{
int count = 0;
while (*pa++ != '\0')
{//pa++;
count++;
}
return count;
}
1
2
3
4
5
6
7
8
c复制代码//my_strlen求字符串长度
int my_strlen(char* pa)
{
if (*pa == 0){
return 0;
}
return 1+my_strlen(pa + 1);//直接返回长度
}

具体的思考方式呢,就是只要第一个字符不是0,那我们就在外面+1并且跳到下一个字符,直到找到‘\0’,那么我们返回0。

ps:

字符指针+1,向后跳一个字节
整型指针+1,向后跳四个字节
指针+1都是向后跳一个元素的地址,指针类型不同向后跳的字节也不同

my_strlen函数思考方法示例

my_strlen求字符串长度函数解析

函数迭代
递归、迭代的区别?
  • 递归是重复调用函数自身实现循环。
  • 迭代是函数内某段代码实现循环,循环代码中变量既参与运算同时也保存结果,当前保存的结果作为下一次循环计算的初始值。
  • 递归循环中,遇到满足终止条件的情况时逐层返回来结束。
  • 迭代则使用计数器结束循环。

当然很多情况都是多种循环混合采用,这要根据具体需求。

Example 3

求n的阶乘

1
2
3
4
5
6
7
c复制代码int fac(int n)
{
if (n <= 1)
return 1;
else
return n * fac(n - 1);
}
Example 4

求第n个斐波那契数

1
2
3
4
5
6
7
c复制代码int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}

但是这个方法效率是非常低的,当数字特别大时,层层拆分下来,时间效率是O(2n)O(2^n)O(2n)。

根据公式可知,第三个斐波那契数可由前两个得到,我们利用这个规律

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码int fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}

上一个c变成了b,上一个b变成了a。如此循环往复。

利用迭代的方式,计算一个数只需要计算n-2次,这样的话时间复杂度就是O(n)O(n)O(n)。效率大大提高。

有这两题我们可以发现,什么时候用递归简单呢?

  1. 有公式有模板的时候
  2. 递归简单,非递归复杂的时候
  3. 有明显问题的时候

学有余力的话,还可以考虑实现俩个经典题目

  1. 汉诺塔问题
  2. 青蛙跳台阶问题

本文转载自: 掘金

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

5hutool源码分析:DateUtil(时间工具类)-解析

发表于 2021-11-10

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

❤️作者简介:大家好,我是小虚竹。Java领域优质创作者🏆,CSDN博客专家认证🏆,华为云享专家认证🏆

❤️技术活,该赏

❤️点赞 👍 收藏 ⭐再看,养成习惯

看本篇文章前,建议先对java源码的日期和时间有一定的了解,如果不了解的话,可以先看这篇文章:

万字博文教你搞懂java源码的日期和时间相关用法

关联文章:

hutool实战(带你掌握里面的各种工具)目录

5hutool实战:DateUtil-解析被格式化的时间


源码分析目的

知其然,知其所以然

项目引用

此博文的依据:hutool-5.6.5版本源码

1
2
3
4
5
xml复制代码        <dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.6.5</version>
</dependency>

方法名称:DateUtil.parseLocalDateTime(java.lang.CharSequence)

方法描述

构建LocalDateTime对象

格式:yyyy-MM-dd HH:mm:ss

源码分析一

1
2
3
4
5
6
7
8
9
10
java复制代码	/**
* 构建LocalDateTime对象
*
* @param dateStr 时间字符串(带格式)
* @param format 使用{@link DatePattern}定义的格式
* @return LocalDateTime对象
*/
public static LocalDateTime parseLocalDateTime(CharSequence dateStr, String format) {
return LocalDateTimeUtil.parse(dateStr, format);
}

parseLocalDateTime(CharSequence dateStr, String format)方法的format要使用DatePattern定义的格式,保证能解析出来。

来看看**LocalDateTimeUtil.parse(dateStr, format)**源码是如何写的:

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
java复制代码//LocalDateTimeUtil
/**
* 解析日期时间字符串为{@link LocalDateTime}
*
* @param text 日期时间字符串
* @param format 日期格式,类似于yyyy-MM-dd HH:mm:ss,SSS
* @return {@link LocalDateTime}
*/
public static LocalDateTime parse(CharSequence text, String format) {
if (null == text) {
return null;
}

DateTimeFormatter formatter = null;
if(StrUtil.isNotBlank(format)){
// 修复yyyyMMddHHmmssSSS格式不能解析的问题
// fix issue#1082
//see https://stackoverflow.com/questions/22588051/is-java-time-failing-to-parse-fraction-of-second
// jdk8 bug at: https://bugs.openjdk.java.net/browse/JDK-8031085
if(StrUtil.startWithIgnoreEquals(format, DatePattern.PURE_DATETIME_PATTERN)){
final String fraction = StrUtil.removePrefix(format, DatePattern.PURE_DATETIME_PATTERN);
if(ReUtil.isMatch("[S]{1,2}", fraction)){
//将yyyyMMddHHmmssS、yyyyMMddHHmmssSS的日期统一替换为yyyyMMddHHmmssSSS格式,用0补
text += StrUtil.repeat('0', 3-fraction.length());
}
formatter = new DateTimeFormatterBuilder()
.appendPattern(DatePattern.PURE_DATETIME_PATTERN)
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.toFormatter();
} else{
formatter = DateTimeFormatter.ofPattern(format);
}
}

return parse(text, formatter);
}

注释里写着修复了JDK8 的一个bug bugs.openjdk.java.net/browse/JDK-…

试试:

1
2
3
4
5
6
java复制代码	@Test
public void localDateTimeTest5() {
String strDate = "20210805220359100";
DateTimeFormatter formatter =DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
System.out.println(formatter.parse(strDate));
}

image-20210805911767

真的会报错。

官方有给出解决方案,但在java9版本修复。下放到8u版本里。

image-20210805224431816

所以hutool这边的写法就好理解了,这个是官方给出的解决方案:修复yyyyMMddHHmmssSSS格式不能解析的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//LocalDateTimeUtil
/**
* 解析日期时间字符串为{@link LocalDateTime}
*
* @param text 日期时间字符串
* @param format 日期格式,类似于yyyy-MM-dd HH:mm:ss,SSS
* @return {@link LocalDateTime}
*/
public static LocalDateTime parse(CharSequence text, String format) {

...
formatter = new DateTimeFormatterBuilder()
.appendPattern(DatePattern.PURE_DATETIME_PATTERN)
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.toFormatter();
...

return parse(text, formatter);
}

最后调用parse(text, formatter);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 解析日期时间字符串为{@link LocalDateTime},格式支持日期时间、日期、时间
*
* @param text 日期时间字符串
* @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter}
* @return {@link LocalDateTime}
*/
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) {
if (null == text) {
return null;
}
if (null == formatter) {
return LocalDateTime.parse(text);
}

return of(formatter.parse(text));
}

首先好习惯,先判断入参是否为空处理。

1
2
java复制代码LocalDateTime.parse(text)//返回LocalDateTime对象
DateTimeFormatter.parse(text)//返回TemporalAccessor对象

都是java8 新提供的API:

万字博文教你搞懂java源码的日期和时间相关用法

最后使用**of(TemporalAccessor)**转化为LocalDateTime时间对象

首先来看下TemporalAccessor:

TemporalAccessor 的实现类包含

  • Instant
  • LocalDateTime
  • ZonedDateTime
  • OffsetDateTime
  • LocalDate
  • LocalTime
  • OffsetTime
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public static LocalDateTime of(TemporalAccessor temporalAccessor) {
if (null == temporalAccessor) {
return null;
}

if(temporalAccessor instanceof LocalDate){
return ((LocalDate)temporalAccessor).atStartOfDay();
}

return LocalDateTime.of(
TemporalAccessorUtil.get(temporalAccessor, ChronoField.YEAR),
TemporalAccessorUtil.get(temporalAccessor, ChronoField.MONTH_OF_YEAR),
TemporalAccessorUtil.get(temporalAccessor, ChronoField.DAY_OF_MONTH),
TemporalAccessorUtil.get(temporalAccessor, ChronoField.HOUR_OF_DAY),
TemporalAccessorUtil.get(temporalAccessor, ChronoField.MINUTE_OF_HOUR),
TemporalAccessorUtil.get(temporalAccessor, ChronoField.SECOND_OF_MINUTE),
TemporalAccessorUtil.get(temporalAccessor, ChronoField.NANO_OF_SECOND)
);
}

首先,由上面可知,LocalDate是temporalAccessor的实现类。((LocalDate)temporalAccessor).atStartOfDay()这个就可以变成LocalDate.atStartOfDay()

1
2
3
java复制代码public LocalDateTime atStartOfDay() {
return LocalDateTime.of(this, LocalTime.MIDNIGHT);
}

LocalTime.MIDNIGHT:

1
2
3
4
java复制代码 /**
* The time of midnight at the start of the day, '00:00'.
*/
public static final LocalTime MIDNIGHT;

LocalDateTime是由LocalDate和LocalTime组合成的。

1
2
3
4
5
java复制代码 public static LocalDateTime of(LocalDate date, LocalTime time) {
Objects.requireNonNull(date, "date");
Objects.requireNonNull(time, "time");
return new LocalDateTime(date, time);
}

然后,TemporalAccessorUtil.get(temporalAccessor, ChronoField.YEAR)这个是hutool的源码,我们来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码	/**
* 安全获取时间的某个属性,属性不存在返回0
*
* @param temporalAccessor 需要获取的时间对象
* @param field 需要获取的属性
* @return 时间的值,如果无法获取则默认为 0
*/
public static int get(TemporalAccessor temporalAccessor, TemporalField field) {
if (temporalAccessor.isSupported(field)) {
return temporalAccessor.get(field);
}

return (int)field.range().getMinimum();
}

判断temporalAccessor是否有支持指定的字段,如果有,直接返回指定字段对应的时间值。如果没有,则执行

(int)field.range().getMinimum(),获取字段对应的最小值。

1
2
3
4
5
6
7
java复制代码		System.out.println(ChronoField.YEAR.range().getMinimum());
System.out.println(ChronoField.MONTH_OF_YEAR.range().getMinimum());
System.out.println(ChronoField.DAY_OF_MONTH.range().getMinimum());
System.out.println(ChronoField.HOUR_OF_DAY.range().getMinimum());
System.out.println(ChronoField.MINUTE_OF_HOUR.range().getMinimum());
System.out.println(ChronoField.SECOND_OF_MINUTE.range().getMinimum());
System.out.println(ChronoField.NANO_OF_SECOND.range().getMinimum());

image-20210805953564

year值初始化时设的最小值和最大值

image-20210805233338612

本文转载自: 掘金

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

Spring Cloud / Alibaba 微服务架构

发表于 2021-11-09

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

上篇文章介绍了如何搭建SpringBoot Admin监控服务器,本篇文章会具体说一下如何本项目中是如何应用的。

一、创建e-commerce-admin子模块

创建对应包及文件,如图所示。修改完pom文件记得要reload project。

image.png

image.png

其中bootstrap.yml文件中,对暴露端点进行配置。include代表需要开放的端点,默认值只打开 health 和 info 两个端点。通过设置星号,可以开放所有端点。

image.png

image.png

启动项目后,可以去到127.0.0.1/e-commerce-admin页面(如上图所示,在bootstrap.yml文件中的配置路径),出现如下图所示则代表已经成功了。

image.png

右上角的三杠图标进去可以看到很多各种各样的信息,包括当前进程、运行时间、CPU使用情况、垃圾回收次数、线程个数等等,可以用来监控我们的工程并进行一个可视化界面的展示。

image.png

二、应用注册到SpringBoot Admin Server

被监控和管理的应用(微服务)注册到Admin Server有两种方式。

方式一

被监控和管理的应用程序使用SpringBoot Admin Client 库,通过HTTP调用注册到SpringBoot Admin Server上。

注:这种方式主要是纯SpringBoot的应用会使用到,微服务架构下基本上不会使用这种方式,麻烦而且没必要。

方式二

首先,被监控和管理的应用程序注册到SpringCloud集成的注册中心(即Nacos中),然后SpringBoot Admin Server通过注册中心会主动获取到被监控和管理的应用程序。

注:这是微服务架构下采用的方式,这种方式简单方便,我们只需要做很小的改动即可接入。直接将我们的工程注册到SpringCloud集成的注册中心上,然后对工程做一些配置即可(暴露Actuator,修改e-commerce-alibaba-nacos-client项目中的bootstrap.yml文件)。

三、验证

启动e-commerce-alibaba-nacos-client微服务(NacosClientApplication),打开我们的Nacos Console页面,可以服务列表下发现有两个服务,说明这两个服务都已经成功启动且被注册上去了。

image.png

打开Spring Boot Admin页面,也可以发现应用墙中有两个服务。

image.png

说明我们的SpringBoot Admin监控服务器已经搭建完成,十分简单且方便。

该Spring Cloud / Alibaba 微服务架构系列文章感谢张勤一老师的指导!

本文转载自: 掘金

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

1…383384385…956

开发者博客

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