Spring-借助Spring使用JDBC访问关系数据库

目标

构建一个应用程序,使用 Spring 的 JdbcTemplate 来访问存储在关系数据库中的数据。

Spring initializr 配置

https://start.spring.io
添加 JDBC API 和 H2 Database 依赖。

创建 Customer 对象

一个简单的数据访问逻辑,你需要管理用户的姓和名。为了在应用层表示这些数据,创建 Customer 类,代码如下 src/main/java/com/example/relationaldataaccess/Customer.java :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.relationaldataaccess;

public class Customer {
private long id;
private String firstName, lastName;

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

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

// getters & setters omitted for brevity
}

存储和取回数据

Spring 提供了一个模版类 JdbcTemplate 来简化 SQL 关系数据库和 JDBC 的交互。

大多数 JDBC 代码都陷入资源获取、连接管理、异常处理和一般错误检查中,这与代码的预期目标完全无关。JdbcTemplate 让你无需再去关心这些和你的业务无关的任务。你只需关注你手头的任务即可。下面的代码 src/main/java/com/example/relationaldataaccess/RelationalDataAccessApplication.java 展示了使用 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
47
48
49
50
51
52
53
package com.example.relationaldataaccess;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@SpringBootApplication
public class RelationalDataAccessApplication implements CommandLineRunner {

private static final Logger log = LoggerFactory.getLogger(RelationalDataAccessApplication.class);

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

@Autowired
JdbcTemplate jdbcTemplate;

@Override
public void run(String... strings) throws Exception {

log.info("Creating tables");

jdbcTemplate.execute("DROP TABLE customers IF EXISTS");
jdbcTemplate.execute("CREATE TABLE customers(" +
"id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))");

// Split up the array of whole names into an array of first/last names
List<Object[]> splitUpNames = Arrays.asList("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long").stream()
.map(name -> name.split(" "))
.collect(Collectors.toList());

// Use a Java 8 stream to print out each tuple of the list
splitUpNames.forEach(name -> log.info(String.format("Inserting customer record for %s %s", name[0], name[1])));

// Uses JdbcTemplate's batchUpdate operation to bulk load data
jdbcTemplate.batchUpdate("INSERT INTO customers(first_name, last_name) VALUES (?,?)", splitUpNames);

log.info("Querying for customer records where first_name = 'Josh':");
jdbcTemplate.query(
"SELECT id, first_name, last_name FROM customers WHERE first_name = ?", new Object[] { "Josh" },
(rs, rowNum) -> new Customer(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name"))
).forEach(customer -> log.info(customer.toString()));
}
}

Spring Boot 支持 H2(一个内存关系数据库引擎)并自动创建一个连接。因为使用了 spring-jdbc,Spring Boot 自动创建 JdbcTemplate,@Autowired JdbcTemplate 域会自动加载并使其可用。

Application 类 实现了 Spring Boot’s 的 CommandLineRunner 接口,意味着 run() 方法会在 application 上下文加载后运行。

首先,使用 JdbcTemplate 的 execute 方法执行一些 DDL 语句。

其次,将一个字符列表用 Java 8 的 streams 按 firstname/lastname 对分割成 Java 数组,列表中每一项生成一个数组,最后集合成一个对象数组列表。

单个插入语句,使用 insert 即可,多个插入语句,最好使用 batchUpdate
使用 ? 来作为占位绑定变量到 SQL 语句,而不直接使用字符串拼接,可以避免 SQL 注入攻击。

最后,使用 query 方法在表中搜索匹配的记录。再次使用 ? 来占位,调用时传递实际的值到其中。最后一个参数时 Java 8 的 lambda 语句,用来转换每个结果行为一个新的 Customer 对象。

Java 8 lambda 对单个方法接口的映射很不错,如 Spring 的 RowMapper。如果使用的是 Java 7 或者更早版本,可以用一个匿名接口实现来实现。

构建一个可执行的 JAR

使用 Gradle

  • 直接运行 ./gradlew bootRun
  • 构建 JAR 文件 ./gradlew build

使用 Maven

  • 直接运行 ./mvnw spring-boot:run
  • 构建 JAR 文件 ./mvnw clean package

运行 JAR

java -jar target/gs-relational-data-access-0.1.0.jar

输出如下:

1
2
3
4
5
6
7
8
9
10
2022-12-02 21:15:50.835  INFO 78029 --- [           main] c.e.r.RelationalDataAccessApplication    : Creating tables
2022-12-02 21:15:50.836 INFO 78029 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2022-12-02 21:15:50.944 INFO 78029 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2022-12-02 21:15:50.958 INFO 78029 --- [ main] c.e.r.RelationalDataAccessApplication : Inserting customer record for John Woo
2022-12-02 21:15:50.958 INFO 78029 --- [ main] c.e.r.RelationalDataAccessApplication : Inserting customer record for Jeff Dean
2022-12-02 21:15:50.958 INFO 78029 --- [ main] c.e.r.RelationalDataAccessApplication : Inserting customer record for Josh Bloch
2022-12-02 21:15:50.958 INFO 78029 --- [ main] c.e.r.RelationalDataAccessApplication : Inserting customer record for Josh Long
2022-12-02 21:15:50.998 INFO 78029 --- [ main] c.e.r.RelationalDataAccessApplication : Querying for customer records where first_name = 'Josh':
2022-12-02 21:15:51.010 INFO 78029 --- [ main] c.e.r.RelationalDataAccessApplication : Customer[id=3, firstName='Josh', lastName='Bloch']
2022-12-02 21:15:51.010 INFO 78029 --- [ main] c.e.r.RelationalDataAccessApplication : Customer[id=4, firstName='Josh', lastName='Long']

Spring-使用Maven构建Java项目

目标

创建一个提供一天中的时间都应用程序,并使用 Maven 进行构建。

创建项目

首先,需要创建一个 Java 项目,来提供给 Maven 进行构建。为了确保注意力集中在 Maven 上,尽可能确保项目足够简单。在你所选择的项目根目录中创建如下目录结构。

创建目录结构

在你所选择的项目根目录中创建如下目录结构,如,使用命令 mkdir -p src/main/java/hello 创建:

1
2
3
4
5
.
└── src
└── main
└── java
└── hello

在 src/main/java/hello 目录中,你可以创建任意 Java 类。为了保持和其余指南的一致性,这里创建两个类文件:HelloWorld.java 和 Greeter.java

src/main/java/hello/HelloWorld.java

1
2
3
4
5
6
7
8
package hello;

public class HelloWorld {
public static void main(String[] args) {
Greeter greeter = new Greeter();
System.out.println(greeter.sayHello());
}
}

src/main/java/hello/Greeter.java

1
2
3
4
5
6
7
package hello;

public class Greeter {
public String sayHello() {
return "Hello world!";
}
}

安装 maven 可以从 https://maven.apache.org/download.cgi 下载一个 zip 压缩文件,只需要下载二进制文件即可,所以可以在下载列表中类似 apache-maven-{version}-bin.zip or apache-maven-{version}-bin.tar.gz 的文件进行下载。

下载后,解压压缩文件,然后将其中的 bin 目录添加到 PATH 中即可。

测试 maven 安装到命令:mvn -v

定义一个简单的 Maven 构建

Maven 项目是用一个 pom.xml 的 XML 文件定义的。该文件给出了项目名称,版本以及所有需要的外部依赖库。

因此,进行项目根目录,创建 pom.xml 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework</groupId>
<artifactId>gs-maven</artifactId>
<packaging>jar</packaging>
<version>0.1.0</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>hello.HelloWorld</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

除了可选的标签外,上面基本上是一个最简单的可用来构建 Java 项目的 pom.xml,主要包含了一下几个配置项:

