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

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


  • 首页

  • 归档

  • 搜索

Go Web学习(1)——标准库http实现server

发表于 2018-01-22

最近放假在家好好学习了一下Go语言,Go作为Google官推的Server语言,因为天生的并发性和完备的标准库让Go语言在服务端如鱼得水。笔者在简单的学习了之后,真的是惊讶连连,好了进入正题。

首先,我们必须实现一个Go Web版的Hello World。

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

import (
"fmt"
"net/http"
"log"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s\n", "Hello World")
})
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatal(err)
}
}

我们可以看到Go语言实现一个Web HelloWorld的简洁程度甚至直接媲美Node.js,不需要任何容器便可以实现一个高并发的简单服务器。下面我们来分析一下这个代码:

首先,我们导入了fmt,http包,log包其实对于HelloWorld来说并没有导入的必要,但是日志输出这个良好习惯还是得遵从。在main()函数的第一行,我们通过http.HandleFunc定义了路由为”/“的响应函数,这个响应函数,接受传来的Request,并对Response做一定的处理即写入HelloWorld然后直接返回给浏览器。然后便可以直接调用http.ListenAndServe来监听本地的8000端口,便可以直接在浏览器上看到HelloWorld。

好,上面的流程其实很简单,有一定Web编程的人便都能明白,接下来我们便从Go的源码中看一看,这段代码究竟是如何实现的。

1
2
3
4
5
6
复制代码// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}

上面这段便是Go源码中对HandleFunc函数的实现,我们可以看到这个函数直接将所有参数全部传递给了DefaultServeMux.HandleFunc来调用。

1
2
3
4
复制代码// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

DefaultServeMux是http包中的全局变量,它的原型是ServeMux这个结构体,我们再往上翻看这个结构体的HandleFunc方法。

1
2
3
4
复制代码// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}

我们可以看到,似乎没完没了,HandleFunc也是直接调用这个结构体的另一个方法Handle,另外HandlerFunc(handler)中的HandlerFunc也只是一个type的定义。

1
复制代码type HandlerFunc func(ResponseWriter, *Request)

这个函数本身并没有实现什么,需要我们自己去实现它的内容。也就是我们上面所提到的响应函数。

1
2
3
复制代码// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler)

终于我们找到了源头,当然这个方法的源代码还比较长,这里就不贴出全部,Handle这个方法接受两个参数,pattern这个string类型的参数表示路由,第二个参数handle它其实是Handler接口。

1
2
3
复制代码type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

可以看到Handler这个接口中只定义了ServeHTTP这一个方法,换句话说,我们也可以直接实现ServeHTTP这个方法来实现Handler这个接口,然后我们便可以传给ServeMux来自定义我们的HelloWorld.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码package main

import (
"fmt"
"net/http"
"log"
)

type CustomHandler struct{}

func (*CustomHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s\n", "Hello World")
}

func main() {
mux := http.NewServeMux()
mux.Handle("/", &CustomHandler{})
err := http.ListenAndServe(":8000", mux)
if err != nil {
log.Fatal(err)
}
}

上面的代码可以看到,我们定义了一个CustomHandler,然后实现了ServeHTTP这个方法从而实现了Handler这个接口,在main方法中,我们通过NewServeMux创建了一个自己的mux而不去使用http内的默认ServerMux。然后调用ListenAndServe方法,并将自己的mux传入,程序便会实现自定义的HelloWorld了。
接下来我们来看一下ListenAndServe这个方法:

1
2
3
4
5
复制代码// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

源码中可以看到该方法会将传入进来的addr参数和handler送给Server这个结构体,从而新建一个server然后调用这个server的ListenAndServe方法,对于Server这个结构它已经是Go语言对于这个方面非常底层的实现了,它非常强大,而且实现了很多的方法,这里不过多阐述,主要是实力不够(笑)。
好,回到正题,既然如此,我们便可以自己创建Server这个实例,来自定义我们的HelloWorld的第二版本。

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
复制代码package main

import (
"fmt"
"net/http"
"log"
"time"
)

type CustomHandler struct{}

var mux = make(map[string]func(http.ResponseWriter, *http.Request))

func Hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s\n", "Hello World")
}

func (*CustomHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler, ok := mux[r.URL.String()]; ok {
handler(w, r)
}
}

func main() {
server := http.Server{
Addr:":8000",
Handler:&CustomHandler{},
ReadHeaderTimeout:5 * time.Second,
}
mux["/"] = Hello
err := server.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}

上面这段代码便是自创server的实现了,这里挑选几条新的代码说明一下,我们定义了一个mux的全局变量,它来装配我们的路由与相应函数的映射,相当于上面的mux.Handle(“/“, …..),这里比较简陋的直接用Map来实现,接下来我们定义了Hello这个响应函数,我们也重写了ServeHTTP这个方法,它会判断request的url路径与我们mux里面的路径是否匹配,如果匹配在从mux中取出相应的响应函数并将w http.ResponseWriter, r *http.Request这两个参数传递给这个相应函数。

在main函数里,我们创建了自己的server,通过端口号,Handler及timeout时间来定义它,然后调用它的ListenAndServe方法,便可以实现与前面两个相同的HelloWorld功能。好了,今天写到这里,太晚了(笑)。

本文转载自: 掘金

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

10分钟入门Shell脚本编程

发表于 2018-01-22

前言

写下这篇文章,是对自己在学习和使用过程中的总结,文笔不是很好,如果有什么问题欢迎沟通交流

Github地址:https://github.com/hi-dhl/fast_guides

Shell是什么

Shell是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务, Shell脚本(shell script),是一种为Shell编写的脚本程序。我们经常说的shell通常都是指shell脚本。

环境和工具

Shell跟java、php、Python编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。

Mac OS,Linux 自带了shell解释器,Windows比较麻烦,因为Win7专业版和旗舰版默认安装PowerShell,标准版和家庭版中就没有安装的,为了方便建议安装cygwin

PHP、Python 也可以作为Shell编程

PHP、Python是属于高级编程语言,但是也可以做Shell编程,因为只要有解释器,也可以用作脚本编程

如下是一个Python Shell Script示例(假设文件名叫op_python_base.py):

1
2
3
4
5
复制代码#!/usr/bin/env python3 //告诉Python从系统环境中找python
# -*- coding: utf-8 -*- //设置为UTF-8编码

for index in range(10):
print(index);

源码:op_python_base

如下是一个PHP Shell Script示例(假设文件名叫op_php_base.php):

1
2
3
4
5
6
7
8
复制代码#!/usr/bin/php
<?php

for($i=0 ;$i<10; $i++){
echo $i;
}

?>

源码:op_php_base

为什么要学习Shell

既然PHP、Python都可以用来写脚本编程,那为什么还要学习陌生、晦涩难懂的Shell,主要有一下几个原因

  • 环境兼容性,Win7专业版和旗舰版默认安装PowerShell,标准版和家庭版中就没有安装的,其他主流的操作系统都预制了Shell解释器,所以使用sh、bash编写,提供给其他人使用是非常方便的,但是PHP、Python 等等需要安装相应的环境
  • 如果你想做一些定时任务比如说检测进程是否存在,自动备份,或者说自动部署环境、服务器之间的数据同步等等sh、bash会是你最好的选择

sh与bash

sh: Bourne shell,POSIX(Portable Operating System Interface)标准的shell解释器,它的二进制文件路径通常是/bin/sh

bash: Bash是Bourne shell的替代品,属GNU Project,二进制文件路径通常是/bin/bash

第一个shell脚本

我们先来看一个例子

我相信写过代码的童鞋,应该对下面的代码很熟悉并不陌生,(假设文件名叫op_base.sh):

1
2
3
4
5
6
7
复制代码#!/usr/bin/env bash
mkdir code
cd code
for ((i=0; i<3; i++)); do
touch test_${i}.txt
echo "shell很简单" >> test_${i}.txt
done

第一行:从系统path中寻找指定脚本的解释程序
第二行:创建 名叫code文件夹
第三行:进入创建的文件夹
第四行:for循环3次
第四行:创建文件
第五行:往创建的文件中写入信息
第六行:结束循环

mkdir, touch,cd,touch,echo都是系统命令,在命令行下可以直接执行
for, do, done 是shell脚本语言 for循环的语法

源码:op_base.sh

编写Shell

新建一个文件,扩展名为sh(sh代表shell),扩展名并不影响脚本执行,见名知意就好,如果你用php,扩展名为php,如果你用Python,扩展名为python

第一行一般是这样:

1
2
3
复制代码#!/usr/bin/php
#!/usr/bin/env python3
#!/usr/bin/env bash

#!”是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行
/env 是系统的PATH目录中查找

运行 Shell 脚本有两种方法:

作为可执行程序

1
2
复制代码chmod +x op_base.sh
./op_base.sh

第一行设置 op_base.sh可执行权限
第二行执行op_base.sh

作为参数

1
复制代码/bin/sh op_base.sh

变量

定义变量时,变量名前不需要加符号和Python一样但是在PHP语言中变量需要加$,如:

1
2
复制代码my_name="jack"
my_name='jack';

ps: 变量名和等号之间不能有空格,变量后面不能有;

