Dunwu Blog

大道至简,知易行难

计算机网络之应用层

HTTP

img

超文本传输协议(英语:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网的数据通信的基础。

设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。通过 HTTP 或者 HTTPS 协议请求的资源由统一资源标识符(Uniform Resource Identifiers,URI)来标识。

安全套接字层超文本传输协议 HTTPS 为了数据传输的安全,HTTPS 在 HTTP 的基础上加入了 SSL 协议,SSL 依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。

:point_right: 扩展阅读:HTTP

DNS

域名服务器 DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。

img

域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。

img

DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输:

  • 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。
  • 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。

:point_right: 扩展阅读:DNS

FTP

文件传送协议 FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:

  • 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。
  • 数据连接:用来传送一个文件数据。

根据数据连接是否是服务器端主动建立,FTP 有主动和被动两种模式:

img

  • 主动模式:服务器端主动建立数据连接,其中服务器端的端口号为 20,客户端的端口号随机,但是必须大于 1024,因为 0~1023 是熟知端口号。
  • 被动模式:客户端主动建立数据连接,其中客户端的端口号由客户端自己指定,服务器端的端口号随机。

主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。

FTPS 是一种对常用的文件传输协议(FTP)添加传输层安全(TLS)和安全套接层(SSL)加密协议支持的扩展协议。

DHCP

动态主机配置协议 DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。

DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。

DHCP 工作过程如下:

  1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
  2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
  3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
  4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。

img

TELNET

远程登录协议 TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。

TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。

电子邮件协议

一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。

邮件协议包含发送协议和读取协议,发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。

SMTP

SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。

POP3

POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。

IMAP

IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。

常用端口

应用 应用层协议 端口号 传输层协议 备注
域名解析 DNS 53 UDP/TCP 长度超过 512 字节时使用 TCP
动态主机配置协议 DHCP 67/68 UDP
简单网络管理协议 SNMP 161/162 UDP
文件传送协议 FTP 20/21 TCP 控制连接 21,数据连接 20
远程终端协议 TELNET 23 TCP
超文本传送协议 HTTP 80 TCP
简单邮件传送协议 SMTP 25 TCP
邮件读取协议 POP3 110 TCP
网际报文存取协议 IMAP 143 TCP

Web 页面请求过程

DHCP 配置主机信息

  • 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。
  • 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。
  • 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。
  • 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。
  • 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。
  • 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。
  • 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。

ARP 解析 MAC 地址

  • 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。
  • 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。
  • 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。
  • 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。
  • DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。
  • 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。
  • 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。

DNS 解析域名

  • 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。
  • 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。
  • 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。
  • 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。
  • 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。

HTTP 请求页面

  • 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。
  • 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。
  • HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。
  • 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。
  • HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。
  • 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。

计算机网络之网络层

网络层(network layer) - 为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组或包进行传送。

  • 主要协议:IPICMP
  • 数据单元:IP 数据报(packet)。
  • 典型设备:网关、路由器。

概述

网络层向上只提供简单灵活的、无连接的、尽最大努力交付的数据报服务。网络层不提供服务质量的承诺,不保证分组交付的时限,所传送的分组可能出错、丢失、重复和失序。进程间通信的可靠性由运输层负责。

IP 协议

网际协议 IP (Internet Protocol) 定义了三种功能:

  1. IP 定义了在 TCP/IP 互联网上数据传送的基本单元和数据格式。
  2. IP 软件完成路由选择功能,选择数据传送的路径。
  3. IP 包含了一组不可靠分组传送的规则,指明了分组处理、差错信息发生以及分组的规则。

相关协议

与 IP 协议配套使用的还有三个协议:

  • 地址解析协议 ARP(Address Resolution Protocol)
  • 网际控制报文协议 ICMP(Internet Control Message Protocol)
  • 网际组管理协议 IGMP(Internet Group Management Protocol)

img

分类的 IP 地址

IP 地址的编址方式经历了三个历史阶段:

  • 分类
  • 子网划分
  • 无分类

分类

由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。

1
IP 地址 ::= {< 网络号 >, < 主机号 >}

img

子网划分

通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。

1
IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}

要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。

注意,外部网络看不到子网的存在。

无分类

无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。

1
IP 地址 ::= {< 网络前缀号 >, < 主机号 >}

CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。

CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。

一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 构成超网

在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。

IP 地址与物理地址

img

  • 物理地址是数据链路层和物理层使用的地址。
  • IP 地址是网络层和以上各层使用的地址,是一种逻辑地址。

img

IP 数据报格式

img

  • 版本 - 有 4(IPv4)和 6(IPv6)两个值。
  • 首部长度 - 占 4 位,因此最大十进制数值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
  • 区分服务 - 用来获得更好的服务,一般情况下不使用。
  • 总长度 - 包括首部长度和数据部分长度。占 16 位,因此数据报的最大长度为 2 16 - 1 = 65535 字节。
  • 生存时间 - TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。
  • 协议 - 指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。
  • 首部检验和 - 因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
  • 标识 - 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。
  • 片偏移 - 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。

img

地址解析协议 ARP

网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。

img

ARP 实现由 IP 地址得到 MAC 地址。

img

每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。

如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。

img

网际控制报文协议 ICMP

ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。

img

ICMP 报文分为差错报告报文和询问报文。

img

Ping

Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。

Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。

Traceroute

Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。

Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。

  • 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文;
  • 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。
  • 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。
  • 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。

虚拟专用网 VPN

由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。

有三个专用地址块:

  • 10.0.0.0 ~ 10.255.255.255
  • 172.16.0.0 ~ 172.31.255.255
  • 192.168.0.0 ~ 192.168.255.255

VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。

下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。

img

网络地址转换 NAT

专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。

在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。

路由器的结构

路由器从功能上可以划分为:路由选择和分组转发。

分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。

img

路由器分组转发流程

  • 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。
  • 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
  • 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
  • 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
  • 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
  • 报告转发分组出错。

img

路由选择协议

路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。

互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。

可以把路由选择协议划分为两大类:

  • 自治系统内部的路由选择:RIP 和 OSPF
  • 自治系统间的路由选择:BGP

内部网关协议 RIP

RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15,超过 15 表示不可达。

RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。

距离向量算法:

  • 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1;
  • 对修改后的 RIP 报文中的每一个项目,进行以下步骤:
  • 若原来的路由表中没有目的网络 N,则把该项目添加到路由表中;
  • 否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。
  • 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。

RIP 协议实现简单,开销小。但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。

内部网关协议 OSPF

开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。

开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。

OSPF 具有以下特点:

  • 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。
  • 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。
  • 只有当链路状态发生变化时,路由器才会发送信息。

所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。

外部网关协议 BGP

BGP(Border Gateway Protocol,边界网关协议)

AS 之间的路由选择很困难,主要是由于:

  • 互联网规模很大;
  • 各个 AS 内部使用不同的路由选择协议,无法准确定义路径的度量;
  • AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。

BGP 只能寻找一条比较好的路由,而不是最佳路由。

每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。

img

计算机网络之数据链路层

数据链路层(Data Link Layer) - 网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。

  • 主要协议:PPPCSMA/CD 等。
  • 数据单元:帧(frame)。
  • 典型设备:二层交换机、网桥、网卡。

简介

  • 链路是从一个节点到相邻节点的一段物理线路,数据链路则是在链路的基础上增加了一些必要的硬件(网络适配器)和软件(协议)。
  • 数据链路层三个基本问题:封装成帧、透明传输、差错检测。
  • 数据链路层有两种信道类型:点对点信道(主要使用 PPP)和广播信道(主要使用 CSMA/CD)。
  • 以太网 MAC 层的地址。
  • 适配器、转发器、集线器、网桥、以太网交换机的作用及使用场合。

基本问题

封装成帧

为网络层传下来的 IP 数据报添加首部和尾部,用于标记帧的开始和结束。

为了提高传输效率,应该让数据部分长度尽可能大于首部和尾部。但是,每种链路层协议都限制了帧的数据部分长度上线——最大传送单元 MTU(Maximum Transfer Unit)

img

透明传输

透明表示:某一个实际存在的事物看起来好像不存在一样。

帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。

img

差错检测

目前数据链路层广泛使用了循环冗余检验 CRC(Cyclic redundancy check)来检查比特差错。

点对点信道

点对点信道使用一对一的点对点通信方式。

对于点对点的链路,点对点协议 PPP(Point-to-Point Protocol)是使用最广泛的数据链路层协议。

PPP 协议

互联网用户通常都要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。

img

PPP(点到点协议)是为在同等单元之间传输数据包这样的简单链路设计的链路层协议。这种链路提供全双工操作,并按照顺序传递数据包。设计目的主要是用来通过拨号或专线方式建立点对点连接发送数据,使其成为各种主机、网桥和路由器之间简单连接的一种共通的解决方案。

img

PPP 的帧格式:

  • F 字段为帧的定界符
  • A 和 C 字段暂时没有意义
  • FCS 字段是使用 CRC 的检验序列
  • 信息部分的长度不超过 1500

广播信道

广播信道(broadcast channel)是通过广播的方式传输信息的信息通道。

所有的节点都在同一个广播信道上发送数据,因此需要有专门的控制方法进行协调,避免发生冲突(冲突也叫碰撞)。

主要有两种控制方法进行协调,一个是使用信道复用技术,一是使用 CSMA/CD 协议。

CSMA/CD 协议

CSMA/CD(Carrier Sense Multiple Access with Collision Detection)即带冲突检测的载波监听多路访问技术(载波监听多点接入/碰撞检测)。

  • 多点接入 :说明这是总线型网络,许多计算机以多点接入的方式连接在一根总线上。
  • 载波监听 :每个主机都必须不停地监听信道。发送前监听,如果忙则等待,如果空闲则发送。
  • 碰撞检测 :即边发送边检测。若检测到信道有干扰信号,则表示产生了碰撞,于是就要停止发送数据,计算出退避等待时间,然后使用 CSMA 方法继续尝试发送。计算退避等待时间采用的是二进制指数退避算法