  • POM 模型版本(总是 4.0.0)
  • 项目所属的组织或机构,通常用一个倒置的域名表示。
  • 项目的库构件的名称(例如,JAR或WAR文件的名称)
  • 项目构建时的版本
  • 项目应被如何打包,默认为 jar 也就是打包为 JAR 文件,设置为 war 则表示打包为 WAR 文件。

注:这里的版本使用的是 Semantic Versioning 的规范。
Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes
  2. MINOR version when you add functionality in a backwards compatible manner
  3. PATCH version when you make backwards compatible bug fixes

    Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

构建 Java 代码

尝试构建,可以使用下面的命令
mvn compile

该命令会运行 Maven 来对 Java 代码进行编译,最终得到 classes 文件

mvn package 命令则会编译并打包,最终生成一个可执行的 jar 文件。

Maven 还会维护一个本地依赖库(一般在一个用户home目录下的 .m2/repository 文件夹中)来为项目依赖提供快速的访问。如果你想要把你项目 JAR 文件安装到本地依赖库中,可以使用 mvn install 命令。

该命令会将你的项目代码编译、测试、打包,然后复制到本地依赖仓库中,留给以后别的项目作为依赖来引用。

定义依赖

添加 Joda Time 库用来打印当前日期和时间。
首先,修改 HelloWorld.java

1
2
3
4
5
6
7
8
9
10
11
12
package hello;

import org.joda.time.LocalTime;

public class HelloWorld {
public static void main(String[] args) {
LocalTime currentTime = new LocalTime();
System.out.println("The current local time is: " + currentTime);
Greeter greeter = new Greeter();
System.out.println(greeter.sayHello());
}
}

在 pom.xml 中添加依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.2</version>
</dependency>
<dependencies>

该 XML 代码块定义了一个这个项目的依赖列表,列表中定义了一个 Joda Time 库的依赖。

元素中,依赖的定义由三个子元素组成:

  1. - 依赖所属组织或机构
  2. - 需要的库
  3. - 需要的库的版本

默认所有的依赖都是被看作 compile 依赖的。也就是说,这些依赖应该在编译期间可用(如果你正在构建的是 WAR 文件,包含在 WAR 包的 /WEB-INF/libs 目录)。

另外,你可以指定 元素来指定以下作用域:

  • provided - 编译项目代码时需要的依赖,但是将会被容器在运行代码时提供。(如 Java Servlet API)
  • test - 编译和运行测试时需要使用的依赖,但不会在构建或者运行项目运行时代码时需要。

现在,如果你使用 mvn compilemvn package 命令,Maven 应该会从 Maven 中央仓库中找到 Joda Time 依赖并成功构建项目。

编写测试

首先添加 JUnit 依赖,在 pom.xml 中添加:

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

然后编写测试文件 src/test/java/hello/GreeterTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package hello;

import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*;

import org.junit.Test;

public class GreeterTest {

private Greeter greeter = new Greeter();

@Test
public void greeterSaysHello() {
assertThat(greeter.sayHello(), containsString("Hello"));
}

}

Maven 使用一个叫做 “surefire” 的插件来运行单元测试。该插件的默认配置会编译并运行 src/test/java 文件夹内所有文件名匹配 *Test 的类文件。你可以使用 mvn test 命令来运行该测试,或者使用 mvn install (test 包含在 install 会进行的所有操作中)

Spring-使用Gradle构建Java项目

目标

创建一个简单的 app 并使用 gradle 构建。

创建项目

首先要创建一个 Java 项目来让 Gradle 进行构建,为了集中注意力到 Gradle 上,所以尽可能地让项目简单一点。

创建项目目录

在项目目录 gs-gradle 下,使用命令 mkdir -p src/main/java/hello 创建子目录结构。

1
2
3
4
└── src
└── main
└── java
└── hello

在 src/main/java/hello 目录中,创建 HelloWorld.java and Greeter.java 两个类文件。

其中 src/main/java/hello/HelloWorld.java 代码如下:

1
2
3
4
5
6
7
8
package hello;

public class HelloWorld {
public static void main(String[] args) {
Greeter greeter = new Greeter();
System.out.println(greeter.sayHello());
}
}

src/main/java/hello/Greeter.java 代码如下:

1
2
3
4
5
6
7
package hello;

public class Greeter {
public String sayHello() {
return "Hello world!";
}
}

安装 Gradle

推荐使用 SDKMAN 或者 Homebrew 安装,我这里使用 Homebrew 安装 brew install gradle 即可。

安装成功后,运行 gradle 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> Task :help

Welcome to Gradle 7.5.1.

Directory '' does not contain a Gradle build.

To create a new build in this directory, run gradle init

For more detail on the 'init' task, see https://docs.gradle.org/7.5.1/userguide/build_init_plugin.html

For more detail on creating a Gradle build, see https://docs.gradle.org/7.5.1/userguide/tutorial_using_tasks.html

To see a list of command-line options, run gradle --help

For more detail on using Gradle, see https://docs.gradle.org/7.5.1/userguide/command_line_interface.html

For troubleshooting, visit https://help.gradle.org

BUILD SUCCESSFUL in 607ms
1 actionable task: 1 executed

Build Java 代码

在项目根目录下,新建 build.gradle 文件,添加 apply plugin: 'java',然后运行命令 gradle build 即可成功构建 jar 包。(不能直接运行,没有声明主类)

定义依赖

在 HelloWorld 类的 main 方法中添加代码,使用 Joda Time 库来获取当前时间并打印。修改后的 HelloWorld.java 如下:

1
2
3
4
5
6
7
8
9
10
11
12
package hello;
import org.joda.time.LocalTime;

public class HelloWorld {
public static void main(String[] args) {
LocalTime currentTime = new LocalTime();
System.out.println("The current local time is: " + currentTime);

Greeter greeter = new Greeter();
System.out.println(greeter.sayHello());
}
}

此时,直接用 gradle build 命令构建会失败,需要修改 build.gradle 配置,添加依赖库。

首先添加 maven 中央仓库

1
2
3
repositories { 
mavenCentral()
}

其次,添加用到的三方库依赖

1
2
3
4
5
6
7
sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
implementation "joda-time:joda-time:2.2"
testImplementation "junit:junit:4.12"
}