Shell中的引号和PHP类似,字符串可以用单引号,也可以用双引号

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的
  • 单引号字串中不能出现单引号(对单引号使用转义符后也不行

双引号:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符

但是在Python中单引号和双引号是没有区别,但是Python 还有三个引号,在三个引号内字符都不会被转义

使用变量

对于已经定义过的变量,使用的适合在前面添加$

1
2
复制代码echo $my_name
echo ${my_name}

变量名外面的花括号是可选的,加不加都行,建议使用第二种形式

注释

以“#”开头的行就是注释,会被解释器忽略。

多行注释

sh里没有多行注释,只能每一行加一个#号。就像这样:

1
2
3
4
5
6
7
8
复制代码#--------------------------------------------
# Author: jack
#
# Notes: 10分钟入门Shell脚本编程
#
# Project home page:
# https://github.com/hi-dhl/fast_guides
#--------------------------------------------

字符串

字符串可以用单引号,也可以用双引号,也可以不用引号。单双引号的区别跟PHP类似

Shell不像其他语言有php、python 有很多数据类型,在Shell中常用的数据类型字符串数字和字符串(ps: 除了数字和字符串,也没啥其它类型好用了,哈哈)

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的
  • 单引号字串中不能出现单引号(对单引号使用转义符后也不行

双引号:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符

字符串操作

拼接字符串

1
2
3
4
复制代码my_name="jack";
my_age="20岁"
echo $my_name $my_age
echo $my_name$my_age

获取字符串长度

1
复制代码echo ${my_name}

截取字符串

1
复制代码echo ${my_name:0:2}

源码:op_str.sh

Shell 数组

定义数组

在Shell中,用括号来表示数组,数组元素用”空格”符号分割开。定义数组的一般形式为:

1
复制代码name=(name1 name2 name3)

还可以单独定义数组的各个分量:

1
2
3
复制代码ary[0]=name1
ary[1]=name2
ary[3]=name3

ps: 可以不使用连续的下标,而且下标的范围没有限制

读取数组

读取数组元素值的一般格式是:

1
复制代码${数组名[下标]}

例如:

1
复制代码echo ${name[0]}

使用@符号可以获取数组中的所有元素,例如:

1
复制代码echo ${name[@]}

获取数组的长度

获取数组长度的方法与获取字符串长度的方法相同,例如:

1
2
3
4
5
6
7
8
9
10
11
复制代码# 取得数组元素的个数
length=${name[@]}
echo $length

# 或者
length=${name[*]}
echo $length

# 取得数组单个元素的长度
lengthn=${name[n]}
echo $length

源码:op_arry.sh

Shell 流程控制

和Java、PHP、Python等语言不一样,sh的流程控制不可为空,如(以下为PHP流程控制写法):

1
2
3
4
5
6
7
复制代码<?php
if (isset($_GET["q"])) {
search(q);
}
else {
// 不做任何事情
}

在sh/bash里可不能这么写,如果else分支没有语句执行,就不要写这个else

if

1
2
3
4
5
6
7
8
9
复制代码if condition1
then
command1
elif condition2
then
command2
else
commandN
fi

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码#!/usr/bin/env bash

a=1
b=2
if [ $a == $b ]
then
echo "a 等于 b"
elif [ $a -gt $b ]
then
echo "a 大于 b"
elif [ $a -lt $b ]
then
echo "a 小于 b"
else
echo "没有符合的条件"

fi

源码:op_if.sh

for 循环

Shell的for循环和Python 有点类似

Python的for循环
1
2
复制代码for index in 1,2,3,4,5:
print(index);
Shell的for循环,第一种写法
1
2
3
复制代码for index in 1 2 3 4 5; do
echo "index="$index
done
Shell的for循环,第二种写法
1
2
3
复制代码for ((i=0; i<5; i++)); do
echo "i="$i
done

源码:op_for.sh

while 语句

while循环用于不断执行一系列命令,也用于从输入文件中读取数据;命令通常为测试条件。

1
2
3
4
5
6
复制代码int=1
while(( $int<=5 ))
do
echo $int
let "int++"
done

源码:op_while.sh

Shell结合系统命令

sh脚本结合系统命令便有了强大的威力,在字符处理领域,有grep、awk、sed三剑客,grep负责找出特定的行,awk能将行拆分成多个字段,sed则可以实现更新插入删除等写操作。

例如定时检测nginx、mysql是否被关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码path=/var/log
log=${path}/httpd-mysql.log

name=(apache mysql)

exs_init[0]="service httpd start"
exs_init[1]="/etc/init.d/mysqld restart"

for ((i=0; i<2; i++)); do
echo "检查${name[i]}进程是否存在"
ps -ef|grep ${name[i]} |grep -v grep
if [ $? -eq 0 ]; then
pid=$(pgrep -f ${name[i]})
echo "`date +"%Y-%m-%d %H:%M:%S"` ${name[$i]} is running with pid $pid" >> ${log}
else
$(${exs_init[i]})
echo "`date +"%Y-%m-%d %H:%M:%S"` ${name[$i]} start success" >> ${log}
fi
done

解释:检测 nginx、mysql进程是否存在,如果不存在了会自动重新启动。
脚本每次运行会写日志的,没事可以去看看该日志文件,如果进程是不是真的经常性不存在,恐怕就要排查一下深层原因了。

源码:check_nginx.sh

编辑 /etc/crontab 文件

1
复制代码crontab -e

在文件最后添加一行:

1
复制代码*/5 * * * * /xxx/check_nginx.sh > /dev/null 2>&1

上表示每 5 分钟,执行一下脚本 /xxx/check_nginx.sh,其中xxx代表路径

/dev/null 2>&1 的意思是该条shell命令将不会输出任何信息到控制台,也不会有任何信息输出到文件中。

1
2
3
4
5
6
7
8
9
10
复制代码# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * command to be executed

添加完配置,需要重启才能生效

1
复制代码service crond restart

本文转载自: 掘金

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

聊聊Dubbo(二):简单入门 0 准备 1 服务端 2

发表于 2018-01-22

0 准备

  1. 安装注册中心:Zookeeper、Dubbox自带的dubbo-registry-simple;
  2. 安装DubboKeeper监控:https://github.com/dubboclub/dubbokeeper;

以上两点准备,不是本文重点,不做详细介绍,安装比较简单,自行查阅相关资料安装学习。

1 服务端

1.2 接口定义

  1. 创建Maven模块:msa-demo-api

msa-demo-api
2. msa-demo-api:配置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
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
复制代码<!-- Dubbox依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.8.4</version>
</dependency>
<!-- END -->

<!-- 如果要使用lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- END -->

<!-- 如果要使用REST风格远程调用 -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
</dependency>
<!-- END -->

<!-- 如果要使用json序列化 -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson-provider</artifactId>
<version>3.0.7.Final</version>
</dependency>
<!-- END -->

<!-- 如果要使用xml序列化 -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<version>3.0.7.Final</version>
</dependency>
<!-- END -->

<!-- 如果要使用netty server -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-netty</artifactId>
<version>3.0.7.Final</version>
</dependency>
<!-- END -->

<!-- 如果要使用Sun HTTP server -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jdk-http</artifactId>
<version>3.0.7.Final</version>
</dependency>
<!-- END -->

<!-- 如果要使用tomcat server -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-logging-juli</artifactId>
<version>8.0.11</version>
</dependency>
<!-- END -->

<!-- 如果要使用Kyro序列化 -->
<dependency>
<groupId>com.esotericsoftware.kryo</groupId>
<artifactId>kryo</artifactId>
<version>2.24.0</version>
</dependency>
<dependency>
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
<version>0.26</version>
</dependency>
<!-- END -->

<!-- 如果要使用FST序列化 -->
<dependency>
<groupId>de.ruedigermoeller</groupId>
<artifactId>fst</artifactId>
<version>1.55</version>
</dependency>
<!-- END -->

<!-- 如果要使用Jackson序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.3.3</version>
</dependency>
<!-- END -->

以上POM配置,从dubbox-2.8.4开始,所有依赖库的使用方式将和dubbo原来的一样:即如果要使用REST、Kyro、FST、Jackson等功能,需要用户自行手工添加相关的依赖。
3. 定义接口:UserService.java

1
2
3
4
5
6
7
8
9
复制代码/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
public interface UserService {
User getUser(Long id);
Long registerUser(User user);
}
  1. 定义REST接口:AnotherUserRestService.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
复制代码package com.alibaba.dubbo.demo.user.facade;
import com.alibaba.dubbo.demo.user.User;
import com.alibaba.dubbo.rpc.protocol.rest.support.ContentType;
import javax.validation.constraints.Min;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
// 在Dubbo中开发REST服务主要都是通过JAX-RS的annotation来完成配置的,
// 在上面的示例中,我们都是将annotation放在服务的实现类中。但其实,我
// 们完全也可以将annotation放到服务的接口上,这两种方式是完全等价的.
//
// 在一般应用中,我们建议将annotation放到服务实现类,这样annotation和
// java实现代码位置更接近,更便于开发和维护。另外更重要的是,我们一般倾向
// 于避免对接口的污染,保持接口的纯净性和广泛适用性。
// 但是,如后文所述,如果我们要用dubbo直接开发的消费端来访问此服务,则annotation必须放到接口上。
// 如果接口和实现类都同时添加了annotation,则实现类的annotation配置会生效,接口上的annotation被直接忽略。
@Path("u")
@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
@Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})
public interface AnotherUserRestService {

@GET
@Path("{id : \\d+}")
// 在一个REST服务同时对多种数据格式支持的情况下,根据JAX-RS标准,
// 一般是通过HTTP中的MIME header(content-type和accept)来指定当前想用的是哪种格式的数据。
// @Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})
// 但是在dubbo中,我们还自动支持目前业界普遍使用的方式,即用一个URL后缀(.json和.xml)来指定
// 想用的数据格式。例如,在添加上述annotation后,直接访问http://localhost:8888/users/1001.json
// 则表示用json格式,直接访问http://localhost:8888/users/1002.xml则表示用xml格式,
// 比用HTTP Header更简单直观。Twitter、微博等的REST API都是采用这种方式。
// 如果你既不加HTTP header,也不加后缀,则dubbo的REST会优先启用在以上annotation定义中排位最靠前的那种数据格式。
// 注意:这里要支持XML格式数据,在annotation中既可以用MediaType.TEXT_XML,也可以用MediaType.APPLICATION_XML,
// 但是TEXT_XML是更常用的,并且如果要利用上述的URL后缀方式来指定数据格式,只能配置为TEXT_XML才能生效。
User getUser(@PathParam("id") @Min(1L) Long id);

@POST
@Path("register")
RegistrationResult registerUser(User user);
}
  1. 定义实体:User.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
复制代码package com.alibaba.dubbo.demo.user;
import lombok.Data;
import org.codehaus.jackson.annotate.JsonProperty;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
// 由于JAX-RS的实现一般都用标准的JAXB(Java API for XML Binding)来序列化和反序列化XML格式数据,
// 所以我们需要为每一个要用XML传输的对象添加一个类级别的JAXB annotation(@XmlRootElement) ,否则序列化将报错。
@Data
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class User implements Serializable {

@NotNull
@Min(1L)
private Long id;

// REST的底层实现会在service的对象和JSON/XML数据格式之间自动做序列化/反序列化。
// 但有些场景下,如果觉得这种自动转换不满足要求,可以对其做定制。
// Dubbo中的REST实现是用JAXB做XML序列化,用Jackson做JSON序列化,
// 所以在对象上添加JAXB或Jackson的annotation即可以定制映射。
@JsonProperty("username")
@XmlElement(name = "username")
@NotNull
@Size(min = 6, max = 50)
private String name;
}
  1. 定义REST响应结果实体:RegistrationResult.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码package com.alibaba.dubbo.demo.user.facade;
import lombok.Data;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
// 此外,如果service方法中的返回值是Java的 primitive类型(如int,long,float,double等),
// 最好为它们添加一层wrapper对象,因为JAXB不能直接序列化primitive类型。这样不但能够解决XML序列化的问题,
// 而且使得返回的数据都符合XML和JSON的规范。
// 这种wrapper对象其实利用所谓Data Transfer Object(DTO)模式,采用DTO还能对传输数据做更多有用的定制。
@Data
@XmlRootElement
public class RegistrationResult implements Serializable {
private Long id;
}

1.3 服务实现

  1. 创建Maven模块:msa-demo-provider

msa-demo-provider
2. msa-demo-provider:配置pom.xml

1
2
3
4
5
6
7
复制代码<!-- Module依赖 START -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>msa-demo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Module依赖 END -->
  1. 实现UserService接口:UserServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码package com.alibaba.dubbo.demo.user;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
@Slf4j
public class UserServiceImpl implements UserService {
private final AtomicLong idGen = new AtomicLong();
public User getUser(Long id) {
User user = new User();
user.setId(id);
user.setName("username" + id);
return user;
}

public Long registerUser(User user) {
// System.out.println("Username is " + user.getName());
return idGen.incrementAndGet();
}
}
  1. 实现REST接口AnotherUserRestService:AnotherUserRestServiceImpl.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
复制代码package com.alibaba.dubbo.demo.user.facade;
import com.alibaba.dubbo.demo.user.User;
import com.alibaba.dubbo.demo.user.UserService;
import com.alibaba.dubbo.rpc.RpcContext;
import lombok.extern.slf4j.Slf4j;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
@Slf4j
public class AnotherUserRestServiceImpl implements AnotherUserRestService {

private UserService userService;

public void setUserService(UserService userService) {
this.userService = userService;
}

public User getUser(Long id) {
System.out.println("Client name is " + RpcContext.getContext().getAttachment("clientName"));
System.out.println("Client impl is " + RpcContext.getContext().getAttachment("clientImpl"));
return userService.getUser(id);
}

public RegistrationResult registerUser(User user) {
Long id = userService.registerUser(user);
RegistrationResult registrationResult = new RegistrationResult();
registrationResult.setId(id);
return registrationResult;
}
}
  1. Dubbox与Spring集成配置:msa-demo-provider.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
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
复制代码 <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 当前应用信息配置 -->
<dubbo:application name="msa-demo-provider" owner="tbr" organization="tbr"/>
<dubbo:monitor address="x.x.x.x:20884"/>

<!-- 多注册中心配置,竖号分隔表示同时连接多个不同注册中心,同一注册中心的多个集群地址用逗号分隔 -->
<dubbo:registry protocol="zookeeper" address="x.x.x.x:2181,x.x.x.x:2181,x.x.x.x:2181"/>
<dubbo:protocol name="dubbo" port="20880" serialization="kryo"/>

<!--
1. 选用了嵌入式的jetty来做rest server,同时,如果不配置server属性,rest协议默认也是选用jetty。
jetty是非常成熟的java servlet容器,并和dubbo已经有较好的集成(目前5种嵌入式server中只有jetty
和后面所述的tomcat、tjws,与dubbo监控系统等完成了无缝的集成),所以,如果你的dubbo系统是单独启动的进程,
你可以直接默认采用jetty即可。dubbo中的rest协议默认将采用80端口.
<dubbo:protocol name="rest" server="jetty"/>

2. 配置选用了嵌入式的tomcat来做rest server。在嵌入式tomcat上,REST的性能比jetty上要好得多(参见后面的基准测试),
建议在需要高性能的场景下采用tomcat。
<dubbo:protocol name="rest" server="tomcat"/>

3. 配置选用嵌入式的netty来做rest server。
<dubbo:protocol name="rest" server="netty"/>

4. 配置选用嵌入式的tjws或Sun HTTP server来做rest server。这两个server实现非常轻量级,
非常方便在集成测试中快速启动使用,当然也可以在负荷不高的生产环境中使用。 注:tjws目前已经
被deprecated掉了,因为它不能很好的和servlet 3.1 API工作。
<dubbo:protocol name="rest" server="tjws"/>
<dubbo:protocol name="rest" server="sunhttp"/>

5. 如果你的dubbo系统不是单独启动的进程,而是部署到了Java应用服务器中,则建议你采用以下配置:
<dubbo:protocol name="rest" server="servlet"/>

6. 通过将server设置为servlet,dubbo将采用外部应用服务器的servlet容器来做rest server。同时,还要在dubbo系统的web.xml中添加如下配置:
<web-app>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/classes/META-INF/spring/dubbo-demo-provider.xml</param-value>
</context-param>

<listener>
<listener-class>com.alibaba.dubbo.remoting.http.servlet.BootstrapListener</listener-class>
</listener>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
即必须将dubbo的BootstrapListener和DispatherServlet添加到web.xml,以完成dubbo的REST功能与外部servlet容器的集成。

其实,这种场景下你依然可以坚持用嵌入式server,但外部应用服务器的servlet容器往往比嵌入式server更加强大
(特别是如果你是部署到更健壮更可伸缩的WebLogic,WebSphere等),另外有时也便于在应用服务器做统一管理、监控等等。

如果将dubbo REST部署到外部Tomcat上,并配置server="servlet",即启用外部的tomcat来做为rest server的底层实现,
则最好在tomcat上添加如下配置:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
minSpareThreads="20"
enableLookups="false"
maxThreads="100"
maxKeepAliveRequests="-1"
keepAliveTimeout="60000"/>
特别是maxKeepAliveRequests="-1",这个配置主要是保证tomcat一直启用http长连接,以提高REST调用性能。
但是请注意,如果REST消费端不是持续的调用REST服务,则一直启用长连接未必是最好的做法。另外,一直启用长连
接的方式一般不适合针对普通webapp,更适合这种类似rpc的场景。所以为了高性能,在tomcat中,dubbo REST应
用和普通web应用最好不要混合部署,而应该用单独的实例。

7. 注意:如果你是用spring的ContextLoaderListener来加载spring,
则必须保证BootstrapListener配置在ContextLoaderListener之前,否则dubbo初始化会出错。
-->

<!--
1. 设置一个所有rest服务都适用的基础相对路径,即java web应用中常说的context path。只需要添加如下contextpath属性即可.
<dubbo:protocol name="rest" port="8888" keepalive="true" server="netty" iothreads="5" threads="100" contextpath="services"/>

2. 可以为rest服务配置线程池大小:
<dubbo:protocol name="rest" threads="500"/>
注意:目前线程池的设置只有当server="netty"或者server="jetty"或者server="tomcat"的时候才能生效。另外,如果server="servlet",由于这时候启用
的是外部应用服务器做rest server,不受dubbo控制,所以这里的线程池设置也无效。

如果是选用netty server,还可以配置Netty的IO worker线程数:
<dubbo:protocol name="rest" iothreads="5" threads="100"/>

