Dunwu Blog

大道至简,知易行难

计算机网络之物理层

摘要

物理层(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);
}

// ...
}

示例源码

完整示例:源码

引申和引用

引申

引用

Spring 访问 Elasticsearch

简介

Elasticsearch 是一个开源的、分布式的搜索和分析引擎。

通过 REST 客户端连接 Elasticsearch

如果在 classpath 路径下存在 org.elasticsearch.client:elasticsearch-rest-client jar 包,Spring Boot 会自动配置并注册一个 RestClient Bean,它的默认访问路径为:localhost:9200

你可以使用如下方式进行定制:

1
2
3
spring.elasticsearch.rest.uris=http://search.example.com:9200
spring.elasticsearch.rest.username=user
spring.elasticsearch.rest.password=secret

您还可以注册实现任意数量的 RestClientBuilderCustomizer bean,以进行更高级的定制。要完全控制注册,请定义 RestClient bean。

如果 classpath 路径有 org.elasticsearch.client:elasticsearch-rest-high-level-client jar 包,Spring Boot 将自动配置一个 RestHighLevelClient,它包装任何现有的 RestClient bean,重用其 HTTP 配置。

通过 Jest 连接 Elasticsearch

如果 classpath 上有 Jest,你可以注入一个自动配置的 JestClient,默认情况下是 localhost:9200。您可以进一步调整客户端的配置方式,如以下示例所示:

1
2
3
4
spring.elasticsearch.jest.uris=http://search.example.com:9200
spring.elasticsearch.jest.read-timeout=10000
spring.elasticsearch.jest.username=user
spring.elasticsearch.jest.password=secret

您还可以注册实现任意数量的 HttpClientConfigBuilderCustomizer bean,以进行更高级的定制。以下示例调整为其他 HTTP 设置:

1
2
3
4
5
6
7
8
static class HttpSettingsCustomizer implements HttpClientConfigBuilderCustomizer {

@Override
public void customize(HttpClientConfig.Builder builder) {
builder.maxTotalConnection(100).defaultMaxTotalConnectionPerRoute(5);
}

}

要完全控制注册,请定义 JestClient bean。

通过 Spring Data 访问 Elasticsearch

要连接到 Elasticsearch,您必须提供一个或多个集群节点的地址。可以通过将 spring.data.elasticsearch.cluster-nodes 属性设置为以逗号分隔的 host:port 列表来指定地址。使用此配置,可以像任何其他 Spring bean 一样注入 ElasticsearchTemplateTransportClient,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring.data.elasticsearch.cluster-nodes=localhost:9300
@Component
public class MyBean {

private final ElasticsearchTemplate template;

public MyBean(ElasticsearchTemplate template) {
this.template = template;
}

// ...

}

如果你添加了自定义的 ElasticsearchTemplateTransportClient @Bean ,就会替换默认的配置。

Elasticsearch Repositories

Spring Data 包含对 Elasticsearch 的 repository 支持。基本原则是根据方法名称自动为您构建查询。

事实上,Spring Data JPA 和 Spring Data Elasticsearch 共享相同的通用基础架构。

源码

完整示例:源码

使用方法:

1
2
3
mvn clean package
cd target
java -jar spring-boot-data-elasticsearch.jar

版本

Spring 和 Elasticsearch 匹配版本:

Spring Data Elasticsearch Elasticsearch Spring Framework Spring Boot
5.0.x 8.5.3 6.0.x 3.0.x
4.4.x 7.17.3 5.3.x 2.7.x
4.3.x 7.15.2 5.3.x 2.6.x
4.2.x[1] 7.12.0 5.3.x 2.5.x
4.1.x[1] 7.9.3 5.3.2 2.4.x
4.0.x[1] 7.6.2 5.2.12 2.3.x
3.2.x[1] 6.8.12 5.2.12 2.2.x
3.1.x[1] 6.2.2 5.1.19 2.1.x
3.0.x[1] 5.5.0 5.0.13 2.0.x
2.1.x[1] 2.4.0 4.3.25 1.5.x

参考资料

SpringBoot 之 banner 定制

简介

Spring Boot 启动时默认会显示以下 LOGO:

1
2
3
4
5
6
7
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.1.RELEASE)

实际上,Spring Boot 支持自定义 logo 的功能。

让我们来看看如何实现的。

只要你在 resources 目录下放置名为 banner.txtbanner.gifbanner.jpgbanner.png 的文件,Spring Boot 会自动加载,将其作为启动时打印的 logo。

  • 对于文本文件,Spring Boot 会将其直接输出。
  • 对于图像文件( banner.gifbanner.jpgbanner.png ),Spring Boot 会将图像转为 ASCII 字符,然后输出。

变量

banner.txt 文件中还可以使用变量来设置字体、颜色、版本号。

变量 描述
${application.version} MANIFEST.MF 中定义的版本。如:1.0
${application.formatted-version} MANIFEST.MF 中定义的版本,并添加一个 v 前缀。如:v1.0
${spring-boot.version} Spring Boot 版本。如:2.1.1.RELEASE.
${spring-boot.formatted-version} Spring Boot 版本,并添加一个 v 前缀。如:v2.1.1.RELEASE
${Ansi.NAME} (or ${AnsiColor.NAME}, ${AnsiBackground.NAME}, ${AnsiStyle.NAME}) ANSI 颜色、字体。更多细节,参考:AnsiPropertySource
${application.title} MANIFEST.MF 中定义的应用名。

示例:

在 Spring Boot 项目中的 resources 目录下添加一个名为 banner.txt 的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
${AnsiColor.BRIGHT_YELLOW}${AnsiStyle.BOLD}
________ ___ ___ ________ ___ __ ___ ___
|\ ___ \|\ \|\ \|\ ___ \|\ \ |\ \|\ \|\ \
\ \ \_|\ \ \ \\\ \ \ \\ \ \ \ \ \ \ \ \ \\\ \
\ \ \ \\ \ \ \\\ \ \ \\ \ \ \ \ __\ \ \ \ \\\ \
\ \ \_\\ \ \ \\\ \ \ \\ \ \ \ \|\__\_\ \ \ \\\ \
\ \_______\ \_______\ \__\\ \__\ \____________\ \_______\
\|_______|\|_______|\|__| \|__|\|____________|\|_______|
${AnsiBackground.WHITE}${AnsiColor.RED}${AnsiStyle.UNDERLINE}
:: Spring Boot :: (v${spring-boot.version})
:: Spring Boot Tutorial :: (v1.0.0)

注:${} 设置字体颜色的变量之间不能换行或空格分隔,否则会导致除最后一个变量外,都不生效。

启动应用后,控制台将打印如下 logo:

img
推荐两个生成字符画的网站,可以将生成的字符串放入这个banner.txt 文件:

配置

application.properties 中与 Banner 相关的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# banner 模式。有三种模式:console/log/off
# console 打印到控制台(通过 System.out)
# log - 打印到日志中
# off - 关闭打印
spring.main.banner-mode = off
# banner 文件编码
spring.banner.charset = UTF-8
# banner 文本文件路径
spring.banner.location = classpath:banner.txt
# banner 图像文件路径(可以选择 png,jpg,gif 文件)
spring.banner.image.location = classpath:banner.gif
used).
# 图像 banner 的宽度(字符数)
spring.banner.image.width = 76
# 图像 banner 的高度(字符数)
spring.banner.image.height =
# 图像 banner 的左边界(字符数)
spring.banner.image.margin = 2
# 是否将图像转为黑色控制台主题
spring.banner.image.invert = false

当然,你也可以在 YAML 文件中配置,例如:

1
2
3
spring:
main:
banner-mode: off

编程

默认,Spring Boot 会注册一个 SpringBootBanner 的单例 Bean,用来负责打印 Banner。

如果想完全个人定制 Banner,可以这么做:先实现 org.springframework.boot.Banner#printBanner 接口来自己定制 Banner。在将这个 Banner 通过 SpringApplication.setBanner(…) 方法注入 Spring Boot。

示例源码

示例源码:spring-boot-banner

参考资料

Spring 访问 MongoDB

简介

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 将数据存储为一个文档,数据结构由键值对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

在 Spring 中,spring-data-mongodb 项目对访问 MongoDB 进行了 API 封装,提供了便捷的访问方式。 Spring Data MongoDB 的核心是一个以 POJO 为中心的模型,用于与 MongoDB DBCollection 交互并轻松编写 Repository 样式的数据访问层。

spring-boot 项目中的子模块 spring-boot-starter-data-mongodb 基于 spring-data-mongodb 项目,做了二次封装,大大简化了 MongoDB 的相关配置。

Spring Boot 快速入门

引入依赖

在 pom.xml 中引入依赖:

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

数据源配置

1
2
3
4
5
spring.data.mongodb.host = localhost
spring.data.mongodb.port = 27017
spring.data.mongodb.database = test
spring.data.mongodb.username = root
spring.data.mongodb.password = root

定义实体

定义一个具有三个属性的 Customer 类:idfirstNamelastName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.data.annotation.Id;

public class Customer {

@Id
public String id;

public String firstName;

public String lastName;

public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

@Override
public String toString() {
return String.format(
"Customer[id=%s, firstName='%s', lastName='%s']",
id, firstName, lastName);
}

}

spring-data-mongodb 会将 Customer 类映射到一个名为 customer 的集合中。如果要更改集合的名称,可以在类上使用 @Document 注解。

创建 Repository

spring-data-mongodb 继承了 Spring Data Commons 项目的能力,所以可以使用其通用 API——Repository

先定义一个 CustomerRepository 类,继承 MongoRepository 接口,并指定其泛型参数:CustomerString。MongoRepository 接口支持多种操作,包括 CRUD 和分页查询。在下面的例子中,定义了两个查询方法:

1
2
3
4
5
6
7
8
9
10
import java.util.List;

import org.springframework.data.mongodb.repository.MongoRepository;

public interface CustomerRepository extends MongoRepository<Customer, String> {

Customer findByFirstName(String firstName);
List<Customer> findByLastName(String lastName);

}

创建 Application

创建一个 Spring Boot 的启动类 Application,并在启动的 main 方法中使用 CustomerRepository 实例访问 MongoDB。

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.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DataMongodbApplication implements CommandLineRunner {

@Autowired
private CustomerRepository repository;

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

@Override
public void run(String... args) {

repository.deleteAll();

// save a couple of customers
repository.save(new Customer("Alice", "Smith"));
repository.save(new Customer("Bob", "Smith"));

// fetch all customers
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : repository.findAll()) {
System.out.println(customer);
}
System.out.println();

// fetch an individual customer
System.out.println("Customer found with findByFirstName('Alice'):");
System.out.println("--------------------------------");
System.out.println(repository.findByFirstName("Alice"));

System.out.println("Customers found with findByLastName('Smith'):");
System.out.println("--------------------------------");
for (Customer customer : repository.findByLastName("Smith")) {
System.out.println(customer);
}
}

}

运行 DataMongodbApplication 的 main 方法后,输出类似如下类容:

1
2
3
4
5
6
7
8
9
10
11
12
Customers found with findAll():
-------------------------------
Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith)
Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith)

Customer found with findByFirstName('Alice'):
--------------------------------
Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith)
Customers found with findByLastName('Smith'):
--------------------------------
Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith)
Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith)

示例源码

更多 Spring 访问 MongoDB 示例请参考:MongoDB 示例源码

参考资料