最后,指定 JAR 包的属性,包括名称和版本号:

1
2
3
4
jar {
archiveBaseName = 'gs-gradle'
archiveVersion = '0.1.0'
}

这样生成的 jar 包名称将会是 gs-gradle-0.1.0.jar

此时,可以用 gradle build 命令构建成功(但暂时无法直接运行生成的 jar)

使用 Gradle Wrapper 构建项目

使用 Gradle Wrapper 的一个好处是不用在系统上安装 gradle。
首先在项目根目录下使用 gradle wrapper --gradle-version 6.0.1 命令 (也可以不指定版本号) , 得到以下目录结构:

1
2
3
4
5
6
7
└── <project folder>
└── gradlew
└── gradlew.bat
└── gradle
└── wrapper
└── gradle-wrapper.jar
└── gradle-wrapper.properties

然后使用 ./gradlew build 命令构建项目,首次运行指定版本的 gradle wrapper 时,会自动下载对应版本的 gradle 二进制文件。
构建完成后的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
build
├── classes
│   └── java
│   └── main
│   └── hello
│   ├── Greeter.class
│   └── HelloWorld.class
├── distributions
│   ├── gs-gradle.tar
│   └── gs-gradle.zip
├── generated
│   └── sources
│   └── annotationProcessor
│   └── java
│   └── main
├── libs
│   └── gs-gradle-0.1.0.jar
├── scripts
│   ├── gs-gradle
│   └── gs-gradle.bat
└── tmp
├── compileJava
└── jar
└── MANIFEST.MF

可以使用 jar tvf build/libs/gs-gradle-0.1.0.jar 命令看一下生成的 jar 包的内容:

1
2
3
4
5
 0 Fri Dec 02 14:40:14 CST 2022 META-INF/
25 Fri Dec 02 14:40:14 CST 2022 META-INF/MANIFEST.MF
0 Fri Dec 02 14:40:14 CST 2022 hello/
988 Fri Dec 02 14:40:14 CST 2022 hello/HelloWorld.class
369 Fri Dec 02 14:40:14 CST 2022 hello/Greeter.class

为了让 JAR 包可运行,需要修改 build.gradle,添加如下代码

1
2
3
apply plugin: 'application'

mainClassName = 'hello.HelloWorld'

此时,就可以运行 app 了,使用 ./gradlew run 命令

1
2
3
4
5
6
7

> Task :run
The current local time is: 14:43:43.042
Hello world!

BUILD SUCCESSFUL in 802ms
2 actionable tasks: 1 executed, 1 up-to-date

Spring-使用 RESTful 服务

目标

创建一个使用 Spring 的 RestTemplate 来接收一个随机的 Spring Boot 引文的应用程序,接收的请求为:
http://localhost:8080/api/random

Spring initializr 配置

https://start.spring.io
添加 Web 依赖即可,其余配置随意。

获取 REST 资源

下载随机生成引文的代码仓库:https://github.com/spring-guides/quoters
运行该程序,即可在浏览器中访问:

引文的格式大概如下:

1
2
3
4
5
6
7
{
type: "success",
value: {
id: 10,
quote: "Really loving Spring Boot, makes stand alone Spring apps easy."
}
}

RestTemplate 使得与RESTful service 的交互变得很简单。甚至可以将返回的数据和你自定义的域类型绑定。

具体来说,首先需要创建一个 domain class 来包含你所需的数据,所以创建 Quote 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.consumingrest;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Quote {
private String type;
private Value value;

public Quote() {}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public Value getValue() {
return value;
}

public void setValue(Value value) {
this.value = value;
}

@Override
public String toString() {
return "Quote{" +
"type='" + type + '\'' +
", value=" + value +
'}';
}
}

其中,@JsonIgnoreProperties 是 Jackson JSON 库中注解,用来指定json中任何不与该类绑定的数据类型都将被忽略掉。

为了将数据和自定义类型绑定,需要指定与 API 返回的 JSON 文档中 key 完全相同的变量名称。假如变量名称不相同,那么就需要用 @JsonProperty 注解来指定准确的 key。

所以,还需要另一个类 Value 来代表引文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.consumingrest;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Value {
private Long id;
private String quote;

public Value() {
}

public Long getId() {
return id;
}

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

public String getQuote() {
return quote;
}

public void setQuote(String quote) {
this.quote = quote;
}

@Override
public String toString() {
return "Value{" +
"id=" + id +
", quote='" + quote + '\'' +
'}';
}
}

修改主程序

主要添加以下几个内容:

  1. 一个 RestTemplate,用 Jackson JSON 库处理输入数据
  2. 一个 CommandLineRunner 来运行 RestTemplate
  3. 一个 logger 来打印日志,将获取到的引文打印出来
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
package com.example.consumingrest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ConsumingRestApplication {
private static final Logger log =
LoggerFactory.getLogger(ConsumingRestApplication.class);

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

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}

@Bean
public CommandLineRunner run(RestTemplate restTemplate) throws Exception {
return args -> {
Quote quote = restTemplate.getForObject("http://localhost:8080/api/random", Quote.class);
log.info(quote.toString());
};
}
}

编译运行

  • 编译打包:./mvnw clean package
  • 运行:java -Dserver.port=8099 -jar target/consuming-rest-0.0.1-SNAPSHOT.jar
  • 运行时需另行指定端口,因为默认的 8080 端口在运行提供数据的 RESTful service(前面的 quoters)

Spring-计划任务

目标

创建一个每 5s 打印一次当前时间的应用程序。

Spring initializr 配置

https://start.spring.io
无需额外添加依赖。

添加依赖库

添加 awaitility 依赖库,使用 maven 管理依赖

1
2
3
4
5
6
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>3.1.2</version>
<scope>test</scope>
</dependency>

注: awaitility 库的较新版本不适应本例程,所以手动指定版本。

新建 ScheduledTasks 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.schedulingtasks;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
log.info("The time is now {}", dateFormat.format(new Date()));
}
}

fixedRate 指定了方法两次调用之间的间隔时间(本次的调用开始时间-上次的调用开始时间),此外还有选项如 fixedDelay 指定的是两次方法调用结束时间的间隔。

开启 Scheduling