3. 注意:如果你是选用外部应用服务器做rest server, 即配置:
<dubbo:protocol name="rest" port="8888" contextpath="services" server="servlet"/>
则必须保证这里设置的port、contextpath,与外部应用服务器的端口、DispatcherServlet的上下文路径(即webapp path加上servlet url pattern)保持一致。

4. Dubbo中的rest服务默认都是采用http长连接来访问,如果想切换为短连接,直接配置:
<dubbo:protocol name="rest" keepalive="false"/>
注意:这个配置目前只对server="netty"和server="tomcat"才能生效。

5. 配置服务器提供端所能同时接收的最大HTTP连接数,防止REST server被过多连接撑爆,以作为一种最基本的自我保护机制:
<dubbo:protocol name="rest" accepts="500" server="tomcat/>
注意:这个配置目前只对server="tomcat"才能生效。

6. 如果rest服务的消费端也是dubbo系统,可以像其他dubbo RPC机制一样,配置消费端调用此rest服务的最大超时时间以及每个消费端所能启动的最大HTTP连接数。
<dubbo:service interface="xxx" ref="xxx" protocol="rest" timeout="2000" connections="10"/>
当然,由于这个配置针对消费端生效的,所以也可以在消费端配置:
<dubbo:reference id="xxx" interface="xxx" timeout="2000" connections="10"/>
但是,通常我们建议配置在服务提供端提供此类配置。按照dubbo官方文档的说法:“Provider上尽量多配置Consumer端的属性,让Provider实现者一开始就思考Provider服务特点、服务质量的问题。”
注意:如果dubbo的REST服务是发布给非dubbo的客户端使用,则这里<dubbo:service/>上的配置完全无效,因为这种客户端不受dubbo控制。

7. Dubbo的REST支持用GZIP压缩请求和响应的数据,以减少网络传输时间和带宽占用,但这种方式会也增加CPU开销。
-->

<!--
1. use tomcat server
2. 用rest协议在8888端口暴露服务
3. Dubbo的REST也支持JAX-RS标准的Filter和Interceptor,以方便对REST的请求与响应过程做定制化的拦截处理。
其中,Filter主要用于访问和设置HTTP请求和响应的参数、URI等等。如:CacheControlFilter.java
Interceptor主要用于访问和修改输入与输出字节流,例如,手动添加GZIP压缩.如:GZIPWriterInterceptor.java
4. 在标准JAX-RS应用中,我们一般是为Filter和Interceptor添加@Provider annotation,然后JAX-RS runtime会
自动发现并启用它们。而在dubbo中,我们是通过添加XML配置的方式来注册Filter和Interceptor.
5. 在此,我们可以将Filter、Interceptor和DynamicFuture这三种类型的对象都添加到extension属性上,多个之间用逗号分隔。(DynamicFuture是另一个接口,可以方便我们更动态的启用Filter和Interceptor,感兴趣请自行google。)
6. 当然,dubbo自身也支持Filter的概念,但我们这里讨论的Filter和Interceptor更加接近协议实现的底层,
相比dubbo的filter,可以做更底层的定制化。
注:这里的XML属性叫extension,而不是叫interceptor或者filter,是因为除了Interceptor和Filter,未来我们
还会添加更多的扩展类型。
7. 如果REST的消费端也是dubbo系统(参见下文的讨论),则也可以用类似方式为消费端配置Interceptor和Filter。但注
意,JAX-RS中消费端的Filter和提供端的Filter是两种不同的接口。例如前面例子中服务端是ContainerResponseFilter接口,
而消费端对应的是ClientResponseFilter.
8. Dubbo的REST也支持JAX-RS标准的ExceptionMapper,可以用来定制特定exception发生后应该返回的HTTP响应。
9. Dubbo rest支持输出所有HTTP请求/响应中的header字段和body消息体。LoggingFilter
-->
<dubbo:protocol name="rest" port="8888" threads="500" contextpath="services" server="tomcat" accepts="500"
extension="com.alibaba.dubbo.demo.extension.TraceInterceptor,
com.alibaba.dubbo.demo.extension.TraceFilter,
com.alibaba.dubbo.demo.extension.ClientTraceFilter,
com.alibaba.dubbo.demo.extension.DynamicTraceBinding,
com.alibaba.dubbo.demo.extension.CustomExceptionMapper,
com.alibaba.dubbo.rpc.protocol.rest.support.LoggingFilter"/>

<!--
use the external tomcat or other server with the servlet approach; the port and contextpath must be exactly the same as those in external server
<dubbo:protocol name="rest" port="8888" contextpath="services" server="servlet"/>
-->

<dubbo:protocol name="http" port="8889"/>
<dubbo:protocol name="hessian" port="8890"/>
<dubbo:protocol name="webservice" port="8892"/>

<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.user.UserService" ref="userService" protocol="dubbo" group="xmlConfig"/>

<!--
1. 为了和其他dubbo远程调用协议保持一致,在rest中作校验的annotation必须放在服务的接口上,
把annotation放在接口上至少有一个好处是,dubbo的客户端可以共享这个接口的信息,dubbo甚
至不需要做远程调用,在本地就可以完成输入校验。

然后按照dubbo的标准方式在XML配置中打开验证:
<dubbo:service interface=xxx.UserService" ref="userService" protocol="rest" validation="true"/>

2. 在dubbo的其他很多远程调用协议中,如果输入验证出错,是直接将RpcException抛向客户端,而在rest中由于客户端经常是非dubbo,甚至非java的系统,所以不便直接抛出Java异常。因此,目前我们将校验错误以XML的格式返回:
<violationReport>
<constraintViolations>
<path>getUserArgument0</path>
<message>User ID must be greater than 1</message>
<value>0</value>
</constraintViolations>
</violationReport>

如果你认为默认的校验错误返回格式不符合你的要求,可以如上面章节所述,添加自定义的ExceptionMapper来自由的定制错误返回格式。
需要注意的是,这个ExceptionMapper必须用泛型声明来捕获dubbo的RpcException,才能成功覆盖dubbo rest默认的异常处理策略。
为了简化操作,其实这里最简单的方式是直接继承dubbo rest的RpcExceptionMapper,并覆盖其中处理校验异常的方法即可.
-->
<dubbo:service interface="com.alibaba.dubbo.demo.user.facade.AnotherUserRestService" ref="anotherUserRestService" protocol="rest" timeout="2000" connections="100" validation="true"/>

<bean id="userService" class="com.alibaba.dubbo.demo.user.UserServiceImpl"/>

<bean id="anotherUserRestService" class="com.alibaba.dubbo.demo.user.facade.AnotherUserRestServiceImpl">
<property name="userService" ref="userService"/>
</bean>

<!--
对于jax-rs和spring mvc,其实我对spring mvc的rest支持还没有太深入的看过,说点初步想法,请大家指正:

spring mvc也支持annotation的配置,其实和jax-rs看起来是非常非常类似的。

我个人认为spring mvc相对更适合于面向web应用的restful服务,比如被AJAX调用,也可能输出HTML之类的,应用中还
有页面跳转流程之类,spring mvc既可以做好正常的web页面请求也可以同时处理rest请求。但总的来说这个restful服务
是在展现层或者叫web层之类实现的

而jax-rs相对更适合纯粹的服务化应用,也就是传统Java EE中所说的中间层服务,比如它可以把传统的EJB发布成restful
服务。在spring应用中,也就把spring中充当service之类的bean直接发布成restful服务。总的来说这个restful服务是
在业务、应用层或者facade层。而MVC层次和概念在这种做比如(后台)服务化的应用中通常是没有多大价值的。

当然jax-rs的有些实现比如jersey,也试图提供mvc支持,以更好的适应上面所说的web应用,但应该是不如spring mvc。

在dubbo应用中,我想很多人都比较喜欢直接将一个本地的spring service bean(或者叫manager之类的)完全透明的发布
成远程服务,则这里用JAX-RS是更自然更直接的,不必额外的引入MVC概念。当然,先不讨论透明发布远程服务是不是最佳实践,
要不要添加facade之类。

当然,我知道在dubbo不支持rest的情况下,很多朋友采用的架构是spring mvc restful调用dubbo (spring) service
来发布restful服务的。这种方式我觉得也非常好,只是如果不修改spring mvc并将其与dubbo深度集成,restful服务不能
像dubbo中的其他远程调用协议比如webservices、dubbo rpc、hessian等等那样,享受诸多高级的服务治理的功能,比如:
注册到dubbo的服务注册中心,通过dubbo监控中心监控其调用次数、TPS、响应时间之类,通过dubbo的统一的配置方式控制其
比如线程池大小、最大连接数等等,通过dubbo统一方式做服务流量控制、权限控制、频次控制。另外spring mvc仅仅负责服务
端,而在消费端,通常是用spring restTemplate,如果restTemplate不和dubbo集成,有可能像dubbo服务客户端那样自动
或者人工干预做服务降级。如果服务端消费端都是dubbo系统,通过spring的rest交互,如果spring rest不深度整合dubbo,
则不能用dubbo统一的路由分流等功能。

当然,其实我个人认为这些东西不必要非此即彼的。我听说spring创始人rod johnson总是爱说一句话,
the customer is always right,其实与其非要探讨哪种方式更好,不如同时支持两种方式就是了,
所以原来在文档中也写过计划支持spring rest annoation,只是不知道具体可行性有多高。

1. JAX-RS中重载的方法能够映射到同一URL地址吗?
http://stackoverflow.com/questions/17196766/can-resteasy-choose-method-based-on-query-params

2. JAX-RS中作POST的方法能够接收多个参数吗?
http://stackoverflow.com/questions/5553218/jax-rs-post-multiple-objects

注:以上备注,均来自:https://dangdangdotcom.github.io/dubbox/rest.html
-->
</beans>
  1. 配置dubbo.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码#dubbo.container=log4j,spring
#dubbo.application.name=demo-provider
#dubbo.application.owner=
#dubbo.registry.address=multicast://224.5.6.7:1234
#dubbo.registry.address=zookeeper://127.0.0.1:2181
#dubbo.registry.address=redis://127.0.0.1:6379
#dubbo.registry.address=dubbo://127.0.0.1:9090
#dubbo.monitor.protocol=registry
#dubbo.protocol.name=dubbo
#dubbo.protocol.port=20880
#dubbo.service.loadbalance=roundrobin
#dubbo.log4j.file=logs/msa-demo-provider.log
#dubbo.log4j.level=INFO
#dubbo.log4j.subdirectory=20880
dubbo.application.logger=slf4j
dubbo.spring.config=classpath*:msa-*.xml

1.4 服务启动

定义服务启动类

1
2
3
4
5
6
7
8
9
10
11
复制代码package com.alibaba.dubbo.demo.provider;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
public class DemoProvider {
public static void main(String[] args) {
com.alibaba.dubbo.container.Main.main(args);
}
}

执行main方法启动,看到以下日志输出时,msa-demo-provider启动成功:

msa-demo-provider启动成功

查看DubboKeeper监控大盘,msa-demo-provider发布服务成功,可以看到我们发布的两个接口:

msa-demo-provider发布服务成功

  1. 客户端

  1. 创建Maven模块:msa-demo-client

msa-demo-client
2. msa-demo-client:配置pom.xml

1
2
3
4
5
6
7
复制代码<!-- Module依赖 START -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>msa-demo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Module依赖 END -->
  1. Dubbox与Spring集成配置:msa-demo-client.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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 当前应用信息配置 -->
<!--<dubbo:application name="msa-demo-client" owner="shark" organization="shark"/>-->

<!-- 多注册中心配置,竖号分隔表示同时连接多个不同注册中心,同一注册中心的多个集群地址用逗号分隔 -->
<!--<dubbo:registry protocol="zookeeper" address="x.x.x.x:2181,x.x.x.x:2181,x.x.x.x:2181"/>-->
<!--<dubbo:monitor address="x.x.x.x:20884"/>-->

<dubbo:reference id="userService" interface="com.alibaba.dubbo.demo.user.UserService" group="xmlConfig"/>
<dubbo:reference id="anotherUserRestService" interface="com.alibaba.dubbo.demo.user.facade.AnotherUserRestService"/>

<!--
directly connect to provider to simulate the access to non-dubbo rest services
<dubbo:reference id="anotherUserRestService" interface="com.alibaba.dubbo.demo.user.facade.AnotherUserRestService" url="rest://localhost:8888/services/"/>
-->
</beans>
  1. 消费端

3.1 消费端实现

  1. 创建Maven模块:msa-demo-consumer

msa-demo-consumer
2. msa-demo-consumer:配置pom.xml

1
2
3
4
5
6
7
复制代码<!-- Module依赖 START -->
<dependency>
<groupId>com.jeasy</groupId>
<artifactId>msa-demo-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Module依赖 END -->
  1. 创建消费端测试类:DemoAction.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
复制代码package com.alibaba.dubbo.demo;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.demo.user.User;
import com.alibaba.dubbo.demo.user.UserService;
import com.alibaba.dubbo.demo.user.facade.AnotherUserRestService;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
public class DemoAction {

private UserService userService;

private AnotherUserRestService anotherUserRestService;

public void setUserService(final UserService userService) {
this.userService = userService;
}

public void setAnotherUserRestService(final AnotherUserRestService anotherUserRestService) {
this.anotherUserRestService = anotherUserRestService;
}

public void start() throws Exception {
User user = new User();
user.setId(1L);
user.setName("larrypage");

System.out.println("SUCCESS: registered user with id by rest" + anotherUserRestService.registerUser(user).getId());
System.out.println("SUCCESS: registered user with id " + userService.registerUser(user));

RpcContext.getContext().setAttachment("clientName", "demo");
RpcContext.getContext().setAttachment("clientImpl", "dubbox rest");
System.out.println("SUCCESS: got user by rest" + anotherUserRestService.getUser(1L));
System.out.println("SUCCESS: got user " + userService.getUser(1L));
}
}
  1. Dubbox与Spring集成配置:msa-demo-consumer.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 当前应用信息配置 -->
<dubbo:application name="msa-demo-consumer" owner="tbr" organization="tbr"/>

