Blog

字符编码-教程(3)-EASCII及ISO 8859字符编码方案

1、计算机出现之后,首先逐渐从美国发展到了欧洲。由于欧洲很多国家所用到的字符中,除了基本的、美国也用的那128个ASCII字符之外,还有很多衍生的拉丁字母等字符。比如,在法语中,字母上方有注音符号;而欧洲其他国家也有各自特有的字符。

考虑到一个字节能够表示的编码实际上有256个(2^8 = 256),而ASCII字符却只用到了一个字节中的低7位(因此在ASCII码中最高位总是为0),编号为0x00~0x7F(十进制为0~127)。也就是说,ASCII只使用了一个字节所能表示的256个编码中的前128个(2^7 = 128)编码,而后128个编码相当于被闲置了。因此,欧洲各国纷纷打起了后面这128个编码的主意。

2、可问题在于,欧洲各国同时都有这样的想法。于是各国针对后面的0x80~0xFF(十进制为128~255)这128个编码分别对应什么样的字符,就有了各自不同的设计。

为了结束欧洲各国这种各自为政的混乱局面,于是又先后设计了两套统一的,既兼容ASCII码,又支持欧洲各国所使用的那些衍生字符的单字节编码方案:一个是EASCII(Extended ASCII)字符编码方案,另一个是ISO/IEC 8859字符编码方案。

3、先来说EASCII码。EASCII码同样也是将ASCII中闲置的最高位(即首位)用来编码新的字符(这些ASCII字符之外的新字符,其最高位总是为1)。换言之,也就是将一个字节中的全部8个比特位用来表示一个字符。比如,法语中的é的编码为130(二进制1000 0010)。

显然,EASCII码虽与ASCII码一样使用单字节编码,但却可以表示最多256个字符(2^8 = 256),比ASCII的128个字符(2^7=128)多了一倍。

因此,在EASCII码中,当第一个比特位(即字节的最高位)为0时,仍表示之前那些常用的ASCII字符(实际的二进制编码为0000 0000 ~ 0111 1111,对应的十进制就是0~127),而为1时就表示补充扩展的其他衍生字符(实际的二进制编码为1000 0000 ~ 1111 1111,对应的十进制就是128~255)。

这样就在ASCII码的基础上,既保证了对ASCII码的兼容性,又补充扩展了新的字符,于是就称之为Extended ASCII(扩展ASCII)码,简称EASCII码。

EASCII码比ASCII码扩充出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号,如下表所示。

扩展ASCII(EASCII)编码表

 

4、不过,EASCII码目前已经很少使用,常用的是ISO/IEC 8859字符编码方案。该方案与EASCII码类似,也同样是在ASCII码的基础上,利用了ASCII的7位编码所没有用到的最高位(首位),将编码范围从原先ASCII码的0x00~0x7F(十进制为0~127),扩展到了0x80~0xFF(十进制为128~255)。

ISO/IEC 8859字符编码方案所扩展的这128个编码中,实际上只有0xA0~0xFF(十进制为160~255)被实际使用。也就是说,只有0xA0~0xFF(十进制为160~255)这96个编码定义了字符,而0x80~0x9F (十进制为128~159)这32个编码并未定义字符。

显然,ISO/IEC 8859字符编码方案同样是单字节编码方案,也同样完全兼容ASCII。

5、注意,与ASCII、EASCII属于单个独立的字符集不同,ISO/IEC 8859是一组字符集的总称,其下共包含了15个字符集,即ISO/IEC 8859-n,其中n=1,2,3,…,15,16(其中12未定义,所以共15个)。

这15个字符集大致上包括了欧洲各国所使用到的字符(甚至还包括一些外来语字符),而且每一个字符集的补充扩展部分(即除了兼容ASCII字符之外的部分)都只实际使用了0xA0~0xFF(十进制为160~255)这96个编码。

其中,ISO/IEC 8859-1收录了西欧常用字符(包括德法两国的字母),目前使用得最为普遍。ISO/IEC 8859-1往往简称为ISO 8859-1,而且还有一个称之为Latin-1(也写作Latin1)的别名。

6、其余从ISO 8859-2到ISO 8859-16各自所收录的字符如下:

ISO 8859-2字符集,也称为Latin-2,收录了东欧字符;

ISO 8859-3字符集,也称为Latin-3,收录了南欧字符;

ISO 8859-4字符集,也称为Latin-4,收录了北欧字符;

ISO 8859-5字符集,也称为Cyrillic,收录了斯拉夫语系字符;

ISO 8859-6字符集,也称为Arabic,收录了阿拉伯语系字符;

ISO 8859-7字符集,也称为Greek,收录了希腊字符;

ISO 8859-8字符集,也称为Hebrew,收录了西伯莱(犹太人)字符;

ISO 8859-9字符集,也称为Latin-5或Turkish,收录了土耳其字符;

ISO 8859-10字符集,也称为Latin-6或Nordic,收录了北欧(主要指斯堪地那维亚半岛)的字符;

ISO 8859-11字符集,也称为Thai,从泰国的TIS620标准字符集演化而来;

ISO 8859-12字符集,目前尚未定义(未定义的原因目前有两种说法:一是原本要设计成一个包含塞尔特语族字符集的“Latin-7”,但后来塞尔特语族变成了ISO 8859-14 / Latin-8;二是原本预留给印度天城体梵文的,但后来却搁置了);

ISO 8859-13字符集,也称为Latin-7,主要函盖波罗的海(Baltic)诸国的文字符号,也补充了一些被Latin-6遗漏的拉脱维亚(Latvian)字符;

ISO 8859-14字符集,也称为Latin-8,它将Latin-1中的某些符号换成塞尔特语(Celtic)的字符;

ISO 8859-15字符集,也称为Latin-9,或者被戏称为Latin-0,它将Latin-1中较少用到的符号删除,换成当初遗漏的法文和芬兰字母,还把英镑和日元之间的金钱符号,换成了欧盟货币符号;

ISO 8859-16字符集,也称为Latin-10,涵盖了阿尔巴尼亚语、克罗地亚语、匈牙利语、意大利语、波兰语、罗马尼亚语及斯洛文尼亚语等东南欧国家语言。

 

字符编码-教程(2)-EBCDIC码与ASCII码的由来

一、为什么需要对字符进行编码

1、计算机一开始发明出来时是用来解决数字计算问题的,后来人们发现,计算机还可以做更多的事,例如文本处理。

但计算机其实挺笨的,它只“认识”010110111000…这样由0和1两个数字组成的二进制数字,这是因为计算机的底层硬件实现就是用电路的开和闭两种状态来表示0和1两个数字的。因此,计算机只可以直接存储和处理二进制数字。

2、为了在计算机上也能表示、存储和处理像文字、符号等等之类的字符,就必须将这些字符转换成二进制数字。

当然,肯定不是我们想怎么转换就怎么转换,否则就会造成同一段二进制数字在不同计算机上显示出来的字符不一样的情况,因此必须得定一个统一的、标准的转换规则。

二、EBCDIC码与ASCII码

1、于是最开始出现了EBCDIC(Extended Binary Coded Decimal Interchange Code扩展二进制编码的十进制交换码)编码标准。EBCDIC码是由国际商用机器公司(IBM)为大型机操作系统而开发设计的,于1964年推出。

在EBCDIC码中,英文字母不是连续排列的,中间出现多次断续,这带来了一些困扰和麻烦。

因此,在后来IBM的个人计算机和工作站操作系统中并没有采用EBCDIC码,而是采用了晚于EBCDIC码推出、且后来成为了英文字符编码工业标准的ASCII编码方案。

EBCDIC编码表

 

2、ASCII码(American Standard Code for Information Interchange美国信息交换标准码),由美国国家标准学会ANSI(American National Standard Institute)于1968年正式制定。后又于1972年被ISO/IEC采用,制定为ISO/IEC 646标准(ISO,即国际标准化组织International Standardization Organization,成立于1946年;IEC,即国际电工技术委员会International Electrotechnical Commission,成立于1906年;ISO/IEC往往用来表示由这两大国际组织联合制定的标准)。

由于ASCII码要晚于EBCDIC码出现(网上也有文章说是ASCII码要早于EBCDIC码开始设计,但1968年ASCII码才正式确定为标准),ASCII码的编码方式参照了EBCDIC码,并吸取了其经验教训,将英文字母进行了连续排列,这方便了程序处理。

3、ASCII编码方案虽然不是最早出现的字符编码方案,但却是最基础、最重要、应用最广泛的字符编码方案。

目前所通行的其他字符编码方案,比如ISO-8859、GB系列(GB2312、GBK、GB18030、GB13000)、Big5、Unicode等等,均直接或间接兼容ASCII码。

而像EBCDIC这样与ASCII完全不兼容的编码方案,基本上处于已淘汰或将要淘汰的境地。

三、ASCII字符编码方案介绍

1、ASCII码使用七个二进制数字(bit比特、位)来表示一个字符,总共表示128个字符(2^7 = 128,二进制编码为0000 0000 ~ 0111 1111,对应的十进制就是0~127)。

由于个人计算机普遍采用8位一个字节来进行存取与处理,因此剩下最高位的那1比特一般为0,但有时也被用作一些通讯系统的奇偶校验位。

ASCII编码表