ScheduledTasks 可以内嵌在别的网络应用程序中,但这里为了演示方便,我们简单打包成一个单独的程序。

所以需要 SchedulingTasksApplication 的 main 方法来调用,只需要额外添加 @EnableScheduling 注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.schedulingtasks;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableScheduling
public class SchedulingTasksApplication {

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

}

运行后,可以直接在命令行看到:

1
2
3
4
2022-12-01 21:46:27.305  INFO 58412 --- [   scheduling-1] c.e.schedulingtasks.ScheduledTasks       : The time is now 21:46:27
2022-12-01 21:46:32.304 INFO 58412 --- [ scheduling-1] c.e.schedulingtasks.ScheduledTasks : The time is now 21:46:32
2022-12-01 21:46:37.309 INFO 58412 --- [ scheduling-1] c.e.schedulingtasks.ScheduledTasks : The time is now 21:46:37
2022-12-01 21:46:42.306 INFO 58412 --- [ scheduling-1] c.e.schedulingtasks.ScheduledTasks : The time is now 21:46:42

Spring-构建 RESTful 网络服务

Spring initializr 配置

https://start.spring.io
添加 Web 依赖即可,其余配置随意。

创建 Greeting 类

要发送的消息格式为:

1
2
3
4
{
"id": 1,
"content": "Hello, World!"
}

包括一个数字类型 id 和 一个字符串类型的 content,因此按照如下创建 Greeting 类来代表要发送的消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.restservice;

public class Greeting {
private final long id;
private final String content;

public Greeting(long id, String content) {
this.id = id;
this.content = content;
}

public long getId() {
return id;
}

public String getContent() {
return content;
}
}

注:包括在 web starter 的 Jackson JSON 库会自动将 Greeting 类转换成 json 格式。

编写 Controller 处理 http 请求

编写 GreetingController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.restservice;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicLong;

@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();

@GetMapping("/greeting")
public Greeting greeting(@RequestParam(value="name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}

RESTful web service controller 与传统 MVC controller 的不同是 HTTP 响应的创建方式。
RestController 会直接返回对象实例 (而不像传统 MVC 那样依赖于 view technology),Jackson JSON 库自动将 Greeting 实例转换成 json 格式的文本。

创建可执行文件 JAR

使用 gradle

  • 直接运行:./gradlew bootRun
  • 创建 jar 包:./gradlew build

使用 maven

  • 直接运行:./mvnw spring-boot:run
  • 创建 jar 包:./mvnw clean package

运行 jar 包

java -jar target/rest-service-0.0.1-SNAPSHOT.jar

测试创建的 service

访问 http://localhost:8080/greeting,将会看到

1
{"id":1,"content":"Hello, World!"}

添加 name 参数的 http 请求, http://localhost:8080/greeting?name=User,将会看到

1
{"id":2,"content":"Hello, User!"}

Java IO&NIO

定义

用户线程:A
内核:K

阻塞 IO 模型

过程模拟:
A -> data = socket.read()
我要读数据,没数据我就等着,有数据就给我。

非阻塞 IO 模型

A -> data = socket.read()
我要读数据,没数据返回给我个 error,我循环着再次发起 read
有数据了则拿数据处理,处理结束后跳出循环。
用户线程不断循环获取 socket 状态

多路复用 IO 模型

A -> data = socket.read()
我要读数据
内核 K 轮询所有 socket 状态,哪个 socket 就绪时,对应的用户线程才调用实际的 IO 读写操作。

信号驱动的 IO 模型

A 我要读数据 -> 在 socket 内注册一个信号函数,用户线程继续执行。
内核数据就绪 -> 发送信号给 A -> A 调用 IO 读写操作

异步 IO 模型

A 我要读数据 -> asynchronous read
内核:我知道了,交给我就好了。
内核:等数据完成,将数据拷贝到用户线程 A,搞定后发消息给 A
A:收到信号,直接用数据就好了,不用再去调用 IO 读写操作。

Spring Guide 记录

入门指南

每个例子大概只需 15-30 分钟即可完成,这些例子都是一些可以快速上手的使用 Spring 构建任何开发任务的类 “Hello World” 的例子。
大多数例子只需要你有一个 JDK 和一个文本编辑器即可。

专题指南

每个指南大概需要 1h 去阅读和理解,比入门指南更广泛或更主观的内容。

教程

这些教程设计为在 2-3 小时内完成,提供对企业应用程序开发主题的更深入的上下文探索,让您准备好实施实际解决方案。

Spring 课程笔记

课程概述

课程主要关注的三个内容

  • Spring framework
  • Spring boot
  • Spring cloud

第一章 初识 Spring

编写第一个 Spring 程序,使用 spring 官网提供的 spring initializr 工具,填写对应的配置后,即可生成一个 spring 程序的骨架。
https://start.spring.io

这里添加了 web 和 spring actuator 两个依赖。

用 idea 打开后,直接在生成的 springbootapplication 类中进行修改,添加 restcontroller 注解,并添加一个 hello path 的 requestMapping 即可完成一个最简单的 spring 程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@RestController
public class HelloSpringApplication {

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

@RequestMapping("/hello")
public String hello() {
return "Hello Spring";
}
}

可以直接运行在内嵌的 tomcat 容器中。

访问 curl http://localhost:8080/hello 即可看到运行结果 Hello Spring%
访问 curl http://localhost:8080/actuator/health 可看到节点的健康状态 {"status":"UP"}%

还可以使用 mvn clean package -Dmaven.test.skip 命令,将 spring 程序打包成 jar 文件
生成的jar包
其中,较大的那个 jar 包是 spring 自动将程序所需的依赖也打包的结果。因此可以直接使用 java -jar 命令运行
java -jar target/hello-spring-0.0.1-SNAPSHOT.jar

假如因为某些原因无法使用 spring-boot-starter-parent 作为项目的 parent,那么只需要按下面这样修改下 pom 文件即可

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
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.5</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

即,添加一个 dependencyManagement 标签,将 spring-boot-starter-parent 完整的导入进来,并在下面的 spring-boot-maven-plugin 添加配置,声明在 repackage 的时候引入。

第二章 JDBC 必知必会

如何配置单数据源

构建项目骨架,引入的依赖有

  • Actuator 监控状态
  • H2 数据库
  • JDBC 操作数据库
  • Lombok 提供一些注解
  • Web 以 web 形式启动,方便访问一些 Actuator 的一些 url

showConnection

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
@SpringBootApplication
@Slf4j
public class DatasourcedemoApplication implements CommandLineRunner {
@Autowired
private DataSource dataSource;

@Autowired
private JdbcTemplate jdbcTemplate;

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

@Override
public void run(String... args) throws Exception {
showConnection();
showData();
}

private void showData() {
jdbcTemplate.queryForList("SELECT * FROM FOO")
.forEach(row -> log.info(row.toString()));
}

private void showConnection() throws SQLException {
log.info(dataSource.toString());
Connection conn = dataSource.getConnection();
log.info(conn.toString());
conn.close();
}
}

运行后,可以看到 spring boot 自动配置了数据源 HikariDataSource,并连接到内存数据库 h2 HikariProxyConnection@395257482 wrapping conn0: url=jdbc:h2:mem:9c9048f5-ff8c-48fd-9f1d-837bd639a404 user=SA

可以打开浏览器,访问 http://localhost:8080/actuator/beans 可以看到所有的 Bean,但是 web 模式下,actuator 默认暴露的 endpoint 只有 health,参见:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.endpoints.exposing。

因此,需要先打开 beans 的暴露,在 application.properties 中添加配置 management.endpoints.web.exposure.include=health,beans 即可。

resources 目录下新建的 schema.sql 和 data.sql 会被 Spring Boot 自动加载,然后 showData 的代码会打印出数据库中的两行由 data.sql 插入的数据。

数据源相关配置属性
通用

  • spring.datasource.url=jdbc:mysql://localhost/test
  • spring.datasource.username=dbuser
  • spring.datasource.password=dbpass
  • spring.datasource.driver-class-name=com.mysql.jdbc.Driver(可选)

初始化内嵌数据库

  • spring.datasource.initialization-mode=embedded(默认值,只初始化内存数据库)|always(总是初始化数据库)|never(不初始化数据库)
  • spring.datasource.schema与spring.datasource.data确定初始化SQL文件
  • spring.datasource.platform=hsqldb | h2 | oracle | mysql | postgresql(与前者对应)

演示 demo 中的 application.properties 的配置

1
2
3
4
5
6
7
8
9
10
11
management.endpoints.web.exposure.include=*
spring.output.ansi.enabled=ALWAYS

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.hikari.maximumPoolSize=5
spring.datasource.hikari.minimumIdle=5
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.maxLifetime=1800000

配置多个数据源

注意事项

  • 不同数据源的配置要分开
  • 关注每次使用的资源
    • 有多个 DataSource 时系统如何判断
    • 对应的设施(事务、ORM 等)如何选择 DataSource

配置方式

方法一 手工配置两组 DataSource 及相关内容
方法二 与 Spring Boot 协同工作

  • 配置 @Primary 类型的 Bean: 配置了该注解的 DataSource 作为主要的 DataSource,Spring Boot 的配置将围绕该 DataSource 进行
  • 排除 Spring Boot 的自动配置
    • DataSourceAutoConfiguration
    • DataSourceTransactionManagerAutoConfiguration
    • JdbcTemplateAutoConfiguration

操作记录

构建项目 multidatasourcedemo ,引入的依赖有

  • Actuator 监控状态
  • H2 数据库
  • JDBC 操作数据库
  • Lombok 提供一些注解
  • Web 以 web 形式启动,方便访问一些 Actuator 的一些 url

application.properties 配置如下

1
2
3
4
5
6
7
8
9
10
management.endpoints.web.exposure.include=*
spring.output.ansi.enabled=ALWAYS

foo.datasource.url=jdbc:h2:mem:foo
foo.datasource.username=sa
foo.datasource.password=

bar.datasource.url=jdbc:h2:mem:bar
bar.datasource.username=sa
bar.datasource.password=

排除 Spring boot 自动配置

1
2
3
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class})