<!-- 多注册中心配置,竖号分隔表示同时连接多个不同注册中心,同一注册中心的多个集群地址用逗号分隔 -->
<dubbo:registry protocol="zookeeper" address="x.x.x.x:2181,x.x.x.x:2181,x.x.x.x:2181"/>
<dubbo:monitor address="x.x.x.x:20884"/>

<bean class="com.alibaba.dubbo.demo.DemoAction" init-method="start">
<property name="userService" ref="userService"/>
<property name="anotherUserRestService" ref="anotherUserRestService"/>
</bean>
</beans>
  1. 配置dubbo.properties
1
2
3
4
5
6
7
8
9
10
11
12
复制代码#dubbo.container=log4j,spring
#dubbo.application.name=demo-consumer
#dubbo.application.owner=
#dubbo.registry.address=multicast://224.5.6.7:1234
#dubbo.registry.address=zookeeper://127.0.0.1:2181
#dubbo.registry.address=redis://127.0.0.1:6379
#dubbo.registry.address=dubbo://127.0.0.1:9090
#dubbo.monitor.protocol=registry
#dubbo.log4j.file=logs/msa-demo-consumer.log
#dubbo.log4j.level=INFO
dubbo.application.logger=slf4j
dubbo.spring.config=classpath*:msa-*.xml

3.2 消费端测试

定义消费启动类:

1
2
3
4
5
6
7
8
9
10
11
复制代码package com.jeasy;
/**
* @author TaoBangren
* @version 1.0
* @since 2017/5/17 上午9:26
*/
public class DemoConsumer {
public static void main(String[] args) {
com.alibaba.dubbo.container.Main.main(args);
}
}

执行main方法启动,看到以下日志输出时,msa-demo-consumer启动成功:

msa-demo-consumer启动成功

同时服务端会输出服务调用日志信息,并调用成功,如下:

服务端调用日志

  1. 规范使用

模块 描述 是否必须
msa-xxx-api 定义接口&实体 必须
msa-xxx-provider 依赖api模块,实现服务接口,提供服务 必须
msa-xxx-client 依赖api模块,Spring配置文件&测试用例,提供给第三方调用服务使用 必须
msa-xxx-consumer 依赖client模块,建议保留该模块,避免client模块直接与应用方紧耦合 可选
  1. 推荐阅读

5.1 Dubbox相关资源

  1. 源码地址 : github.com/dangdangdot…
  2. 在Dubbo中开发REST风格的远程调用 : dangdangdotcom.github.io/dubbox/rest…
  3. 在Dubbo中使用高效的Java序列化 : dangdangdotcom.github.io/dubbox/seri…
  4. 使用JavaConfig方式配置dubbox : dangdangdotcom.github.io/dubbox/java…
  5. Dubbo Jackson序列化使用说明 : dangdangdotcom.github.io/dubbox/jack…
  6. Demo : dangdangdotcom.github.io/dubbox/demo…
  7. 当当网开源Dubbox,扩展Dubbo服务框架支持REST风格远程调用 : www.infoq.com/cn/news/201…
  8. Dubbox Wiki : github.com/dangdangdot…

5.2 Dubbo相关资源

  1. 源码地址 : github.com/alibaba/dub…
  2. Dubbo Wiki : github.com/alibaba/dub…
  3. http://dubbo.io/

本文转载自: 掘金

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

Java面试必问,ThreadLocal终极篇

发表于 2018-01-21

占小狼 转载请注明原创出处,谢谢!

前言

在面试环节中,考察”ThreadLocal”也是面试官的家常便饭,所以对它理解透彻,是非常有必要的.

有些面试官会开门见山的提问:

  • “知道ThreadLocal吗?”
  • “讲讲你对ThreadLocal的理解”

当然了,也有面试官会慢慢引导到这个话题上,比如提问“在多线程环境下,如何防止自己的变量被其它线程篡改”,将主动权交给你自己,剩下的靠自己发挥。

那么ThreadLocal可以做什么,在了解它的应用场景之前,我们先看看它的实现原理,只有知道了实现原理,才好判断它是否符合自己的业务场景。

ThreadLocal是什么

首先,它是一个数据结构,有点像HashMap,可以保存”key : value”键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。

1
2
3
复制代码ThreadLocal<String> localName = new ThreadLocal();
localName.set("占小狼");
String name = localName.get();

在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值占小狼,同时在线程1中通过localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。

这是为什么,如何实现?不过之前也说了,ThreadLocal保证了各个线程的数据互不干扰。

看看set(T value)和get()方法的源码

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 void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

可以发现,每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取。

所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。

那每个线程中的ThreadLoalMap究竟是什么?

ThreadLoalMap

本文分析的是1.7的源码。

从名字上看,可以猜到它也是一个类似HashMap的数据结构,但是在ThreadLocal中,并没实现Map接口。

在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

hash冲突

没有链表结构,那发生hash冲突了怎么办?

先看看ThreadLoalMap中插入一个key-value的实现

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
复制代码private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647。

在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置

可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。

内存泄露

ThreadLocal可能导致内存泄漏,为什么?
先看看Entry的实现:

1
2
3
4
5
6
7
8
9
复制代码static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

如何避免内存泄露

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

1
2
3
4
5
6
7
复制代码ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("占小狼");
// 其它业务逻辑
} finally {
localName.remove();
}

End
我是占小狼
如果读完觉得有收获的话,欢迎点赞加关注

本文转载自: 掘金

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

Android进程框架:进程的创建、启动与调度流程

发表于 2018-01-21

关于作者

郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢迎交流技术方面的问题,可以去我的Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。

文章目录

  • 一 进程的创建与启动流程
  • 二 进程的优先级
  • 三 进程的调度流程

Android系统的启动流程如下图(点击查看大图)所示:


Loader层

  1. 当手机处于关机状态时,长按电源键开机,引导芯片开始从固化在Boot ROM里的预设代码开始执行,然后加载引导程序Boot Loader到RAM。
  2. Boot Loader被加载到RAM之后开始执行,该程序主要完成检查RAM,初始化硬件参数等功能。

Kernel层

  1. 引导程序之后进入Android内核层,先启动swapper进程(idle进程),该进程用来初始化进程管理、内存管理、加载Display、Camera Driver、Binder Driver等相关工作。
  2. swapper进程进程之后再启动kthreadd进程,该进程会创建内核工作线程kworkder、软中断线程ksoftirqd、thernal等内核守护进程,kthreadd进程是所有内核进程的鼻祖。

Native层

  1. 接着会启动init进程,init进程是所有用户进程的鼻祖,它会接着孵化出ueventd、logd、healthd、installd、adbd、lmkd等用户守护进程,启动ServiceManager来管理系统
    服务,启动Bootnaim开机动画。
  2. init进程通过解析init.rc文件fork生成Zygote进程,该进程是Android系统第一个Java进程,它是所有Java进程父进程,该进程主要完成了加载ZygoteInit类,注册Zygote Socket
    服务套接字;加载虚拟机;预加载Class;预加载Resources。

Framework层

  1. init进程接着fork生成Media Server进程,该进程负责启动和管理整个C++ Framwork(包含AudioFlinger、Camera Service等服务)。
  2. Zygote进程接着会fork生成System Server进程,该进程负责启动和管理整个Java Framwork(包含ActivityManagerService、WindowManagerService等服务)。

App层

Zygote进程孵化出的第一个应用进程是Launcher进程(桌面),它还会孵化出Browser进程(浏览器)、Phone进程(电话)等。我们每个创建的应用都是一个单独的进程。

通过上述流程的分析,想必读者已经对Android的整个进程模型有了大致的理解。作为一个应用开发者我们往往更为关注Framework层和App层里进程的创建与管理相关原理,我们来
一一分析。

一 进程的创建与启动流程

在正式介绍进程之前,我们来思考一个问题,何为进程,进程的本质是什么?🤔

我们知道,代码是静态的,有代码和资源组成的系统要想运行起来就需要一种动态的存在,进程就是程序的动态执行过程。何为进程?
进程就是处理执行状态的代码以及相关资源的集合,包括代码端段、文件、信号、CPU状态、内存地址空间等。

进程使用task_struct结构体来描述,如下所示:

  • 代码段:编译后形成的一些指令
  • 数据段:程序运行时需要的数据
    • 只读数据段:常量
    • 已初始化数据段:全局变量,静态变量
    • 未初始化数据段(bss):未初始化的全局变量和静态变量
  • 堆栈段:程序运行时动态分配的一些内存
  • PCB:进程信息,状态标识等

关于进程的更多详细信息,读者可以去翻阅Linux相关书籍,这里只是给读者带来一种整体上的理解,我们的重心还是放在进程再Android平台上的应用。

在文章开篇的时候,我们提到了系统中运行的各种进程,那么这些进程如何被创建呢?🤔

我们先来看看我们最熟悉的应用进程是如何被创建的,前面我们已经说来每一个应用都运行在一个单独的进程里,当ActivityManagerService去启动四大组件时,
如果发现这个组件所在的进程没有启动,就会去创建一个新的进程,启动进程的时机我们在分析四大组件的启动流程的时候也有讲过,这里再总结一下:

  • Activity ActivityStackSupervisor.startSpecificActivityLocked()
  • Service ActiveServices.bringUpServiceLocked()
  • ContentProvider ActivityManagerService.getContentProviderImpl()
    = Broadcast BroadcastQueue.processNextBroadcast()

这个新进程就是zygote进程通过复制自身来创建的,新进程在启动的过程中还会创建一个Binder线程池(用来做进程通信)和一个消息循环(用来做线程通信)
整个流程如下图所示:

  1. 当我们点击应用图标启动应用时或者在应用内启动一个带有process标签的Activity时,都会触发创建新进程的请求,这种请求会先通过Binder
    发送给system_server进程,也即是发送给ActivityManagerService进行处理。
  2. system_server进程会调用Process.start()方法,会先收集uid、gid等参数,然后通过Socket方式发送给Zygote进程,请求创建新进程。
  3. Zygote进程接收到创建新进程的请求后,调用ZygoteInit.main()方法进行runSelectLoop()循环体内,当有客户端连接时执行ZygoteConnection.runOnce()
    方法,最后fork生成新的应用进程。
  4. 新创建的进程会调用handleChildProc()方法,最后调用我们非常熟悉的ActivityThread.main()方法。

注:整个流程会涉及Binder和Socket两种进程通信方式,这个我们后续会有专门的文章单独分析,这个就不再展开。

整个流程大致就是这样,我们接着来看看具体的代码实现,先来看一张进程启动序列图:


从第一步到第三步主要是收集整理uid、gid、groups、target-sdk、nice-name等一系列的参数,为后续启动新进程做准备。然后调用openZygoteSocketIfNeeded()方法
打开Socket通信,向zygote进程发出创建新进程的请求。

注:第二步中的Process.start()方法是个阻塞操作,它会一直等待进程创建完毕,并返回pid才会完成该方法。

我们来重点关注几个关键的函数。

1.1 Process.openZygoteSocketIfNeeded(String abi)

关于Process类与Zygote进程的通信是如何进行的呢?🤔

Process的静态内部类ZygoteState有个成员变量LocalSocket对象,它会与ZygoteInit类的成员变量LocalServerSocket对象建立连接,如下所示:

客户端

1
2
3
复制代码public static class ZygoteState {
final LocalSocket socket;
}

服务端

1
2
3
4
复制代码public class ZygoteInit {
//该Socket与/dev/socket/zygote文件绑定在一起
private static LocalServerSocket sServerSocket;
}

我们来具体看看代码里的实现。

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
复制代码 public static class ZygoteState {

public static ZygoteState connect(String socketAddress) throws IOException {
DataInputStream zygoteInputStream = null;
BufferedWriter zygoteWriter = null;
//创建LocalSocket对象
final LocalSocket zygoteSocket = new LocalSocket();

try {
//将LocalSocket与LocalServerSocket建立连接,建立连接的过程就是
//LocalSocket对象在/dev/socket目录下查找一个名称为"zygote"的文件
//然后将自己与其绑定起来,这样就建立了连接。
zygoteSocket.connect(new LocalSocketAddress(socketAddress,
LocalSocketAddress.Namespace.RESERVED));

//创建LocalSocket的输入流,以便可以接收Zygote进程发送过来的数据
zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream());

//创建LocalSocket的输出流,以便可以向Zygote进程发送数据。
zygoteWriter = new BufferedWriter(new OutputStreamWriter(
zygoteSocket.getOutputStream()), 256);
} catch (IOException ex) {
try {
zygoteSocket.close();
} catch (IOException ignore) {
}

throw ex;
}

String abiListString = getAbiList(zygoteWriter, zygoteInputStream);
Log.i("Zygote", "Process: zygote socket opened, supported ABIS: " + abiListString);

return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter,
Arrays.asList(abiListString.split(",")));
}
}

建立Socket连接的流程很明朗了,如下所示:

  1. 创建LocalSocket对象。
  2. 将LocalSocket与LocalServerSocket建立连接,建立连接的过程就是LocalSocket对象在/dev/socket目录下查找一个名称为”zygote”的文件,然后将自己与其绑定起来,这样就建立了连接。
  3. 创建LocalSocket的输入流,以便可以接收Zygote进程发送过来的数据。
  4. 创建LocalSocket的输出流,以便可以向Zygote进程发送数据。

1.2 ZygoteInit.main(String argv[])

ZygoteInit是Zygote进程的启动类,该类会预加载一些类,然后便开启一个循环,等待通过Socket发过来的创建新进程的命令,fork出新的
子进程。

