前端通过增加XHR钩子来全局增加header

引言

前端通过修改 XHR 原型来全局增加 header 是采用 XMLHttpRequest 加 hook 方式实现一个简单业务场景。这样可以提高代码的可维护性和可扩展性,减少重复代码的编写。

比如,在用户登录后,后端返回了一个 token,前端需要在发送的每个请求中都携带这个 token 以进行认证。通过修改 XHR 原型来全局增加 header,可以实现全局性的认证信息添加,无需在每个请求中手动设置 header。

模拟接口请求 mock 参数的时候也需要全局拦截 xhr 请求,这个时候就需要 hook 对应的 send、open 函数了。

hook钩子函数

在JavaScript中,”hook”(钩子)是一种编程模式,它允许开发者在特定的代码执行点插入自定义的逻辑。钩子函数是用于在这些执行点执行自定义逻辑的函数。

钩子函数通常被设计成可拦截或修改某个操作的执行流程。它们允许开发者在关键步骤中插入自定义的代码,以满足特定的需求,例如添加额外的验证、修改数据、记录日志等。

在JavaScript中,钩子函数可以通过以下两种方式实现:

使用原生提供的钩子函数:有些JavaScript库或框架提供了一些特定的钩子函数,供开发者在特定的时机插入自己的代码。例如,在Vue.js中,可以使用created钩子函数在实例被创建后执行自定义逻辑。

1
2
3
4
5
new Vue({
created() {
// 自定义逻辑
}
});

自定义钩子函数:开发者可以根据需要在自己的代码中定义钩子函数。这些钩子函数可以是普通的函数,在代码的特定位置被调用。例如,在一个JavaScript类中,可以定义一个钩子方法,用于执行一些特定的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass {
constructor() {
// 构造函数
}

beforeMethod() {
// 在方法执行之前执行的钩子函数
}

myMethod() {
this.beforeMethod(); // 在方法执行之前调用钩子函数
// 方法的实际逻辑
this.afterMethod(); // 在方法执行之后调用钩子函数
}

afterMethod() {
// 在方法执行之后执行的钩子函数
}
}

const instance = new MyClass();
instance.myMethod(); // 执行方法,同时触发钩子函数

通过使用钩子函数,开发者可以在适当的时机执行自定义的逻辑,以满足特定的需求。这种模式提供了更大的灵活性和可扩展性,并允许代码的修改不影响原有的执行流程。

编程例子

通过hook方式实现修改XMLHttpRequest的send或者open函数来全局增加header。

方式一,采用闭包修改钩子函数:

1
2
3
4
5
6
7
8
9
10
11
12
(function (open, send) {
XMLHttpRequest.prototype.open = function () {
open.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
/**
* 接口请求前增加自定义业务逻辑处理
*/
this.setRequestHeader("diyHeader", "diyHeader666666666");
send.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open, XMLHttpRequest.prototype.send);

方式二,通过hook自定义函数修改:

1
2
3
4
5
6
7
8
9
10
11
function hookXhr(func) {
const origin = func;
return function () {
// arguments 是一个对应于传递给函数的参数的类数组对象。
console.log(arguments);
this.setRequestHeader("diyHeader", "diyHeader666666666");
return origin.apply(this, arguments);
};
}
XMLHttpRequest.prototype.send = hookXhr(XMLHttpRequest.prototype.send);
// XMLHttpRequest.prototype.open = hookXhr(XMLHttpRequest.prototype.open);

tips

编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其他方法所调用。

而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为模块模式(module pattern):

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
var makeCounter = function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

参考

高并发编程基础线程知识说明

引言

现在几乎所有操作系统都支持多任务执行,其中每个任务被视为一个进程。在每个进程内部,至少有一个线程在运行,线程也被称为轻量级进程。

线程可以看作是程序执行的一条路径,每个线程都有自己的局部变量表、程序计数器(指向当前正在执行的指令)以及各自的生命周期。现代操作系统通常支持同时运行多个线程。例如,在启动Java虚拟机(JVM)时,操作系统会创建一个新的进程(即JVM进程),并在该进程中生成多个派生或创建的线程。

线程生命周期

flowchart TD
    A[新建New] --> B[就绪Runnable]
    B --> C[运行Running]
    C --> D[阻塞Blocked]
    D --> E[就绪Runnable]
    E --> F[运行Running]
    F --> G[终止Terminated]
    D --> H[等待Waiting]
    H --> I[唤醒Notify]
    I --> E
    D --> J[超时等待Timed Waiting]
    J --> K[唤醒Notify]
    K --> E

在JDK17版本的JVM线程的生命周期共7个状态,可以在java.lang.Thread.State枚举类看到,具体如下:

  1. 新建状态(New):当一个Thread类或其子类的对象被创建时,该线程处于新建状态。此时它尚未启动,即没有开始执行run()方法。
  2. 就绪状态(Runnable):当线程对象调用了start()方法之后,该线程进入就绪状态。此时该线程已经有了执行资格,只等待CPU的调度,也就是分配时间片。
  3. 运行状态(Running):当就绪状态的线程获得了CPU时间片,开始执行run()方法时,该线程进入运行状态。
  4. 阻塞状态(Blocked):在某些情况下,线程可能会暂时失去对CPU的控制权,暂停执行。这时线程进入阻塞状态。例如,线程调用了sleep()方法、I/O操作、等待synchronized锁等都会使线程进入阻塞状态。
  5. 等待状态(Waiting):当线程执行wait()、join()、park()等方法之后,线程进入等待状态。此时线程不会占用CPU资源,也不会释放持有的锁,需要其他线程的唤醒才能继续执行。
  6. 超时等待状态(Timed Waiting):与等待状态类似,但是可以设置等待的时间。当线程调用了带有时间参数的sleep()、wait()、join()方法或者LockSupport.parkNanos()、LockSupport.parkUntil()方法时,线程进入超时等待状态。
  7. 终止状态(Terminated):线程执行完run()方法后,或者出现异常而结束时,线程进入终止状态。此时线程已经彻底结束,不会再回到其他状态。

这些线程状态在Java中非常重要,理解它们的含义和转换规则有助于我们编写高效、正确的多线程程序。

线程的生命周期是从新建状态开始,通过调用 start() 方法进入可运行状态,然后可能进入阻塞、等待或者被中断,最后进入终止状态。JVM 管理线程状态的转换,可以通过 Thread 类的状态相关方法来查询当前线程的状态。

Running状态的转换

  • 直接进入 TERMINATED 状态,比如用JDK已经不推荐使用的stop法或者判断某个逻辑标识。
  • 进入 BLOCKED 状态,比如调用了 sleep 或者 wait 方法而加入了 waitSet 中。
  • 进行某个阻塞的 IO 操作,比如因网络数据的读写而进入了 BLOCKED 状态获取某个锁资源,从而加人到该锁的阻塞队列中而进入了 BLOCKED 状态。
  • 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE 状态。
  • 线程主动调用 yield 方法,放弃 CPU 执行权,进入 RUNNABLE 状态。

Blocked状态的转换

  • 直接进人 TERMINATED 状态,比如调用JDK已经不推荐使用的 stop 方法或者意外死亡(JVM Crash)。
  • 线程阻塞的操作结束,比如读取了想要的数据字节进入到 RUNNABLE 状态。
  • 线程完成了指定时间的休眠,进入到了 RUNNABLE 状态。
  • Wait 中的线程被其他线程 notify/notifyall 唤醒,进入RUNNABLE状态。
  • 线程获取到了某个锁资源,进人 RUNNABLE 状态。
  • 线程在阻塞过程中被打断,比如其他线程调用了 interrupt 方法,进入RUNNABLE状态。

Terminated状态的形成

  • 线程运行正常结束,结束生命周期。
  • 线程运行出错意外结束。
  • JVM Crash,导致所有的线程都结束。

线程的创建

创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式。

  • 重写Thread的run方法。
  • 实现 Runnable 接口的run方法,并且将Runnable 实例用作构造Thread 的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread extends Thread {
public void run() {
// 定义线程执行的任务
System.out.println("This is a new thread.");
}

public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}

public class MyRunnable implements Runnable {
public void run() {
// 定义线程执行的任务
System.out.println("This is a new thread.");
}

public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable); // 将实现了 Runnable 接口的对象作为参数传递给 Thread 类的构造方法
thread.start(); // 启动线程
}
}

线程中的策略模式

无论是 Runnable 接口的 run() 方法,还是 Thread 类本身的 run() 方法,都遵循了将线程的控制逻辑与业务逻辑分离的原则,以实现职责分明、功能单一的设计思想。这种设计方式与 GoF(Gang of Four)设计模式中的策略模式有相似之处。

在策略模式中,将可变的算法封装成独立的策略类,并通过接口或抽象类与调用者进行解耦。调用者可以根据需要选择不同的策略来完成特定的任务。类似地,Java 中的线程创建方式也将线程的执行逻辑封装在一个单独的类(实现 Runnable 接口或继承 Thread 类)中,通过调用 start() 方法来启动线程。

使用这种设计模式,可以使线程控制逻辑与业务逻辑分离,提高代码的可维护性和可扩展性。例如,可以根据不同的业务需求,定义不同的 Runnable 实现类或 Thread 子类,并在启动线程时选择合适的线程对象,从而实现不同的业务逻辑。

总结来说,Java 中线程的创建方式与策略设计模式相似,都体现了将控制逻辑与具体业务逻辑分离的设计原则,以实现代码的灵活性和可扩展性。

线程中的Runnable复用