配置数据源

  1. 配置 DataSourceProperties
  2. 配置 DataSource
  3. 配置 PlatformTransactionManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
@ConfigurationProperties("foo.datasource")
public DataSourceProperties fooDataSourceProperties() {
return new DataSourceProperties();
}

@Bean
public DataSource fooDataSource() {
DataSourceProperties dataSourceProperties = fooDataSourceProperties();
log.info("foo datasource: {}", dataSourceProperties.getUrl());
return dataSourceProperties.initializeDataSourceBuilder().build();
}

@Bean
@Resource
public PlatformTransactionManager fooTxManager(DataSource fooDataSource) {
return new DataSourceTransactionManager(fooDataSource);
}

HikariCP

性能很快:字节码优化和很多的小改进的累积

  • Spring 1.x 默认使用的 Tomcat 连接池,要使用 HikariCP 的话,需要移除 tomcat-jdbc 依赖,配置 spring.datasource.type=com.zaxxer.hikari.HikariDataSource
  • Spring 2.x 默认使用的就是 HikariCP 连接池了,可以直接配置 spring.datasource.hikari.*

HikariCP 常用配置

1
2
3
4
5
spring.datasource.hikari.maximumPoolSize=5
spring.datasource.hikari.minimumIdle=5
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.maxLifetime=1800000

官网:https://github.com/brettwooldridge/HikariCP

Java IO

Java 输入与输出

输入/输出流

概述

  • 输入流:可以从中读取一个字节序列的对象
  • 输出流:可以向其中写入一个字节序列的对象

其中,字节序列的来源或者目的地可以是:

  1. 文件(大多数情况下指文件)
  2. 网络连接
  3. 内存块

抽象类 InputStream 和 OutputStream 构成了输入/输出(I/O)类层次结构的基础

读写字节

InputStream 类有一个抽象方法 abstract int read(), 这个方法读入一个字节,并返回读入的字节,或者在遇到输入源结尾时返回 -1.

在设计具体的输入流类时,必须覆盖这个方法以提供适用的功能。

OutputStream 类有一个抽象方法 abstract void write(), 这个方法可以向某个输出位置写出一个字节。

read 和 write 方法在执行时都将阻塞,直至字节确实被读入或者写出。这就意味着如果流不能被立即访问(通常因为网络连接忙),那么当前线程将被阻塞。

available 方法可以检查当前可读入的字节数量,这意味着像下面这样的代码片段就不可能被阻塞:

1
2
3
4
5
int bytesAvailable = in.available();
if (bytesAvailable > 0) {
byte[] data = new byte[bytesAvailable];
in.read(data);
}

完成对输入/输出流的读写后,应该调用 close 方法关闭输入/输出流,close 方法会释放掉占用的操作系统资源。
如果一个应用程序打开了过多的输入/输出流而没有关闭,系统资源将被耗尽。
关闭一个输出流的同时还会自动冲刷该输出流的缓冲区:所有被临时置于缓冲区中,以便用更大的包的形式传递的字节在关闭输出流时都将被送出。
如果不关闭文件,那么写出的最后一个包可能永远得不到传递。当然,也可以使用 flush 方法手动冲刷缓冲区,但不关闭流总之不是个好习惯。