局域网

局域网 LAN(Local Area Network)是指在某一区域内由多台计算机互联成的计算机组。

局域网的拓扑结构通常为总线型和环型。

局域网技术主要有:以太网、令牌环网、FDDI 网和无线局域网等。

以太网

以太网(Ethernet)是一种星型拓扑结构局域网。

以太网是目前应用最广泛的局域网。

以太网使用 CSMA/CD 协议。

MAC 地址

MAC 地址(Media Access Control Address),也称为以太网地址或物理地址,它是一个用来确认网上设备位置的地址。

MAC 地址长度为 6 字节(48 位),用于唯一标识网络适配器(网卡)。

一台主机拥有多少个网络适配器就有多少个 MAC 地址。

img

设备

适配器

网络适配器一般指网卡。

网卡是工作在链路层的网络组件,是局域网中连接计算机和传输介质的接口,不仅能实现与局域网传输介质之间的物理连接和电信号匹配,还涉及帧的发送与接收、帧的封装与拆封、介质访问控制、数据的编码与解码以及数据缓存的功能等。

网卡和局域网之间的通信是通过电缆或双绞线以串行传输方式进行的。而网卡和计算机之间的通信则是通过计算机主板上的 I/O 总线以并行传输方式进行。

集线器

集线器(Hub)的主要功能是对接收到的信号进行再生整形放大,以扩大网络的传输距离,同时把所有节点集中在以它为中心的节点上。

使用集线器可以在物理层扩展以太网。

网桥

网桥(Bridge)是早期的两端口二层网络设备,用来连接不同网段。网桥的两个端口分别有一条独立的交换信道,不是共享一条背板总线,可隔离冲突域。网桥比集线器(Hub)性能更好,集线器上各端口都是共享同一条背板总线的。后来,网桥被具有更多端口、同时也可隔离冲突域的交换机(Switch)所取代。

以太网交换机

以太网交换机是基于以太网传输数据的交换机,以太网采用共享总线型传输媒体方式的局域网。以太网交换机的结构是每个端口都直接与主机相连,并且一般都工作在全双工方式。交换机能同时连通许多对端口,使每一对相互通信的主机都能像独占通信媒体那样,进行无冲突地传输数据。

  • 以太网交换机的每个端口都直接与主机相连,并且一般都工作在全双工方式。
  • 交换机能同时连通许多对的端口,使每一对相互通信的主机都能像独占通信媒体那样,进行无冲突地传输数据。
  • 用户独占传输媒体的带宽,若一个接口到主机的带宽是 10Mbit 每秒,那么有 10 个接口的交换机的总容量是 100Mbit 每秒。这是交换机的最大优点。

计算机网络之物理层

摘要

物理层(Physical Layer) - 物理层只接收和发送一串比特(bit)流,不考虑信息的意义和信息结构。

  • 数据单元:比特流。
  • 典型设备:光纤、同轴电缆、双绞线、中继器和集线器。

通信系统模型

img

通信系统模型分为三大部分:源系统(包括信源和发送器)、传输系统、目的系统(包括信宿接收器)。

重要概念:

  • 信源 - 也叫源点。产生各类信息的实体。
  • 信道 - 通信的通道,是信号传输的媒介。
  • 信宿 - 传输信息的归宿。
  • 码元 - 在数字通信中常常用时间间隔相同的符号来表示一个二进制数字,这样的时间间隔内的信号称为(二进制)码元。

通信方式

img

有三种通信方式:

  • 单工通信:单向传输
  • 半双工通信:双向交替传输
  • 全双工通信:双向同时传输

通信信号

通信的目的是传送消息。如语音、文字、图像、视频都是消息。数据时传送消息的实体。信号是数据的电气或电磁的表现。

模拟信号和数字信号

  • 模拟信号 - 模拟信号是连续的信号。
  • 数字信号 - 数字信号是离散的信号。

调制解调

重要概念:

  • 基带信号 - 来自信源的信号叫做基带信号。
  • 调制 - 将各种数字基带信号转换成适于信道传输的数字调制信号(已调信号或频带信号)。简单来说:调制即,数字 -> 模拟。
  • 解调 - 在接收端将收到的数字频带信号还原成数字基带信号。简单来说:解调即,模拟 -> 数字。

📌 提示:我们上网时所用到的调制解调器(俗称“猫”),指的就是转换数字和模拟信号的机器。

信号要在信道上传输就要经过调制。

调制分为:基带调制和带通调制

基本带通调制方法

img

如果你收听过广播,一定经常听到 AM、FM 这两个关键词,这是什么意思呢?答案如下:

  • 调幅(AM) - 即载波的振幅随基带数字信号而变化。
  • 调频(FM) - 即载波的频率随基带数字信号而变化。
  • 调相(PM) - 即载波的初始相位随基带数字信号而变化。

📌 提示:我们收听广播时,为了接收不同广播台的信号,就要调整 AM 或 FM,指的就是这里的调制方法。

通信媒介

通信媒介分为两大类:

  • 导引型 - 双绞线、电缆、光纤
  • 非导引型 - 无线、红外线、大气、激光

信道复用

信道复用就是将用于传输信道的总带宽划分成若干个子频带(或称子信道),每一个子信道传输一路信号。

  • 频分复用
  • 时分复用
  • 波分复用
  • 码分复用

计算机网络指南

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

img

💡 指南

学习之前,先看一下入门三问:

一、什么是计算机网络?

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

——摘自百度百科

二、为什么学习计算机网络?

计算机网络是计算机科学的基础课程,也是计算机专业考研必考科目,可见其重要性。作为一名程序员,了解计算机网络,对于 Web 领域,通信领域的开发有莫大的帮助。

在浏览器中访问网页的原理是什么?Wifi 是如何工作的?防火墙是如何保障网络安全的?什么是安全证书?Cookie 和 Session 是什么东西?。。。

如果你接触过这些技术,如果你想了解这些技术的原理,那么你就有必要学习一下计算机网络了。

三、如何学习计算机网络?

本人有 2 年通信领域开发经验,从事通信设备上的协议开发。就我个人的学习经验来看,学习计算机网络可以分为以下阶段:

  • 基础阶段——一般性的了解网络协议分层及各层功能
    • 了解计算机网络协议分层(OSI)有哪些层,分层的依据是什么(即每层的功能是什么)
    • 了解每层的主要通信设备有哪些;
    • 了解每层有哪些重要网络协议,这些协议有什么作用,基本原理是什么?
    • 了解每层的传输数据形式(如:报文、帧等)
  • 进阶阶段——系统学习计算机网络知识,将各层主要协议功能串联起来
    • 学习 TCP/IP 详解 卷 1、卷 2、卷 3(内容详实,但文字较为晦涩,不适合初学者)
  • 专业阶段——根据业务领域,有针对性的学习
    • 网络协议很多,而且专业性非常强。精通所有协议,几乎是不可能的,所以有必要根据自己的业务领域,有针对性的深入学习协议。如果你是做 web 开发,那么你很有必要认真学习一下 HTTP、DNS 协议;如果你是做路由器、交换机领域通信开发,那么你应该更深入学习一下 IP/TCP/UDP 协议。。。
    • 如何深入学习协议,最好的学习方式,就是深入学习 RFC,并结合实际的协议报文去了解。

核心概念

  • 计算机网络 - 计算机网络(computer network),通常也简称网络,是利用通信设备和线路将地理位置不同的、功能独立的多个计算机系统连接起来,以功能完善的网络软件实现网络的硬件软件及资源共享信息传递的系统。简单的说即连接两台或多台计算机进行通信的系统。
  • 互联网 - 互联网(Internet),即网络的网络。

拓扑结构

计算机网络的拓扑结构可分为:

img

  • 网型拓扑网型网(Mesh network)
  • 环型拓扑环型网(Ring network)
  • 星型拓扑星型网(Star network)
  • 树状拓扑树型网(Tree network)
  • 总线拓扑总线网(Bus network)

作用范围

  • 广域网 WAN(Wide Area Network)
  • 城域网 MAN(Metropolitan Area Network)
  • 局域网 LAN(Local Area Network)
  • 个人区域网 PAN(Personal Area Network)

性能指标

  • 速率 - 速率的单位是 bit/s(比特每秒)。
  • 带宽(bandwidth) - 带宽有以下两种不同的意义。
    • 信号的带宽是指该信号所包含的各种不同频率成分所占据的频率范围。这种意义的带宽的单位是赫 (或千赫,兆赫,吉赫等)。
    • 网络的带宽表示在单位时间内从网络中的某一点到另一点所能通过的最高数据率。这种意义的带宽的单位是 bit/s(比特每秒)。
  • 吞吐量(throughput) - 吞吐量表示在单位时间内通过某个网络(或信道、接口)的数据量。例如,对于一个 100 Mbit/s 的以太网,其额定速率是 100 Mbit/s。
  • 时延(delay)
    • 总时延 = 排队时延 + 处理时延 + 传输时延 + 传播时延

网络分层

计算机网络如何分层?各层的作用是什么?各层的主要协议、设备分别是什么?

这是学习计算机网络知识宏观层面必须要了解的核心点。知道了这些,对于网络的体系结构就基本上了解了。

img

计算机网络分层一般有三种划分体系:OSI 分层;五层协议分层;TCP/IP 协议分层。

  • OSI 的七层体系结构概念清楚,理论完整,但是比较复杂且不实用,所以并不流行。
  • 五层协议分层是一种折中方案,在现实中更为流行。

img

物理层

物理层(Physical Layer)只接收和发送一串比特(bit)流,不考虑信息的意义和信息结构。

扩展阅读:计算机网络之物理层

  • 关键词:调制、解调、数字信号、模拟信号、通信媒介、信道复用
  • 数据单元:比特流。
  • 典型设备:光纤、同轴电缆、双绞线、中继器和集线器。

数据链路层

网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,数据链路层(Data Link Layer)就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。