ZygoteInit的入口函数就是main()方法,如下所示:

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
复制代码public class ZygoteInit {

public static void main(String argv[]) {
// Mark zygote start. This ensures that thread creation will throw
// an error.
ZygoteHooks.startZygoteNoThreadCreation();

try {
//...
registerZygoteSocket(socketName);
//...
//开启循环
runSelectLoop(abiList);

closeServerSocket();
} catch (MethodAndArgsCaller caller) {
caller.run();
} catch (Throwable ex) {
Log.e(TAG, "Zygote died with exception", ex);
closeServerSocket();
throw ex;
}
}

// 开启一个选择循环,接收通过Socket发过来的命令,创建新线程
private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {

ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();

//sServerSocket指的是Socket通信的服务端,在fds中的索引为0
fds.add(sServerSocket.getFileDescriptor());
peers.add(null);

//开启循环
while (true) {
StructPollfd[] pollFds = new StructPollfd[fds.size()];
for (int i = 0; i < pollFds.length; ++i) {
pollFds[i] = new StructPollfd();
pollFds[i].fd = fds.get(i);
pollFds[i].events = (short) POLLIN;
}
try {
//处理轮询状态,当pollFds有时间到来时则往下执行,否则阻塞在这里。
Os.poll(pollFds, -1);
} catch (ErrnoException ex) {
throw new RuntimeException("poll failed", ex);
}
for (int i = pollFds.length - 1; i >= 0; --i) {

//采用IO多路复用机制,当接收到客户端发出的连接请求时或者数据处理请求到来时则
//往下执行,否则进入continue跳出本次循环。
if ((pollFds[i].revents & POLLIN) == 0) {
continue;
}
//索引为0,即为sServerSocket,表示接收到客户端发来的连接请求。
if (i == 0) {
ZygoteConnection newPeer = acceptCommandPeer(abiList);
peers.add(newPeer);
fds.add(newPeer.getFileDesciptor());
}
//索引不为0,表示通过Socket接收来自对端的数据,并执行相应的操作。
else {
boolean done = peers.get(i).runOnce();
//处理完成后移除相应的文件描述符。
if (done) {
peers.remove(i);
fds.remove(i);
}
}
}
}
}
}

可以发现ZygoteInit在其入口函数main()方法里调用runSelectLoop()开启了循环,接收Socket发来的请求。请求分为两种:

  1. 连接请求
  2. 数据请求

没有连接请求时Zygote进程会进入休眠状态,当有连接请求到来时,Zygote进程会被唤醒,调用acceptCommadPeer()方法创建Socket通道ZygoteConnection

1
2
3
4
5
6
7
8
复制代码private static ZygoteConnection acceptCommandPeer(String abiList) {
try {
return new ZygoteConnection(sServerSocket.accept(), abiList);
} catch (IOException ex) {
throw new RuntimeException(
"IOException during accept()", ex);
}
}

然后调用runOnce()方法读取连接请求里的数据,然后创建新进程。

此外,连接的过程中服务端接受的到客户端的connect()操作会执行accpet()操作,建立连接手,客户端通过write()写数据,服务端通过read()读数据。

1.3 ZygoteConnection.runOnce()

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
复制代码class ZygoteConnection {

boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {

String args[];
Arguments parsedArgs = null;
FileDescriptor[] descriptors;

try {
//读取客户端发过来的参数列表
args = readArgumentList();
descriptors = mSocket.getAncillaryFileDescriptors();
} catch (IOException ex) {
Log.w(TAG, "IOException on command socket " + ex.getMessage());
closeSocket();
return true;
}

//... 参数处理

try {

//... 参数处理


//调用Zygote.forkAndSpecialize(来fork出新进程
pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,
parsedArgs.appDataDir);
} catch (ErrnoException ex) {
logAndPrintError(newStderr, "Exception creating pipe", ex);
} catch (IllegalArgumentException ex) {
logAndPrintError(newStderr, "Invalid zygote arguments", ex);
} catch (ZygoteSecurityException ex) {
logAndPrintError(newStderr,
"Zygote security policy prevents request: ", ex);
}

try {
//pid == 0时表示当前是在新创建的子进程重磅执行
if (pid == 0) {
// in child
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);

// should never get here, the child is expected to either
// throw ZygoteInit.MethodAndArgsCaller or exec().
return true;
}
// pid < 0表示创建新进程失败,pid > 0 表示当前是在父进程中执行
else {
// in parent...pid of < 0 means failure
IoUtils.closeQuietly(childPipeFd);
childPipeFd = null;
return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);
}
} finally {
IoUtils.closeQuietly(childPipeFd);
IoUtils.closeQuietly(serverPipeFd);
}
}
}

该方法主要用来读取进程启动参数,然后调用Zygote.forkAndSpecialize()方法fork出新进程,该方法是创建新进程的核心方法,它主要会陆续调用三个
方法来完成工作:

  1. preFork():先停止Zygote进程的四个Daemon子线程的运行以及初始化GC堆。这四个Daemon子线程分别为:Java堆内存管理现场、堆线下引用队列线程、析构线程与监控线程。
  2. nativeForkAndSpecialize():调用Linux系统函数fork()创建新进程,创建Java堆处理的线程池,重置GC性能数据,设置进程的信号处理函数,启动JDWP线程。
  3. postForkCommon():启动之前停止的Zygote进程的四个Daemon子线程。

上面的方法都完成会后,新进程会创建完成,并返回pid,接着就调用handleChildProc()来启动新进程。handleChildProc()方法会接着调用RuntimeInit.zygoteInit()来
完成新进程的启动。

1.4 RuntimeInit.zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)

这个就是个关键的方法了,它主要用来创建一些运行时环境,我们来看一看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class RuntimeInit {

public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
throws ZygoteInit.MethodAndArgsCaller {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from zygote");

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "RuntimeInit");
redirectLogStreams();
//创建应用进程的时区和键盘等通用信息
commonInit();
//在应用进程中创建一个Binder线程池
nativeZygoteInit();
//创建应用信息
applicationInit(targetSdkVersion, argv, classLoader);
}
}

该方法主要完成三件事:

  1. 调用commonInit()方法创建应用进程的时区和键盘等通用信息。
  2. 调用nativeZygoteInit()方法在应用进程中创建一个Binder线程池。
  3. 调用applicationInit(targetSdkVersion, argv, classLoader)方法创建应用信息。

Binder线程池我们后续的文章会分析,我们重点来看看applicationInit(targetSdkVersion, argv, classLoader)方法的实现,它主要用来完成应用的创建。

该方法里的argv参数指的就是ActivityThread,该方法会调用invokeStaticMain()通过反射的方式调用ActivityThread类的main()方法。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码public class RuntimeInit {

private static void applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
throws ZygoteInit.MethodAndArgsCaller {
//...

// Remaining arguments are passed to the start class's static main
invokeStaticMain(args.startClass, args.startArgs, classLoader);
}

private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader)
throws ZygoteInit.MethodAndArgsCaller {
Class<?> cl;

//通过反射调用ActivityThread类的main()方法
try {
cl = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
"Missing class when invoking static main " + className,
ex);
}

Method m;
try {
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
throw new RuntimeException(
"Missing static main on " + className, ex);
} catch (SecurityException ex) {
throw new RuntimeException(
"Problem getting static main on " + className, ex);
}
//...
}
}

走到ActivityThread类的main()方法,我们就很熟悉了,我们知道在main()方法里,会创建主线程Looper,并开启消息循环,如下所示:

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 final class ActivityThread {

public static void main(String[] args) {
//...
Environment.initForCurrentUser();
//...
Process.setArgV0("<pre-initialized>");
//创建主线程looper
Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
//attach到系统进程
thread.attach(false);

if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

//主线程进入循环状态
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}
}

前面我们从Process.start()开始讲起,分析了应用进程的创建及启动流程,既然有启动就会有结束,接下来我们从
Process.killProcess()开始讲起,继续分析进程的结束流程。

二 进程的优先级

进程按照优先级大小不同又可以分为实时进程与普通进程。


prio值越小表示进程优先级越高,

  • 静态优先级:优先级不会随时间改变,内核也不会修改,只能通过系统调用改变nice值,优先级映射公式为:static_prio = MAX_RT_PRIO + nice + 20,其中MAX_RT_PRIO = 100,那么取值区间为[100, 139];对应普通进程;
  • 实时优先级:取值区间为[0, MAX_RT_PRIO -1],其中MAX_RT_PRIO = 100,那么取值区间为[0, 99];对应实时进程;
  • 懂爱优先级:调度程序通过增加或者减少进程优先级,来达到奖励IO消耗型或按照惩罚CPU消耗型的进程的效果。区间范围[0, MX_PRIO-1],其中MX_PRIO = 140,那么取值区间为[0,139];

三 进程调度流程

进程的调度在Process类里完成。

3.1 优先级调度

优先级调度方法

1
复制代码setThreadPriority(int tid, int priority)

进程的优先级以及对应的nice值如下所示:

  • THREAD_PRIORITY_LOWEST 19 最低优先级
  • THREAD_PRIORITY_BACKGROUND 10 后台
  • THREAD_PRIORITY_LESS_FAVORABLE 1 比默认略低
  • THREAD_PRIORITY_DEFAULT 0 默认
  • THREAD_PRIORITY_MORE_FAVORABLE -1 比默认略高
  • THREAD_PRIORITY_FOREGROUND -2 前台
  • THREAD_PRIORITY_DISPLAY -4 显示相关
  • THREAD_PRIORITY_URGENT_DISPLAY -8 显示(更为重要),input事件
  • THREAD_PRIORITY_AUDIO -16 音频相关
  • THREAD_PRIORITY_URGENT_AUDIO -19 音频(更为重要)

3.2 组优先级调度

进程组优先级调度方法

1
2
复制代码setProcessGroup(int pid, int group)
setThreadGroup(int tid, int group)

组优先级及对应取值

  • THREAD_GROUP_DEFAULT -1 仅用于setProcessGroup,将优先级<=10的进程提升到-2
  • THREAD_GROUP_BG_NONINTERACTIVE 0 CPU分时的时长缩短
  • THREAD_GROUP_FOREGROUND 1 CPU分时的时长正常
  • THREAD_GROUP_SYSTEM 2 系统线程组
  • THREAD_GROUP_AUDIO_APP 3 应用程序音频
  • THREAD_GROUP_AUDIO_SYS 4 系统程序音频

3.3 调度策略

调度策略设置方法

1
复制代码setThreadScheduler(int tid, int policy, int priority)
  • SCHED_OTHER 默认 标准round-robin分时共享策略
  • SCHED_BATCH 批处理调度 针对具有batch风格(批处理)进程的调度策略
  • SCHED_IDLE 空闲调度 针对优先级非常低的适合在后台运行的进程
  • SCHED_FIFO 先进先出 实时调度策略,android暂未实现
  • SCHED_RR 循环调度 实时调度策略,android暂未实现

3.4 进程adj调度

另外除了这些基本的调度策略,Android系统还定义了两个和进程相关的状态值,一个就是定义在ProcessList.java里的adj值,另一个
是定义在ActivityManager.java里的procState值。

定义在ProcessList.java文件,oom_adj划分为16级,从-17到16之间取值。

  • UNKNOWN_ADJ 16 一般指将要会缓存进程,无法获取确定值
  • CACHED_APP_MAX_ADJ 15 不可见进程的adj最大值 1
  • CACHED_APP_MIN_ADJ 9 不可见进程的adj最小值 2
  • SERVICE_B_AD 8 B List中的Service(较老的、使用可能性更小)
  • PREVIOUS_APP_ADJ 7 上一个App的进程(往往通过按返回键)
  • HOME_APP_ADJ 6 Home进程
  • SERVICE_ADJ 5 服务进程(Service process)
  • HEAVY_WEIGHT_APP_ADJ 4 后台的重量级进程,system/rootdir/init.rc文件中设置
  • BACKUP_APP_ADJ 3 备份进程 3
  • PERCEPTIBLE_APP_ADJ 2 可感知进程,比如后台音乐播放 4
  • VISIBLE_APP_ADJ 1 可见进程(Visible process) 5
  • FOREGROUND_APP_ADJ 0 前台进程(Foreground process) 6
  • PERSISTENT_SERVICE_ADJ -11 关联着系统或persistent进程
  • PERSISTENT_PROC_ADJ -12 系统persistent进程,比如telephony
  • SYSTEM_ADJ -16 系统进程
  • NATIVE_ADJ -17 native进程(不被系统管理)

更新进程adj值的方法定义在ActivityManagerService中,分别为:

  • updateOomAdjLocked:更新adj,当目标进程为空,或者被杀则返回false;否则返回true;
  • computeOomAdjLocked:计算adj,返回计算后RawAdj值;
  • applyOomAdjLocked:应用adj,当需要杀掉目标进程则返回false;否则返回true。

那么进程的adj值什么时候会被更新呢?🤔

Activity

  • ActivityManagerService.realStartActivityLocked: 启动Activity
  • ActivityStack.resumeTopActivityInnerLocked: 恢复栈顶Activity
  • ActivityStack.finishCurrentActivityLocked: 结束当前Activity
  • ActivityStack.destroyActivityLocked: 摧毁当前Activity

Service

  • ActiveServices.realStartServiceLocked: 启动服务
  • ActiveServices.bindServiceLocked: 绑定服务(只更新当前app)
  • ActiveServices.unbindServiceLocked: 解绑服务 (只更新当前app)
  • ActiveServices.bringDownServiceLocked: 结束服务 (只更新当前app)
  • ActiveServices.sendServiceArgsLocked: 在bringup或则cleanup服务过程调用 (只更新当前app)

BroadcastReceiver

  • BroadcastQueue.processNextBroadcast: 处理下一个广播
  • BroadcastQueue.processCurBroadcastLocked: 处理当前广播
  • BroadcastQueue.deliverToRegisteredReceiverLocked: 分发已注册的广播 (只更新当前app)

ContentProvider

  • ActivityManagerService.removeContentProvider: 移除provider
  • ActivityManagerService.publishContentProviders: 发布provider (只更新当前app)
  • ActivityManagerService.getContentProviderImpl: 获取provider (只更新当前app)