即使某个输入/输出流类实现了抽象类 InputStream/OutputStream 中的 read/write 抽象方法,但应用软件的程序员也很少使用,因为大家跟感兴趣的不是对于字节的读取/写入,而是对数字、字符串和对象的读取/写入。
所以我们可以使用众多的从基本的 InputStream/OutputStream 类派生出的子类中更实用的方法。

不过在这里我们还是先对 InputStream/OutputStream 类的方法做一个总结:

java.io.InputStream 1.8

修饰符和类型 方法名和描述
int available()
返回一个字节数,指在不被方法的下一个调用阻塞的情况下,输入流中能被读取(或者跳过)的字节数估计值。
void close()
关闭当前输入流并释放任何与当前输入流相关联的系统资源。
void mark(int readlimit)
在输入流的当前位置打一个标记(并非所有的流都支持这个特性)。
如果从输入流中已经读入的字节多于 readlimit个,则这个流允许忽略这个标记。
boolean markSupported()
如果这个流支持 mark 和 reset 方法,则返回 true,否则返回 false。
abstract int read()
从输入流中读取下一个字节,并返回该字节。这个read方法在碰到输入流的结尾时返回 -1。
int read(byte[] b)
从输入流中读取一些字节,并存到数组 b 中,返回实际读到的字节数,或者在输入流的结尾时返回 -1。这个方法最多读入 b.length 个字节
int read(byte[] b, int off, int len)
从输入流中读取 len 个字节到数组 b 中偏移 off 的位置,返回实际读入的字节数,或者在碰到输入流的结尾时返回 -1。
参数:
b: 数据读入的字节数组
off: 第一个读入字节应该被放置的位置在 b 中的偏移量
len: 读入字节的最大数量
void reset()
将输入流的当前位置重置到最后一次调用 mark 标记的位置。
long skip(long n)
在输入流中跳过 n 个字节的数据。

java.io.OutputStream 1.8

修饰符和类型 方法名和描述
void close()
关闭输出流并释放任何与当前输出流相关的系统资源。
void flush()
冲刷当前输出流,强制发送当前所有缓冲区的数据到目的地。
void write(byte[] b)
将指定字节数组中的所有字节到写入到当前输出流中。
void write(byte[] b, int off, int len)
将指定字节数组中 off 位置开始且长度为 len 的字节到写入到当前输出流中。
abstract void write(int b)
向当前输出流中写入一个字节的数据

完整的流家族

字节流

图2-1 输入流和输出流的层次结构

InputStream 的子类结构

  • AudioInputStream
  • ByteArrayInputStream
  • FileInputStream
  • FilterInputStream
    • BufferedInputStream
    • CheckedInputStream
    • CipherInputStream
    • DataInputStream: 以二进制的形式写所有的基本 Java 类型
    • DeflaterInputStream
    • DigestInputStream
    • InflaterInputStream
      • GZIPInputStream
      • ZipInputStream: 以常见的 ZIP 压缩格式写文件
        • JarInputStream
    • LineNumberInputStream
    • ProgressMonitorInputStream
    • PushbackInputStream
  • ObjectInputStream
  • PipedInputStream
  • SequenceInputStream
  • StringBufferInputStream

OutputStream 的子类结构

  • ByteArrayOutputStream
  • FileOutputStream
  • FilterOutputStream
    • BufferedOutputStream
    • CheckedOutputStream
    • CipherOutputStream
    • DataOutputStream: 以二进制的形式写所有的基本 Java 类型
    • DeflaterOutputStream
      • GZIPOutputStream
      • ZipOutputStream: 以常见的 ZIP 压缩格式写文件
        • JarOutputStream
    • DigestOutputStream
    • InflaterOutputStream
    • PrintStream
  • ObjectOutputStream
  • PipedOutputStream

字符流

对于 Unicode 文本,可以使用抽象类 Reader 和 Writer 的子类。Reader 和 Writer 类的基本方法与 InputStream 和 Outputstream 中的方法类似

abstract int read() 方法返回一个 Unicode 码元(一个在0~65535之间的整数),或者在碰到文件结尾时返回 -1。
abstract void write(int c) 方法需要传递一个 Unicode 码元
Reader 和 writer 的层次结构
图2-2 Reader 和 Writer 的层次结构

附加接口

图2-3 Closeable、Flushable、Readable 和 Appendable 接口

  1. Closeable: 只有一个方法 void close() throws IOException 关闭这个 Closeable
  2. Flushable: 只有一个方法 void flush() throws IOException 冲刷这个 Flushable
  3. Readable: 只有一个方法 int read(java.nio.CharBuffer cb) throws IOException
    1. read 方法表示尝试向 cb 中读入其可持有数量的 char 值。返回读入的 char 值的数量,或者当从这个 Readable 中无法再获得更多的值时返回 -1。
    2. CharBuffer 类拥有按顺序和随机读写访问的方法,它表示一个内存中的缓冲区或者一个内存映像的文件。
  4. Appendable: 有三个用于添加单个字符和字符序列的方法
    1. Appendable append(CharSequence csq) throws IOException 向这个 Appendable 中追加给定的码元序列中的所有码元。
    2. Appendable append(CharSequence csq, int start, int end) throws IOException 向这个 Appendable 中追加给定的码元序列中 [start, end) 区间内的所有码元。
    3. Appendable append(char c) throws IOException 向这个 Appendable 中追加给定的码元。
    4. CharSequence 接口描述了一个 char 值序列的基本属性,String、CharBuffer、String Builder 和 StringBuffer 都实现了它。
      1. char charAt(int index) 返回给定索引处的码元
      2. int length 返回在这个序列中的码元数量
      3. CharSequence subSequence(int startIndex, int endIndex) 返回由区间 [startIndex, endIndex) 内的所有码元构成的 CharSequence
      4. String toString() 返回这个序列中所有码元构成的字符串

InputStream、OutputStream、Reader 和 Writer 都实现了 Closeable 接口,而 OutputStream 和 Writer 还实现了 Flushable 接口。

在流类的家族中,只有 Writer 实现了 Appendable 接口。

组合输入/输出流过滤器

FileInputStream 和 FileOutputStream 可以提供附着在一个磁盘文件上的输入流和输出流,而你只需要向其构造器提供文件名或者文件的完整路径名。例如:
FileInputStream fin = new FileInputStream("employee.dat"); .
这行代码可以查看在用户目录下名为 “employee.dat” 的文件。

注: 所有在 java.io 中的类都将相对路径名解释为以用户当前工作目录(java项目的根目录,或者假如是单个java文件那就是java文件本身所在的目录)开始,你可以通过调用 `System.getProperty("user.dir")` 来获取这个信息。 对于可移植的程序,应该使用程序所运行平台的文件分割符,可以用常量字符串 java.io.File.separator 获取。