扩展阅读:计算机网络之数据链路层

  • 关键词:点对点信道、广播信道、PPPCSMA/CD、局域网、以太网、MAC、适配器、集线器、网桥、交换机
  • 主要协议:PPPCSMA/CD 等。
  • 数据单元:帧(frame)。
  • 典型设备:二层交换机、网桥、网卡。

网络层

网络层(network layer)为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组或包进行传送。

扩展阅读:计算机网络之网络层

  • 关键词:IPICMPARP、路由
  • 主要协议:IP
  • 数据单元:IP 数据报(packet)。
  • 典型设备:网关、路由器。

传输层

传输层(transport layer)为两台主机中进程间的通信提供通用的数据传输服务。

扩展阅读:计算机网络之网络层

  • 关键词:UDPTCP、滑动窗口、拥塞控制、三次握手
  • 主要协议:TCPUDP
  • 数据单元:报文段(segment)或用户数据报。

~~会话层~~

~~会话层(Session Layer)不参与具体的传输,它提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制。~~

~~表示层~~

~~表示层(Presentation Layer)是为在应用过程之间传送的信息提供表示方法的服务,它关心的只是发出信息的语法与语义。表示层要完成某些特定的功能,主要有不同数据编码格式的转换,提供数据压缩、解压缩服务,对数据进行加密、解密。~~

应用层

应用层(application layer)通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间通信和交互的规则。

扩展阅读:计算机网络之应用层

  • 关键词:HTTPDNSFTPTELNETDHCP
  • 主要协议:HTTPDNSSMTPTelnetFTPSNMP 等。
  • 数据单元:报文(message)。

资源

🚪 传送

| 回首頁 |

Spring 之 JDBC

JDBC 是 Java 语言中用来规范客户端程序如何访问数据库的应用程序接口,提供了增、删、改、查数据库的方法。

JDBC 入门示例

JDBC 的工作步骤大致如下:

  1. 创建实体类。
  2. 声明数据库读写接口的 DAO 接口。定义 DAO 的好处在于对于数据层上层的业务,调用 DAO 时仅关注对外暴露的读写方法,而不考虑底层的具体持久化方式。这样,便于替换持久化方式。
  3. 创建一个 DAO 接口的实现类,使用 Spring 的 JDBC 模板去实现接口。
  4. 最后,定义一个 DAO 接口的实现类的 JavaBean,并将数据源注入进去。