重写 Thread 类的 run() 方法和实现 Runnable 接口的 run() 方法有一个关键的不同点。Thread 类的 run() 方法是无法共享的,也就是说,一个线程的 run() 方法不能被另一个线程当作自己的执行单元。相比之下,使用 Runnable 接口可以实现线程执行单元的共享。通过传递同一个实现了 Runnable 接口的对象给多个 Thread 实例,可以使多个线程共享同一个执行单元,从而提高代码的复用性和可维护性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyRunnable implements Runnable {
public void run() {
// 定义线程执行的任务
System.out.println("This is a new thread.");
}

public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
Thread thread2 = new Thread(myRunnable);
thread2.start(); // 启动线程
}
}

例子

创建线程

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
package engineer.concurrent.battle.onebasic;

import java.util.concurrent.TimeUnit;

public class TryConcurrent {

public static void main(String[] args) {
new Thread(TryConcurrent::writeCode).start();
listenMusic();
}

private static void listenMusic() {
for(;;){
System.out.println("music is good");
sleep(1);
}
}


private static void writeCode() {
for(;;){
System.out.println("write code and work hard");
sleep(1);
}
}

private static void sleep(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

多线程排队模拟

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
/**
* 叫号机排队模拟,通过多线程并发
*/
public class TicketWindow extends Thread {
private final String name;
private final static int MAX = 100;
private static AtomicInteger index = new AtomicInteger(1);

public TicketWindow(String name) {
this.name = name;
}

public void run() {
while (index.get() <= MAX) {
System.out.println(name + "柜台正在排队,排队号码为:" + index);
index.getAndIncrement();
}
}

public static void main(String[] args) {
new TicketWindow("一号窗口").start();
new TicketWindow("二号窗口").start();
new TicketWindow("三号窗口").start();
new TicketWindow("四号窗口").start();
}
}

参考

  • 《Java高并发编程详解:多线程与架构设计》
  • Java Thread Doc

高并发编程基础-引言

5W1H

这里用“六何”的分析方法概括下即将开始的新主题高并发编程基础系列文章。

what何事:

高并发编程指同一时间进行大量任务的处理,同时保持过程稳定和结果一致。Java中使用多线程技术来支撑高并发的场景。“高并发编程基础”指的就是多线程这块技术的内容。

随着JDK版本的迭代,JDK已经出到23,本文主要基于JDK17版本的源码和api来说明多线程编程技术的使用。一个是因为这个版本稳定兼容性好,第二个是相较于8的版本更加新和优秀。

why何因:

Java为多线程提供了基本的工具来支持在多核处理器进行编程的工具类,通过对这款内容的学习可以加深对并发框架的使用原理的了解以及满足日常多线程开发过程的使用。

where何地:

“高并发编程基础”会发表在各大技术论坛(掘金、知乎、CSDN等)、公众号和博客(https://r0ad.github.io/)中。

when何时:

预计需要花费一个月时间完成整个基础教程的输出。

who何人:

适合希望使用或学习多线程编程或想要自我检查学习成果的人。不足之处可以随时交流指出

how何法:

通过源码分析加DEMO实战加图片文字说明的方式输出整个系文档。

大纲

整个“高并发编程基础”可能的大纲如下,随着后续迭代可能进行增删。

  • 线程基础知识说明
  • Thread构造函数使用说明
  • Thread常用API使用说明
  • 线程安全与数据同步
  • 并发中的基础概念Monitor
  • 线程间通信
  • AQS的原理和实现
  • Java中锁的概念和原理
  • 对象共享中的可见性问题
  • ThreadGroup的使用
  • Hook线程以及捕获线程执行异常
  • 线程池原理以及自定义线程池
  • 线程上下文通讯
  • 单例模式与多线程
  • Lock的使用
  • CAS 原子操作及相关类
  • Future 和 FutureTask
  • 线程池工作原理
  • ThreadLocal 底层原理
  • 等等

参考

由于水平限制,“高并发编程基础”参考了很多资料来编写。

  • JDK 17 官方文档,用于自查和权威核对 https://docs.oracle.com/en/java/javase/17/
  • 涉及JDK源码、基础原理介绍的书:《Java高并发编程详解:多线程与架构设计》(基于JDK8)
  • 涉及Java标准介绍和多线程基础说明的书: 《Java多线程编程核心技术》(基于JDK8)

关于作者

来自一线全栈程序员nine的八年探索与实践,持续迭代中。欢迎关注“雨林寻北”或添加个人卫星codetrend(备注技术)。

Gitlab使用或替换外部Nginx方法说明

Gitlab 版本没更新就会导致依赖的组件库版本没更新,如果Nginx有漏洞,则需要升级Gitlab,或者第二个选择就是使用外部的Nginx作为服务容器。

升级步骤

** 请勿直接在生产或者线上主机上执行。

具体操作步骤如下:

  1. 备份配置文件 cp /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.20230822.bak
  2. 禁用捆绑的 NGINX,在 /etc/gitlab/gitlab.rb 中设置:
1
nginx['enable'] = false
  1. 下载正确的网络服务器配置,访问地址: GitLab recipes repository

下面以http的Nginx为例说明,把配置文件放入/etc/nginx/conf.d(默认):

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
upstream gitlab-workhorse {
# On GitLab versions before 13.5, the location is
# `/var/opt/gitlab/gitlab-workhorse/socket`. Change the following line
# accordingly.
server unix:/var/opt/gitlab/gitlab-workhorse/sockets/socket;
}

## Normal HTTP host
server {
## Either remove "default_server" from the listen line below 如果遇到问题可以删除 default_server
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)n 0.0.0.0:80 default_server;
listen 0.0.0.0:8088 default_server; # 修改你需要监听的端口
listen [::]:8088 default_server;
server_name localhost; ## Replace this with something like gitlab.example.com # 修改配置的域名
server_tokens off; ## Don't show the nginx version number, a security best practice
root /opt/gitlab/embedded/service/gitlab-rails/public; # 默认位置就是这里

## See app/controllers/application_controller.rb for headers set

## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;

location / {
client_max_body_size 0;
gzip off;

## https://github.com/gitlabhq/gitlabhq/issues/694
## Some requests take more than 30 seconds.
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_redirect off;

proxy_http_version 1.1;

proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_pass http://gitlab-workhorse;
}
}
  1. 执行 sudo gitlab-ctl reconfigure 命令以使更改生效。

  2. 启动 nginx。

遇到的问题

  1. 权限不够,界面返回502。解决办法是修改nginx配置文件的启动用户为root或者授权给对应用户。
  2. 出现如下错误:
1
connect() to unix:/var/opt/gitlab/gitlab-workhorse/sockets/socket failed (13:Permission denied) while connecting to upstream

选择以下选项之一进行修复:

  • 更新到 14.3 或更高版本,其中包含更新的 SELinux 策略。
  • 手动获取和更新策略:
1
2
wget https://gitlab.com/gitlab-org/omnibus-gitlab/-/raw/a9d6b020f81d18d778fb502c21b2c8f2265cabb4/files/gitlab-selinux/rhel/7/gitlab-13.5.0-gitlab-shell.pp
semodule -i gitlab-13.5.0-gitlab-shell.pp

参考

Windows端微信多开的技巧

Windows端微信会根据用户ID创建对应的目录,用于数据的保护和隔离。也就是说微信本身和QQ一样是支持多个同时登陆的,但是在PC端通过常规的方式打开微信只会打开同一个微信。

下面通过简单的教程教会微信多开的方法。

通过脚本多开

  1. 安装Windows版本微信,下载地址: https://weixin.qq.com/
  2. 找到对应的安装位置。有几种方法。
    • 在桌面找到微信图标,右键“属性”,点击“快捷方式”,其中“目标”的值就是接下来需要的内容。这里的内容是 “C:\Program Files (x86)\Tencent\WeChat\WeChat.exe”
    • 在桌面找到微信图标,右键“属性”,点击“快捷方式”,点击“打开文件所在位置”,其中地址栏的路径和文件名“WeChat.exe”就是我们需要的内容。这里的路径也是 “C:\Program Files (x86)\Tencent\WeChat\WeChat.exe”
  3. 在桌面创建一个文本文件,重命名为“微信多开.bat”。
1
2
start C:\"Program Files (x86)\Tencent\WeChat\WeChat.exe"
start C:\"Program Files (x86)\Tencent\WeChat\WeChat.exe"

注意事项如下:

  1. 需要多开几个就复制几行。
  2. 路径的盘符也就是上面的“C”需要放在引号外面。

下次需要多开微信的时候直接运行“微信多开.bat”即可多开。

使用命令提示符多开

  1. 安装Windows版本微信,下载地址: https://weixin.qq.com/
  2. 找到对应的安装位置。在桌面找到微信图标,右键“属性”,点击“快捷方式”,点击“打开文件所在位置”,其中地址栏的路径和文件名“WeChat.exe”就是我们需要的内容。这里的路径也是 “C:\Program Files (x86)\Tencent\WeChat\WeChat.exe”
  3. 在文件夹路径里面输入“cmd”即可打开命令提示符。
  4. 输入命令:start WeChat.exe & WeChat.exe 并按回车。即可实现多开。

基于Linux系统Java服务启停的通用shell

引言

应用程序的启停最为显著的特征是端口的占用情况,例如Nginx、Tomcat。除此之外也可以通过进程的文件信息判断进程启停情况。在Linux系统常用的两个命令分别为 lsofps。在应用的启停中通过监听端口去判断是否存在进行启停是更合理的一种方式。实际使用过程中都会使用到。

lsof用法

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
NAME
lsof - list open files

SYNOPSIS
lsof [ -?abChKlnNOPRtUvVX ] [ -A A ] [ -c c ] [ +c c ] [ +|-d d ] [
+|-D D ] [ +|-e s ] [ +|-E ] [ +|-f [cfgGn] ] [ -F [f] ] [ -g [s] ] [
-i [i] ] [ -k k ] [ +|-L [l] ] [ +|-m m ] [ +|-M ] [ -o [o] ] [ -p s ]
[ +|-r [t[m<fmt>]] ] [ -s [p:s] ] [ -S [t] ] [ -T [t] ] [ -u s ] [ +|-w
] [ -x [fl] ] [ -z [z] ] [ -Z [Z] ] [ -- ] [names]

DESCRIPTION
Lsof revision 4.89 lists on its standard output file information about
files opened by processes for the following UNIX dialects:

Apple Darwin 9 and Mac OS X 10.[567]
FreeBSD 8.[234], 9.0, 10.0 and 11.0 for AMD64-based systems
Linux 2.1.72 and above for x86-based systems
Solaris 9, 10 and 11

(See the DISTRIBUTION section of this manual page for information on
how to obtain the latest lsof revision.)

An open file may be a regular file, a directory, a block special file,
a character special file, an executing text reference, a library, a
stream or a network file (Internet socket, NFS file or UNIX domain
socket.) A specific file or all the files in a file system may be
selected by path.

Instead of a formatted display, lsof will produce output that can be
parsed by other programs. See the -F, option description, and the OUT‐
PUT FOR OTHER PROGRAMS section for more information.

In addition to producing a single output list, lsof will run in repeat
mode. In repeat mode it will produce output, delay, then repeat the
output operation until stopped with an interrupt or quit signal. See
the +|-r [t[m<fmt>]] option description for more information.


ps用法

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
NAME
ps - report a snapshot of the current processes.

SYNOPSIS
ps [options]

DESCRIPTION
ps displays information about a selection of the active processes. If you want a repetitive update of the selection and the displayed information, use top(1) instead.

This version of ps accepts several kinds of options:

1 UNIX options, which may be grouped and must be preceded by a dash.
2 BSD options, which may be grouped and must not be used with a dash.
3 GNU long options, which are preceded by two dashes.

Options of different types may be freely mixed, but conflicts can appear. There are some synonymous options, which are functionally identical, due to the many standards and ps
implementations that this ps is compatible with.

Note that "ps -aux" is distinct from "ps aux". The POSIX and UNIX standards require that "ps -aux" print all processes owned by a user named "x", as well as printing all
processes that would be selected by the -a option. If the user named "x" does not exist, this ps may interpret the command as "ps aux" instead and print a warning. This
behavior is intended to aid in transitioning old scripts and habits. It is fragile, subject to change, and thus should not be relied upon.

By default, ps selects all processes with the same effective user ID (euid=EUID) as the current user and associated with the same terminal as the invoker. It displays the
process ID (pid=PID), the terminal associated with the process (tname=TTY), the cumulated CPU time in [DD-]hh:mm:ss format (time=TIME), and the executable name (ucmd=CMD).
Output is unsorted by default.

The use of BSD-style options will add process state (stat=STAT) to the default display and show the command args (args=COMMAND) instead of the executable name. You can override
this with the PS_FORMAT environment variable. The use of BSD-style options will also change the process selection to include processes on other terminals (TTYs) that are owned by
you; alternately, this may be described as setting the selection to be the set of all processes filtered to exclude processes owned by other users or not on a terminal. These
effects are not considered when options are described as being "identical" below, so -M will be considered identical to Z and so on.

Except as described below, process selection options are additive. The default selection is discarded, and then the selected processes are added to the set of processes to be
displayed. A process will thus be shown if it meets any of the given selection criteria.

通过监听端口停止应用

使用lsof加关键词LISTEN获取端口,脚本如下:

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

#!/bin/bash
#author: sunz
#file name: killProcessByPort.sh
## 参数通过运行时传入
port1=$1
MSG="shutdown port %s at pid %s %s \n"

killByPort(){
pids=$(lsof -i:$port1 | grep LISTEN | awk '{print $2}' |xargs)
pids_len=${#pids[*]}

if test $pids_len -ne 1
then
printf "port %s has been killed or not start yet. \n" $port1
fi

for pid in $pids; do
printf "shutdown port %s at pid %s %s \n" $port1 $pid 'start'
kill -9 $pid
CheckKillResult $? $pid
done
}

CheckKillResult(){
result=$1
pid=$2
if test $result -eq 0
then
printf "shutdown port %s at pid %s %s \n" $port1 $pid "successs"
else
printf "shutdown port %s at pid %s %s \n" $port1 $pid "failed"
fi
}

killByPort


# eg $killProcessByPort 9430

通过文件名停止应用

使用ps+awk加应用名关键词获取pid,脚本如下:

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

#!/bin/bash
#author: sunz
#file name: killProcessByName.sh
## 参数通过运行时传入
name=$1
MSG="shutdown app %s at pid %s %s \n"

killByName(){
pids=$(ps -ef | grep $name |grep java | awk '{print $2}' |xargs)
pids_len=${#pids[*]}

if test $pids_len -ne 1
then
printf "app %s has been killed or not start yet. \n" $name
fi

for pid in $pids; do
printf "shutdown app %s at pid %s %s \n" $name $pid 'start'
kill -9 $pid
CheckKillResult $? $pid
done
}

CheckKillResult(){
result=$1
pid=$2
if test $result -eq 0
then
printf "shutdown app %s at pid %s %s \n" $name $pid "successs"
else
printf "shutdown app %s at pid %s %s \n" $name $pid "failed"
fi
}

killByName


# eg $killProcessByName spring-boot.jar

通用启动Java程序脚本

通过函数式编写启动Java程序脚本有如下优点:

  • 简化启动应用的脚本维护;
  • 统一维护一类程序的JVM参数;

脚本信息如下:

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
#!/bin/bash
#author: sunz
#file name: startJavaProcess.sh
## 参数通过运行时传入

APP=$1
startJavaProcess(){
echo "start $APP "
## JVM参数基于Java8
JVM=" -Xmx1344M -Xms1344M -Xmn448M -XX:MaxMetaspaceSize=256M -XX:MetaspaceSize=256M -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+CMSClassUnloadingEnabled -XX:+ParallelRefProcEnabled -XX:+CMSScavengeBeforeRemark "
## 特殊版本java的路径可以通过全路径制定
nohup java $JVM -jar $APP >/dev/null 2>&1 &
CheckStartResult $? $APP
}

CheckStartResult(){
result=$1
pid=$2
if test $result -eq 0
then
printf "startup %s %s \n" $2 "successs"
else
printf "startup %s %s \n" $2 "failed"
fi
}

startJavaProcess

# eg $startJavaProcess zuul-boot-2.0.0-SNAPSHOT.jar

通过 ~/.bashrc 简化程序脚本

.bashrc 在用户登录时获取该文件的 aslias 等信息。维护人员可以通过别名简化程序脚本,增加维护效率。

.bash_profile 文件可以维护环境变量,也可以简化该脚本。

.bash_profile 修改过程如下:

1
2
3
4
5
6
7
8
9
10
# 编辑 bash_profile
# vi .bash_profile
SHELL_HOME="/home/sunz/文档/files/shell/cshell"
killProcessByPort='sh $SHELL_HOME/killProcessByPort.sh '
export killProcessByPort
startJavaProcess='sh $SHELL_HOME/startJavaProcess.sh '
export startJavaProcess

# 更新 bash_profile
# source .bash_profile

.bashrc 修改过程如下:

1
2
3
4
5
6
# 编辑 bash_profile
# vi .bash_profile
# 使用 alias 方法
SHELL_HOME="/home/sunz/文档/files/shell/cshell"
alias startJavaProcess='sh $SHELL_HOME/startJavaProcess.sh '
alias killProcessByPort='sh $SHELL_HOME/killProcessByPort.sh '

注意不同系统的文件名可能存在差异。

以user-gatewayin-svc为例子说明使用

服务部署路径信息 /app/user_projects/user-gatewayin-svc ,目录结构如下:

1
2
3
4
drwxr-xr-x 3 app sunz     4096 2月   1 13:40 config
-rw-r--r-- 1 app sunz 86 2月 1 13:47 start.sh
-rw-r--r-- 1 app sunz 19 12月 9 15:57 stop.sh
-rw-r--r-- 1 app sunz 57520835 1月 5 16:10 zuul-boot-2.0.0-SNAPSHOT.jar

其中 start.sh 内容如下;

1
$startJavaProcess "zuul-boot-2.0.0-SNAPSHOT.jar --spring.profiles.active=dev,dev-in"

其中 stop.sh 内容如下;

1
$killProcess 9450

Visual Studio Code 中开发前端常用插件

Visual Studio Code 是一款开源全平台的代码开发工具,支持三大平台Windows、Linux、Mac。除了Mac平台没有使用过外,Linux的开发体验和Windows基本一致。

虽然Visual Studio Code 支持多种语言的开发,但是在后端有IntelliJ IDEA这样的开源版本提供使用,所以在Java Web开发中一般使用IDEA开发后端,Vs Code开发前端。

Visual Studio Code 作为一款轻量级的IDE,本身不是很强大,但是在开发插件的支持下,前端代码开发也能如鱼得水。

以下基于笔者开发Web过程中常用插件的推荐。

Prettier - Code formatter

代码格式对于开发过程来说是很重要的一件事,统一的代码格式能够代码更好的代码阅读体验。

Prettier 支持多种语言的代码格式化,包括 Js、Vue、Html、Css等等。

通过搜索 Prettier - Code formatter 安装。

Vue Language Features (Volar)

Vue 官方出品支持Vue框架源码开发的插件,使得开发Vue源码更加方便。

包括 Vue文件的高亮显示、ESLint的语法集成支持、代码格式化等等。开发Vue必备。

通过搜索 Volar 安装。

其它开发vue的辅助推荐还包括 Vue Peek 文件跳转、 vue-helper 对 Element-UI, VUX, IVIEW 的增加开发体验。

JavaScript (ES6) code snippets

提供 ES6 语法的代码提示,通过缩写就能写出常用的代码。

例如输入 clo 就能打印对象 console.log('object :>> ', object);

支持的缩写包括 Import and export 、 Various methods 、Console methods 等等。

通过搜索 JavaScript (ES6) code snippets 安装。

Markdown Preview Enhanced

程序员写文档必备的markdown语法,这款插件提供markdown的文档、图标语法、函数语法的支持。还可以导出和预览markdown文档。

通过搜索 Markdown Preview Enhanced 安装。

markdownlint

markdown的语法检查支持,还提供了一部分程序修正功能。很实用、很方便。对于不熟悉markdown语法的人很是有用。

通过搜索 markdownlint 安装。

GitLens — Git supercharged

VsCode的git使用体验不是很好,通过该插件能增强git的使用过程。

通过搜索 GitLens 安装。

参考

RabbitMQ 的安装和使用

引言

RabbitMQ 作为一个开源的消息中间件广泛使用。

  • 支持分布式部署。
  • 异步消息传递,支持多种消息协议、消息队列、送达确认、灵活的队列路由、多种交换类型。
  • 提供多种监听和管理工具,HTTP-API, 命令行工具command line tool, UI界面。

安装

容器安装

目前最新版本安装和启动命令如下:

1
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.11-management

Linux或Windows安装

通过官网提供的安装包安装即可。具体安装方法可查看 https://www.rabbitmq.com/install-windows.html

  • 先安装Erlang。
  • 安装对应的 RabbitMQ 安装包。

RabbitMQ访问

通过UI界面访问对应的系统。

  • 登录地址 127.0.0.1:15762
  • 账号密码默认 guest\guest 。

Springboot集成RabbitMQ

  1. 修改依赖加入RabbitMQ启动项目,此处以maven为例子说明。
1
2
3
4
5
6
7

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

  1. 增加 rabbitmq 配置。
1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: boot-rabbitmq
# rabbitmq 配置
rabbitmq:
# Redis 服务器地址
host: 127.0.0.1
# 连接端口号
port: 5672
username: guest
password: guest
  1. 增加 rabbit相关配置。
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

/**
* 自定义注入配置bean相关组件
*/
@Configuration
@Slf4j
public class RabbitMQConfig {

// 自动装配RabbitMQ的链接工厂实例
@Autowired
private CachingConnectionFactory connectionFactory;
// 自动装配消息监听器所在的容器工厂配置类实例
@Autowired
private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;

/**
* 单一消费者
* @return
*/
@Bean(name = "singleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(1);
factory.setPrefetchCount(1);
return factory;
}

/**
* 多个消费者
* @return
*/
@Bean(name = "multiListenerContainer")
public SimpleRabbitListenerContainerFactory multiListenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factoryConfigurer.configure(factory,connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.NONE);
factory.setConcurrentConsumers(10);
factory.setMaxConcurrentConsumers(15);
factory.setPrefetchCount(10);
return factory;
}

/**
* RabbitMQ发送消息的操作组件实例
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(){
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause));
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message));
return rabbitTemplate;
}
}

Springboot使用RabbitMQ

  1. 启动的配置类注入对应的队列,包括队列名称、交换机、路由。
  2. 使用 RabbitTemplate 作为生产者发送消息。
  3. @RabbitListener 作为消费者监听对应队列消费消息。
1
2
3
4
5
## 环境变量配置
mq.env=local
mq.basic.info.queue.name=${mq.env}.middleware.mq.basic.info.queue
mq.basic.info.exchange.name=${mq.env}.middleware.mq.basic.info.exchange
mq.basic.info.routing.key.name=${mq.env}.middleware.mq.basic.info.routing.key
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
/**
* springboot 启动时去5672 端口监听配置的所有监听队列,
* 若这个队列不存在,监听不到则会报错,需要在程序启动时注入这个队列
*/

//定义读取配置文件的环境变量实例
@Autowired
private Environment env;

/**创建简单消息模型:队列、交换机和路由 **/

//创建队列
@Bean(name = "basicQueue")
public Queue basicQueue(){
return new Queue(env.getProperty("mq.basic.info.queue.name"),true);
}

//创建交换机:在这里以DirectExchange为例,在后面章节中我们将继续详细介绍这种消息模型
@Bean
public DirectExchange basicExchange(){
return new DirectExchange(env.getProperty("mq.basic.info.exchange.name"),true,false);
}
//创建绑定
@Bean
public Binding basicBinding(){
return BindingBuilder.bind(basicQueue()).to(basicExchange()).with(env.getProperty("mq.basic.info.routing.key.name"));
}

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
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private Environment env;


/**
* 发送消息
* @param message
*/
public void sendMsg(String message){
if (!ObjectUtils.isEmpty(message)){
try {
rabbitTemplate.setExchange(env.getProperty("mq.basic.info.exchange.name"));
rabbitTemplate.setRoutingKey(env.getProperty("mq.basic.info.routing.key.name"));

Message msg=MessageBuilder.withBody(message.getBytes("utf-8"))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();

rabbitTemplate.convertAndSend(msg);

log.info("基本消息模型-生产者-发送消息:{} ",message);
}catch (Exception e){
log.error("基本消息模型-生产者-发送消息发生异常:{} ",message,e.fillInStackTrace());
}
}
}
1
2
3
4
5
6
7
8
9
@RabbitListener(queues = "${mq.basic.info.queue.name}",containerFactory = "singleListenerContainer")
public void consumeMsg(@Payload byte[] msg){
try {
String message=new String(msg,"utf-8");
log.info("基本消息模型-消费者-监听消费到消息:{} ",message);
}catch (Exception e){
log.error("基本消息模型-消费者-发生异常:",e.fillInStackTrace());
}
}

RabbitMQ常见消息模式

Simple 模式

Simple 模式是最简单的一个模式,由一个生产者,一个队列,一个消费者组成,生产者将消息通过交换机(此时,图中并没有交换机的概念,如不定义交换机,会使用默认的交换机)把消息存储到队列,消费者从队列中取出消息进行处理。

Fanout 模式

Fanout——发布订阅模式,是一种广播机制。

此模式包括:一个生产者、一个交换机 (exchange)、多个队列、多个消费者。生产者将消息发送到交换机,交换机不存储消息,将消息存储到队列,消费者从队列中取消息。如果生产者将消息发送到没有绑定队列的交换机上,消息将丢失。

Direct 模式

Direct 模式是在 Fanout 模式基础上添加了 routing key,Fanout(发布/订阅)模式是交换机将消息存储到所有绑定的队列中,而 Direct 模式是在此基础上,添加了过滤条件,交换机只会将消息存储到满足 routing key 的队列中。

Topic 模式

Topic 模式是生产者通过交换机将消息存储到队列后,交换机根据绑定队列的 routing key 的值进行通配符匹配,如果匹配通过,消息将被存储到该队列,如果 routing key 的值匹配到了多个队列,消息将会被发送到多个队列;如果一个队列也没匹配上,该消息将丢失。

RabbitMQ常见使用场景

解耦、削峰、异步

解耦

在微服务架构体系中,微服务A需要与微服务B进行通信,传统的做法是A调用B的接口。但这样做如果系统B无法访问或连接超时,系统A需要等待,直到系统B做出响应,并且A与B存在严重的耦合现象。如果引入消息队列进行系统AB的通信,流程是这样的:

  • 系统A将消息存储到消息队列中,返回成功信息
  • 系统B从队列中获取消息,进行处理操作
  • 系统A将消息放到队列中,就不用关心系统B是否可以获取等其他事情了,实现了两个系统间的解耦。

使用场景:

短信、邮件通知

削峰

系统A每秒请求100个,系统可以稳定运行,但如果在秒杀活动中,每秒并发达到1w个,但系统最大处理能力只能每秒处理 1000 个,所以,在秒杀活动中,系统服务器会出现宕机的现象。如果引入 MQ ,可以解决这个问题。每秒 1w个请求会导致系统崩溃,那我们让用户发送的请求都存储到队列中,由于系统最大处理能力是每秒1000个请求,让系统A每秒只从队列中拉取1000个请求,保证系统能稳定运行,在秒杀期间,请求大量进入到队列,积压到MQ中,而系统每秒只从队列中取1000个请求处理。这种短暂的高峰期积压是没问题的,因为高峰期一旦过去,每秒请求数迅速递减,而系统每秒还是从队列中取1000个请求进行处理,系统会快速将积压的消息消费掉。

使用场景:

秒杀活动
团抢活动

异步

用户注册,需要发送注册邮件和注册短信,传统的做法有两种:串行、并行。

  • 串行方式:将注册信息写库后(50ms),发送邮件(50ms),再发送短信(50ms),任务完成后,返回客户端,共耗时(150ms)
  • 并行方式:将注册信息写库后(50ms),开启子线程让发送邮件和发送短信同时进行(50ms),返回客户端,共耗时(100ms)
  • 引入MQ,将注册信息写库(50ms),将发送邮件和短信的操作写入队列(5ms),返回客户端,而消费者什么时候从队列中取消息进行处理,不用关心,共耗时(55ms)

使用场景:

将不是必须等待响应结果的业务逻辑进行异步处理

参考