FileInputStream 只支持在字节级别上的读写,没有任何读入数值类型的方法,而 DataInputStream 只能读入数值类型,无法从文件中获取数据。

Java 中使用了一种灵巧的机制:某些输入流(例如 FileInputStream 和由 URL 类的 openstream 方法返回的输入流)可以从文件和其他更外部的位置上获取字节,而其他输入流(例如 DataInputStream)可以将字节组装到更为有用的数据类型中。
因此,为了从文件中读入数字,首先需要创建一个 FileInputStream,然后将其传递给 DataInputStream 的构造器:

1
2
3
FileInputStream fin = new FileInputStream("employee.dat");
DataInputStream in = new DataInputStream(fin);
double x = in.readDouble();

再次查看上面的 字节流 的类层次结构图,可以看到 FilterInputStream 和 FileOutputStream 类的子类用于向处理字节的输入/输出流添加额外功能。

可以通过嵌套过滤器来添加多重功能。例如,输入流在默认情况下是不被缓冲区缓存的,也就是说,每个对 read 的调用都会请求操作系统再分发一个字节。相比之下,请求一个数据块并将其置于缓冲区中会显得更加高效。如果我们想使用缓冲机制,以及用于文件的数据输入方法,那么就需要使用下面这种相当复杂的构造器序列:

1
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("employee.data")));

当多个输入流连接在一起时,需要跟踪各个中介输入流(intermediate input stream)。例如,当读入输入时,你经常需要预览下一个字节,以了解它是否时你想要的值。Java 提供了用于此目的的 PushbackInputStream:
PushbackInputStream pbin = new PushbackInputStream(new BufferedInputStream(new FileInputStream("employee.dat")));

现在你可以预读下一个字节: int b = pbin.read();
并且在它并非你所期望的值时将其推回流中。if (b != '<') pbin.unread(b); .
但是读入和推回时可应用于可回推(pushback)输入流的仅有的方法。如果你希望能够预先浏览并且还可以读入数字,那么你就需要一个既是可回推输入流,又是一个数据输入流的引用。
DataInputStream din = new DataInputStream(pbin=new PushbackInputStream(new BufferedInputStream(new FileInputStream("employee.dat"))));
当然,在其他编程语言的输入/输出流类库中,诸如缓冲机制和预览等细节都是自动处理的。因此,相比较而言,Java 就有一些麻烦,它必须将多个流过滤器组合起来。
但是,这种混合并匹配过滤器类以构建真正有用的输入/输出流的能力,将带来极大的灵活性,例如,你可以从一个 ZIP 压缩文件中通过使用下面的输入流序列来读入数字:

1
2
ZipInputStream zin = new ZipInputStream(new FileInputStream("employee.zip"));
DataInputStream din = new DataInputStream(zin);

图2-4 过滤器流序列

文本输入与输出

保存数据时可以选择二进制格式或者文本格式。
二进制格式的 I/O 高速且高效,但不适合人来阅读,相对的,文本格式适合人来阅读,但 I/O 速度相对低一些。

读写文本格式

存储文本字符串时,需要考虑字符编码方式。在 Java 内部使用 UTF-16 的编码方式,字符串 “1234” 编码为 “00 31 00 32 00 33 00 34”。
UTF-8 的编码方式则在互联网上最为常用,字符串 “1234” 编码为 4A 6F 73 C3 A9

OutputStreamWriter 类将使用选定的字符编码方式,把 Unicode 码元的输出流转换为字节流。而 InputStreamReader 类则将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生的 Unicode 码元的读入器。
例如,下面的代码可以让一个输入读入器从控制台读入键盘敲击的信息,并将其转换为 Unicode:
Reader in = new InputStreamReader(System.in);
这个 InputStreamReader 会使用主机系统所使用的默认字符编码方式。应该总是在 InputStreamReader的构造器中选择一种具体的编码方式,例如:
Reader in = new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);

如何写出文本输出

可以使用 PrintWriter 完成文本输出的任务,这个类拥有以文本格式打印字符串和数字的方法,可以使用下面的方法构造:
PrintWriter out = new PrintWriter("employee.txt", "UTF-8");
为了输出到 PrintWriter,需要使用与使用 System.out 时相同的 print、println 和 printf 方法。
你可以用这些方法来打印数字(int、short、long、float、double)、字符、boolean 值和对象。

如何读入文本输入

最简单的处理任意文本的方式就是使用 Scanner 类,我们可以从任何输入流中构建 Scanner 对象。

1
2
3
4
5
6
7
8
9
10
try (Scanner in = new Scanner(new InputStreamReader(inputStream,
StandardCharsets.UTF_8))) {
String line;
while (in.hasNextLine()) {
// do something with line
line = in.nextLine();
int a = in.nextInt();
double b = in.nextDouble();
}
}

或者,我们也可以将短小的文本文件像下面这样读入到一个字符串中:
String content = new String(Files.readAllBytes(path), charset);
如果想要将这个文件按行读入,可以调用:
List<String> lines = Files.readAllLines(path, charset);
如果文件太大,那么可以将行惰性处理为一个 Stream 对象:

1
2
3
try (Stream<String> lines = Files.lines(path, charset)) {
...
}

在早期的 Java 版本中,处理文本输入的唯一方式就是通过 BufferedReader 类。它的 readLine 方法会产生一行文本,或者在无法获得更多输入时返回 null。典型的输入循环看起来像下面这样:

1
2
3
4
5
6
7
8
InputStream inputStream = Files.newInputStream(Paths.get("employee.txt"));
try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,
StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
// do something with line
}
}

如今,BufferedReader 类又有了一个 lines 方法,可以产生一个 Stream 对象,但是与 Scanner 不同,BufferedReader 没有用于任何读入数字的方法。

字符编码方式

Java 针对字符使用的是 Unicode 标准,每个字符或 “编码点” 都对应一个 21 位的整数。基于 Unicode 标准有很多种编码方式。
编码方式:指将 Unicode 字符对应的 21 位整数转换成 字节 的方式。

最常见的编码方式是 UTF-8, 它会将每个 Unicode 编码点编码为 1 到 4 个字节的序列,也就是说 UTF-8 是一种可变长的编码方式。
UTF-8 的好处是传统的包含了英语中所有用到字符的 ASCII 字符集中每个字符经过 UTF-8 编码后都只占用一个字节。
表2-1 UTF-8 编码方式

字符范围 编码方式
0…7F 0a6a5a4a3a2a1a0
80…7FF 110a10a9a8a7a6 10a5a4a3a2a1a0
800…7FFF 1110a15a14a13a12 10a11a10a9a8a7a6 10a5a4a3a2a1a0
10000…10FFFF 11110a20a19a18 10a17a16a15a14a13a12 10a11a10a9a8a7a6 10a5a4a3a2a1a0