假设,我们要通过 Spring + JDBC 访问一张 Mysql 数据表 useruser 表的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 创建用户表
CREATE TABLE `user` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名',
`age` INT(3) NOT NULL DEFAULT 0 COMMENT '年龄',
`address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址',
`email` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件',
PRIMARY KEY (`id`),
UNIQUE (`name`)
) COMMENT = '用户表';

INSERT INTO `user` (`name`, `age`, `address`, `email`)
VALUES ('张三', 18, '北京', 'xxx@163.com');
INSERT INTO `user` (`name`, `age`, `address`, `email`)
VALUES ('李四', 19, '上海', 'xxx@163.com');

定义实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Objects;

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
private String address;
private String email;
}

定义 DAO 接口

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
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;

/**
* user 表 Dao 接口
*
* @author <a href="mailto:forbreak@163.com">Zhang Peng</a>
* @since 2019-11-18
*/
public interface UserDao {

// DML
// -------------------------------------------------------------------
void insert(User user);

void batchInsert(List<User> users);

void deleteByName(String name);

void deleteAll();

void update(User user);

Integer count();

List<User> list();

User queryByName(String name);

JdbcTemplate getJdbcTemplate();

// DDL
// -------------------------------------------------------------------
void truncate();

void recreateTable();

}

定义 DAO 实现类

通过 JdbcTemplate 执行对应数据源符合语法的 SQL,即可完成各种数据库访问。

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
package io.github.dunwu.springboot.core.data.jdbc;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

/**
* user 表 Dao 接口实现类
*
* @author <a href="mailto:forbreak@163.com">Zhang Peng</a>
* @since 2019-11-18
*/
@Repository
public class UserDaoImpl implements UserDao {

private JdbcTemplate jdbcTemplate;

public UserDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

@Override
public void insert(User user) {
jdbcTemplate.update("INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)",
user.getName(), user.getAge(), user.getAddress(), user.getEmail());
}

@Override
@Transactional(rollbackFor = Exception.class)
public void batchInsert(List<User> users) {
String sql = "INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)";

List<Object[]> params = new ArrayList<>();

users.forEach(user -> {
params.add(new Object[] { user.getName(), user.getAge(), user.getAddress(), user.getEmail() });
});
jdbcTemplate.batchUpdate(sql, params);
}

@Override
public void deleteByName(String name) {
jdbcTemplate.update("DELETE FROM user WHERE name = ?", name);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAll() {
jdbcTemplate.execute("DELETE FROM user");
}

@Override
public void update(User user) {
jdbcTemplate.update("UPDATE user SET name=?, age=?, address=?, email=? WHERE id=?",
user.getName(), user.getAge(), user.getAddress(), user.getEmail(), user.getId());
}

@Override
public Integer count() {
try {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class);
} catch (EmptyResultDataAccessException e) {
return null;
}
}

@Override
public List<User> list() {
return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class));
}

@Override
public User queryByName(String name) {
try {
return jdbcTemplate.queryForObject("SELECT * FROM user WHERE name = ?",
new BeanPropertyRowMapper<>(User.class), name);
} catch (EmptyResultDataAccessException e) {
return null;
}
}

@Override
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}

@Override
public void truncate() {
jdbcTemplate.execute("TRUNCATE TABLE user");
}

@Override
public void recreateTable() {
jdbcTemplate.execute("DROP TABLE IF EXISTS user");

String sqlStatement =
"CREATE TABLE IF NOT EXISTS user (\n"
+ " id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',\n"
+ " name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名',\n"
+ " age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄',\n"
+ " address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址',\n"
+ " email VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件',\n"
+ " PRIMARY KEY (id)\n"
+ ") COMMENT = '用户表';";
jdbcTemplate.execute(sqlStatement);
}

}

测试类

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
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
@Rollback
@SpringBootTest(classes = { SpringBootDataJdbcApplication.class })
public class DataJdbcMysqlDataSourceTest {

@Autowired
private UserDao userDAO;

@BeforeEach
public void before() {
userDAO.truncate();
}

@Test
public void insert() {
userDAO.insert(new User("张三", 18, "北京", "user1@163.com"));
User linda = userDAO.queryByName("张三");
assertThat(linda).isNotNull();
}

@Test
public void batchInsert() {
List<User> users = new ArrayList<>();
users.add(new User("张三", 18, "北京", "user1@163.com"));
users.add(new User("李四", 19, "上海", "user1@163.com"));
users.add(new User("王五", 18, "南京", "user1@163.com"));
users.add(new User("赵六", 20, "武汉", "user1@163.com"));

userDAO.batchInsert(users);
int count = userDAO.count();
assertThat(count).isEqualTo(4);

List<User> list = userDAO.list();
assertThat(list).isNotEmpty().hasSize(4);
list.forEach(user -> {
log.info(user.toString());
});
}

@Test
public void delete() {
List<User> users = new ArrayList<>();
users.add(new User("张三", 18, "北京", "user1@163.com"));
users.add(new User("李四", 19, "上海", "user1@163.com"));
users.add(new User("王五", 18, "南京", "user1@163.com"));
users.add(new User("赵六", 20, "武汉", "user1@163.com"));
userDAO.batchInsert(users);

userDAO.deleteByName("张三");
User user = userDAO.queryByName("张三");
assertThat(user).isNull();

userDAO.deleteAll();
List<User> list = userDAO.list();
assertThat(list).isEmpty();
}

@Test
public void update() {
userDAO.insert(new User("张三", 18, "北京", "user1@163.com"));
User oldUser = userDAO.queryByName("张三");
oldUser.setName("张三丰");
userDAO.update(oldUser);
User newUser = userDAO.queryByName("张三丰");
assertThat(newUser).isNotNull();
}

}

Spring Boot JDBC

完整示例:spring-boot-data-jdbc

引入 Spring Boot 依赖

你可以通过 Spring Boot 官方的初始化器(Spring Initializr)选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

配置数据源

引入依赖后,需要在 application.propertiesapplication.yml 文件中指定数据源配置。

下面是一个最基本的数据源配置示例:

1
2
3
4
spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.username = root
spring.datasource.password = root

需要根据实际情况,替换 urlusernamepassword

测试

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
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;
import javax.sql.DataSource;

@Slf4j
@SpringBootApplication
public class SpringBootDataJdbcApplication implements CommandLineRunner {

private final JdbcTemplate jdbcTemplate;

public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

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

@Override
public void run(String... args) throws Exception {
DataSource dataSource = jdbcTemplate.getDataSource();

Connection connection;
if (dataSource != null) {
connection = dataSource.getConnection();
} else {
log.error("连接数据源失败!");
return;
}

if (connection != null) {
log.info("数据源 Url: {}", connection.getMetaData().getURL());
} else {
log.error("连接数据源失败!");
}
}

}

运行 main 方法后,控制台会输出以下内容,表示数据源连接成功:

1
20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8

Spring JDBC

完整示例:spring-data-jdbc

spring-boot-starter-data-jdbc 引入了 spring-jdbc ,其 JDBC 特性就是基于 spring-jdbc

引入 Spring 依赖

在 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
    <dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
</dependencies>
</project>

基于 JDBC 驱动的数据源配置

下面是一个 mysql 的 JDBC 数据源配置实例:

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns="http://www.springframework.org/schema/beans"
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.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">

<!-- 引入配置文件 -->
<context:property-placeholder location="classpath:properties/mysql.properties" />

<!-- 使用JDBC驱动的数据源 -->
<!-- (1)在每个连接请求时都会返回一个新建的连接。性能不高 -->
<bean id="dataSource1" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>

<!-- (2)在每个连接请求时都会返回同一个连接。不适用于多线程 -->
<bean id="dataSource2" class="org.springframework.jdbc.datasource.SingleConnectionDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>

<!-- JDBC模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg ref="dataSource1" />
</bean>
<bean id="userDao" class="io.github.dunwu.springboot.data.jdbc.UserDaoImpl">
<constructor-arg ref="jdbcTemplate" />
</bean>

<!-- 初始化数据表结构 -->
<jdbc:initialize-database data-source="dataSource1" ignore-failures="ALL">
<jdbc:script location="classpath:sql/schema.sql" />
<jdbc:script location="classpath:sql/data.sql" />
</jdbc:initialize-database>
</beans>

测试

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

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.io.IOException;
import java.sql.SQLException;

@SuppressWarnings("all")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:data/spring-mysql.xml" })
public class MysqlJdbcTest {

@Autowired
private ApplicationContext ctx;

@Before
public void before() {
ctx = JdbcDemo.getMysqlApplicationContext();
}

@Test
public void testExecJdbcOper() throws SQLException, IOException {
UserDao userDao = (UserDaoImpl) ctx.getBean("userDao");
JdbcDemo.execJdbcOper(userDao);
}

@After
public void after() {
((ClassPathXmlApplicationContext) ctx).close();
}

}

JdbcTemplate API

Spring 将数据访问的样板式代码提取到模板类中。Spring 提供了 3 个 JDBC 模板类:

  • JdbcTemplate:最基本的 Spring JDBC 模板,这个模板支持最简单的 JDBC 数据库访问功能以及简单的索引参数查询。
  • SimpleJdbcTemplate:改模板类利用 Java 5 的一些特性,如自动装箱、泛型以及可变参数列表来简化 JDBC 模板的使用。
  • NamedParameterJdbcTemplate:使用该模板类执行查询时,可以将查询值以命名参数的形式绑定到 SQL 中,而不是使用简单的索引参数。

spring-jdbc 最核心的 API 无疑就是 JdbcTemplate,可以说所有的 JDBC 数据访问,几乎都是围绕着这个类去工作的。Spring 对数据库的操作在 Jdbc 层面做了深层次的封装,利用依赖注入,把数据源配置装配到 JdbcTemplate 中,再由 JdbcTemplate 负责具体的数据访问。

JdbcTemplate 主要提供以下几类方法:

  • execute 方法:可以用于执行任何 SQL 语句,一般用于执行 DDL 语句;
  • update 方法及 batchUpdate 方法:update 方法用于执行新增、修改、删除等语句;batchUpdate 方法用于执行批处理相关语句;
  • query 方法及 queryForXXX 方法:用于执行查询相关语句;
  • call 方法:用于执行存储过程、函数相关语句。

为了方便演示,以下增删改查操作都围绕一个名为 user 的表(该表的主键 id 是自增序列)进行,该表的数据实体如下:

1
2
3
4
5
6
7
public class User {
private Integer id;
private String name;
private Integer age;

// 省略 getter/setter
}

数据实体只要是一个纯粹的 Java Bean 即可,无需任何注解修饰。

execute 方法

使用 execute 执行 DDL 语句,创建一个名为 test 的数据库,并在此数据库下新建一个名为 user 的表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void recreateTable() {
jdbcTemplate.execute("DROP DATABASE IF EXISTS test");
jdbcTemplate.execute("CREATE DATABASE test");
jdbcTemplate.execute("USE test");
jdbcTemplate.execute("DROP TABLE if EXISTS user");
jdbcTemplate.execute("DROP TABLE if EXISTS user");
// @formatter:off
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE user (id int (10) unsigned NOT NULL AUTO_INCREMENT,\n")
.append("name varchar (64) NOT NULL DEFAULT '',\n")
.append("age tinyint (3) NOT NULL DEFAULT 0,\n")
.append("PRIMARY KEY (ID));\n");
// @formatter:on
jdbcTemplate.execute(sb.toString());
}

update 方法

新增数据

1
2
3
public void insert(String name, Integer age) {
jdbcTemplate.update("INSERT INTO user(name, age) VALUES(?, ?)", name, age);
}

删除数据

1
2
3
public void delete(String name) {
jdbcTemplate.update("DELETE FROM user WHERE name = ?", name);
}

修改数据

1
2
3
public void update(User user) {
jdbcTemplate.update("UPDATE USER SET name=?, age=? WHERE id=?", user.getName(), user.getAge(), user.getId());
}

批处理

1
2
3
4
5
6
7
8
9
10
public void batchInsert(List<User> users) {
String sql = "INSERT INTO user(name, age) VALUES(?, ?)";

List<Object[]> params = new ArrayList<>();

users.forEach(item -> {
params.add(new Object[] {item.getName(), item.getAge()});
});
jdbcTemplate.batchUpdate(sql, params);
}

query 方法

查单个对象

1
2
3
4
5
6
7
8
public User queryByName(String name) {
try {
return jdbcTemplate
.queryForObject("SELECT * FROM user WHERE name = ?", new BeanPropertyRowMapper<>(User.class), name);
} catch (EmptyResultDataAccessException e) {
return null;
}
}

查多个对象

1
2
3
public List<User> list() {
return jdbcTemplate.query("select * from USER", new BeanPropertyRowMapper(User.class));
}

获取某个记录某列或者 count、avg、sum 等函数返回唯一值

1
2
3
4
5
6
7
public Integer count() {
try {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class);
} catch (EmptyResultDataAccessException e) {
return null;
}
}

SpringBoot JDBC 配置

JdbcTemplateAutoConfiguration 类

JdbcTemplateAutoConfigurationJdbcTemplate 自动配置类,它负责实例化 JdbcTemplate

1
2
3
4
5
6
7
8
9
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {

}

JdbcTemplateAutoConfiguration 类的源码解读:

  • @AutoConfigureAfter(DataSourceAutoConfiguration.class) 表明 JdbcTemplateAutoConfiguration 必须在 DataSourceAutoConfiguration 执行完之后才开始工作,这意味着:JdbcTemplate 的初始化必须在 DataSource 初始化之后。
  • JdbcPropertiesJdbcTemplateAutoConfiguration 的配置选项类,允许使用者通过设置选项控制 JdbcTemplate 初始化行为。
  • @Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class }) 表明引入 JdbcTemplateConfigurationNamedParameterJdbcTemplateConfiguration 两个配置类,具体的实例化 JdbcTemplate 的工作也是放在这两个配置中完成。

JdbcTemplateConfiguration 类

JdbcTemplateConfiguration 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {

@Bean
@Primary
JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
JdbcProperties.Template template = properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
}
return jdbcTemplate;
}

}

JdbcTemplateConfiguration 源码解读:JdbcTemplateConfiguration 中根据 DataSourceJdbcProperties 实例化了一个 JdbcTemplate

NamedParameterJdbcTemplateConfiguration 类

NamedParameterJdbcTemplateConfiguration 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(JdbcTemplate.class)
@ConditionalOnMissingBean(NamedParameterJdbcOperations.class)
class NamedParameterJdbcTemplateConfiguration {

@Bean
@Primary
NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) {
return new NamedParameterJdbcTemplate(jdbcTemplate);
}

}

NamedParameterJdbcTemplateConfiguration 源码解读:NamedParameterJdbcTemplateConfiguration 中根据 JdbcTemplate 实例化了一个 NamedParameterJdbcTemplate

spring-data-jdbc

Spring Data 项目包含了对 JDBC 的存储库支持,并将自动为 CrudRepository 上的方法生成 SQL。对于更高级的查询,提供了 @Query 注解。

当 classpath 上存在必要的依赖项时,Spring Boot 将自动配置 Spring Data 的 JDBC 存储库。它们可以通过 spring-boot-starter-data-jdbc 的单一依赖项添加到项目中。如有必要,可以通过将 @EnableJdbcRepositories 批注或 JdbcConfiguration 子类添加到应用程序来控制 Spring Data JDBC 的配置。

更多 Spring Data JDBC 细节,可以参考 Spring Data JDBC 官方文档

参考资料

Spring 之 JPA

JPA 为对象关系映射提供了一种基于 POJO 的持久化模型。

  • 简化数据持久化代码的开发
  • 为 Java 社区屏蔽不同持久化 API 的差异

快速入门

(1)在 pom.xml 中引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

(2)设置启动注解

1
2
3
4
5
6
7
8
9
10
11
12
// 【可选】指定扫描的 Entity 目录,如果不指定,会扫描全部目录
@EntityScan("io.github.dunwu.springboot.data.jpa")
// 【可选】指定扫描的 Repository 目录,如果不指定,会扫描全部目录
@EnableJpaRepositories(basePackages = {"io.github.dunwu.springboot.data.jpa"})
// 【可选】开启 JPA auditing 能力,可以自动赋值一些字段,比如创建时间、最后一次修改时间等等
@EnableJpaAuditing
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

(3)配置

1
2
3
4
5
6
7
8
9
# 数据库连接
spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.username = root
spring.datasource.password = root
# 是否打印 JPA SQL 日志
spring.jpa.show-sql = true
# Hibernate的DDL策略
spring.jpa.hibernate.ddl-auto = create-drop

(4)定义实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Objects;
import javax.persistence.*;

@Entity
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(unique = true)
private String name;

private Integer age;

private String address;

private String email;

public User(String name, Integer age, String address, String email) {
this.name = name;
this.age = age;
this.address = address;
this.email = email;
}

@Override
public int hashCode() {
return Objects.hash(id, name);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}

if (!(o instanceof User)) {
return false;
}

User user = (User) o;

if (id != null && id.equals(user.id)) {
return true;
}

return name.equals(user.name);
}

}

(5)定义 Repository

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

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@RepositoryRestResource(collectionResourceRel = "user", path = "user")
public interface UserRepository extends JpaRepository<User, Long> {

User findUserById(@PathVariable("id") Long id);

/**
* 根据用户名查找用户
* <p>
* 示例:http://localhost:8080/user/search/findByName?name=lisi
*
* @param name 用户名
* @return {@link User}
*/
User findUserByName(@Param("name") String name);

/**
* 根据邮箱查找用户
* <p>
* 示例:http://localhost:8080/user/search/findByEmail?email=xxx@163.com
*
* @param email 邮箱
* @return {@link User}
*/
@Query("from User u where u.email=:email")
List<User> findByEmail(@Param("email") String email);

/**
* 根据用户名删除用户
*
* @param name 用户名
*/
@Transactional(rollbackFor = Exception.class)
void deleteByName(@Param("name") String name);

}

(6)测试

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
@Slf4j
@SpringBootTest(classes = { DataJpaApplication.class })
public class DataJpaTests {

@Autowired
private UserRepository repository;

@BeforeEach
public void before() {
repository.deleteAll();
}

@Test
public void insert() {
User user = new User("张三", 18, "北京", "user1@163.com");
repository.save(user);
Optional<User> optional = repository.findById(user.getId());
assertThat(optional).isNotNull();
assertThat(optional.isPresent()).isTrue();
}

@Test
public void batchInsert() {
List<User> users = new ArrayList<>();
users.add(new User("张三", 18, "北京", "user1@163.com"));
users.add(new User("李四", 19, "上海", "user1@163.com"));
users.add(new User("王五", 18, "南京", "user1@163.com"));
users.add(new User("赵六", 20, "武汉", "user1@163.com"));
repository.saveAll(users);

long count = repository.count();
assertThat(count).isEqualTo(4);

List<User> list = repository.findAll();
assertThat(list).isNotEmpty().hasSize(4);
list.forEach(this::accept);
}

private void accept(User user) { log.info(user.toString()); }

@Test
public void delete() {
List<User> users = new ArrayList<>();
users.add(new User("张三", 18, "北京", "user1@163.com"));
users.add(new User("李四", 19, "上海", "user1@163.com"));
users.add(new User("王五", 18, "南京", "user1@163.com"));
users.add(new User("赵六", 20, "武汉", "user1@163.com"));
repository.saveAll(users);

repository.deleteByName("张三");
assertThat(repository.findUserByName("张三")).isNull();

repository.deleteAll();
List<User> list = repository.findAll();
assertThat(list).isEmpty();
}

@Test
public void findAllInPage() {
List<User> users = new ArrayList<>();
users.add(new User("张三", 18, "北京", "user1@163.com"));
users.add(new User("李四", 19, "上海", "user1@163.com"));
users.add(new User("王五", 18, "南京", "user1@163.com"));
users.add(new User("赵六", 20, "武汉", "user1@163.com"));
repository.saveAll(users);

PageRequest pageRequest = PageRequest.of(1, 2);
Page<User> page = repository.findAll(pageRequest);
assertThat(page).isNotNull();
assertThat(page.isEmpty()).isFalse();
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getTotalPages()).isEqualTo(2);

List<User> list = page.get().collect(Collectors.toList());
System.out.println("user list: ");
list.forEach(System.out::println);
}

@Test
public void update() {
User oldUser = new User("张三", 18, "北京", "user1@163.com");
oldUser.setName("张三丰");
repository.save(oldUser);

User newUser = repository.findUserByName("张三丰");
assertThat(newUser).isNotNull();
}

}

常用 JPA 注解

实体

@Entity

@MappedSuperclass

当多个实体有共同的属性字段,比如说 id,则可以把它提炼出一个父类,并且加上 @MappedSuperclass,则实体基类就可以继承了。

@Table

当实体名和表名不一致时,可以通过 @Table(name="CUSTOMERS") 的形式来明确指定一个表名。

主键

@Id

@Id 注解用于声明一个实体类的属性映射为数据库的主键。

@GeneratedValue

@GeneratedValue 用于标注主键的生成策略,通过 strategy 属性指定。

默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto increment。

javax.persistence.GenerationType 中定义了以下几种可供选择的策略:

1
2
3
4
5
6
public enum GenerationType {
TABLE,
SEQUENCE,
IDENTITY,
AUTO
}
  • IDENTITY:采用数据库 ID 自增长的方式来自增主键字段,Oracle 不支持这种方式;
  • AUTO: JPA 自动选择合适的策略,是默认选项;
  • SEQUENCE:通过序列产生主键,通过 @SequenceGenerator 注解指定序列名,MySql 不支持这种方式
  • TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。

也就是如果你没有指定 strategy 属性,默认策略是 AUTO,JPA 会根据你使用的数据库来自动选择策略,比如说我使用的是 mysql 则,自动的主键策略就是 IDENTITY (auto increment)。

映射

@Column

当你的 entity 属性名和数据库中的字段名不一致,可以使用 @Column 明确指定,它也可以设置一些属性

1
@Column(length = 10, nullable = false, unique = true)
1
2
@Column(columnDefinition = "INT(3)")
private int age;

@Column 支持的参数:

  • unique 属性表示该字段是否为唯一标识,默认为 false。如果表中有一个字段需要唯一标识,则既可以使用该标记,也可以使用 @Table 标记中的 @UniqueConstraint
  • nullable 属性表示该字段是否可以为 null 值,默认为 true。
  • insertable 属性表示在使用 INSERT 插入数据时,是否需要插入该字段的值。
  • updatable 属性表示在使用 UPDATE 更新数据时,是否需要更新该字段的值。insertableupdatable 属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。
  • columnDefinition 属性表示创建表时,该字段创建的 SQL 语句,一般用于通过 Entity 生成表定义时使用。
  • table 属性表示当映射多个表时,指定表的表中的字段。默认值为主表的表名。
  • length 属性表示字段的长度,当字段的类型为 varchar 时,该属性才有效,默认为 255 个字符。
  • precision 属性和 scale 属性表示精度,当字段类型为 double 时,precision 表示数值的总长度,scale 表示小数点所占的位数。

@JoinTable

@JoinColumn

关系

表关系映射(双向映射)

  • @OneToOne:一对一关系
  • @OneToMany:一对多
  • @ManyToMany(不推荐使用,而是采用用中间对象,把多对多拆成两个对多一关系)

字段映射(单向映射):

  • @Embedded@Embeddable 嵌入式关系(单向映射)
  • @ElementCollection 集合一对多关系(单向映射)

@OneToOne

@OneToOne 表示一对一关系

@OneToMany

@OneToMany 表示一对多关系

@ManyToOne

@ManyToMany

OrderBy

查询

查询方式有:

  • 方法名字方式查询

  • @Query 注解方式查询

  • 动态 SQL 方式查询

  • Example 方式查询

JpaRepository 提供了如下表所述的内置查询

  • List<T> findAll(); - 返回所有实体
  • List<T> findAllById(Iterable<ID> var1); - 返回指定 id 的所有实体
  • T getOne(ID var1); - 根据 id 返回对应的实体,如果未找到,则返回空。
  • List<T> findAll(Sort var1); - 返回所有实体,按照指定顺序返回。
  • Page<T> findAll(Pageable var1); - 返回实体列表,实体的 offset 和 limit 通过 pageable 来指定

方法名字方式查询方式

Spring Data 通过查询的方法名和参数名来自动构造一个 JPA QQL 查询。

1
2
3
public interface UserRepository extends JpaRepository<User, Integer> {
public User findByName(String name);
}

方法名和参数名要遵守一定的规则,Spring Data JPA 才能自动转换为 JPQL:

  • 方法名通常包含多个实体属性用于查询,属性之间可以使用 ANDOR 连接,也支持 BetweenLessThanGreaterThanLike

  • 方法名可以以 findBygetByqueryBy 开头;

  • 查询结果可以排序,方法名包含 OrderBy+属性+ASC(DESC);

  • 可以通过 TopFirst 来限定查询的结果集;

  • 一些特殊的参数可以出现在参数列表里,比如 PageeableSort

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 根据名字查询,且按照名字升序
List<Person> findByLastnameOrderByFirstnameAsc(String name);

// 根据名字查询,且使用翻页查询
Page<User> findByLastname(String lastname, Pageable pageable);

// 查询满足条件的前10个用户
List<User> findFirst10ByLastname(String lastname, Sort sort);

// 使用And联合查询
List<Person> findByFirstnameAndLastname(String firstname, String lastname);

// 使用Or查询
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);

// 使用like查询,name 必须包含like中的%或者?
public User findByNameLike(String name);
Keyword Sample JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = 1?
Between findByStartDateBetween … where x.startDate between 1? and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> age) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

@Query 注解方式查询

注解 @Query 允许在方法上使用 JPQL。

其中操作针对的是对象名和对象属性名,而非数据库中的表名和字段名。

1
2
@Query("select u form User u where u.name=?1 and u.depantment.id=?2");
public User findUser(String name, Integer departmentId);
1
2
@Query("form User u where u.name=?1 and u.depantment.id=?2");
public User findUser(String name, Integer departmentId);

如果使用 SQL 而不是 JPSQL,可以使用 nativeQuery 属性,设置为 true。

1
2
@Query(value="select * from user where name=?1 and department_id=?2", nativeQuery=true)
public User nativeQuery(String name, Integer departmentId);

无论 JPQL,还是 SQL,都支持”命名参数”:

1
2
@Query(value="select * from user where name=:name and department_id=:departmentId", nativeQuery=true)
public User nativeQuery2(String name, Integer departmentId);

如果 SQL 活着 JPQL 查询结果集并非 Entity,可以用 Object[] 数组代替,比如分组统计每个部分的用户数

1
2
@Query(value="select department_id,count(*) from user group by department_id", nativeQuery=true)
public List<Object[]> queryUserCount()

这条查询将返回数组,对象类型依赖于查询结果,被示例中,返回的是 StringBigInteger 类型

查询时可以使用 PageableSort 来完成翻页和排序。

1
2
@Query("select u from User u where department.id=?1")
public Page<User> QueryUsers(Integer departmentId, Pageable page);

@Query 还允许 SQL 更新、删除语句,此时必须搭配 @Modifying 使用,比如:

1
2
3
@Modifying
@Query("update User u set u.name= ?1 where u.id= ?2")
int updateName(String name, Integer id);

动态 SQL 方式查询

可参考:SpringDataJpa 中的复杂查询和动态查询,多表查询

Example 方式查询

允许根据实体创建一个 Example 对象,Spring Data 通过 Example 对象来构造 JPQL。但是使用不灵活条件是 AND,不能使用 or,时间的大于小于,between 等。

继承 JpaRepository

1
2
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
1
2
3
4
5
6
7
8
9
10
11
public List<User> getByExample(String name) {
Department dept = new Department();
dept.setId(1);

User user = new User();
user.setName(name);
user.setDepartment(dept);
Example<User> example = Example.of(user);
List<User> list = userDao.findAll(example);
return list
}

以上代码首先创建了 User 对象,设置 查询条件,名称为参数 name,部门 id 为 1,通过 Example.of 构造了此查询。

大部分查询并非完全匹配查询,ExampleMatcher 提供了更多的条件指定.比如以 xxx 开头的所有用户,则可以使用以下代码构造

1
2
3
ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("xxx",
GenericPropertyMatchers.startsWith().ignoreCase());
Example<User> example = Example.of(user, matcher);

排序 Sort

Sort 对象用来指定排序,最简单的 Sort 对象构造可以传入一个属性名列表(不是数据库列名,是属性名)。默认采用升序排序。

1
2
3
Sort sort = new Sort("id");
//Sort sort = new Sort(Direction.DESC, "id");
return userDao.findAll(sort);

Hibernate 根据 Sort 构造了排序条件,Sort(“id”) 表示按照 id 采用默认 升序进行排序

其他 Sort 的构造方法还包括以下主要的一些:

  • public Sort(String... properties),按照指定的属性列表升序排序。
  • public Sort(Sort.Direction direction, String... properties),按照指定属性列表排序,排序由 direction 指定,direction 是一个枚举类型,有 Direction.ASCDirection.DESC
  • public Sort(Sort.Order... orders),可以通过 Order 静态方法来创建
    • public static Sort.Order asc(String property)
    • public static Sort.Order desc(String property)

分页 Page 和 Pageable

Pageable 接口用于构造翻页查询,PageRequest 是其实现类,可以通过提供的工厂方法创建 PageRequest:

注意我这边使用的是 sring boot 2.0.2 ,jpa 版本是 2.0.8,新版本与之前版本的操作方法有所不同。

  • public static PageRequest of(int page, int size)

  • public static PageRequest of(int page, int size, Sort sort) - 也可以在 PageRequest 中加入排序

  • public static PageRequest of(int page, int size, Direction direction, String... properties),或者自定义排序规则

page 是从 0 开始,表示查询页,size 指每页的期望行数。

Spring Data 翻页查询总是返回 Page 对象,Page 对象提供了以下常用的方法

  • int getTotalPages();,总的页数
  • long getTotalElements(); - 返回总数
  • List<T> getContent(); - 返回此次查询的结果集

核心 API

参考资料

SpringBoot 之属性加载详解

加载 property 顺序

Spring Boot 加载 property 顺序如下:

  1. Devtools 全局配置 (当 devtools 被激活 ~/.spring-boot-devtools.properties).
  2. 测试环境中的 @TestPropertySource 注解配置
  3. 测试环境中的属性 properties@SpringBootTest测试注解.
  4. 命令行参数
  5. SPRING_APPLICATION_JSON 属性
  6. ServletConfig 初始化参数
  7. ServletContext 初始化参数
  8. JNDI attributes from 通过 java:comp/env 配置的 JNDI 属性
  9. Java 系统属性 (System.getProperties())
  10. 操作系统环境比那里
  11. RandomValuePropertySource 加载 random.* 形式的属性
  12. jar 包外的 application-{profile}.propertiesapplication-{profile}.yml 配置
  13. jar 包内的 application-{profile}.propertiesapplication-{profile}.yml 配置
  14. jar 包外的 application.propertiesapplication.yml 配置
  15. jar 包内的 application.propertiesapplication.yml 配置
  16. @PropertySource 绑定的配置
  17. 默认属性 (通过 SpringApplication.setDefaultProperties 指定)

随机属性

RandomValuePropertySource 类用于配置随机值。

示例:

1
2
3
4
5
6
my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}

命令行属性

默认情况下, SpringApplication 会获取 -- 参数(例如 --server.port=9000 ),并将这个 property 添加到 Spring 的 Environment 中。

如果不想加载命令行属性,可以通过 SpringApplication.setAddCommandLineProperties(false) 禁用。

Application 属性文件

SpringApplication 会自动加载以下路径下的 application.properties 配置文件,将其中的属性读到 Spring 的 Environment 中。

  1. 当前目录的 /config 子目录
  2. 当前目录
  3. classpath 路径下的 /config package
  4. classpath 根路径

注:

以上列表的配置文件会根据顺序,后序的配置会覆盖前序的配置。

你可以选择 YAML(yml) 配置文件替换 properties 配置文件。

如果不喜欢 application.properties 作为配置文件名,可以使用 spring.config.name 环境变量替换:

1
$ java -jar myproject.jar --spring.config.name=myproject

可以使用 spring.config.location 环境变量指定配置文件路径:

1
$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties

Profile 特定属性

如果定义 application-{profile}.properties 形式的配置文件,将被视为 profile 环境下的特定配置。

可以通过 spring.profiles.active 参数来激活 profile,如果没有激活的 profile,默认会加载 application-default.properties 中的配置。

属性中的占位符

application.properties 中的值会被 Environment 过滤,所以,可以引用之前定义的属性。

1
2
app.name=MyApp
app.description=${app.name} is a Spring Boot application

注:你可以使用此技术来创建 Spring Boot 属性变量。请参考: Section 77.4, “Use ‘Short’ Command Line Arguments

YAML 属性

Spring 框架有两个类支持加载 YAML 文件。

  • YamlPropertiesFactoryBean 将 YAML 文件的配置加载为 Properties
  • YamlMapFactoryBean 将 YAML 文件的配置加载为 Map

示例 1

1
2
3
4
5
6
7
environments:
dev:
url: http://dev.example.com
name: Developer Setup
prod:
url: http://another.example.com
name: My Cool App

等价于:

1
2
3
4
environments.dev.url=http://dev.example.com
environments.dev.name=Developer Setup
environments.prod.url=http://another.example.com
environments.prod.name=My Cool App

YAML 支持列表形式,等价于 property 中的 [index]

1
2
3
4
my:
servers:
- dev.example.com
- another.example.com

等价于

1
2
my.servers[0]=dev.example.com
my.servers[1]=another.example.com

访问属性

YamlPropertySourceLoader 类会将 YAML 配置转化为 Spring Environment 类中的 PropertySource 。然后,你可以如同 properties 文件中的属性一样,使用 @Value 注解来访问 YAML 中配置的属性。

多 profile 配置

1
2
3
4
5
6
7
8
9
10
11
12
server:
address: 192.168.1.100
---
spring:
profiles: development
server:
address: 127.0.0.1
---
spring:
profiles: production & eu-central
server:
address: 192.168.1.120

YAML 的缺点

注:YAML 注解中的属性不能通过 @PropertySource 注解来访问。所以,如果你的项目中使用了一些自定义属性文件,建议不要用 YAML。

属性前缀

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
package com.example;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix="acme")
public class AcmeProperties {

private boolean enabled;

private InetAddress remoteAddress;

private final Security security = new Security();

public boolean isEnabled() { ... }

public void setEnabled(boolean enabled) { ... }

public InetAddress getRemoteAddress() { ... }

public void setRemoteAddress(InetAddress remoteAddress) { ... }

public Security getSecurity() { ... }

public static class Security {

private String username;

private String password;

private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

public String getUsername() { ... }

public void setUsername(String username) { ... }

public String getPassword() { ... }

public void setPassword(String password) { ... }

public List<String> getRoles() { ... }

public void setRoles(List<String> roles) { ... }

}
}

相当于支持配置以下属性:

  • acme.enabled
  • acme.remote-address
  • acme.security.username
  • acme.security.password
  • acme.security.roles

然后,你需要使用 @EnableConfigurationProperties 注解将属性类注入配置类中。

1
2
3
4
@Configuration
@EnableConfigurationProperties(AcmeProperties.class)
public class MyConfiguration {
}

属性松散绑定规则

Spring Boot 属性名绑定比较松散。

以下属性 key 都是等价的:

Property Note
acme.my-project.person.first-name - 分隔
acme.myProject.person.firstName 驼峰命名
acme.my_project.person.first_name _ 分隔
ACME_MYPROJECT_PERSON_FIRSTNAME 大写字母

属性转换

如果需要类型转换,你可以提供一个 ConversionService bean (一个名叫 conversionService 的 bean) 或自定义属性配置 (一个 CustomEditorConfigurer bean) 或自定义的 Converters (被 @ConfigurationPropertiesBinding 注解修饰的 bena)。

时间单位转换

Spring 使用 java.time.Duration 类代表时间大小,以下场景适用:

  • 除非指定 @DurationUnit ,否则一个 long 代表的时间为毫秒。
  • ISO-8601 标准格式( java.time.Duration 的实现就是参照此标准)
  • 你也可以使用以下支持的单位:
    • ns - 纳秒
    • us - 微秒
    • ms - 毫秒
    • s - 秒
    • m - 分
    • h - 时
    • d - 天

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ConfigurationProperties("app.system")
public class AppSystemProperties {

@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30);

private Duration readTimeout = Duration.ofMillis(1000);

public Duration getSessionTimeout() {
return this.sessionTimeout;
}

public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}

public Duration getReadTimeout() {
return this.readTimeout;
}

public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}

}

数据大小转换

Spring 使用 DataSize 类代表数据大小,以下场景适用:

  • long 值(默认视为 byte)
  • 你也可以使用以下支持的单位:
    • B
    • KB
    • MB
    • GB
    • TB

示例:

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
@ConfigurationProperties("app.io")
public class AppIoProperties {

@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize bufferSize = DataSize.ofMegabytes(2);

private DataSize sizeThreshold = DataSize.ofBytes(512);

public DataSize getBufferSize() {
return this.bufferSize;
}

public void setBufferSize(DataSize bufferSize) {
this.bufferSize = bufferSize;
}

public DataSize getSizeThreshold() {
return this.sizeThreshold;
}

public void setSizeThreshold(DataSize sizeThreshold) {
this.sizeThreshold = sizeThreshold;
}

}

校验属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {

@NotNull
private InetAddress remoteAddress;

@Valid
private final Security security = new Security();

// ... getters and setters

public static class Security {

@NotEmpty
public String username;

// ... getters and setters

}

}

你也可以自定义一个名为 configurationPropertiesValidator 的校验器 Bean。获取这个 @Bean 的方法必须声明为 static

示例源码

示例源码:spring-boot-property

参考资料

SpringBoot 之应用 EasyUI

EasyUI 是一个简单的用户界面组件的集合。由于 EasyUI 已经封装好大部分 UI 基本功能,能帮用户减少大量的 js 和 css 代码。所以,EasyUI 非常适合用于开发简单的系统或原型系统。

本文示例使用技术点:

  • Spring Boot:主要使用了 spring-boot-starter-web、spring-boot-starter-data-jpa
  • EasyUI:按需加载,并没有引入所有的 EasyUI 特性
  • 数据库:为了测试方便,使用 H2

img

简介

什么是 EasyUI?

  • easyui 是基于 jQuery、Angular.、Vue 和 React 的用户界面组件的集合。
  • easyui 提供了构建现代交互式 javascript 应用程序的基本功能。
  • 使用 easyui,您不需要编写许多 javascript 代码,通常通过编写一些 HTML 标记来定义用户界面。
  • 完整的 HTML5 网页框架。
  • 使用 easyui 开发你的产品时可以大量节省你的时间和规模。
  • easyui 使用非常简单但功能非常强大。

Spring Boot 整合 EasyUI

配置

application.properties 修改:

1
2
spring.mvc.view.prefix = /views/
spring.mvc.view.suffix = .html

引入 easyui

EasyUI 下载地址:http://www.jeasyui.cn/download.html

src/main/resources/static 目录下引入 easyui。

然后在 html 中引用:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link
rel="stylesheet"
type="text/css"
href="../lib/easyui/themes/bootstrap/easyui.css"
/>
<link
rel="stylesheet"
type="text/css"
href="../lib/easyui/themes/icon.css"
/>
<link
rel="stylesheet"
type="text/css"
href="../lib/easyui/themes/color.css"
/>
<script type="text/javascript" src="../lib/easyui/jquery.min.js"></script>
<script
type="text/javascript"
src="../lib/easyui/jquery.easyui.min.js"
></script>
<script
type="text/javascript"
src="../lib/easyui/locale/easyui-lang-zh_CN.js"
></script>
</head>
<body>
<!-- 省略 -->
</body>
</html>

引入 easyui 后,需要使用哪种组件,可以查看相关文档或 API,十分简单,此处不一一赘述。

实战

引入 maven 依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
</dependencies>

使用 JPA

为了使用 JPA 技术访问数据,我们需要定义 Entity 和 Repository

定义一个 Entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
public class User {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String phone;
private String email;

protected User() {}

public User(String firstName, String lastName, String phone, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.phone = phone;
this.email = email;
}

// 略 getter/setter
}

定义一个 Repository:

1
2
3
4
public interface UserRepository extends CrudRepository<User, Long> {

List<User> findByLastName(String lastName);
}

使用 Web

首页 Controller,将 web 请求定向到指定页面(下面的例子定向到 index.html)

1
2
3
4
5
6
7
8
9
@Controller
public class IndexController {

@RequestMapping(value = {"", "/", "index"})
public String index() {
return "index";
}

}

此外,需要定义一个 Controller,提供后台的 API 接口

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
@Controller
public class UserController {

@Autowired
private UserRepository customerRepository;

@RequestMapping(value = "/user", method = RequestMethod.GET)
public String user() {
return "user";
}

@ResponseBody
@RequestMapping(value = "/user/list")
public ResponseDTO<User> list() {
Iterable<User> all = customerRepository.findAll();
List<User> list = IteratorUtils.toList(all.iterator());
return new ResponseDTO<>(true, list.size(), list);
}

@ResponseBody
@RequestMapping(value = "/user/add")
public ResponseDTO<User> add(User user) {
User result = customerRepository.save(user);
List<User> list = new ArrayList<>();
list.add(result);
return new ResponseDTO<>(true, 1, list);
}

@ResponseBody
@RequestMapping(value = "/user/save")
public ResponseDTO<User> save(@RequestParam("id") Long id, User user) {
user.setId(id);
customerRepository.save(user);
List<User> list = new ArrayList<>();
list.add(user);
return new ResponseDTO<>(true, 1, list);
}

@ResponseBody
@RequestMapping(value = "/user/delete")
public ResponseDTO delete(@RequestParam("id") Long id) {
customerRepository.deleteById(id);
return new ResponseDTO<>(true, null, null);
}

}

使用 EasyUI

接下来,我们要使用前面定义的后台接口,仅需要在 EasyUI API 中指定 url 即可。

请留意下面示例中的 url 字段,和实际接口是一一对应的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
<!DOCTYPE html>
<html>
<head>
<title>Complex Layout - jQuery EasyUI Demo</title>
<meta charset="UTF-8" />
<link
rel="stylesheet"
type="text/css"
href="../lib/easyui/themes/bootstrap/easyui.css"
/>
<link
rel="stylesheet"
type="text/css"
href="../lib/easyui/themes/icon.css"
/>
<link
rel="stylesheet"
type="text/css"
href="../lib/easyui/themes/color.css"
/>
<script type="text/javascript" src="../lib/easyui/jquery.min.js"></script>
<script
type="text/javascript"
src="../lib/easyui/jquery.easyui.min.js"
></script>
<script
type="text/javascript"
src="../lib/easyui/locale/easyui-lang-zh_CN.js"
></script>
<style type="text/css">
body {
font-family: microsoft yahei;
}
</style>
</head>
<body>
<div style="width:100%">
<h2>基本的 CRUD 应用</h2>
<p>数据来源于后台系统</p>

<table
id="dg"
title="Custom List"
class="easyui-datagrid"
url="/user/list"
toolbar="#toolbar"
pagination="true"
rownumbers="true"
fitColumns="true"
singleSelect="true"
>
<thead>
<tr>
<th field="id" width="50">ID</th>
<th field="firstName" width="50">First Name</th>
<th field="lastName" width="50">Last Name</th>
<th field="phone" width="50">Phone</th>
<th field="email" width="50">Email</th>
</tr>
</thead>
</table>
<div id="toolbar">
<a
href="javascript:void(0)"
class="easyui-linkbutton"
iconCls="icon-add"
plain="true"
onclick="newUser()"
>添加</a
>
<a
href="javascript:void(0)"
class="easyui-linkbutton"
iconCls="icon-edit"
plain="true"
onclick="editUser()"
>修改</a
>
<a
href="javascript:void(0)"
class="easyui-linkbutton"
iconCls="icon-remove"
plain="true"
onclick="destroyUser()"
>删除</a
>
</div>

<div
id="dlg"
class="easyui-dialog"
style="width:400px"
data-options="closed:true,modal:true,border:'thin',buttons:'#dlg-buttons'"
>
<form
id="fm"
method="post"
novalidate
style="margin:0;padding:20px 50px"
>
<h3>User Information</h3>
<div style="margin-bottom:10px">
<input
name="firstName"
class="easyui-textbox"
required="true"
label="First Name:"
style="width:100%"
/>
</div>
<div style="margin-bottom:10px">
<input
name="lastName"
class="easyui-textbox"
required="true"
label="Last Name:"
style="width:100%"
/>
</div>
<div style="margin-bottom:10px">
<input
name="phone"
class="easyui-textbox"
required="true"
label="Phone:"
style="width:100%"
/>
</div>
<div style="margin-bottom:10px">
<input
name="email"
class="easyui-textbox"
required="true"
validType="email"
label="Email:"
style="width:100%"
/>
</div>
</form>
</div>
<div id="dlg-buttons">
<a
href="javascript:void(0)"
class="easyui-linkbutton c6"
iconCls="icon-ok"
onclick="saveUser()"
style="width:90px"
>Save</a
>
<a
href="javascript:void(0)"
class="easyui-linkbutton"
iconCls="icon-cancel"
onclick="javascript:$('#dlg').dialog('close')"
style="width:90px"
>Cancel</a
>
</div>
</div>

<script type="text/javascript">
var url

function newUser() {
$('#dlg')
.dialog('open')
.dialog('center')
.dialog('setTitle', 'New User')
$('#fm').form('clear')
url = '/user/add'
}

function editUser() {
var row = $('#dg').datagrid('getSelected')
if (row) {
$('#dlg')
.dialog('open')
.dialog('center')
.dialog('setTitle', 'Edit User')
$('#fm').form('load', row)
url = '/user/save'
}
}

function saveUser() {
$('#fm').form('submit', {
url: url,
onSubmit: function() {
return $(this).form('validate')
},
success: function(result) {
var result = eval('(' + result + ')')
if (result.errorMsg) {
$.messager.show({
title: 'Error',
msg: result.errorMsg
})
} else {
$('#dlg').dialog('close') // close the dialog
$('#dg').datagrid('reload') // reload the user data
}
}
})
}

function destroyUser() {
var row = $('#dg').datagrid('getSelected')
if (row) {
$.messager.confirm(
'Confirm',
'Are you sure you want to destroy this user?',
function(r) {
if (r) {
$.post(
'/user/delete',
{ id: row.id },
function(result) {
if (result.success) {
$('#dg').datagrid('reload') // reload the user data
} else {
$.messager.show({
// show error message
title: 'Error',
msg: result.errorMsg
})
}
},
'json'
)
}
}
)
}
}
</script>
</body>
</html>

完整示例

请参考 源码

运行方式:

1
2
mvn clean package -DskipTests=true
java -jar target/

在浏览器中访问:http://localhost:8080/

引用和引申

SpringBoot 之集成 Json

简介

Spring Boot 支持的 Json 库

Spring Boot 支持三种 Json 库:

  • Gson
  • Jackson
  • JSON-B

Jackson 是 Spring Boot 官方推荐的默认库。

Spring Boot 提供了 Jackson 的自动配置,Jackson 是 spring-boot-starter-json 的一部分。当 Jackson 在类路径上时,会自动配置 ObjectMapper bean。

Spring Boot 提供了 Gson 的自动配置。当 Gson 在 classpath 上时,会自动配置 Gson bean。提供了几个 spring.gson.* 配置属性来自定义配置。为了获得更多控制,可以使用一个或多个 GsonBuilderCustomizer bean。

Spring Boot 提供了 JSON-B 的自动配置。当 JSON-B API 在 classpath 上时,将自动配置 Jsonb bean。首选的 JSON-B 实现是 Apache Johnzon,它提供了依赖关系管理。

Spring Web 中的序列化、反序列化

以下注解都是 spring-web 中提供的支持。

@ResponseBody

@Responsebody 注解用于将 Controller 的方法返回的对象,通过适当的 HttpMessageConverter 转换为指定格式后,写入到 HTTP Response 对象的 body 数据区。一般在异步获取数据时使用。通常是在使用 @RequestMapping 后,返回值通常解析为跳转路径,加上 @Responsebody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP 响应正文中。

示例:

1
2
3
4
5
@ResponseBody
@RequestMapping(name = "/getInfo", method = RequestMethod.GET)
public InfoDTO getInfo() {
return new InfoDTO();
}

@RequestBody

@RequestBody 注解用于读取 HTTP Request 请求的 body 部分数据,使用系统默认配置的 HttpMessageConverter 进行解析,然后把相应的数据绑定到要返回的对象上;再把 HttpMessageConverter 返回的对象数据绑定到 controller 中方法的参数上。

request 的 body 部分的数据编码格式由 header 部分的 Content-Type 指定。

示例:

1
2
3
4
@RequestMapping(name = "/postInfo", method = RequestMethod.POST)
public void postInfo(@RequestBody InfoDTO infoDTO) {
// ...
}

@RestController

Spring 4 以前:

如果需要返回到指定页面,则需要用 @Controller 配合视图解析器 InternalResourceViewResolver

如果需要返回 JSON,XML 或自定义 mediaType 内容到页面,则需要在对应的方法上加上 @ResponseBody 注解。

Spring 4 以后,新增了 @RestController 注解:

它相当于 @Controller + @RequestBody

如果使用 @RestController 注解 Controller,则 Controller 中的方法无法返回 jsp 页面,或者 html,配置的视图解析器 InternalResourceViewResolver 将不起作用,直接返回内容。

指定类的 Json 序列化、反序列化

如果使用 Jackson 序列化和反序列化 JSON 数据,您可能需要编写自己的 JsonSerializerJsonDeserializer 类。自定义序列化程序通常通过模块向 Jackson 注册,但 Spring Boot 提供了另一种 @JsonComponent 注释,可以更容易地直接注册 Spring Beans。

您可以直接在 JsonSerializerJsonDeserializer 实现上使用 @JsonComponent 注释。您还可以在包含序列化程序/反序列化程序作为内部类的类上使用它,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import org.springframework.boot.jackson.*;

@JsonComponent
public class Example {

public static class Serializer extends JsonSerializer<SomeObject> {
// ...
}

public static class Deserializer extends JsonDeserializer<SomeObject> {
// ...
}

}

ApplicationContext 中的所有 @JsonComponent bean 都会自动注册到 Jackson。因为 @JsonComponent 是使用 @Component 进行元注释的,所以通常的组件扫描规则适用。

Spring Boot 还提供了 JsonObjectSerializerJsonObjectDeserializer 基类,它们在序列化对象时提供了标准 Jackson 版本的有用替代方法。有关详细信息,请参阅 Javadoc 中的 JsonObjectSerializerJsonObjectDeserializer

@JsonTest

使用 @JsonTest 可以很方便的在 Spring Boot 中测试序列化、反序列化。

使用 @JsonTest 相当于使用以下自动配置:

1
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration org.springframework.boot.test.autoconfigure.json.JsonTestersAutoConfiguration

@JsonTest 使用示例:

想试试完整示例,可以参考:源码

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
@JsonTest
@RunWith(SpringRunner.class)
public class SimpleJsonTest {

private final Logger log = LoggerFactory.getLogger(this.getClass());

@Autowired
private JacksonTester<InfoDTO> json;

@Test
public void testSerialize() throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
InfoDTO infoDTO = new InfoDTO("JSON测试应用", "1.0.0", sdf.parse("2019-01-01 12:00:00"));
JsonContent<InfoDTO> jsonContent = json.write(infoDTO);
log.info("json content: {}", jsonContent.getJson());
// 或者使用基于JSON path的校验
assertThat(jsonContent).hasJsonPathStringValue("@.appName");
assertThat(jsonContent).extractingJsonPathStringValue("@.appName").isEqualTo("JSON测试应用");
assertThat(jsonContent).hasJsonPathStringValue("@.version");
assertThat(jsonContent).extractingJsonPathStringValue("@.version").isEqualTo("1.0.0");
assertThat(jsonContent).hasJsonPathStringValue("@.date");
assertThat(jsonContent).extractingJsonPathStringValue("@.date").isEqualTo("2019-01-01 12:00:00");
}

@Test
public void testDeserialize() throws Exception {
String content = "{\"appName\":\"JSON测试应用\",\"version\":\"1.0.0\",\"date\":\"2019-01-01\"}";
InfoDTO actual = json.parseObject(content);
assertThat(actual.getAppName()).isEqualTo("JSON测试应用");
assertThat(actual.getVersion()).isEqualTo("1.0.0");
}
}

Spring Boot 中的 json 配置

Jackson 配置

当 Spring Boot 的 json 库为 jackson 时,可以使用以下配置属性(对应 JacksonProperties 类):

1
2
3
4
5
6
7
8
9
10
11
12
spring.jackson.date-format= # Date format string or a fully-qualified date format class name. For instance, `yyyy-MM-dd HH:mm:ss`.
spring.jackson.default-property-inclusion= # Controls the inclusion of properties during serialization. Configured with one of the values in Jackson's JsonInclude.Include enumeration.
spring.jackson.deserialization.*= # Jackson on/off features that affect the way Java objects are deserialized.
spring.jackson.generator.*= # Jackson on/off features for generators.
spring.jackson.joda-date-time-format= # Joda date time format string. If not configured, "date-format" is used as a fallback if it is configured with a format string.
spring.jackson.locale= # Locale used for formatting.
spring.jackson.mapper.*= # Jackson general purpose on/off features.
spring.jackson.parser.*= # Jackson on/off features for parsers.
spring.jackson.property-naming-strategy= # One of the constants on Jackson's PropertyNamingStrategy. Can also be a fully-qualified class name of a PropertyNamingStrategy subclass.
spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized.
spring.jackson.time-zone= # Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10".
spring.jackson.visibility.*= # Jackson visibility thresholds that can be used to limit which methods (and fields) are auto-detected.

GSON 配置

当 Spring Boot 的 json 库为 gson 时,可以使用以下配置属性(对应 GsonProperties 类):

1
2
3
4
5
6
7
8
9
10
11
spring.gson.date-format= # Format to use when serializing Date objects.
spring.gson.disable-html-escaping= # Whether to disable the escaping of HTML characters such as '<', '>', etc.
spring.gson.disable-inner-class-serialization= # Whether to exclude inner classes during serialization.
spring.gson.enable-complex-map-key-serialization= # Whether to enable serialization of complex map keys (i.e. non-primitives).
spring.gson.exclude-fields-without-expose-annotation= # Whether to exclude all fields from consideration for serialization or deserialization that do not have the "Expose" annotation.
spring.gson.field-naming-policy= # Naming policy that should be applied to an object's field during serialization and deserialization.
spring.gson.generate-non-executable-json= # Whether to generate non executable JSON by prefixing the output with some special text.
spring.gson.lenient= # Whether to be lenient about parsing JSON that doesn't conform to RFC 4627.
spring.gson.long-serialization-policy= # Serialization policy for Long and long types.
spring.gson.pretty-printing= # Whether to output serialized JSON that fits in a page for pretty printing.
spring.gson.serialize-nulls= # Whether to serialize null fields.

Spring Boot 中使用 Fastjson

国内很多的 Java 程序员更喜欢使用阿里的 fastjson 作为 json lib。那么,如何在 Spring Boot 中将其替换默认的 jackson 库呢?

你需要做如下处理:

(1)引入 fastjson jar 包:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>

(2)实现 WebMvcConfigurer 接口,自定义 configureMessageConverters 接口。如下所示:

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
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

private final Logger log = LoggerFactory.getLogger(this.getClass());

/**
* 自定义消息转换器
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 清除默认 Json 转换器
converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);

// 配置 FastJson
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(SerializerFeature.QuoteFieldNames, SerializerFeature.WriteEnumUsingToString,
SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat,
SerializerFeature.DisableCircularReferenceDetect);

// 添加 FastJsonHttpMessageConverter
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
fastJsonHttpMessageConverter.setFastJsonConfig(config);
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
converters.add(fastJsonHttpMessageConverter);

// 添加 StringHttpMessageConverter,解决中文乱码问题
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
converters.add(stringHttpMessageConverter);
}

// ...
}

示例源码

完整示例:源码

引申和引用

引申

引用