另外,Lowmemorykiller也会根据当前的内存情况逐级进行进程释放,一共有六个级别(上面加粗的部分):

  • CACHED_APP_MAX_ADJ
  • CACHED_APP_MIN_ADJ
  • BACKUP_APP_ADJ
  • PERCEPTIBLE_APP_ADJ
  • VISIBLE_APP_ADJ
  • FOREGROUND_APP_ADJ

定义在ActivityManager.java文件,process_state划分18类,从-1到16之间取值

  • PROCESS_STATE_CACHED_EMPTY 16 进程处于cached状态,且为空进程
  • PROCESS_STATE_CACHED_ACTIVITY_CLIENT 15 进程处于cached状态,且为另一个cached进程(内含Activity)的client进程
  • PROCESS_STATE_CACHED_ACTIVITY 14 进程处于cached状态,且内含Activity
  • PROCESS_STATE_LAST_ACTIVITY 13 后台进程,且拥有上一次显示的Activity
  • PROCESS_STATE_HOME 12 后台进程,且拥有home Activity
  • PROCESS_STATE_RECEIVER 11 后台进程,且正在运行receiver
  • PROCESS_STATE_SERVICE 10 后台进程,且正在运行service
  • PROCESS_STATE_HEAVY_WEIGHT 9 后台进程,但无法执行restore,因此尽量避免kill该进程
  • PROCESS_STATE_BACKUP 8 后台进程,正在运行backup/restore操作
  • PROCESS_STATE_IMPORTANT_BACKGROUND 7 对用户很重要的进程,用户不可感知其存在
  • PROCESS_STATE_IMPORTANT_FOREGROUND 6 对用户很重要的进程,用户可感知其存在
  • PROCESS_STATE_TOP_SLEEPING 5 与PROCESS_STATE_TOP一样,但此时设备正处于休眠状态
  • PROCESS_STATE_FOREGROUND_SERVICE 4 拥有给一个前台Service
  • PROCESS_STATE_BOUND_FOREGROUND_SERVICE 3 拥有给一个前台Service,且由系统绑定
  • PROCESS_STATE_TOP 2 拥有当前用户可见的top Activity
  • PROCESS_STATE_PERSISTENT_UI 1 persistent系统进程,并正在执行UI操作
  • PROCESS_STATE_PERSISTENT 0 persistent系统进程
  • PROCESS_STATE_NONEXISTENT -1 不存在的进程

根据上面说描述的adj值和state值,我们又可以按照重要性程度的不同,将进程划分为五级:

前台进程

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

  • 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
  • 托管某个 Service,后者绑定到用户正在交互的 Activity
  • 托管正在“前台”运行的 Service(服务已调用 startForeground())
  • 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
  • 托管正执行其 onReceive() 方法的 BroadcastReceiver

通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:

  • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
  • 托管绑定到可见(或前台)Activity 的 Service。

可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

服务进程

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关
心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

后台进程

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

空进程

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

本文转载自: 掘金

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

Python web 开发框架 Pyramid

发表于 2018-01-19

博客地址:https://ask.hellobi.com/blog/zhiji 欢迎大家来交流学习。

在Python web 开发框架里有多种选择,有**Django、Tornado、Flask、web2py、Pylons、Pyramid等等,之前写过Django、Tornado,今天我们主要学习Pyramid**,还是从官方文档学起。

官方文档 = 新华字典

  • 官方文档
  • 英文教程:docs.pylonsproject.org/projects/py…
  • Creating Your First Pyramid Application

#1.Pyramid
Pyramid 是 Pylons 项目下面一系列已经发行的软件中的一员。**Pylons 官网**描述了 Pyramid 和 Pylons Project 的关系。

Pyramid以其高效率和快节奏的开发能力而出名。官方文档是这样描述的:Pyramid is a small, fast, down-to-earth Python web framework. It is developed as part of the Pylons Project. It is licensed under a BSD-like license. 此开源Web框架有一个独立于平台的MVC结构,提供了开发的最简途径。此外,它还是高效开发重用代码的首选平台之一。

##Pyramid 和其他 web 框架(由Pyramid英文文档翻译)
第一个 Pyramid 版本的前生(叫做 repoze.bfg )创建于 2008 年。2010 年末,我们把 repoze.bfg 改名为 Pyramid 并于同年11月份合并到 Pylons 项目中。

Pyramid 的灵感来源于 Zope、Pylons 1.0 和 Django ,最终,Pyramid 向它们各自借鉴一些概念和特性并组成一个独特的框架。

Pyramid 的许多特性都要追溯到 Zope 。像 Zope 应用程序一样,Pyramid 应用程序是易于扩展的:如果你遵守一定的规则,那么你的应用程序将会被重用、改进、重构,甚至被第三方开发者扩展而不用 fork 原始程序。 Traversal 和 declarative security 等概念都是在 Zope 中首先被提出来的。

Pyramid 的 URL dispatch 概念受 Pylons 1.0 版本的 Routes 系统启发的,Pyramid 像 Pylons 1.0 版本一样采用自由政策。它没有指定你应该使用哪个数据库,它的内置模板只是为了方便。实际上,它仅仅提供一种将 URL 映射到 view 代码上的机制,以及调用那些 views 的规则。你可以免费使用第三方组件来满足你项目的需求。

Pyramid 经常使用的 view 这一概念来自 Django 。Pyramid 的文档风格比起 Zope 更像 Django 。

类似 Pylons 1.0 版本,却不像 Zope ,一个 Pyramid 应用程序开发者可以使用一个完整的语句命令去执行一个框架常用的配置任务比如增加一个 view 或者一个 route 。在 Zope 里面 ZCML 也有类似功能。Pyramid 支持随时可用的命令语句配置和基于修饰符的配置;ZCML 通过一个扩展包 pyramd_zcml 使用。

既不像 Zope ,也不像 “full-stack” 这样的框架比如 Django ,Pyramid 对于你使用哪一种持续化的机制构建应用程序不做任何假设。Zope 应用程序依赖于 ZODB;Pyramid 也允许你创建 ZODB 程序但却不依赖 ZODB 本身。同样,Django 倾向于假定你想要把你的应用程序数据存储在一个关系型数据库中。Pyramid 从不做这些假设,它允许你使用关系型数据库但是不鼓励也不阻止的决定。

其他的 Python web framework 都宣称他们自己是一个类成员 web framework 叫做model-view-controller 框架,Pyramid 也属于这一类。

##框架 VS 库 (由Pyramid英文文档翻译)

一个 框架 和一个 库 最大的区别在于:库里面的代码被你写的代码 调用 ,而框架则是 调用 你写的代码。使用一系列的库来创建应用程序通常在刚开始的时候要比使用框架简单,因为你可以有选择性地放弃一些控制权给不是你写的库代码。但是当你使用一个框架的时候,你必须放弃绝大部分的控制权交给那些不是你写的代码:整个框架。你不是必须使用一个框架来创建一个 WEB 应用程序在使用 Python 的情况下。一大批丰富的库都被已经被开发出来。然而在实际应用中,使用框架去创建应用要比使用一系列的库更加实用,如果这个框架提供的一些列功能都符合你的项目要求。

#2.Pyramid的安装
官网讲的还是蛮清楚的,照着来就行。

  • 安装setuptools
    下载**ez_setup.py**(进入该页面后网页另存为ez_setup.py即可,记住存在D;\python目录下)。cmd进入D:\python目录,执行
1
复制代码python ez_setup.py

  • 安装virtualenv
    用python目录下的Script/easy_install程序安装virtualenv:
1
复制代码easy_install virtualenv
  • 用virtualenv创建工作区
1
复制代码virtualenv --no-site-packages env
  • 安装pyramid
    执行完上面的步骤后应该多了一个env文件,cd env文件夹,然后执行
1
复制代码easy_install pyramid

#3.Pyramid使用

###1.创建第一个pyramid应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response


def hello_world(request):
return Response('Hello %(name)s!'%request.matchdict)

if __name__ == '__main__':
#创建了一个Configuration类的实例config
config = Configurator()
#注册了一个以/hello/开头的URL路由,路由的名字就叫'hello'
config.add_route('hello','/hello/{name}')
#注册了一个view callable函数 URL Path(比如/hello/world)->route(比如'hello')->view callable(比如hello_world函数)
config.add_view(hello_world,route_name='hello')
#pyramid.config.Configurator.make_wsgi_app()方法来创建WSGI应用程序
app = config.make_wsgi_app()
#启动了一个WSGI服务
server = make_server('0.0.0.0',8080,app)
#serve_forever()方法创建了一个循环来接受外界的request请求
server.serve_forever()

打开你的浏览器,输运行运行入http://localhost:8080/hello/world,将会显示“Hello world!”。
现在我们对这个示例程序有了一个基本的认识,接下来一步一步分析它是如何工作的。

  • Imports 包
    第2行引入了pyramid.config模块的Configurator类,第10行创建了它的一个实例,然后通过这个实例来配置我们的应用。
    跟其他Python web框架一样,Pyramid 用 WSGI协议来将一个应用程序和web服务器联系到一起。而第一行用到的wsgiref模块就是WSGI服务的一种封装,现在wsgiref已经被引入Python 标准库了。
    第三行引入了pyramid.response.Response,用来返回response信息。
  • View Callable 声明
    第六行定义了一个hello_world函数,传入request参数,返回pyramid.response.Response类的一个实例。通过调用request对象的matchdict方法来输入匹配到的name路径。由于我们访问的是http://localhost:8080/hello/world,所以匹配到的是world并以“hello world”的字符串返回,如果你访问的是http://localhost:8080/hello/pyramid,那么返回的将是“hello pyramid!”。
    这个函数被称为 view callable(你可以叫它视图调用,但我还是觉得用英文的比较好)。 一个视图调用 接受一个参数:request 。 它将返回一个response对象。 一个view callable不一定是一个函数,也可以是一个类或一个实例, 但是这里为了简单起见,我们用了函数。
    一个view callable总是伴随着调用 request对象。 一个request对象就代表着一个通过被激活的WSGI服务器传送到pyramid的HTTP请求。
    一个view callable还需要返回一个response对象。因为一个response对象拥有所有来制定一个实际的HTTP 响应所必要的信息。这个对象通过 wsgi服务器,也就是Pyramid,转化为文本信息发送回请求的浏览器。为了返回一个response,每个view callable创建的一个response实例。在 hello_world函数中, 一个字符串作为response的body来返回。
  • Application Configuration 应用程序配置
    第10-15行是应用程序的配置信息。
    第10行创建了一个Configuration类的实例config,通过这个实例来对我们的Pyramid应用进行配置,包括路由,ip,端口等信息。调用config的各种方法设置应用程序注册表(application registry),对我们的应用程序进行注册。什么是application registry?下面是官方解释:

第11行调用pyramid.config.Configurator.add_route()方法,注册了一个以/hello/开头的URL路由,路由的名字就叫’hello’。
第12行config.add_v运行iew(hello_world, route_name=’hello’),注册了一个view callable函数(也就是hello_world函数),当名为’hello’的路由被匹配时应该调用这个函数。 这三者的对应关系也就是URL Path(比如/hello/world)->route(比如’hello’)->view callable(比如hello_world函数)。这样,一个前台页面就和一个后台处理方法对应起来了。
WSGI Application Creation 创建WSGI应用程序
当所有的配置工作完成后,python脚本通过pyramid.config.Configurator.make_wsgi_app()方法来创建WSGI应用程序。这个方法返回一个WSGI应用程序对象并传递给app引用,让WSGI服务器来使用。WSGI是一个让服务器能和python应用程序交流的协议。这里不打算深入探讨WSGI,如果你有兴趣,可以去官网wsgi.org了解更多。
WSGI Application Serving
最后两行,我们启动了一个WSGI服务。make_server(‘0.0.0.0’,8080,app)方法绑定了ip和端口,最后一个参数传递我们的app对象(一个router),也就是我们想服务的那个应用程序。serve_forever()方法创建了一个循环来接受外界的request请求。

运行测试

#4.Pyramid部分语法

  • locals()用法:locals()可以直接将函数中所有的变量全部传给模板。当然这可能会传递一些多余的参数,有点浪费内存的嫌疑。
  • render()方法是render_to_response的一个崭新的快捷方式,前者会自动使用RequestContext。而后者必须coding出来,这是最明显的区别,当然前者更简洁。
1
2
3
4
5
6
复制代码return render_to_response('blog_add.html',{'blog': blog, 
'form': form, 'id': id, 'tag': tag},
context_instance=RequestContext(request))

return render(request, 'blog_add.html',
{'blog': blog, 'form': form, 'id': id, 'tag': tag})

本文转载自: 掘金

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

一次Nodejs的性能问题排查

发表于 2018-01-19

前几天,我们发现了一个性能问题,当数据量过大时,由于请求超时,导致我们的web应用的某个页面数据加载不出来。这篇文章用来记录下当时的排查过程,以及一些思考。

初步定位问题

通过查看请求日志,我们定位到了Node的某个接口。一开始,我们认为是MySQL的IO耗时过长导致的,但是通过查看MySQL的慢日志并没有发现相关的查询语句,从而排除了MySQL查询的因素。

由于当数据量过大时,才出现的这个问题,所以我们将怀疑点转移到了嵌套的2层for循环中。通过在测试环境进行打点输出执行时间,发现确实是这2层for循环导致的,在数据量过大时,消耗了将近20s。我们知道Node不擅长做CPU密集型计算,所以在循环里面并没有做复杂的计算逻辑,只是一些判断和object的拆解、组合,为什么会消耗这么长的时间呢?

定位问题细节

为了方便测试,我将数据导了一份到本地,然后在本地写了一个sample来进行测试和进一步定位。通过WebStorm自带的V8 profiling工具,执行并分析了这段代码。下面是V8 profiling log的分析图:

WX20180118-115135.png

通过这张图片可以看出来,cpu耗时最高的是lodash中的名为copyObject的方法。逐个检查代码中用到了lodash的地方,最终定位到了_.defaults方法。由于我们object的key是固定的,所以我们将_.defaults方法去掉了,直接赋值给object的相应key,像下面这样:

1
2
3
4
5
复制代码const res = {
'key1': object.key1 || source1.key1 || source2.key1,
'key2': object.key2 || source1.key2 || source2.key2,
'key3': object.key3 || source1.key3 || source2.key3,
}

修改后,我们成功使该接口的请求时间降低到了8~9s。

进一步优化

其实,8~9s的耗时也是无法接受的。当然优化要一步一步地来,接下来我们准备做的方案就是将数据拆分返回,类似分页查询那样,这样的话,每个接口的请求耗时都比较小,只不过前端需要多发送几次请求而已。

思考 —— defaults方法剖析

问题虽然解决了,但是秉着一颗与源码死磕到底的心,我还是想去源码里面看看到底是什么消耗了过多的CPU时间。下面是defaults方法的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码function defaults(object, ...sources) {
object = Object(object)
sources.forEach((source) => {
if (source != null) {
source = Object(source)
for (const key in source) {
const value = object[key]
if (value === undefined ||
(eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))) {
object[key] = source[key]
}
}
}
})
return object
}

可以看到,首先他为了防止传入的数据不是object,而分别对object,source调用了Object()方法。然后用forEach()方法循环sources这个object数组。接着循环source中的每一个key,查看校验object中有没有该key值,如果没有,则赋值:object[key] = source[key]

可优化点:

  • object()方法在我们的使用场景下,没有必要,因为传入的必然是object。
  • forEach()方法的执行效率,比普通的for(let i = 0; i < arr.length; i ++)要低不少。
  • (eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))这一部分的判断,在我们的使用场景下,也没有必要。

总结

最后,作为一个完备、全面的第三方库来说,适应各种情况的输入值并做完整的参数校验是很有必要的。所以当你需要使用某个第三方库的时候,不妨想一想是否有必要,是否可以直接用更简单直接的方式实现。有时候,最直接的方式,也许是最快的 :)。

下一篇:Java:关于值传递你需要了解的事情

本文转载自: 掘金

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

SpringBoot+Vue前后端分离,使用SpringSe

发表于 2018-01-18

当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异。笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方案,希望这个系列能够给小伙伴一些帮助。本系列文章并不是手把手的教程,主要介绍了核心思路并讲解了核心代码,完整的代码小伙伴们可以在GitHub上star并clone下来研究。另外,原本计划把项目跑起来放到网上供小伙伴们查看,但是之前买服务器为了省钱,内存只有512M,两个应用跑不起来(已经有一个V部落开源项目在运行),因此小伙伴们只能将就看一下下面的截图了,GitHub上有部署教程,部署到本地也可以查看完整效果。


项目地址:https://github.com/lenve/vhr

上篇文章我们对项目做了一个整体的介绍,从本文开始,我们就来实现我们的权限管理模块。由于前后端分离,因此我们先来完成后台接口,完成之后,可以先用POSTMAN或者RESTClient等工具进行测试,测试成功之后,我们再来着手开发前端。

本文是本系列的第二篇,建议先阅读前面的文章有助于更好的理解本文:

1.SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(一)

创建SpringBoot项目

在IDEA中创建SpringBoot项目,创建完成之后,添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码`<dependencies>`
   <dependency>
       <groupId>org.mybatis.spring.boot</groupId>
       <artifactId>mybatis-spring-boot-starter</artifactId>
       <version>1.3.1</version>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid</artifactId>
       <version>1.0.29</version>
   </dependency>
   <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
   </dependency>
</dependencies>

这些都是常规的依赖,有SpringBoot、SpringSecurity、Druid数据库连接池,还有数据库驱动。

然后在application.properties中配置数据库,如下:

1
2
3
4
5
6
复制代码`spring.datasource.type=com.alibaba.druid.pool.DruidDataSource`
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/vhr?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123

server.port=8082

OK,至此,我们的工程就创建好了。

创建Hr和HrService

首先我们需要创建Hr类,即我们的用户类,该类实现了UserDetails接口,该类的属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码`public class Hr implements UserDetails {`
   private Long id;
   private String name;
   private String phone;
   private String telephone;
   private String address;
   private boolean enabled;
   private String username;
   private String password;
   private String remark;
   private List<Role> roles;
   private String userface;
   //getter/setter省略
}

如果小伙伴对属性的含义有疑问,可以参考1.权限数据库设计.

UserDetails接口默认有几个方法需要实现,这几个方法中,除了isEnabled返回了正常的enabled之外,其他的方法我都统一返回true,因为我这里的业务逻辑并不涉及到账户的锁定、密码的过期等等,只有账户是否被禁用,因此只处理了isEnabled方法,这一块小伙伴可以根据自己的实际情况来调整。另外,UserDetails中还有一个方法叫做getAuthorities,该方法用来获取当前用户所具有的角色,但是小伙伴也看到了,我的Hr中有一个roles属性用来描述当前用户的角色,因此我的getAuthorities方法的实现如下:

1
2
3
4
5
6
7
复制代码`public Collection<? extends GrantedAuthority> getAuthorities() {`
   List<GrantedAuthority> authorities = new ArrayList<>();
   for (Role role : roles) {
       authorities.add(new SimpleGrantedAuthority(role.getName()));
   }
   return authorities;
}

即直接从roles中获取当前用户所具有的角色,构造SimpleGrantedAuthority然后返回即可。

创建好Hr之后,接下来我们需要创建HrService,用来执行登录等操作,HrService需要实现UserDetailsService接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码`@Service`
@Transactional
public class HrService implements UserDetailsService {

   @Autowired
   HrMapper hrMapper;

   @Override
   public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
       Hr hr = hrMapper.loadUserByUsername(s);
       if (hr == null) {
           throw new UsernameNotFoundException("用户名不对");
       }
       return hr;
   }
}

这里最主要是实现了UserDetailsService接口中的loadUserByUsername方法,在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户不存在,则抛出UsernameNotFoundException异常,否则直接将查到的Hr返回。HrMapper用来执行数据库的查询操作,这个不在本系列的介绍范围内,所有涉及到数据库的操作都将只介绍方法的作用。

自定义FilterInvocationSecurityMetadataSource

FilterInvocationSecurityMetadataSource有一个默认的实现类DefaultFilterInvocationSecurityMetadataSource,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色,我们照猫画虎,自己也定义一个FilterInvocationSecurityMetadataSource,如下:

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
复制代码`@Component`
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
   @Autowired
   MenuService menuService;
   AntPathMatcher antPathMatcher = new AntPathMatcher();

   @Override
   public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
       //获取请求地址
       String requestUrl = ((FilterInvocation) o).getRequestUrl();
       if ("/login_p".equals(requestUrl)) {
           return null;
       }
       List<Menu> allMenu = menuService.getAllMenu();
       for (Menu menu : allMenu) {
           if (antPathMatcher.match(menu.getUrl(), requestUrl)&&menu.getRoles().size()>0) {
               List<Role> roles = menu.getRoles();
               int size = roles.size();
               String[] values = new String[size];
               for (int i = 0; i < size; i++) {
                   values[i] = roles.get(i).getName();
               }
               return SecurityConfig.createList(values);
           }
       }
       //没有匹配上的资源,都是登录访问
       return SecurityConfig.createList("ROLE_LOGIN");
   }

   @Override
   public Collection<ConfigAttribute> getAllConfigAttributes() {
       return null;
   }

   @Override
   public boolean supports(Class<?> aClass) {
       return FilterInvocation.class.isAssignableFrom(aClass);
   }
}

关于自定义这个类,我说如下几点:

1.一开始注入了MenuService,MenuService的作用是用来查询数据库中url pattern和role的对应关系,查询结果是一个List集合,集合中是Menu类,Menu类有两个核心属性,一个是url pattern,即匹配规则(比如/admin/**),还有一个是List
,即这种规则的路径需要哪些角色才能访问。

2.我们可以从getAttributes(Object o)方法的参数o中提取出当前的请求url,然后将这个请求url和数据库中查询出来的所有url pattern一一对照,看符合哪一个url pattern,然后就获取到该url pattern所对应的角色,当然这个角色可能有多个,所以遍历角色,最后利用SecurityConfig.createList方法来创建一个角色集合。

3.第二步的操作中,涉及到一个优先级问题,比如我的地址是/employee/basic/hello,这个地址既能被 /employee/**匹配,也能被/employee/basic/**匹配,这就要求我们从数据库查询的时候对数据进行排序,将 /employee/basic/**类型的url pattern放在集合的前面去比较。

4.如果getAttributes(Object o)方法返回null的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。但是在我的整个业务中,并不存在这样的请求,我这里的要求是,所有未匹配到的路径,都是认证(登录)后可访问,因此我在这里返回一个ROLE_LOGIN的角色,这种角色在我的角色数据库中并不存在,因此我将在下一步的角色比对过程中特殊处理这种角色。

5.如果地址是/login_p,这个是登录页,不需要任何角色即可访问,直接返回null。

6.getAttributes(Object o)方法返回的集合最终会来到AccessDecisionManager类中,接下来我们再来看AccessDecisionManager类。

自定义AccessDecisionManager

自定义UrlAccessDecisionManager类实现AccessDecisionManager接口,如下:

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
复制代码`@Component`
public class UrlAccessDecisionManager implements AccessDecisionManager {
   @Override
   public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, AuthenticationException {
       Iterator<ConfigAttribute> iterator = collection.iterator();
       while (iterator.hasNext()) {
           ConfigAttribute ca = iterator.next();
           //当前请求需要的权限
           String needRole = ca.getAttribute();
           if ("ROLE_LOGIN".equals(needRole)) {
               if (authentication instanceof AnonymousAuthenticationToken) {
                   throw new BadCredentialsException("未登录");
               } else
                   return;
           }
           //当前用户所具有的权限
           Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
           for (GrantedAuthority authority : authorities) {
               if (authority.getAuthority().equals(needRole)) {
                   return;
               }
           }
       }
       throw new AccessDeniedException("权限不足!");
   }

   @Override
   public boolean supports(ConfigAttribute configAttribute) {
       return true;
   }

   @Override
   public boolean supports(Class<?> aClass) {
       return true;
   }
}

关于这个类,我说如下几点:

1.decide方法接收三个参数,其中第一个参数中保存了当前登录用户的角色信息,第三个参数则是UrlFilterInvocationSecurityMetadataSource中的getAttributes方法传来的,表示当前请求需要的角色(可能有多个)。

2.如果当前请求需要的权限为ROLE_LOGIN则表示登录即可访问,和角色没有关系,此时我需要判断authentication是不是AnonymousAuthenticationToken的一个实例,如果是,则表示当前用户没有登录,没有登录就抛一个BadCredentialsException异常,登录了就直接返回,则这个请求将被成功执行。

3.遍历collection,同时查看当前用户的角色列表中是否具备需要的权限,如果具备就直接返回,否则就抛异常。

4.这里涉及到一个all和any的问题:假设当前用户具备角色A、角色B,当前请求需要角色B、角色C,那么是要当前用户要包含所有请求角色才算授权成功还是只要包含一个就算授权成功?我这里采用了第二种方案,即只要包含一个即可。小伙伴可根据自己的实际情况调整decide方法中的逻辑。

自定义AccessDeniedHandler

通过自定义AccessDeniedHandler我们可以自定义403响应的内容,如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码`@Component`
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
   @Override
   public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
       resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
       resp.setCharacterEncoding("UTF-8");
       PrintWriter out = resp.getWriter();
       out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
       out.flush();
       out.close();
   }
}

配置WebSecurityConfig

最后在webSecurityConfig中完成简单的配置即可,如下:

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
复制代码`@Configuration`
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   HrService hrService;
   @Autowired
   UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
   @Autowired
   UrlAccessDecisionManager urlAccessDecisionManager;
   @Autowired
   AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(hrService);
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
       web.ignoring().antMatchers("/index.html", "/static/**");
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
               .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                   @Override
                   public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                       o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                       o.setAccessDecisionManager(urlAccessDecisionManager);
                       return o;
                   }
               }).and().formLogin().loginPage("/login_p").loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password").permitAll().failureHandler(new AuthenticationFailureHandler() {
           @Override
           public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
               httpServletResponse.setContentType("application/json;charset=utf-8");
               PrintWriter out = httpServletResponse.getWriter();
               StringBuffer sb = new StringBuffer();
               sb.append("{\"status\":\"error\",\"msg\":\"");
               if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
                   sb.append("用户名或密码输入错误,登录失败!");
               } else if (e instanceof DisabledException) {
                   sb.append("账户被禁用,登录失败,请联系管理员!");
               } else {
                   sb.append("登录失败!");
               }
               sb.append("\"}");
               out.write(sb.toString());
               out.flush();
               out.close();
           }
       }).successHandler(new AuthenticationSuccessHandler() {
           @Override
           public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
               httpServletResponse.setContentType("application/json;charset=utf-8");
               PrintWriter out = httpServletResponse.getWriter();
               ObjectMapper objectMapper = new ObjectMapper();
               String s = "{\"status\":\"success\",\"msg\":" + objectMapper.writeValueAsString(HrUtils.getCurrentHr()) + "}";
               out.write(s);
               out.flush();
               out.close();
           }
       }).and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler);
   }
}

关于这个配置,我说如下几点:

1.在configure(HttpSecurity http)方法中,通过withObjectPostProcessor将刚刚创建的UrlFilterInvocationSecurityMetadataSource和UrlAccessDecisionManager注入进来。到时候,请求都会经过刚才的过滤器(除了configure(WebSecurity web)方法忽略的请求)。

2.successHandler中配置登录成功时返回的JSON,登录成功时返回当前用户的信息。

3.failureHandler表示登录失败,登录失败的原因可能有多种,我们根据不同的异常输出不同的错误提示即可。

OK,这些操作都完成之后,我们可以通过POSTMAN或者RESTClient来发起一个登录请求,看到如下结果则表示登录成功:

关注公众号,可以及时接收到最新文章:

本文转载自: 掘金

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

Python进程VS线程

发表于 2018-01-17

#1.进程和线程
队列:
1、进程之间的通信: q = multiprocessing.Queue()
2、进程池之间的通信: q = multiprocessing.Manager().Queue()
3、线程之间的通信: q = queue.Queue()
##1.功能

  • 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
  • 线程,能够完成多任务,比如 一个QQ中的多个聊天窗口

##2.定义的不同

  • 进程是系统进行资源分配和调度的一个独立单位.
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

##3.区别

  • 一个程序至少有一个进程,一个进程至少有一个线程.
  • 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  • 线程不能够独立执行,必须依存在进程中

##4.优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
#2.同步的概念
##1.多线程开发可能遇到的问题
假设两个线程t1和t2都要对num=0进行增1运算,t1和t2都各对num修改10次,num的最终的结果应该为20。
但是由于是多线程访问,有可能出现下面情况:
在num=0时,t1取得num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得num=0。然后t2对得到的值进行加1并赋给num,使得num=1。然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给num。这样,明明t1和t2都完成了1次加1工作,但结果仍然是num=1。

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
复制代码from threading import Thread
import time

g_num = 0

def test1():
global g_num
for i in range(1000000):
g_num += 1

print("---test1---g_num=%d"%g_num)

def test2():
global g_num
for i in range(1000000):
g_num += 1

print("---test2---g_num=%d"%g_num)


p1 = Thread(target=test1)
p1.start()

# time.sleep(3) #取消屏蔽之后 再次运行程序,结果的不同

p2 = Thread(target=test2)
p2.start()

print("---g_num=%d---"%g_num)

运行结果却不是2000000:

1
2
3
复制代码---g_num=129699---
---test2---g_num=1126024
---test1---g_num=1135562

取消屏蔽之后,再次运行结果如下:

1
2
3
复制代码---test1---g_num=1000000
---g_num=1025553---
---test2---g_num=2000000

问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。
##2.同步

  • 同步就是协同步调,按预定的先后次序进行运行。
  • 如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

##3.解决线程不安全的方法
可以通过线程同步来解决

  1. 系统调用t1,然后获取到num的值为0,此时上一把锁,即不允许其他现在操作num
  2. 对num的值进行+1
  3. 解锁,此时num的值为1,其他的线程就可以使用num了,而且是num的值不是0而是1
  4. 同理其他线程在对num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性

#3.互斥锁

  • 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
    线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
  • 互斥锁为资源引入一个状态:锁定/非锁定。
  • 某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
  • threading模块中定义了Lock类,可以方便的处理锁定:*
1
2
3
4
5
6
复制代码#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([blocking])
#释放
mutex.release()

其中,锁定方法acquire可以有一个blocking参数。

  • 如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
  • 如果设定blocking为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
复制代码from threading import Thread, Lock
import time
g_num = 0
def test1():
global g_num
for i in range(1000000):
#True表示堵塞 即如果这个锁在上锁之前已经被上锁了,那么这个线程会在这里一直等待到解锁为止
#False表示非堵塞,即不管本次调用能够成功上锁,都不会卡在这,而是继续执行下面的代码
mutexFlag = mutex.acquire(True)
if mutexFlag:
g_num += 1
mutex.release()

print("---test1---g_num=%d"%g_num)
def test2():
global g_num
for i in range(1000000):
mutexFlag = mutex.acquire(True) #True表示堵塞
if mutexFlag:
g_num += 1
mutex.release()

print("---test2---g_num=%d"%g_num)
#创建一个互斥锁
#这个锁默认是未上锁的状态
mutex = Lock()
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
print("---g_num=%d---"%g_num)

运行结果:

1
2
3
复制代码---g_num=19446---
---test1---g_num=1699950
---test2---g_num=2000000

加入互斥锁后,运行结果与预期相符。
我们可以模拟一下卖票的程序:

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
复制代码# Python主要通过标准库中的threading包来实现多线程
import threading
import time
import os
def doChore(): # 作为间隔 每次调用间隔0.5s
time.sleep(0.5)
def booth(tid):
global i
global lock
while True:
lock.acquire() # 得到一个锁,锁定
if i != 0:
i = i - 1 # 售票 售出一张减少一张
print(tid, ':now left:', i) # 剩下的票数
doChore()
else:
print("Thread_id", tid, " No more tickets")
os._exit(0) # 票售完 退出程序
lock.release() # 释放锁
doChore()
#全局变量
i = 15 # 初始化票数
lock = threading.Lock() # 创建锁
def main():
# 总共设置了3个线程
for k in range(3):
# 创建线程; Python使用threading.Thread对象来代表线程
new_thread = threading.Thread(target=booth, args=(k,))
# 调用start()方法启动线程
new_thread.start()
if __name__ == '__main__':
main()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码0 :now left: 14
1 :now left: 13
0 :now left: 12
2 :now left: 11
1 :now left: 10
0 :now left: 9
1 :now left: 8
2 :now left: 7
0 :now left: 6
1 :now left: 5
2 :now left: 4
0 :now left: 3
2 :now left: 2
0 :now left: 1
1 :now left: 0
Thread_id 2 No more tickets
  • 上锁解锁过程
    当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
    每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
    线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
  • 锁的好处:*
  • 确保了某段关键代码只能由一个线程从头到尾完整地执行
    锁的坏处:
  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

#4.多线程-非共享数据
对于多线程中全局变量和局部变量是否共享

  • 多线程局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码#coding=utf-8
import threading
import time

class MyThread(threading.Thread):
# 重写 构造方法
def __init__(self,num,sleepTime):
threading.Thread.__init__(self)
self.num = num
self.sleepTime = sleepTime

def run(self):
self.num += 1
time.sleep(self.sleepTime)
print('线程(%s),num=%d'%(self.name, self.num))

if __name__ == '__main__':
mutex = threading.Lock()
t1 = MyThread(100,5)
t1.start()
t2 = MyThread(200,1)
t2.start()

运行结果:

1
2
复制代码线程(Thread-2),num=201
线程(Thread-1),num=101
  • 多线程全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码import threading
from time import sleep
def test(sleepTime):
num = 1
sleep(sleepTime)
num+=1
print('---(%s)--num=%d'%(threading.current_thread(), num))
if __name__ == '__main__':
t1 = threading.Thread(target = test,args=(5,))
t2 = threading.Thread(target = test,args=(1,))

t1.start()
t2.start()

运行结果:

1
2
复制代码---(<Thread(Thread-2, started 10876)>)--num=2
---(<Thread(Thread-1, started 7484)>)--num=2
  • 在多线程开发中,全局变量是多个线程都共享的数据,而局部变量等是各自线程的,是非共享的

#5.同步应用

  • 多个线程有序执行
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
复制代码from threading import Thread,Lock
from time import sleep
class Task1(Thread):
def run(self):
while True:
if lock1.acquire():
print("------Task 1 -----")
sleep(0.5)
lock2.release()
class Task2(Thread):
def run(self):
while True:
if lock2.acquire():
print("------Task 2 -----")
sleep(0.5)
lock3.release()
class Task3(Thread):
def run(self):
while True:
if lock3.acquire():
print("------Task 3 -----")
sleep(0.5)
lock1.release()
#使用Lock创建出的锁默认没有“锁上”
lock1 = Lock()
#创建另外一把锁,并且“锁上”
lock2 = Lock()
lock2.acquire()
#创建另外一把锁,并且“锁上”
lock3 = Lock()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
...........`
  • 可以使用互斥锁完成多个任务,有序的进程工作,这就是线程的同步

#6.生产者与消费者模式

  • Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
  • 用FIFO队列实现上述生产者与消费者问题的代码如下:
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
复制代码import threading,time
from queue import Queue
class Producer(threading.Thread):
def run(self):
global queue
count = 0
while True:
if queue.qsize() < 1000:
for i in range(100):
count = count +1
msg = '生成产品'+str(count)
queue.put(msg)
print(msg)
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global queue
while True:
if queue.qsize() > 100:
for i in range(3):
msg = self.name + '消费了 '+queue.get()
print(msg)
time.sleep(1)
if __name__ == '__main__':
queue = Queue()
for i in range(500):
queue.put('初始产品'+str(i))
for i in range(2):
p = Producer()
p.start()
for i in range(5):
c = Consumer()
c.start()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码生成产品1
生成产品2
生成产品1
生成产品3
生成产品2
生成产品4
Thread-3消费了 初始产品0
生成产品3
生成产品5
Thread-3消费了 初始产品1
生成产品4
生成产品6
Thread-4消费了 初始产品2
Thread-3消费了 初始产品3
生成产品5
生成产品7
Thread-4消费了 初始产品4
生成产品6
生成产品8
Thread-5消费了 初始产品5
Thread-4消费了 初始产品6
............

此时就出现生产者与消费者的问题
##1.Queue的说明
1.对于Queue,在多线程通信之间扮演重要的角色
2.添加数据到队列中,使用put()方法
3.从队列中取数据,使用get()方法
4.判断队列中是否还有数据,使用qsize()方法

##2.生产者消费者模式的说明

  • 使用生产者和消费者模式的原因
    在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
  • 生产者消费者模式
    生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
    ##3.ThreadLocal
    在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
    ###1.使用函数传参的方法
1
2
3
4
5
6
7
8
9
10
11
复制代码def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)

说明:用局部变量也有问题,因为每个线程处理不同的Student对象,不能共享。
###2.使用全局字典的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码import threading
# 创建字典对象:
myDict={}
def process_student():
# 获取当前线程关联的student:
std = myDict[threading.current_thread()]
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
myDict[threading.current_thread()] = name
process_student()
t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()

运行结果;

1
2
复制代码Hello, yongGe (in Thread-A)
Hello, 老王 (in Thread-B)

这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点low。
###3.使用ThreadLocal的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('erererbai',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()

运行结果:

1
2
复制代码Hello, erererbai (in Thread-A)
Hello, 老王 (in Thread-B)

说明:
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

  • 一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题

本文转载自: 掘金

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

springboot(十七):使用Spring Boot上传

发表于 2018-01-16

上传文件是互联网中常常应用的场景之一,最典型的情况就是上传头像等,今天就带着带着大家做一个Spring Boot上传文件的小案例。

1、pom包配置

我们使用Spring Boot最新版本1.5.9、jdk使用1.8、tomcat8.0。

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
复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

引入了spring-boot-starter-thymeleaf做页面模板引擎,写一些简单的上传示例。

2、启动类设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码@SpringBootApplication
public class FileUploadWebApplication {

public static void main(String[] args) throws Exception {
SpringApplication.run(FileUploadWebApplication.class, args);
}

//Tomcat large file upload connection reset
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbedded() {
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
tomcat.addConnectorCustomizers((TomcatConnectorCustomizer) connector -> {
if ((connector.getProtocolHandler() instanceof AbstractHttp11Protocol<?>)) {
//-1 means unlimited
((AbstractHttp11Protocol<?>) connector.getProtocolHandler()).setMaxSwallowSize(-1);
}
});
return tomcat;
}

}

tomcatEmbedded这段代码是为了解决,上传文件大于10M出现连接重置的问题。此异常内容GlobalException也捕获不到。

详细内容参考:Tomcat large file upload connection reset

3、编写前端页面

上传页面

1
2
3
4
5
6
7
8
9
10
复制代码<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h1>Spring Boot file upload example</h1>
<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="file" /><br/><br/>
<input type="submit" value="Submit" />
</form>
</body>
</html>

非常简单的一个Post请求,一个选择框选择文件,一个提交按钮,效果如下:

上传结果展示页面:

1
2
3
4
5
6
7
8
9
复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<h1>Spring Boot - Upload Status</h1>
<div th:if="${message}">
<h2 th:text="${message}"/>
</div>
</body>
</html>

效果图如下:

4、编写上传控制类

访问localhost自动跳转到上传页面:

1
2
3
4
复制代码@GetMapping("/")
public String index() {
return "upload";
}

上传业务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码@PostMapping("/upload") 
public String singleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:uploadStatus";
}

try {
// Get the file and save it somewhere
byte[] bytes = file.getBytes();
Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
Files.write(path, bytes);

redirectAttributes.addFlashAttribute("message",
"You successfully uploaded '" + file.getOriginalFilename() + "'");

} catch (IOException e) {
e.printStackTrace();
}

return "redirect:/uploadStatus";
}

上面代码的意思就是,通过MultipartFile读取文件信息,如果文件为空跳转到结果页并给出提示;如果不为空读取文件流并写入到指定目录,最后将结果展示到页面。

MultipartFile是Spring上传文件的封装类,包含了文件的二进制流和文件属性等信息,在配置文件中也可对相关属性进行配置,基本的配置信息如下:

  • spring.http.multipart.enabled=true #默认支持文件上传.
  • spring.http.multipart.file-size-threshold=0 #支持文件写入磁盘.
  • spring.http.multipart.location=# 上传文件的临时目录
  • spring.http.multipart.max-file-size=1Mb # 最大支持文件大小
  • spring.http.multipart.max-request-size=10Mb # 最大支持请求大小

最常用的是最后两个配置内容,限制文件上传大小,上传时超过大小会抛出异常:

更多配置信息参考这里:Common application properties

5、异常处理

1
2
3
4
5
6
7
8
9
复制代码@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MultipartException.class)
public String handleError1(MultipartException e, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("message", e.getCause().getMessage());
return "redirect:/uploadStatus";
}
}

设置一个@ControllerAdvice用来监控Multipart上传的文件大小是否受限,当出现此异常时在前端页面给出提示。利用@ControllerAdvice可以做很多东西,比如全局的统一异常处理等,感兴趣的同学可以下来了解。

6、总结

这样一个使用Spring Boot上传文件的简单Demo就完成了,感兴趣的同学可以将示例代码下载下来试试吧。

参考:

Spring Boot file upload example

示例代码-github

示例代码-码云

本文转载自: 掘金

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

1…900901902…956

开发者博客

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