另一种常见的编码方式是 UTF-16,它会将每个 Unicode 编码点编码为 1 个或 2 个 16 位值。这是一种在 Java 字符串中使用的编码方式。实际上,有两种形式的 UTF-16,分别是 高位优先低位优先 。对于 16 位值 0x2122,在高位优先格式中,高位字节会先出现:0x21 后面跟着 0x22;但是在低位优先格式中,则是 0x22 后面跟着 0x21 。为了表示使用的是哪一种格式,文件可以以 “字节顺序标记” 开头,这个标记为 16 位数值 0xFEFF。读入器可以使用这个值来确定字节顺序,然后丢弃它。

表2-2 UTF-16 编码方式

字符范围 编码方式
0…FFFF a15a14a13a12a11a10a9a8 a7a6a5a4a3a2a1a0
10000…10FFFF 110110b19b18 b17b16a15a14a13a12a11a10 110111a9a8 a7a6a5a4a3a2a1a0
其中 b19b18b17b16 = a20a19a18a17a16 - 1

注:某些程序会在 UTF-8 编码的文件开头处添加一个字节顺序标记,虽然 UTF-8 标准中不存在字节顺序的问题,但 Unicode 标准允许添加字节顺序标记。因此需要注意将输入中发现的所有先导的 \uFEFF 字节顺序标记丢弃掉。

除了 UTF 编码方式外,还有一些编码方式,它们各自有覆盖了适用于特定用户人群的字符范围。
例如,ISO 8859-1 是一种单字节的编码,它包含了西欧各种语言中用到的带有重音符号的字符,而 Shift-JIS 是一种用于日文字符的可变长编码。

不存在任何可靠的方式可以自动地探测出字节流中所使用的字符编码方式,因此应该总是明确指定编码方式。例如,在编写网页时,应该检查 Content-Type 头信息。

  • 平台使用的编码方式可以使用静态方法 Charset.defaultCharset() 返回。
  • Charset.availableCharsets() 方法会返回所有可用的 Charset 实例

StandardCharsets 类具有类型为 Charset 的静态变量,用于表示每种 Java 虚拟机都必须支持的字符编码方式:

  • StandardCharsets.UTF_8
  • StandardCharsets.UTF_16
  • StandardCharsets.UTF_16BE
  • StandardCharsets.UTF_16LE
  • StandardCharsets.ISO_8859_1
  • StandardCharsets.US_ASCII

为了获得另一种编码方式的 Charset,可以使用静态的 forName 方法:Charset shiftJIS = Charset.forName("Shift-JIS");
在读入或写出文本时,应该使用 Charset 对象。例如我们可以像下面这样将一个字节数组转换为字符串:
String str = new String(bytes, StandardCharsets.UTF_8);

注意: 在不指定任何编码方式时,有些方法(例如 String(byte[]) 构造器)会使用默认的平台编码方式(Charset.defaultCharset()),而其他方法(例如 Files.readAllLines)会使用 UTF-8 。

读写二进制数据

DataInput 和 DataOutput 接口

DataOutput 接口定义了下面用于以二进制格式写数组、字符、boolean 值和字符串的方法:

  • writeChars(String s): 写出字符串中的所有字符
  • writeByte(int b)
  • writeInt(int i): 将一个整数写出为 4 字节的二进制数量值,不管整数本身的位数
  • writeShort(int s)
  • writeLong(long l)
  • writeFloat(float f)
  • writeDouble(double d): 将一个 double 值写出为 8 字节的二进制数量值。
  • writeChar(int c)
  • writeBoolean(boolean b)
  • writeUTF: 使用修订版的 8 位 Unicode 转换格式写出字符串。这种方式和直接使用标准的 UTF-8 编码方式不同,其中,Unicode 码元序列首先用 UTF-16 表示,其结果之后使用 UTF-8 规则进行编码。(修订后的方式对于编码大于 0xFFFF 的字符的处理有所不同,这是为了向后兼容在 Unicode 还没有超过 16 位时构建的虚拟机)因为没有其他方法会使用 UTF-8 的这种修订,所以你应该只在写出用于 Java 虚拟机的字符串时才使用 writeUTF 方法,例如,当你需要编写一个生成字节码的程序时。对于其他场合,都应该使用 writeChars 方法。

为了读回数据,可以使用 DataInput 接口中定义的下列方法:

  • readInt
  • readShort
  • readLong
  • readFloat
  • readDouble
  • readChar
  • readBoolean
  • readByte
  • readUTF
  • readFully(byte[] b): 将字节读入到数组 b 中,其间阻塞直至所有字节都读入
  • readFully(byte[] b, int off, int len): 将字节读入数组 b,其间阻塞直至所有字节都读入,off 数据其实位置的偏移量,len 读入字节的最大数量
  • skipBytes(int n) 跳过 n 个字节,其间阻塞直至所有字节都被跳过

DataInputStream 类实现了 DataInput 接口,为了从文件中读入二进制数据,可以将 DataInputStream 和某个字节源组合,例如 FileInputStream:
DataInputStream in = new DataInputStream(new FileInputStream("employee.dat"));
与此类似,要想写出二进制数据,你可以使用实现了 DataOutput 接口的 DataOutputStream 类:
DataOutputStream out = new DataOutputStream(new FileOutputStream("employee.dat"));

Java 中所有值都是按照高位在前的模式写出的。

随机访问文件

RandomAccessFile 类可以实现在文件中的任何位置查找或写入数据,磁盘文件都是随机访问的,但是网络套接字通信的输入/输出流却不是。你可以打开一个随机访问文件,只用于读入或者同时用于读写,你可以通过使用字符串 “r”(用于读入访问) 或 “rw”(用于读入/写出访问)作为构造器的第二个参数来指定这个选项。

1
2
RandomAccessFile in = new RandomAccessFile("employee.dat", "r");
RandomAccessFile inOut = new RandomAccessFile("employee.dat", "rw");

当你将已有文件作为 RandomAccessFile 打开时,这个文件并不会被删除。
RandomAccessFile 有一个表示下一个将被访问(读入或者写出)的字节所处位置的文件指针,seek 方法可以用来将这个文件指针设置到文件中的任意字节位置,seek 的参数时一个 long 类型的整数,它的值位于 0 到文件按照字节来度量的长度之间。
RandomAccessFile 其他的一些方法:

  • long getFilePointer 方法将返回文件指针的当前位置。
  • long length() 方法返回文件按照字节来度量的长度。
  • RandomAccessFile 类同时实现了类 DataInput 和 DataOutput 接口。

ZIP 文档

对象输入/输出流与序列化

操作文件

内存映射文件

正则表达式

NIO

待搜索

  • Copyrights © 2022 qusong
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信