Bin(二进制)
Oct(八进制)
Dec(十进制)
Hex(十六进制)
缩写/字符
解释
0000 0000
0
0
00
NUL(null)
空字符
0000 0001
1
1
01
SOH(start of headline)
标题开始
0000 0010
2
2
02
STX (start of text)
正文开始
0000 0011
3
3
03
ETX (end of text)
正文结束
0000 0100
4
4
04
EOT (end of transmission)
传输结束
0000 0101
5
5
05
ENQ (enquiry)
请求
0000 0110
6
6
06
ACK (acknowledge)
收到通知
0000 0111
7
7
07
BEL (bell)
响铃
0000 1000
10
8
08
BS (backspace)
退格
0000 1001
11
9
09
HT (horizontal tab)
水平制表符
0000 1010
12
10
0A
LF (NL line feed, new line)
换行键
0000 1011
13
11
0B
VT (vertical tab)
垂直制表符
0000 1100
14
12
0C
FF (NP form feed, new page)
换页键
0000 1101
15
13
0D
CR (carriage return)
回车键
0000 1110
16
14
0E
SO (shift out)
不用切换
0000 1111
17
15
0F
SI (shift in)
启用切换
0001 0000
20
16
10
DLE (data link escape)
数据链路转义
0001 0001
21
17
11
DC1 (device control 1)
设备控制1
0001 0010
22
18
12
DC2 (device control 2)
设备控制2
0001 0011
23
19
13
DC3 (device control 3)
设备控制3
0001 0100
24
20
14
DC4 (device control 4)
设备控制4
0001 0101
25
21
15
NAK (negative acknowledge)
拒绝接收
0001 0110
26
22
16
SYN (synchronous idle)
同步空闲
0001 0111
27
23
17
ETB (end of trans. block)
结束传输块
0001 1000
30
24
18
CAN (cancel)
取消
0001 1001
31
25
19
EM (end of medium)
媒介结束
0001 1010
32
26
1A
SUB (substitute)
代替
0001 1011
33
27
1B
ESC (escape)
换码(溢出)
0001 1100
34
28
1C
FS (file separator)
文件分隔符
0001 1101
35
29
1D
GS (group separator)
分组符
0001 1110
36
30
1E
RS (record separator)
记录分隔符
0001 1111
37
31
1F
US (unit separator)
单元分隔符
0010 0000
40
32
20
(space)
空格
0010 0001
41
33
21
!
叹号
0010 0010
42
34
22
双引号
0010 0011
43
35
23
#
井号
0010 0100
44
36
24
$
美元符
0010 0101
45
37
25
%
百分号
0010 0110
46
38
26
&
和号
0010 0111
47
39
27
闭单引号
0010 1000
50
40
28
(
开括号
0010 1001
51
41
29
)
闭括号
0010 1010
52
42
2A
*
星号
0010 1011
53
43
2B
+
加号
0010 1100
54
44
2C
,
逗号
0010 1101
55
45
2D
减号/破折号
0010 1110
56
46
2E
.
句号
00101111
57
47
2F
/
斜杠
00110000
60
48
30
0
数字0
00110001
61
49
31
1
数字1
00110010
62
50
32
2
数字2
00110011
63
51
33
3
数字3
00110100
64
52
34
4
数字4
00110101
65
53
35
5
数字5
00110110
66
54
36
6
数字6
00110111
67
55
37
7
数字7
00111000
70
56
38
8
数字8
00111001
71
57
39
9
数字9
00111010
72
58
3A
:
冒号
00111011
73
59
3B
;
分号
00111100
74
60
3C
<
小于
00111101
75
61
3D
=
等号
00111110
76
62
3E
>
大于
00111111
77
63
3F
?
问号
01000000
100
64
40
@
电子邮件符号
01000001
101
65
41
A
大写字母A
01000010
102
66
42
B
大写字母B
01000011
103
67
43
C
大写字母C
01000100
104
68
44
D
大写字母D
01000101
105
69
45
E
大写字母E
01000110
106
70
46
F
大写字母F
01000111
107
71
47
G
大写字母G
01001000
110
72
48
H
大写字母H
01001001
111
73
49
I
大写字母I
01001010
112
74
4A
J
大写字母J
01001011
113
75
4B
K
大写字母K
01001100
114
76
4C
L
大写字母L
01001101
115
77
4D
M
大写字母M
01001110
116
78
4E
N
大写字母N
01001111
117
79
4F
O
大写字母O
01010000
120
80
50
P
大写字母P
01010001
121
81
51
Q
大写字母Q
01010010
122
82
52
R
大写字母R
01010011
123
83
53
S
大写字母S
01010100
124
84
54
T
大写字母T
01010101
125
85
55
U
大写字母U
01010110
126
86
56
V
大写字母V
01010111
127
87
57
W
大写字母W
01011000
130
88
58
X
大写字母X
01011001
131
89
59
Y
大写字母Y
01011010
132
90
5A
Z
大写字母Z
01011011
133
91
5B
[
开方括号
01011100
134
92
5C
\
反斜杠
01011101
135
93
5D
]
闭方括号
01011110
136
94
5E
^
脱字符
01011111
137
95
5F
_
下划线
01100000
140
96
60
`
开单引号
01100001
141
97
61
a
小写字母a
01100010
142
98
62
b
小写字母b
01100011
143
99
63
c
小写字母c
01100100
144
100
64
d
小写字母d
01100101
145
101
65
e
小写字母e
01100110
146
102
66
f
小写字母f
01100111
147
103
67
g
小写字母g
01101000
150
104
68
h
小写字母h
01101001
151
105
69
i
小写字母i
01101010
152
106
6A
j
小写字母j
01101011
153
107
6B
k
小写字母k
01101100
154
108
6C
l
小写字母l
01101101
155
109
6D
m
小写字母m
01101110
156
110
6E
n
小写字母n
01101111
157
111
6F
o
小写字母o
01110000
160
112
70
p
小写字母p
01110001
161
113
71
q
小写字母q
01110010
162
114
72
r
小写字母r
01110011
163
115
73
s
小写字母s
01110100
164
116
74
t
小写字母t
01110101
165
117
75
u
小写字母u
01110110
166
118
76
v
小写字母v
01110111
167
119
77
w
小写字母w
01111000
170
120
78
x
小写字母x
01111001
171
121
79
y
小写字母y
01111010
172
122
7A
z
小写字母z
01111011
173
123
7B
{
开花括号
01111100
174
124
7C
|
垂线
01111101
175
125
7D
}
闭花括号
01111110
176
126
7E
~
波浪号
01111111
177
127
7F
DEL (delete)
删除

 

2、ASCII字符集共计有128个字符(见上表),码点编号(即字符编号)从0到127(二进制为从0000 0000到0111 1111,十六进制为从0x00到0x7F),二进制最高位都是0。其中:

1)0~31:控制字符或通讯专用字符(不可显示不可打印字符),如0x07(BEL响铃)会让计算机发出哔的一声、0x00(NUL空,注意不是空格)通常用于指示字符串的结束、0x0D(CR回车)和0x0A(LF换行)用于指示打印机的打印针头退到行首(即回车)并移到下一行(即换行)等。

2)32~126:可显示可打印字符(其中32为可显示但不可打印的空格字符),48~57为0-9的阿拉伯数字,65~90为26个大写英文字母,97~122为26个小写英文字母,其余的是一些标点符号、运算符号等。

3)127:控制字符DEL。

3、这时候的字符编解码非常简单,比如若要将字符序列编码为二进制流写入存储设备,只需要将该字符序列里的各个字符在ASCII字符集中的字符编号(即码点编号),直接以一个二进制字节写入存储设备即可,字符编号就是字符编码,中间不需要经过特别的编码算法进行字符编号到字符编码的转换计算,更不存在所谓码元序列到字节序列的转换。

字符编码-教程(1)-概述与基本知识

编码的本质是:将人类世界的信息,以某种规则,变成二进制信息。解码的本质是:将二进制信息还原成人类世界的信息。

所谓的乱码:就是 解码和 编码的 规则 不配套罢了。

本系列教程主要是为了 仔细探讨计算机中的编码过程,方便程序开发时迅速解决乱码问题。

Ⅰ教程提纲


1、基本概念讲解,了解编码模型。

2、EBCDIC码与ASCII码的由来

3、EASCII及ISO 8859字符编码方案

4、字节序、本地序、网络序和多字节码元

5、简体汉字编码与半角和圆角

6、简体汉字的区位码、国标码、内码、外码及字型码

7、国际化-Unicode与ANSI

8、字符串长度与字符编码

Ⅱ 编码入门常识


一、位

1、即比特(Bit),亦称二进制位、比特位、位元、位,指二进制数中的一位,是计算机中信息表示的最小单位。

Bit是Binary digit(二进制数位)的缩写,由数学家John Wilder Tukey提出,习惯上以小写字母b表示,如8比特可表示为8b。

2、每个比特有0和1两个可能的值,除了代表数值本身之外,还可代表:

  • 数值的正、负;
  • 两种状态,如电灯的开、关,某根导线上电压的有、无,等等;
  • 一个抽象逻辑上的是、否。

二、字节

1、在计算机中,通常都会使用一连串的位(比特),称之为位串(bit string比特串)。很显然,计算机系统都不会让你使用任意长度的位串,而是使用某个特定长度的位串。

一些常见的位串长度形式具有约定好的名称,如,半字节(nibble,貌似用的不多)代表四个位的组合,字节(byte)代表8个位的组合;还有字(word)、双字(Double word,简写为Dword)、四字(Quad word,简写为Qword)、十字节(Ten byte,简写为Tbyte)。

2、字节(byte),又称为位元组,音译为“拜特”(但很少使用这个译名),是计算机中计量存储容量和传输容量的一种基本计量单位,是由连续的、固定数量的位(即比特)所组成的位串(即比特串),一般由8个位组成,即1 byte = 8 bit。习惯上用大写的B表示,如3字节可表示为3B。

现代个人计算机(PC)的存储器编址,一般是以字节为单位的,称之为按字节编址,因此字节一般也是存储器的最小存取单元以及处理器的最小寻址单位(也有按位寻址、按字寻址等等,但在个人计算机上应用不普遍,这里不讨论)。

3、字节作为存储器的最小存取单元以及处理器的最小寻址单位这一重要特点,跟字符编码的关系极为密切(比如,码元的单字节与多字节、字节序的大端序与小端序等,都与以字节为基础的基本数据类型密切相关,详见后文介绍)。

4、习惯上,按照下面的图来排列一个字节上的各个位的顺序,即按照从右到左的顺序,依次为最低位(第0位)到最高位(第7位):

5、注意,字节不一定非得是8位,以前也有过4位、6位或7位作为一个字节的标准,比如IBM 701(36位字长,18位为一字节)、IBM 702(7位字长,7位为一字节)、CDC 6600(60位字长,12位为一字节byte)等,只是现代计算机的事实标准就是用8位来代表一个字节(最终形成这一事实标准除了历史原因和商业原因之外,最重要的原因应该是由于二进制的特性:2的次方计算更方便快捷)。

正是因为这个原因,在很多较为严谨的技术规格文献中,为了避免产生歧义,更倾向于使用8位组(Octet)而不是字节(Byte)这个术语来强调8比特位串。

不过,由于大众基本上都将字节理解为8比特位的8位组,因此一般文章中如果未作特别说明,基本上都将8位组直接称之为字节。

三、字与字长

1、虽然字节是大多数现代计算机的最小存储单元和传输单元,但并不代表它是计算机可以最高效地处理的数据单位。

一般来说,计算机可以最高效地处理的数据大小,应该与其字的字长相同,这就涉及到了字及字长的概念。

字(Word)在计算机中,一串比特位(位串、比特串)是作为一个整体来处理或运算的,这串比特位称为一个计算机字,简称字。字通常分为若干个字节(每个字节一般是8位)。

字长(Word Length)即字的长度,是指计算机的每个字所包含的位数。字长决定了CPU一次操作所处理的实际比特位数量的多少。字长由CPU对外数据通路的数据总线宽度决定。

2、计算机处理数据的速率,显然和它一次能加工的位数以及进行运算的快慢有关。如果一台计算机的字长是另一台计算机的两倍,若两台计算机的速度相同,在相同的时间内,前者能做的工作一般是后者的两倍。因此,字长与计算机的功能和用途有很大的关系,是计算机的一个重要技术指标。

在目前来讲,桌面平台的处理器字长正处于从32位向64位过渡的时期,嵌入式设备基本稳定在32位,而在某些专业领域(如高端显卡),处理器字长早已经达到了64位乃至更多的128位。

四、字符集

1、字符集(Character Set、Charset),字面上的理解就是字符的集合,是一个自然语言文字系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括文字、数字、字母、音节、标点符号、图形符号等。

例如ASCII字符集,定义了128个字符;GB2312定义了7445个字符。而计算机系统中提到的字符集准确地来说,指的是已编号的字符的有序集合(但不一定是连续的)

2、常见字符集有ASCII字符集、ISO 8859系列字符集、GB系列字符集(GB2312、GBK、GB18030)、BIG5字符集、Unicode字符集等。

五、编码

 编码(Encode)是信息从一种形式或格式转换为另一种形式或格式的过程,比如用预先规定的方法将字符(文字、数字、符号等)、图像、声音或其它对象转换成规定的电脉冲信号或二进制数字。

六、解码

编码(Decode)为编码的逆过程。

七、字符编码

1、字符编码(Character Encoding)是把字符集中的字符按一定格式(形式、方式)编码为某指定集合中某一对象(比如由0和1两个数字所组成的位串模式、由0~9十个数字所组成的自然数序列、电脉冲等)的过程,亦即在字符集与指定集合两者之间建立一个对应关系(映射关系)的过程。这是信息处理的一项基础技术。

而在计算机科学中,通常以字符集来表达信息,以计算机为基础的信息处理系统则利用电子元件(硬件)的不同状态的组合来表示、存储和处理信息。

2、电子元件不同状态(一般是开和关或称为开和闭两种状态)的组合能代表数字系统中的数字(比如开和关代表二进制中的0和1),因此字符编码的过程也就可以理解为将字符转换映射为计算机可以接受的二进制数字的过程,其目的是为了便于字符在计算机中表示、存储、处理和传输(包括在网络中传输)。

常见的例子包括将拉丁字母表编码成摩斯电码和ASCII码。其中,ASCII将字母、数字和其它符号进行编号,并且在计算机中直接用7比特的二进制数字来表示这个编号。通常会额外地在最高位(即首位)再增加一个扩充的比特位“0”,以便于计算机系统刚好以1个字节(8比特位)的方式来进行处理、存储和传输。

 

Ⅲ 字符编码体系模型

1、字符编码模型(Character Encoding Model)是反映字符编码系统的结构特点和各构成部分相互关系的模型框架。

2、由于历史的原因,早期一般认为字符集字符编码是同义词,并不需要进行严格区分。因此在像ASCII这样的简单字符集为代表的传统字符编码模型中,这两个概念的含义几乎是等同的。

因为在传统字符编码模型中,基本上都是将字符集里的字符进行编号(字符编号转化为二进制数后一般不超过一个字节),然后该字符编号就是字符的编码

但是,由统一码(Unicode)和通用字符集(UCS)为代表的现代字符编码模型则没有直接采用ASCII这样的简单字符集的编码思路,而是采用了一个全新的编码思路。

3、这个全新的编码思路将字符集与字符编码的概念更为细致地分解为了以下几个方面:

1)有哪些字符;

2)这些字符的编号是什么;

3)这些编号如何编码成一系列逻辑层面有限大小的数字,即码元序列;

4)这些逻辑层面的码元序列如何转换为(映射为)物理层面的字节流(字节序列);

5)在某些特殊的传输环境中(比如Email),再进一步将字节序列进行适应性编码处理。

这几个方面作为一个整体,于是构成了现代字符编码模型

4、现代字符编码模型之所以要分解为这么几个方面,其核心思想是创建一个能够用不同方式来编码的通用字符集。注意这里的关键词:“不同方式”与“通用”。

这意味着,同一个字符集,可以通用于不同的编码方式;也就是说,可以采用不同的编码方式来对同一个字符集进行编码。字符集与编码方式之间的关系可以是一对多的关系。

更进一步而言,在传统字符编码模型中,字符编码方式与字符集是紧密结合在一起的;而在现代字符编码模型中,字符编码方式与字符集脱钩了。用软件工程的专业术语来说,就是将之前紧密耦合在一起的字符编码方式与字符集解耦了。

因此,为了正确地表示这个现代字符编码模型,需要采用更多比“字符集”和“字符编码”更为精确的概念术语来描述。

5、在Unicode Technical Report (UTR统一码技术报告) #17《UNICODE CHARACTER ENCODING MODEL》中,现代字符编码模型分为了5个层次,并引入了更多的概念术语来描述(下面所涉及到的一些全新的概念术语,这里只做简介,暂时不作解释,但后文会陆续进行详细解释):

1层 抽象字符表ACR(Abstract Character Repertoire抽象字符清单)明确字符的范围(即确定支持哪些字符)

2层 编号字符集CCS(Coded Character Set)用数字编号表示字符(即用数字给抽象字符表ACR中的字符进行编号)

3层 字符编码方式CEF(Character Encoding Form字符编码形式、字符编码格式、字符编码规则)将字符编号编码为逻辑上的码元序列(即逻辑字符编码)

4层 字符编码模式CES(Character Encoding Scheme)将逻辑上的码元序列映射为物理上的字节序列(即物理字符编码)

5层 传输编码语法TES(Transfer Encoding Syntax)将字节序列作进一步的适应性编码处理

大白话总结:现代字符编码五层模型

第一层:抽象字符表(ACR)

就是建立一个集合,决定哪些字符需要放入集合中。

第二层:编号字符集(CCS)

将字符集合进行编号,比如 Unicode编号。

第三层:字符编码方式(CEF)

将字符集合的编号,以某种编码方式进行编码,形成编号与编码的一对一映射。
比如UTF-8编码,UTF-16编码。

第四层:字符编码模式(CES)

经过 CEF 编码好的二进制字符流,需要依据情况判断是否需要  字节序 编码。
字节序问题:部分多字节的编码方案中,在读取和写入数据文档时,操作系统需要判断首字节先读取或写入还是尾字节先读取还是写入【这就是Little endian和Big endian问题】。

当然网络传输中,TCP/IP为任意整数数据项定义了统一的网络字节顺序(network byte order):大端字节顺序!例如IP地址:它放在包头中跨越网络被携带。在IP地址结构中存放的地址总是以(大端法)网络字节顺序存放的,即使主机字节顺序(host byte order)是小端法。

单字节编码不存在字节序问题,有些多字节编码考虑了相邻字节的规则,也叫单字节码元,也没有问题。只有多字节编码中,没有考虑相邻编码规则的,属于多字节码元的,才会有字节序问题:如UTF-16 ,UTF-32。

第五层:传输编码语法(TES)

在某些特殊的传输环境中(比如Email),再进一步将字节序列进行适应性编码处理。


本系列文章,参考如下:

字节序问题:

https://www.cnblogs.com/benbenalin/p/6882293.html

http://imweb.io/topic/57fe263b2a25000c315a3d8a

https://zhidao.baidu.com/question/554750118.html

https://baike.baidu.com/item/%E5%AD%97%E8%8A%82%E5%BA%8F/1457160

https://www.cnblogs.com/dragon2012/p/5020259.html

编码转换:

https://blog.csdn.net/garybrother/article/details/3988741

http://www.mytju.com/classcode/tools/encode_big5.asp

https://www.qqxiuzi.cn/bianma/Unicode-UTF.php

https://www.qqxiuzi.cn/zh/hanzi-big5-bianma.php

 

字符编码-原码反码与补码

java中byte转换int时为何与0xff进行与运算

在剖析该问题前请看如下代码

public static String bytes2HexString(byte[] b) {   
  String ret = "";   
  for (int i = 0; i < b.length; i++) {   
   String hex = Integer.toHexString(b[ i ] & 0xFF);   
   if (hex.length() == 1) {   
    hex = '0' + hex;   
   }   
   ret += hex.toUpperCase();   
  }   
  return ret;   
}

上面是将byte[]转化十六进制的字符串,注意这里b[ i ] & 0xFF将一个byte和 0xFF进行了与运算,然后使用Integer.toHexString取得了十六进制字符串,可以看出
b[ i ] & 0xFF运算后得出的仍然是个int,那么为何要和 0xFF进行与运算呢?直接 Integer.toHexString(b[ i ]);,将byte强转为int不行吗?答案是不行的.

其原因在于:
1.byte的大小为8bits而int的大小为32bits
2.java的二进制采用的是补码形式

在这里先温习下计算机基础理论

byte是一个字节保存的,有8个位,即8个0、1。
8位的第一个位是符号位,
也就是说0000 0001代表的是数字1
1000 0000代表的就是-1
所以正数最大位0111 1111,也就是数字127
负数最大为1111 1111,也就是数字-128

上面说的是二进制原码,但是在java中采用的是补码的形式,下面介绍下什么是补码

1、反码:
一个数如果是正,则它的反码与原码相同;
一个数如果是负,则符号位为1,其余各位是对原码取反;

2、补码:利用溢出,我们可以将减法变成加法
对于十进制数,从9得到5可用减法:
9-4=5    因为4+6=10,我们可以将6作为4的补数
改写为加法:
9+6=15(去掉高位1,也就是减10)得到5.

对于十六进制数,从c到5可用减法:
c-7=5    因为7+9=16 将9作为7的补数
改写为加法:
c+9=15(去掉高位1,也就是减16)得到5.

在计算机中,如果我们用1个字节表示一个数,一个字节有8位,超过8位就进1,在内存中情况为(100000000),进位1被丢弃。

⑴一个数为正,则它的原码、反码、补码相同
⑵一个数为负,刚符号位为1,其余各位是对原码取反,然后整个数加1

– 1的原码为                10000001
– 1的反码为                11111110
+ 1
– 1的补码为                11111111

0的原码为                 00000000
0的反码为                 11111111(正零和负零的反码相同)
+1
0的补码为               100000000(舍掉打头的1,正零和负零的补码相同)

Integer.toHexString的参数是int,如果不进行&0xff,那么当一个byte会转换成int时,由于int是32位,而byte只有8位这时会进行补位,
例如补码11111111的十进制数为-1转换为int时变为11111111111111111111111111111111好多1啊,呵呵!即0xffffffff但是这个数是不对的,这种补位就会造成误差。
和0xff相与后,高24比特就会被清0了,结果就对了。

—-
Java中的一个byte,其范围是-128~127的,而Integer.toHexString的参数本来是int,如果不进行&0xff,那么当一个byte会转换成int时,对于负数,会做位扩展,举例来说,一个byte的-1(即0xff),会被转换成int的-1(即0xffffffff),那么转化出的结果就不是我们想要的了。

而0xff默认是整形,所以,一个byte跟0xff相与会先将那个byte转化成整形运算,这样,结果中的高的24个比特就总会被清0,于是结果总是我们想要的。

来自:https://www.cnblogs.com/Free-Thinker/p/6529584.html

springmvc中级-HttpMessageConverter详解

一、HttpMessageConverter概述

HttpMessageConverter<T> 是 Spring3.0 新添加的一个接 口,负责将请求信息转换为一个对象(类型为 T),将对象( 类型为 T)输出为响应信息。


使用 HttpMessageConverter<T> 将请求信息转化并绑定到处理方法的入 参中或将响应结果转为对应类型的响应信息,Spring 提供了两种途径:
–使用 @RequestBody / @ResponseBody 对处理方法进行标注
–使用 HttpEntity<T> / ResponseEntity<T> 作为处理方法的入参或返回值

当控制器处理方法使用到 @RequestBody/@ResponseBody 或HttpEntity<T>/ResponseEntity<T> 时, Spring 首先根据请求头或响应头的Accept 属性选择匹配的 HttpMessageConverter,  进而根据参数类型或 泛型类型的过滤得到匹配的 HttpMessageConverter, 若找不到可用的HttpMessageConverter 将报错

  • @RequestBody@ResponseBody 不需要成对出现。

ajax发送和接收乱码问题:

因为HttpMessageConverter的几个实现类,都是先查询http请求头的编码方式,然后再解码的,如果没有指定就使用默认的编码方式。比如iso-8859-1。这样会使得中文乱码。

​碰到使用ajax调用spring mvc时出现乱码,这里写一下解决方法。​

一:ajax传入spring mvc时,@RequestBody出现乱码:

这是ajax调用时,传入data编码有误导致,通过以下方式解决(红色字体部分):

前台ajax调用代码:

$.ajax({
  url : baseUrl + “/crlandIsp/commonInfoCreate/save.do”,
  type : “POST”,
  cache : false,
  data : mdata,
  contentType : “text/html;charset=UTF-8”,
  dataType : “json”,
  success : function(result) {

  },
  error : function(xhr, ajaxOptions, thrownError) {
  

  }
 });

二:spring mvc通过@ResponseBody返回时,ajax接收到乱码结果:

修改spring mvc controller代码(添加红色部分),指定响应头编码方式

 @RequestMapping(value = “/queryDetail”,produces = “text/html;charset=UTF-8”)
   public @ResponseBody String save(@RequestBody String body) throws Exception {
       try {

。。。。

}

方法二、修改全局的默认配置编码

<mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes" value = "text/html;charset=UTF-8" />
            </bean>
        </mvc:message-converters>
</mvc:annotation-driven>

网上也有这种,但是你们是不是<mvc:annotation-driven>不能配置子集
这里只要把你们的spring-mvc-.xsd改成4就可以在<mvc:annotation-driven>下配置了,如下图

主要还是spring-mvc-4.0.xsd。改了之后再eclipse中按ALT+/就会出现如下提示

这边还要注意一点

<property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" />

text/plain这个还是会中文乱码,用text/html就不会


二、HttpMessageConverter应用

1、测试请求体信息

(1)测试带上传文件

utf-8编码的文件:春节.txt

good春节OK
hello

文件二进制为:

676f 6f64 e698 a5e8 8a82 4f4b 0d0a 6865
6c6c 6f

(2)测试网页:

<form action="testRequestBody" method="post" enctype="multipart/form-data" accept-charset="gb2312">
File:<input type="file" name="name-file"/>
Desc:<input type="text" name="name-desc"/>
<input type="submit" name="name-submit" value="submit" />
</form>

(2)测试请求体的控制器:

@ResponseBody
@RequestMapping("/testRequestBody")
public String testHttpMessageConverter(@RequestBody String body){
	System.out.println(body);
	return "helloworld! " + new Date();
}

浏览器fiddler2抓包结果:
(将二进制流用gb2312解码的结果,很明显文件上传时传的是原始二进制流,原始信息是utf-8编码,上传时没有在重新经过编码,于是春节,二字的utf-8编码是 E6 98 A5 E8 8A 82 的二进制流,抓包解码时,将E6 98 A5 E8 8A 82用GB2312解码,于是出现乱码   鏄ヨ妭    ,至于表单中的其他信息 ,因为都采用了gb2312编码,所以解码时就正确了,不会出现乱码 )

POST http://192.168.1.131:8080/springmvc-2/testRequestBody HTTP/1.1
Host: 192.168.1.131:8080
Connection: keep-alive
Content-Length: 410
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.1.131:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrvTDZPfQOBKllDXv
Referer: http://192.168.1.131:8080/springmvc-2/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: JSESSIONID=5E065281AC2C661A0098111FB38F3745

------WebKitFormBoundaryrvTDZPfQOBKllDXv
Content-Disposition: form-data; name="name-file"; filename="春节.txt"
Content-Type: text/plain

good鏄ヨ妭OK
hello
------WebKitFormBoundaryrvTDZPfQOBKllDXv
Content-Disposition: form-data; name="name-desc"

春节
------WebKitFormBoundaryrvTDZPfQOBKllDXv
Content-Disposition: form-data; name="name-submit"

submit
------WebKitFormBoundaryrvTDZPfQOBKllDXv--

服务器控制台输出结果:【输出的是请求体】

------WebKitFormBoundaryrvTDZPfQOBKllDXv
Content-Disposition: form-data; name="name-file"; filename="´º½Ú.txt"
Content-Type: text/plain

good春节OK
hello
------WebKitFormBoundaryrvTDZPfQOBKllDXv
Content-Disposition: form-data; name="name-desc"

´º½Ú
------WebKitFormBoundaryrvTDZPfQOBKllDXv
Content-Disposition: form-data; name="name-submit"

submit
------WebKitFormBoundaryrvTDZPfQOBKllDXv--

特别备注:控制台返回的请求体中:

##### 针对 filename="´º½Ú.txt" ,
##### 这个 ´º½Ú 是浏览器 用gb2312编码 春节 两字成 B4 BA BD DA 的二进制流,放入请求体中
##### 解码时 springmvc 查看http头,发现没有编码指明,于是默认用 iso-8859-1解码 造成乱码   
Content-Disposition: form-data; name="name-file"; filename="´º½Ú.txt"
##### 下面第三行 也是同样的 原因
Content-Disposition: form-data; name="name-desc"

´º½Ú
##### 总结来说,multipart/form-data 编码的表单,表单中信息都是按 表单规定的编码方式,
##### 直接编码成 二进制流。然后存放到 请求体中。而表单其他自动生成的信息,都是ASCII编码
##### 其实也可以说是按照 表单默认 的编码方式,因为http 用到的编码都是 ASCII码的扩展码,都兼容支持ASCII码
##### 当然 文件上传时 没有进行编码,直接是原始文件的二进制流加入到 请求体中 比如下面的文件信息:
Content-Type: text/plain

good春节OK
hello
------WebKitFormBoundaryrvTDZPfQOBKllDXv
##### 上面的例子中:原始文件信息 已经说过了 是用utf-8编码 成二进制流,utf-8兼容ASCII码,
##### 当 springmvc 接收请求体时,因为发现请求头中没有编码信息 ,所以采用默认的iso-8859-1解码,这个兼容ASCII码
##### 所以  英文字母能够正常解码 ,中文 春节 二字,一开始utf-8编码成 E6 98 A5 E8 8A 82 的二进制流,
##### 后来解码时 ,用ios-8859-1将这些二进制流错误的解码了,导致出现乱码  春节

=========================================================

如果将表单设置成utf-8编码,效果就不一样了。

<form action="testRequestBody" method="post" enctype="multipart/form-data" accept-charset="UTF-8">
File:<input type="file" name="name-file"/>
Desc:<input type="text" name="name-desc"/>
<input type="submit" name="name-submit" value="submit" />
</form>

用utf-8解码抓包时的二进制流:

POST http://192.168.1.131:8080/springmvc-2/testRequestBody HTTP/1.1
Host: 192.168.1.131:8080
Connection: keep-alive
Content-Length: 414
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.1.131:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarySIE2A85QOHmfv0rl
Referer: http://192.168.1.131:8080/springmvc-2/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: JSESSIONID=69D0B1B2F9AAE721AED15F660424C708

------WebKitFormBoundarySIE2A85QOHmfv0rl
Content-Disposition: form-data; name="name-file"; filename="春节.txt"
Content-Type: text/plain

good春节OK
hello
------WebKitFormBoundarySIE2A85QOHmfv0rl
Content-Disposition: form-data; name="name-desc"

春节
------WebKitFormBoundarySIE2A85QOHmfv0rl
Content-Disposition: form-data; name="name-submit"

submit
------WebKitFormBoundarySIE2A85QOHmfv0rl--

查看服务器控制台输出结果:
所有乱码  春节  都是因为春节的utf-8 编码成二进制流  E6 98 A5 E8 8A 82
后来用iso-8859-1 解码 E6 98 A5 E8 8A 82 时,出现乱码   æ˜¥èŠ‚

------WebKitFormBoundarySIE2A85QOHmfv0rl
Content-Disposition: form-data; name="name-file"; filename="春节.txt"
Content-Type: text/plain

good春节OK
hello
------WebKitFormBoundarySIE2A85QOHmfv0rl
Content-Disposition: form-data; name="name-desc"

春节
------WebKitFormBoundarySIE2A85QOHmfv0rl
Content-Disposition: form-data; name="name-submit"

submit
------WebKitFormBoundarySIE2A85QOHmfv0rl--

为什么,控制台打印出来的是乱码呢?

debug解析过程:

核心靠AbstractHttpMessageConverter.java 解析body
解码时,首先看请求头的编码,如果没有,则默认iso-8859-1编码。

//类方法 有删减
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {

	public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");


	private volatile List<Charset> availableCharsets;

	private boolean writeAcceptCharset = true;


	/**
	 * A default constructor that uses {@code "ISO-8859-1"} as the default charset.
	 * @see #StringHttpMessageConverter(Charset)
	 */
	public StringHttpMessageConverter() {
		this(DEFAULT_CHARSET);
	}

	/**
	 * A constructor accepting a default charset to use if the requested content
	 * type does not specify one.
	 */
	public StringHttpMessageConverter(Charset defaultCharset) {
		super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
	}


	/**
	 * Indicates whether the {@code Accept-Charset} should be written to any outgoing request.
	 * <p>Default is {@code true}.
	 */
	public void setWriteAcceptCharset(boolean writeAcceptCharset) {
		this.writeAcceptCharset = writeAcceptCharset;
	}



	@Override
	protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
		Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
		return StreamUtils.copyToString(inputMessage.getBody(), charset);
	}



	@Override
	protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
		if (this.writeAcceptCharset) {
			outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
		}
		Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
		StreamUtils.copy(str, charset, outputMessage.getBody());
	}




}

所以,解决方法也好办了:

1.客户的发送方法稍微改一下,这个方式是最好的:

connection.setRequestProperty("content-type", "text/html;charset=GBk");

2.如果发送方无论如何都没法改,只能服务器端处理的时候,要么所有接收到的数据都 new String(data.getByte(“iso-8859-1″),”GBK”),要么直接自定义StringHttpConverter,将readInternal方法中的charset获取改为不先从请求头中读取,直接硬编码为GBK(当然这样做的后果就是只能接收GBK的数据了).【好像不能自定义,不过上文提到了可以转发配置中配置,修改StringHttpConverter的默认编码值】

2、文件下载

只需要在控制器中,编辑 ResponseEntity<byte[]>就行了。

@RequestMapping("/testResponseEntity")
	public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException{
		byte [] body = null;
		ServletContext servletContext = session.getServletContext();
		InputStream in = servletContext.getResourceAsStream("/files/abc.txt");
		body = new byte[in.available()];
		in.read(body);
		
		HttpHeaders headers = new HttpHeaders();
		headers.add("Content-Disposition", "attachment;filename=abc.txt");
		
		HttpStatus statusCode = HttpStatus.OK;
		
		ResponseEntity<byte[]> response = new ResponseEntity<byte[]>(body, headers, statusCode);
		return response;
	}

 

3、返回json

添加jackson jar包,编写控制器。【如果有中文的json,】

@ResponseBody
@RequestMapping("/testJson")
public Collection<Employee> testJson(){
	return employeeDao.getAll();
}

测试链接:http://localhost:8080/springmvc-2/testJson

 

参考:

测试 request body 问题

https://www.cnblogs.com/chyu/p/5517788.html

https://blog.csdn.net/u013239111/article/details/51347949

http://blog.sina.com.cn/s/blog_b1f34f990102vcm4.html

 

springmvc踩坑-jackson与spring的版本兼容问题

一开始 springmvc  4.3.9版本中,用控制器 handler 返回 json 时,用了 2.1.5 的jackson包,发现还是报错 找不到 类方法。

Jul 15, 2018 1:31:48 PM org.apache.catalina.core.StandardContext filterStart
SEVERE: Exception starting filter HttpPutFormContentFilter
java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/util/DefaultIndenter
	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.init(AbstractJackson2HttpMessageConverter.java:97)
	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.<init>(AbstractJackson2HttpMessageConverter.java:90)
	at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.<init>(MappingJackson2HttpMessageConverter.java:66)
	at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.<init>(MappingJackson2HttpMessageConverter.java:57)
	at org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter.<init>(AllEncompassingFormHttpMessageConverter.java:61)
	at org.springframework.web.filter.HttpPutFormContentFilter.<init>(HttpPutFormContentFilter.java:63)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.lang.Class.newInstance(Class.java:442)
	at org.apache.catalina.core.DefaultInstanceManager.newInstance(DefaultInstanceManager.java:119)
	at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:258)
	at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:105)
	at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4689)
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5329)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:147)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1407)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1397)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.core.util.DefaultIndenter
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1332)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1166)
	... 23 more

于是改用 jackson 2.9.6的包时,发现成功了,不再报错。

原因是Spring 3.x 和4.X处理JSON的一个类不一样

spring3.x是org.springframework.http.converter.json.MappingJacksonHttpMessageConverter

spring4.x是org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

springmvc中级-返回json详解

一、测试页面

<script type="text/javascript" src="scripts/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
   //测试 json 函数
	$(function(){
		$("#testJson").click(function(){
			var url = this.href;
			var args = {};
			//这个是jQuery版的 ajax 函数 
			$.post(url, args, function(data){
				for(var i = 0; i < data.length; i++){
					var id = data[i].id;
					var lastName = data[i].lastName;
					
					alert(id + ": " + lastName);
				}
			});
			return false;
		});
	})
</script>
</head>

<body>
<a href="testJson" id="testJson">测试json</a>
</body>

二、编辑控制器

返回的是:JSON 对应的对象或集合。
然后 在方法上添加@ResponseBody 注解

@ResponseBody
@RequestMapping("/testJson")
public Collection<Employee> testJson(){
	return employeeDao.getAll();
}

三、遇到转换错误

SEVERE: Servlet.service() for servlet [dispatcherServlet] in context with path [/springmvc-2] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: No converter found for return value of type: class java.util.HashMap$Values] with root cause

后来我发现:缺少转换的jar包。将hashmap 转换成 json。
http://repo1.maven.org/maven2/com/fasterxml/jackson/core/
下载三个jar包:jackson-annotations;jackson-core;jackson-databind

特别强调一个bug:

一开始:下载的三个jar包 jackson-annotations;jackson-core;jackson-databind 版本号是 :2.1.5 结果还是 报错了:

nested exception is java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/util/DefaultIndenter

于是在网上下载了 三个jar包 的 最新版 :2.9.6
结果终于好了,没有再报错了。

springmvc中级-数据绑定流程-数据转化数据格式化数据校验

一、数据绑定流程

  • 1. Spring MVC 主框架将 ServletRequest 对象及目标方 法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder 实例对象
  • 2. DataBinder 调用装配在 Spring MVC 上下文中的 ConversionService 组件进行数据类型转换、数据格式 化工作。将 Servlet 中的请求信息填充到入参对象中
  • 3. 调用 Validator 组件对已经绑定了请求消息的入参对象 进行数据合法性校验,并最终生成数据绑定结果BindingData 对象
  • 4. Spring MVC 抽取 BindingResult 中的入参对象和校验 错误对象,将它们赋给处理方法的响应入参

Spring MVC 通过反射机制对目标处理方法进行解析,将请 求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder,运行机制如下:

查看代码-数据绑定流程:

WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
		if (binder.getTarget() != null) {
			if (!mavContainer.isBindingDisabled(name)) {
				bindRequestParameters(binder, webRequest);
			}
			validateIfApplicable(binder, parameter);
			if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
				throw new BindException(binder.getBindingResult());
			}
		}

@InitBinder

  • @InitBinder 标识的方法,可以对 WebDataBinder 对 象进行初始化。WebDataBinder 是 DataBinder 的子类,用 于完成由表单字段到 JavaBean 属性的绑定
  • @InitBinder方法不能有返回值,它必须声明为void。
  • @InitBinder方法的参数通常是是 WebDataBinder
//在控制器中编写的代码
//对内些无法自动绑定的对象,进行特殊处理。比如 表单中的选择器上传Id值,然后这里可以改成name值 
@InitBinder
public void initBinder(WebDataBinder binder){
       //这里代表:将表单中的lastName 不赋值给 bean中的 lastName
       binder.setDisallowedFields("lastName");
}

二、自定义类型转换器

ConversionService 是 Spring 类型转换体系的核心接口。

可以利用 ConversionServiceFactoryBean 在 Spring 的 IOC容器中定义一个 ConversionService. Spring 将自动识别出IOC 容器中的 ConversionService,并在 Bean 属性配置及Spring  MVC 处理方法入参绑定等场合使用它进行数据的转换。

可通过 ConversionServiceFactoryBeanconverters 属性 注册自定义的类型转换器。

======================================================

  • Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到ConversionServiceFactroyBean 中:

Converter<S,T>:将 S 类型对象转为 T 类型对象

ConverterFactory:将相同系列多个 “同质” Converter 封装在一 起。如果希望将一种类型的对象转换为另一种类型及其子类的对 象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类

GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换

======================================================

代码参照之前的crud:

测试页面:

<form action="testConversionServiceConverer" method="POST">
	<!-- lastname-email-gender-department.id 例如: [email protected] -->
	Employee: <input type="text" name="employee"/>
	<input type="submit" value="Submit"/>
</form>

控制器处理:

@RequestMapping("/testConversionServiceConverer")
public String testConverter(@RequestParam("employee") Employee employee){
	System.out.println("save: " + employee);
	employeeDao.save(employee);
	return "redirect:/emps";
}

自定义转换器

将表单的 字符串类型 employee  转换成 bean类型  employee;用于表单提交时,传入参数的类型转换。

package com.test.springmvc.test;

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import com.test.springmvc.crud.entities.Department;
import com.test.springmvc.crud.entities.Employee;

@Component
public class EmployeeConverter implements Converter<String, Employee> {
//自定义转换器必须要  Converter<String, Employee>  然后再去转发配置器中注册
	@Override
	public Employee convert(String source) {
		if(source != null){
			String [] vals = source.split("-");
			//[email protected]
			if(vals != null && vals.length == 4){
				String lastName = vals[0];
				String email = vals[1];
				Integer gender = Integer.parseInt(vals[2]);
				Department department = new Department();
				department.setId(Integer.parseInt(vals[3]));
				
				Employee employee = new Employee(null, lastName, email, gender, department);
				System.out.println(source + "--convert--" + employee);
				return employee;
			}
		}
		return null;
	}

}

 编辑转发配置器

<!-- 配置 ConversionService ,注意下面是 ConversionServiceFactoryBean  -->
	<bean id="conversionService"  class="org.springframework.format.support.ConversionServiceFactoryBean">
		<property name="converters">
			<set>
				<ref bean="employeeConverter"/>
			</set>
		</property>	
	</bean>

<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>

三、数据格式化

<mvc:annotation-driven/> 默认创建的 ConversionService 实例即为 FormattingConversionServiceFactroyBean。装配了 FormattingConversionServiceFactroyBean 后,就可 以在 Spring MVC 入参绑定及模型数据输出时使用注解驱动 了。

对属性对象的输入/输出进行格式化,从其本质上讲依然 属于 “类型转换” 的范畴。
Spring 在格式化模块中定义了一个实现ConversionService 接口的FormattingConversionService 实现类,该实现类扩展了 GenericConversionService,因此它既具有类型转换的 功能,又具有格式化的功能

FormattingConversionService 拥有一个FormattingConversionServiceFactroyBean 工厂类,  后者用于在 Spring 上下文中构造前者。

FormattingConversionServiceFactroyBean 内部已经注册了 :

–NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用 @NumberFormat 注解
–JodaDateTimeFormatAnnotationFormatterFactroy:支持对日期类型的属性使用 @DateTimeFormat 注解

日期格式化

@DateTimeFormat 注解可对
java.util.Date、java.util.Calendar、java.long.Long 时间 类型进行标注:

pattern 属性:类型为字符串。指定解析/格式化字段数据的模式, 如:”yyyy-MM-dd hh:mm:ss”

iso 属性:类型为 DateTimeFormat.ISO。指定解析/格式化字段数据 的ISO模式,包括四种:ISO.NONE(不使用) -- 默认、ISO.DATE(yyyy-MM-dd) 、ISO.TIME(hh:mm:ss.SSSZ)、ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)

style 属性:字符串类型。通过样式指定日期时间的格式,由两位字 符组成,第一位表示日期的格式,第二位表示时间的格式:S:短日 期/时间格式、M:中日期/时间格式、L:长日期/时间格式、F:完整 日期/时间格式、-:忽略日期或时间格式

数值格式化

@NumberFormat 可对类似数字类型的属性进行标 注,它拥有两个互斥的属性:

style:类型为 NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型)、Style.CURRENCY(货币类型)、 Style.PERCENT(  百分数类型)

pattern:类型为 String,自定义样式, 如patter="#,###";

格式化示例-在crud项目基础上

1、编辑转发配置器

<mvc:annotation-driven></mvc:annotation-driven>

2、编辑表单

<form:form action="${pageContext.request.contextPath }/emp" method="POST" 
	modelAttribute="employee">
	
       <!-- 其他表单项已经省略,主要看对 date 和 salary 表单项的格式化  -->
      	
        <!--  
		1. 数据类型转换
		2. 数据类型格式化
		3. 数据校验. 
		1). 如何校验 ? 注解 ?
		①. 使用 JSR 303 验证标准
		②. 加入 hibernate validator 验证框架的 jar 包
		③. 在 SpringMVC 配置文件中添加 <mvc:annotation-driven />
		④. 需要在 bean 的属性上添加对应的注解
		⑤. 在目标方法 bean 类型的前面添加 @Valid 注解
		2). 验证出错转向到哪一个页面 ?
		注意: 需校验的 Bean 对象和其绑定结果对象或错误对象时成对出现的,它们之间不允许声明其他的入参
		3). 错误消息 ? 如何显示, 如何把错误消息进行国际化
	-->
	Birth: <form:input path="birth"/>
	<br>
	Salary: <form:input path="salary"/>
	<br>
	<input type="submit" value="Submit"/>
</form:form>

3、编辑entity中的employee.java

###### 属性注解 ######
	
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;
	
@NumberFormat(pattern="#,###,###.#")
private Float salary;

###### 属性方法 ######
public Date getBirth() {
	return birth;
}

public void setBirth(Date birth) {
	this.birth = birth;
}

public Float getSalary() {
	return salary;
}

public void setSalary(Float salary) {
	this.salary = salary;
}

4、补充说明

前面三步操作,已经可以支持数据格式化了。如果想加入自定义转换器的话,需要修改一下转发配置器:

<!-- 配置 ConversionService ,下面由原来的 ConversionServiceFactoryBean 转变成 FormattingConversionServiceFactoryBean -->
	<bean id="conversionService"  class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
		<property name="converters">
			<set>
				<ref bean="employeeConverter"/>
			</set>
		</property>	
	</bean>

<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>

四、出错信息

数据格式类型转换出错信息

在上节的数据格式化中:
@DateTimeFormat(pattern=”yyyy-MM-dd”)
@NumberFormat(pattern=”#,###,###.#”)
这两个注解就是用来格式化的,如果表单中的数据,不符合要求格式时,校验框架会发现错误,并在请求的网页显示错误。但是控制台没有打印错误。这时我们可以在控制器添加错误解析代码就行:

@RequestMapping(value="/emp", method=RequestMethod.POST)
	public String save(Employee employee, BindingResult result, 
			Map<String, Object> map){
		System.out.println("save: " + employee);
		//验证 是否存在 错误信息
		if(result.getErrorCount() > 0){
			System.out.println("出错了!");
			
			for(FieldError error:result.getFieldErrors()){
				System.out.println(error.getField() + ":" + error.getDefaultMessage());
			}
			
			//若验证出错, 则转向定制的页面
			map.put("departments", departmentDao.getDepartments());
			return "addEmp";
		}
		
		employeeDao.save(employee);
		return "redirect:/emps";
	}
  • JSR 303 是 Java 为 Bean 数据合法性校验提供的标准, 它已经包含在 JavaEE 6.0 中 .
  • JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max、@Past 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证
  • Hibernate Validator 是 JSR 303 的一个参考实现,除支持 所有标准的校验注解外,它还支持以下的扩展注解 @Email,@Length,@NotEmpty,@Range

注意:@Past 代表属性是过去式 ,比如生日;@NotEmpty 代表属性不为空;这些类似的校验注解都需要添加相应的校验jar包。比如:
validation-api-1.1.0.CR1.jar
hibernate-validator-5.0.0.CR2.jar
hibernate-validator-annotation-processor-5.0.0.CR2.jar

添加完jar包后,实际运行时遇到:

Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: com/fasterxml/classmate/Filter

异常提示缺少类,需要导入jar包:需要导入hibernate中的classmate.jar即可

=========================================================

  • Spring 4.0 拥有自己独立的数据校验框架,同时支持 JSR303 标准的校验框架。
  • Spring 在进行数据绑定时,可同时调用校验框架完成数据校 验工作。在 Spring MVC 中,可直接通过注解驱动的方式 进行数据校验
  • Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的Validator 接口,也实现了 JSR 303 的 Validator 接口。只要 在 Spring 容器中定义了一个 LocalValidatorFactoryBean,即可将其注入到需要数据校 验的 Bean 中。
  • Spring 本身并没有提供 JSR303 的实现,所以必须将JSR303 的实现者的 jar 包放到类路径下。

数据校验出错信息

@DateTimeFormat(pattern=”yyyy-MM-dd”)
@NumberFormat(pattern=”#,###,###.#”)
上面两个已经在 mvc-annotation中的FormattingConversionService 注册好了。不需要额外添加什么。

如果需要在javabean中,实现其他的校验功能
第一步:需要在bean的属性添加:@NotEmpty,@Email,@Past 等注解。如:

@NotEmpty
private String lastName;

@Email
private String email;

第二步:添加jar包

validation-api-1.1.0.CR1.jar
hibernate-validator-5.0.0.CR2.jar
hibernate-validator-annotation-processor-5.0.0.CR2.jar

classmate-1.0.0.jar

第三步:编辑控制器 (关键是在 形参bean上 添加 @Valid 注解)

 //处理添加雇员请求,并重定向到 	@RequestMapping("/emps")
    @RequestMapping(value="emp", method=RequestMethod.POST)
    public String addEmp(@Valid Employee employee ,BindingResult result, 
			Map<String, Object> map){
		System.out.println("save: " + employee);
		//验证 是否存在 错误信息
		if(result.getErrorCount() > 0){
			System.out.println("出错了!");
			
			for(FieldError error:result.getFieldErrors()){
				System.out.println(error.getField() + ":" + error.getDefaultMessage());
			}
			
			//若验证出错, 则转向定制的页面
			map.put("departments", departmentDao.getDepartments());
			return "addEmp";
		}
		
    	employeeDao.save(employee);
    	return "redirect:/emps";
		
	}

结果:参考控制台 输出信息:

save: Employee [id=null, lastName=12, email=123, gender=0, department=Department [id=101, departmentName=null], birth=null, salary=null]
出错了!
birth:Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'birth'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@javax.validation.constraints.Past @org.springframework.format.annotation.DateTimeFormat java.util.Date] for value '213'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [213]
salary:Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Float' for property 'salary'; nested exception is java.lang.NumberFormatException: multiple points
email:not a well-formed email address

补充:
<mvc:annotation-driven/> 会默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标 注 @valid 注解即可让 Spring MVC 在完成数据绑定后执行 数据校验的工作,并将校 验结果保存在被校验入参对象之后的 BindingResult 或 Errors 入参中。常用方法:
FieldError getFieldError(String field)
List getFieldErrors()
Object getFieldValue(String field)
Int getErrorCount()

Spring MVC 是通过对处理方法签名的规约来保存校验结果 的:前一个表单/命令对象的校验结果保存到随后的入参中,这个保存校验结果的入参必须是 BindingResultErrors 类型,这两个类都位于org.springframework.validation 包中

======================================================
需校验的 Bean 对象和其绑定结果对象或错误对象时成对出现的,它们之间不允许声明其他的入参。

//BindingResult 扩展了 Errors 接口。在控制器中,两种可以都可以用来放校验结果
//Errors 接口提供了获取错误信息的方法,如 getErrorCount() 或getFieldErrors(String field)。
public String handle 91(@Valid User user, BindingResult userBindingResult,  String sessionId,ModelMap mm, @Valid Dept dept, Errors deptErrors){

}

页面展示错误信息

  • Spring MVC 除了会将表单/命令对象的校验结果保存到对 应的 BindingResult 或 Errors 对象中外,还会将所有校验 结果保存到 隐含模型
  • 即使处理方法的签名中没有对应于表单/命令对象的结果 入参,校验结果也会保存在 “隐含对象” 中。
  • 隐含模型中的所有数据最终将通过 HttpServletRequest 的 属性列表暴露给 JSP 视图对象,因此在 JSP 中可以获取 错误信息
  • 在 JSP 页面上可通过 <form:errors path=“userName”>显示错误消息

页面展示国际化出错信息

1、新建国际化文件

i18n.properties

NotEmpty.employee.lastName=LastName\u4E0D\u80FD\u4E3A\u7A7A.
Email.employee.email=Email\u5730\u5740\u4E0D\u5408\u6CD5

###Birth不能是一个将来的时间
Past.employee.birth=Birth\u4E0D\u80FD\u662F\u4E00\u4E2A\u5C06\u6765\u7684\u65F6\u95F4. 

###########上面是数据校验 的错误信息,下面是类型转换 的错误信息############

####Birth不是一个日期
typeMismatch.employee.birth=Birth\u4E0D\u662F\u4E00\u4E2A\u65E5\u671F. 

数据校验错误key值:以校验注解类名为前缀+结合 modleAttribute+属性名属性类型名
比如key值:NotEmpty.employee.lastName
value值:LastName\u4E0D\u80FD\u4E3A\u7A7A.

类型转换错误key值:类型转换错误码+结合 modleAttribute+属性名属性类型名
比如key值:typeMismatch.employee.birth
value值:Birth\u4E0D\u662F\u4E00\u4E2A\u65E5\u671F.

类型转换错误:都会在隐含模型 中创建错误消息。其错误代码前缀说明如下:
required:必要的参数不存在。如 @RequiredParam(“param1”)标注了一个入参,但是该参数不存在
typeMismatch:在数据绑定时,发生数据类型不匹配的问题
methodInvocation:Spring MVC 在调用处理方法时发生了错误

2、在转发配置器中注册国际化资源文件:

<!-- 配置国际化资源文件 -->
<bean id="messageSource"
	class="org.springframework.context.support.ResourceBundleMessageSource">
	<property name="basename" value="i18n"></property>	
</bean>

 

 

 

springmvc中级-mvc:annotation-driven解析

一、常用场景

1、编辑转发器配置器,实现特定url直接跳转时,不需要handler处理。但是会导致其他需要handler处理的url,失效。所以用 mvc:annotation-driven  解决失效问题。

2、编辑转发器配置器,实现静态资源请求,不需要handler处理。但是会导致其他需要handler处理的url,失效。所以用 mvc:annotation-driven  解决失效问题。

3、自定义类型转换器时,需要 mvc:annotation-driven的 ConversionService 属性,来标记已自定义的类型转换器。

二、应用解析

<mvc:annotation-driven /> 会自动注册RequestMappingHandlerMappingRequestMappingHandlerAdapter 与ExceptionHandlerExceptionResolver  三个bean。

还将提供以下支持:
–支持使用 ConversionService 实例对表单参数进行类型转换
–支持使用 @NumberFormat @DateTimeFormat注解完成数据类型的格式化
–支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证
–支持使用 @RequestBody@ResponseBody 注解

springmvc运算-基础练手项目(5)-项目代码小结

一、项目代码概览

二、配置文件

1、web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

2、转发配置器

在web.xml中已经取了名字叫: springmvc.xml

<?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:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

	<!-- 配置自动扫描的包 -->
	<context:component-scan base-package="com.test.springmvc"></context:component-scan>

	<!-- 配置视图解析器 -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/views/"></property>
		<property name="suffix" value=".jsp"></property>
	</bean>

   <!--  
		default-servlet-handler 将在 SpringMVC 上下文中定义一个 DefaultServletHttpRequestHandler,
		它会对进入 DispatcherServlet 的请求进行筛查, 如果发现是没有经过映射的请求, 就将该请求交由 WEB 应用服务器默认的 
		Servlet 处理. 如果不是静态资源的请求,才由 DispatcherServlet 继续处理

		一般 WEB 应用服务器默认的 Servlet 的名称都是 default.
		若所使用的 WEB 服务器的默认 Servlet 名称不是 default,则需要通过 default-servlet-name 属性显式指定
		
	-->
	<mvc:default-servlet-handler/>
	<!-- 前面的 mvc:default-servlet-handler 可以解决静态资源了,但是让其他经过handler的请求,又失效了,所以配置了下面的 mvc:annotation-driven 来解决问题 -->
	<mvc:annotation-driven></mvc:annotation-driven>	
	

</beans>

三、处理逻辑

1、handler

EmployeeHandler.java

package com.test.springmvc.crud.handlers;

import java.util.Map;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.test.springmvc.crud.dao.DepartmentDao;
import com.test.springmvc.crud.dao.EmployeeDao;
import com.test.springmvc.crud.entities.Employee;



@Controller
public class EmployeeHandler {

	@Autowired
	private EmployeeDao employeeDao;
	
	@Autowired
	private DepartmentDao departmentDao;
	
	//该控制器中,运行所有方法前的预处理
	//【这里主要是为了在修改修改雇员信息中,率先创建pojo并进行赋值,
	//避免表单因为缺少一些字段导致赋值给pojo的属性一些为空,当然pojo是同一个】
	@ModelAttribute
	public void getEmployee(@RequestParam(value="id",required=false) Integer id,
			Map<String, Object> map){
		if(id != null){
			map.put("employee", employeeDao.get(id));
		}
	}

	//利用pojo更新雇员信息
	@RequestMapping(value="/emp",method=RequestMethod.PUT)
	public String updateEmp(Employee employee){
		employeeDao.save(employee);
		return "redirect:/emps";
	}
	
	//跳转到更新雇员信息页面
	@RequestMapping(value="/emp/{id}",method=RequestMethod.GET)
	public String doUpdateEmp(@PathVariable("id") Integer id,Map<String, Object> map){
		map.put("employee", employeeDao.get(id));
		map.put("departments", departmentDao.getDepartments());
		return "updateEmp";
	}

	
	
	//在所有雇员信息页,进行删除雇员操作  //跳转部分不需要了,本身就是跳转到显示有雇员信息页
	@RequestMapping(value="/emp/{id}",method=RequestMethod.DELETE)
	public String deleteEmp(@PathVariable("id") Integer id){
        employeeDao.delete(id);	
        System.out.println("hello delete");
		return "redirect:/emps";
	}

	
	//跳转到添加雇员信息页面
    @RequestMapping("/doAddEmp")
	public String doAddEmp(Map<String, Object> map){
    	//为了提供在添加页面,部门列表
    	map.put("departments",departmentDao.getDepartments());
    	//为了提供雇员信息的回显值,添加一个空的雇员信息
    	map.put("employee", new Employee());
    	return "addEmp";
    }
	
    //处理添加雇员请求,并重定向到 	@RequestMapping("/emps")
    @RequestMapping(value="emp", method=RequestMethod.POST)
    public String addEmp(Employee employee) {
    	employeeDao.save(employee);
    	return "redirect:/emps";
		
	}
	
	//跳转到查看所有雇员信息页
    @RequestMapping("doGetEmps")
	public String doGetEmps(){
    	return "getEmps";
    }
    
	//查看所有雇员信息,采用默认的get方法,属于crud操作
	@RequestMapping("/emps")
	public String getEmps(Map<String, Object> map){
		map.put("employees", employeeDao.getAll());
		return "showGetEmps";
	}


	
}

2、bean

Employee.java

package com.test.springmvc.crud.entities;

import java.util.Date;

import javax.validation.constraints.Past;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;

public class Employee {

	private Integer id;
	
	private String lastName;

	private String email;
	//1 male, 0 female
	private Integer gender;
	
	private Department department;
	

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public Integer getGender() {
		return gender;
	}

	public void setGender(Integer gender) {
		this.gender = gender;
	}

	public Department getDepartment() {
		return department;
	}

	public void setDepartment(Department department) {
		this.department = department;
	}

	
	@Override
	public String toString() {
		return "Employee [id=" + id + ", lastName=" + lastName + ", email="
				+ email + ", gender=" + gender + ", department=" + department
				+ "]";
	}

	public Employee(Integer id, String lastName, String email, Integer gender,
			Department department) {
		super();
		this.id = id;
		this.lastName = lastName;
		this.email = email;
		this.gender = gender;
		this.department = department;
	}

	public Employee() {
		// TODO Auto-generated constructor stub
	}
}

Department.java

package com.test.springmvc.crud.entities;

public class Department {

	private Integer id;
	private String departmentName;

	public Department() {
		// TODO Auto-generated constructor stub
	}
	
	public Department(int i, String string) {
		this.id = i;
		this.departmentName = string;
	}

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getDepartmentName() {
		return departmentName;
	}

	public void setDepartmentName(String departmentName) {
		this.departmentName = departmentName;
	}

	@Override
	public String toString() {
		return "Department [id=" + id + ", departmentName=" + departmentName
				+ "]";
	}
	
}

3、dao

EmployeeDao.java

package com.test.springmvc.crud.dao;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.test.springmvc.crud.entities.Department;
import com.test.springmvc.crud.entities.Employee;


@Repository
public class EmployeeDao {

	private static Map<Integer, Employee> employees = null;
	
	@Autowired
	private DepartmentDao departmentDao;
	
	static{
		employees = new HashMap<Integer, Employee>();

		employees.put(1001, new Employee(1001, "E-AA", "[email protected]", 1, new Department(101, "D-AA")));
		employees.put(1002, new Employee(1002, "E-BB", "[email protected]", 1, new Department(102, "D-BB")));
		employees.put(1003, new Employee(1003, "E-CC", "[email protected]", 0, new Department(103, "D-CC")));
		employees.put(1004, new Employee(1004, "E-DD", "[email protected]", 0, new Department(104, "D-DD")));
		employees.put(1005, new Employee(1005, "E-EE", "[email protected]", 1, new Department(105, "D-EE")));
	}
	
	private static Integer initId = 1006;
	
	public void save(Employee employee){
		if(employee.getId() == null){
			employee.setId(initId++);
		}
		
		employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
		employees.put(employee.getId(), employee);
	}
	
	public Collection<Employee> getAll(){
		return employees.values();
	}
	
	public Employee get(Integer id){
		return employees.get(id);
	}
	
	public void delete(Integer id){
		employees.remove(id);
	}
}

DepartmentDao.java

package com.test.springmvc.crud.dao;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Repository;

import com.test.springmvc.crud.entities.Department;


@Repository
public class DepartmentDao {

	private static Map<Integer, Department> departments = null;
	
	static{
		departments = new HashMap<Integer, Department>();
		
		departments.put(101, new Department(101, "D-AA"));
		departments.put(102, new Department(102, "D-BB"));
		departments.put(103, new Department(103, "D-CC"));
		departments.put(104, new Department(104, "D-DD"));
		departments.put(105, new Department(105, "D-EE"));
	}
	
	public Collection<Department> getDepartments(){
		return departments.values();
	}
	
	public Department getDepartment(Integer id){
		return departments.get(id);
	}
	
}

四、jsp页面

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<a href="doGetEmps">转到请求所有雇员信息页</a>
<br>
<a href="doAddEmp">转到添加雇员信息页</a>

</body>
</html>

getEmps.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<a href="emps">请求所有雇员信息(超链接属于get请求)</a>

</body>
</html>

showGetEmps.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Get All</title>


<!--  
	SpringMVC 处理静态资源:
	1. 为什么会有这样的问题:
	优雅的 REST 风格的资源URL 不希望带 .html 或 .do 等后缀
	若将 DispatcherServlet 请求映射配置为 /, 
	则 Spring MVC 将捕获 WEB 容器的所有请求, 包括静态资源的请求, SpringMVC 会将他们当成一个普通请求处理, 
	因找不到对应处理器将导致错误。
	2. 解决: 在 SpringMVC 的配置文件中配置 <mvc:default-servlet-handler/>
-->
<script type="text/javascript" src="scripts/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
	$(function(){
		alert("hello jquery");
		$(".delete").click(function(){
			var href = $(this).attr("href");
			$("form").attr("action", href).submit();			
			return false;
		});
	})
</script>


</head>
<body>

    <form action="" method="POST">
		<input type="hidden" name="_method" value="DELETE"/>
	</form>

	<c:if test="${empty requestScope.employees}">
没有任何员工信息
</c:if>
	<c:if test="${ !empty requestScope.employees }">
		<table border="1" cellpadding="10" cellspacing="0">
			<tr>
				<th>ID</th>
				<th>LastName</th>
				<th>Email</th>
				<th>Gender</th>
				<th>Department</th>
				<th>Edit</th>
				<th>Delete</th>
			</tr>

			<c:forEach items="${requestScope.employees }" var="emp">
			<tr>
			<td>${emp.id}</td>
            <td>${emp.lastName}</td>
            <td>${emp.email}</td>	            
			<td>${emp.gender==0? "Female":"Male"}</td>					
			<td>${emp.department.departmentName}</td>			
			<td><a href="emp/${emp.id}">Edit</a></td>
			<td><a class="delete" href="emp/${emp.id}">Delete</a></td>			
			</tr>
			</c:forEach>
		</table>
	</c:if>


</body>
</html>

addEmp.jsp

<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Map"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

	<!--  
		1. WHY 使用 form 标签呢 ?
		可以更快速的开发出表单页面, 而且可以更方便的进行表单值的回显
		2. 注意:
		可以通过 modelAttribute 属性指定绑定的模型属性,
		若没有指定该属性,则默认从 request 域对象中读取 command 的表单 bean
		如果该属性值也不存在,则会发生错误。
		
		大白话:form:form 表单标签,在springmvc中是一定要回显的,哪怕是第一次也要支持回显。
		所以第一次请求时,form:form 表单标签会从 request域中的 关键字中查找 默认值是command(如果 form:form标签 指定modelAttribute="XXX",则按照XXX关键字来)  查看属性值,用来填充表单值。
	
	-->
	
    <!-- 表单 路径也可以是  action="emp" -->
	<form:form action="${pageContext.request.contextPath }/emp" method="post" modelAttribute="employee">
		<!-- path标签对应 html表单标签的 name属性值,支持级联 如 path="department.id" -->
       LastName:<form:input path="lastName"  />
		<br>
       Email:<form:input path="email" />
		<%
			Map<String, String> genders = new HashMap();
				genders.put("1", "Male");
				genders.put("0", "Female");
				request.setAttribute("genders", genders);
		%>
		<br>
     Gender:<form:radiobuttons path="gender" items="${genders}" />
     <br>
     Department:<form:select path="department.id" items="${departments}" itemLabel="departmentName" itemValue="id"></form:select>
     <br>
		<input type="submit" value="submit" />
	</form:form>

</body>
</html>

updateEmp.jsp

<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Map"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

	<!--  
		1. WHY 使用 form 标签呢 ?
		可以更快速的开发出表单页面, 而且可以更方便的进行表单值的回显
		2. 注意:
		可以通过 modelAttribute 属性指定绑定的模型属性,
		若没有指定该属性,则默认从 request 域对象中读取 command 的表单 bean
		如果该属性值也不存在,则会发生错误。
		
		大白话:form:form 表单标签,在springmvc中是一定要回显的,哪怕是第一次也要支持回显。
		所以第一次请求时,form:form 表单标签会从 request域中的 关键字中查找 默认值是command(如果 form:form标签 指定modelAttribute="XXX",则按照XXX关键字来)  查看属性值,用来填充表单值。
	
	-->
    <!-- 表单 路径不可以是  action="emp" ,因为是 相对路径 只会在最后一个 目录 替换文件
    比如 http://localhost:8080/springmvc-2/emp/1003 提交后 变成 http://localhost:8080/springmvc-2/emp/emp  -->
	<form:form action="${pageContext.request.contextPath }/emp" method="post" modelAttribute="employee">
	<!-- pageContext.request.contextPath 如果项目放在根目录,就返回 -> ""  (空字符串)   如果不是就返回-> "/项目名" -->
       
       <c:if test="${employee.id == null }">
			<!-- 这个lastName的属性是不会出现的,
			这里用 if条件是因为 updateEmp.jsp 和 addEmp.jsp 本来混在一起用if 作为判断区分的 -->
			<!-- path标签对应 html表单标签的 name属性值,支持级联 如 path="department.id" -->		
			LastName: <form:input path="lastName"/>
		</c:if>
		<br>
       
       Email:<form:input path="email" />
       
		<form:hidden path="id"/>
		<input type="hidden" name="_method" value="PUT"/>
			<%-- 对于 _method 不能使用 form:hidden 标签, 因为 modelAttribute 对应的 bean 中没有 _method 这个属性 --%>
			<%-- <form:hidden path="_method" value="PUT"/> --%>
		
		<%
			Map<String, String> genders = new HashMap <String, String>() ;
				genders.put("1", "Male");
				genders.put("0", "Female");
				request.setAttribute("genders", genders);
		%>
		<br>
     Gender:<form:radiobuttons path="gender" items="${genders}" />
     <br>
     Department:<form:select path="department.id" items="${departments}" itemLabel="departmentName" itemValue="id"></form:select>
     <br>
		<input type="submit" value="submit" />
	</form:form>

</body>
</html>