Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

一、预处理器

指令 描述
#include 包含一个源代码文件(扩展名为 .h 的头文件
#define
#undef
定义宏
取消已定义的宏
#if
#else
#elif
#endif
条件编译

参考:

二、头文件

https://www.runoob.com/cprogramming/c-header-files.html

https://www.runoob.com/cprogramming/c-standard-library.html

三、基础语法

1、标识符(identifier)

是用来标识变量、函数、类、模块,或任何其他用户自定义项目的名称。

一个标识符只能以:

  • 字母 A-Za-z 或下划线 _ 开始;
  • 后跟零个或多个字母、下划线和数字(0-9)。

2、保留字(关键字)

3、注释

单行注释(行注释)://

多行注释(块注释):/* ... */

4、变量与常量

C++ 变量类型

C++ 数据类型

C++ 修饰符类型

C++ 变量作用域

4.1、基本数据类型

C 语言的基本数据类型:

类型 关键字 存储大小 备注
布尔型 bool sizeof(bool) = 1 字节
字符型 char sizeof(char) = 1 字节
整型 int sizeof(int) = 4 字节
浮点型 float sizeof(float) = 4 字节 C++ 中,小数默认为浮点型。
双浮点型 double sizeof(double) = 8 字节

基本数据类型

C 语言各种数据类型的内存映像(32 位平台):

基本数据类型

参考:

【计算机如何编码大数和小数-哔哩哔哩】 https://b23.tv/8rEeTf0

4.2、修饰符

C++ 允许在 charintdouble 数据类型前放置修饰符。修饰符用于改变基本类型的含义,所以它更能满足各种情境的需求。

修饰符 int double char
signed Y Y
unsigned Y Y
long Y Y
short Y

4.3、作用域

在 C++ 中:

  • 作用域可分为:
    • 全局作用域
    • 局部作用域
    • 语句作用域
  • 作用域优先级:范围越小,优先级越高
  • 如果希望在局部作用域中使用同名的全局变量,可以在该变量前使用:作用域运算符 ::
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

// 全局变量
int x = 10;

int main()
{
// 局部变量
int x = 100;
cout << x << endl; // 100
cout << ::x << endl; // 10
}

4.4、常量定义

在 C++ 中,有两种简单的定义 C++ 常量的方式:

  • 使用 #define 预处理器进行宏定义
  • 使用 const 关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

// 定义常量(使用预处理器)
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'

int main()
{
// 定义常量(使用 const 关键字)
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';

return 0;
}

5、运算符

5.1、位运算符

5.2、算术运算符

运算符 描述 实例
+ 把两个操作数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母 B / A 将得到 2
% 取模运算符,整除后的余数 B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
-- 自减运算符,整数值减少 1 A– 将得到 9

5.3、赋值运算符

运算符 描述 实例
= 简单的赋值运算符,把右边操作数的值赋给左边操作数 C = A + B 将把 A + B 的值赋给 C
+= 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C *= A 相当于 C = C * A
/= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%= 求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
<<= 左移且赋值运算符 C <<= 2 等同于 C = C << 2
>>= 右移且赋值运算符 C >>= 2 等同于 C = C >> 2
&= 按位与且赋值运算符 C &= 2 等同于 C = C & 2
^= 按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
|= 按位或且赋值运算符 C |= 2 等同于 C = C | 2

5.4、关系运算符

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 不为真。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 不为真。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 不为真。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。

5.5、逻辑运算符

运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。 (A && B) 为 false。
|| 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。 (A || B) 为 true。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。 !(A && B) 为 true。

5.6、其它运算符

运算符 描述
:: 作用域运算符 Scope operator,用于引用全局变量 ::code、引用某个命名空间的函数或变量 namespace::code 等等。
& 取地址运算符 Address-of operator (&) 返回变量的地址。例如 &a 将给出变量的实际内存地址。
* 间接寻址运算符 Dereference operator (*) 指向一个变量。例如,*var 返回操作数所指定地址的变量的值。
.(点)和 ->(箭头) 成员运算符用于引用结构体共用体的成员。
sizeof sizeof 运算符返回变量的存储大小。例如,sizeof(int) 返回 4 个字节。
Cast 强制转换运算符把一种数据类型转换为另一种数据类型。例如,int(2.2000) 将返回 2。

6、控制语句

C 语言有九种控制语句。 可分成以下三类:

6.1、选择语句

  • ifelse 语句
  • switch 语句

6.2、循环语句

  • while 语句
  • do while 语句
  • for 语句

6.3、跳转语句

  • break 语句
  • continue 语句
  • goto语句(此语句尽量少用,因为这不利结构化程序设计,滥用它会使程序流程无规律、可读性差)

参考

https://www.cplusplus.com/doc/tutorial/

https://www.cplusplus.com/doc/tutorial/variables/

https://www.cplusplus.com/doc/tutorial/namespaces/

C 语言入门教程 - 阮一峰

环境准备

打开 VScode,进入 Extensions,二选一搜索并安装以下扩展:

方式一:官方插件 C/C++

Using Clang in Visual Studio Code 教程指引如何使用官方插件 C/C++ 进行构建与调试。

按教程配置 .vscode 文件夹的三个文件:

  • tasks.json (编译器构建设置)
  • launch.json (调试器设置)
  • c_cpp_properties.json (编译器路径和 IntelliSense 设置)

⇧⌘B 编译源文件:

1
/usr/bin/clang++ -std=c++17 -stdlib=libc++ -g /Users/wuqd/Documents/workspace/cpp/HelloWorld.cpp -o /Users/wuqd/Documents/workspace/cpp/HelloWorld.out

编译成功后:

  • 可以在「终端」输入 ./HelloWorld.out 以直接运行编译文件;

  • 还可以按 F5 调试。

方式二:三方插件 Code Runner

安装第三方插件:Code Runner

settings.json 新增配置如下,将编译文件的后缀名改为 .out,以便 .gitignore:

1
2
3
"code-runner.executorMap": {
"cpp": "cd $dir && g++ $fileName -o $fileNameWithoutExt.out && $dir$fileNameWithoutExt.out"
}

⌃⌥N 编译并运行:

1
cd "/Users/wuqd/Documents/workspace/cpp/" && g++ HelloWorld.cpp -o HelloWorld.out && "/Users/wuqd/Documents/workspace/cpp/"HelloWorld.out

常见问题

DIFFERENCE BETWEEN g++ & gcc

GCC 代表 GNU Compiler Collections,主要用于编译 C 和 C++ 语言。它还可以用于编译 Objective C 和 Objective C++。编译源代码文件时需要的最重要的选项是源程序的名称,其余每个参数都是可选的,如警告、调试、链接库、目标文件等。

g++ 命令是 GNU C++ 编译器调用的命令,用于源代码的预处理、编译、汇编和链接以生成可执行文件。

g++ gcc
g++ is used to compile C++ program. gcc is used to compile C program.
g++ can compile any .c or .cpp files but they will be treated as C++ files only. gcc can compile any .c or .cpp files but they will be treated as C and C++ respectively.
Command to compile C++ program through g++ is g++ fileName.cpp -o binary command to compile C program through gcc is gcc fileName.c -o binary
Using g++ to link the object files, files automatically links in the std C++ libraries. gcc does not do this.
g++ compiles with more predefined macros. gcc compiles C++ files with more number of predefined macros. Some of them are #define GXX_WEAK 1, #define __cplusplus 1, #define __DEPRECATED 1, etc

DIFFERENCE BETWEEN gcc & make

gcc是编译器 而make不是 make是依赖于Makefile来编译多个源文件的工具 在Makefile里同样是用gcc(或者别的编译器)来编译程序.

gcc是编译一个文件,make是编译多个源文件的工程文件的工具。

make是一个命令工具,是一个解释makefile中指令的命令工具。

make就是一个gcc/g++的调度器,通过读入一个文件(默认文件名为Makefile或者makefile),执行一组以gcc/g++为主的shell命令序列。输入文件主要用来记录文件之间的依赖关系和命令执行顺序。

gcc是编译工具;

make是定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译;

也就是说make是调用gcc的。

cannot edit in read-only editor

GBK to UTF-8 乱码问题

https://www.cnblogs.com/kingsonfu/p/11010086.html

参考

Using Clang in Visual Studio Code

http://www.cplusplus.com/doc/tutorial/introduction/

C++ 在线编译器

C++ Insights

C++ Compiler Explorer

VSCode 其它插件推荐:

  • C/C++ Clang Command Adapter
  • C++ Intellisense

日志门面:

日志门面——SLF4J

优点:

  • 通过依赖配置,在编译期静态绑定真正的日志框架库。

    What has changed in SLF4J version 2.0.0?

    More visibly, slf4j-api now relies on the ServiceLoader mechanism to find its logging backend. SLF4J 1.7.x and earlier versions relied on the static binder mechanism which is no loger honored by slf4j-api version 2.0.x. More specifically, when initializing the LoggerFactory class will no longer search for the StaticLoggerBinder class on the class path.

    Instead of “bindings” now org.slf4j.LoggerFactory searches for “providers”. These ship for example with slf4j-nop-2.0.x.jar, slf4j-simple-2.0.x.jar or slf4j-jdk14-2.0.x.jar.

  • 不需要使用 logger.isDebugEnabled() 来解决日志因为字符拼接产生的性能问题。SLF4J 的方式是使用 {} 作为字符串替换符。

  • 提供了很多桥接方案,以便灵活替换日志库。

缺点:

  • 旧版不支持 Lambda 表达式(slf4j-api-2.0.0-alpha 支持但还未 GA)。但日志框架 Log4j 2.4 及以上版本支持 Java 8 Lambda,参考 12,例如:

    1
    logger.debug("This {} and {} with {} ", () -> this, () -> that, () -> compute());

SLF4J bindings

http://www.slf4j.org/manual.html#swapping

concrete bindings

Logback

https://logback.qos.ch/

推荐使用 SLF4J bound to logback-classic 的经典组合:

1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

传递依赖如下:

1
2
3
ch.qos.logback:logback-classic:jar:1.2.3:compile
+- ch.qos.logback:logback-core:jar:1.2.3:compile
\- org.slf4j:slf4j-api:jar:1.7.25:compile

Log4j 2.x

https://logging.apache.org/log4j/2.x/

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.16.0</version>
</dependency>

传递依赖如下:

1
2
3
4
org.apache.logging.log4j:log4j-slf4j-impl:jar:2.16.0:compile
+- org.slf4j:slf4j-api:jar:1.7.25:compile
+- org.apache.logging.log4j:log4j-api:jar:2.16.0:compile
\- org.apache.logging.log4j:log4j-core:jar:2.16.0:runtime

参考:

Log4j 1.x

https://logging.apache.org/log4j/1.2/

On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life.

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.32</version>
</dependency>

传递依赖如下:

1
2
3
org.slf4j:slf4j-log4j12:jar:1.7.32:compile
+- org.slf4j:slf4j-api:jar:1.7.32:compile
\- log4j:log4j:jar:1.2.17:compile

java.util.logging (JUL)

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.32</version>
</dependency>

传递依赖如下:

1
2
org.slf4j:slf4j-jdk14:jar:1.7.32:compile
\- org.slf4j:slf4j-api:jar:1.7.32:compile

日志框架——Logback

Configuration

https://logback.qos.ch/manual/configuration.html

Configuration file syntax

Configuring Appenders

Logback 配置文件样例:

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<springProperty scope="context" name="LOG_PATH" source="log.path" defaultValue="/data/logs"/>
<springProperty scope="context" name="ACTIVE_PROFILE" source="spring.profiles.active" defaultValue="dev" />
<springProperty scope="context" name="LOG_PATTERN" source="log.pattern" defaultValue="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %5p [%t] %logger{50}:%L [%X{traceId}] --> %m%n" />
<springProperty scope="context" name="MONGO_URI" source="spring.data.mongodb.uri" />

<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<!-- 每天归档日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/project-${ACTIVE_PROFILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<fileNamePattern>${LOG_PATH}/project-${ACTIVE_PROFILE}-%i.log.%d{yyyyMMdd}</fileNamePattern>
<!--日志文件保留天数-->
<maxHistory>90</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<!-- 自定义 MongoDB Appender -->
<appender name="MONGO" class="com.test.MongoDBAppender"></appender>

<logger name="org.springframework" level="WARN"/>

<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
<appender-ref ref="MONGO" />
</root>

</configuration>

Appenders

有三种 Appenders:

  1. Logback 官方提供的 Appenders。
  2. 第三方提供的 Appenders(logstash、…)。
  3. 自定义 Appenders。

官方 Appenders

Logback 官方提供的 Appenders 如下:http://logback.qos.ch/manual/appenders.html

Appender

其中,常用的两个 Appender 继承结构如下:

  • ch.qos.logback.core.ConsoleAppender
  • ch.qos.logback.core.rolling.RollingFileAppender

OutputStreamAppender

其中,ch.qos.logback.core.rolling.RollingFileAppender 可选的策略类如下:

Policy

第三方 Appenders

https://github.com/logstash/logstash-logback-encoder

Provides logback encoders, layouts, and appenders to log in JSON and other formats supported by Jackson.

自定义 Appenders

通过自定义 Appenders,可以将日志输出到任意位置,如 MongoDB、Kafka 等服务。

下面演示一个简单的同步 Appender,如需异步 Appender,参考这里

首先,新建 Appender,继承自抽象类:

UnsynchronizedAppenderBase

ILoggingEvent

代码如下:

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
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory;

public class MongoDBAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {

private SimpleMongoClientDbFactory mongoDbFactory;
private MongoTemplate mongoTemplate;
private final ReentrantLock lock = new ReentrantLock(false);

@Override
public void start() {
String uri = this.getContext().getProperty("MONGO_URI");
mongoDbFactory = new SimpleMongoClientDbFactory(uri);
mongoTemplate = new MongoTemplate(mongoDbFactory);
super.start();
}

@Override
protected void append(ILoggingEvent eventObject) {
lock.lock();
try {
PayLog log = new PayLog();
log.setTraceId(eventObject.getMDCPropertyMap().getOrDefault(MDCUtils.LOG_TRACE_ID, null));
log.setOper(eventObject.getMDCPropertyMap().getOrDefault(MDCUtils.OPER, null));
log.setLevel(eventObject.getLevel().toString());
log.setLogger(eventObject.getLoggerName());
log.setThread(eventObject.getThreadName());
log.setMessage(eventObject.getFormattedMessage());
log.setTimestamp(eventObject.getTimeStamp());
mongoTemplate.insert(log, "payLog");
} finally {
lock.unlock();
}
}

}

最后,验证 MongoDB 数据。

Encoders & Layouts

https://logback.qos.ch/manual/encoders.html

https://logback.qos.ch/manual/layouts.html

Logback 提供的 ch.qos.logback.core.encoder.Encoderch.qos.logback.core.Layout 的默认实现如下:

Layout

一般有几种使用组合方式:

自定义 Pattern 格式

基于自定义 Pattern 格式,直接使用 ch.qos.logback.classic.encoder.PatternLayoutEncoder 即可:

1
2
3
4
5
6
7
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
...
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

还可以输出扩展信息,例如 %X{traceId}

预定义格式

基于预定义格式,需要使用 ch.qos.logback.core.encoder.LayoutWrappingEncoder,并搭配相应 Layout 实现。例如输出成 HTML 格式:

1
2
3
4
5
6
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
...
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout" />
</encoder>
</appender>

Mapped Diagnostic Contexts (MDC)

https://logback.qos.ch/manual/mdc.html

To uniquely stamp each request, the user puts contextual information into the MDC, the abbreviation of Mapped Diagnostic Context.

The MDC class contains only static methods. It lets the developer place information in a diagnostic context that can be subsequently retrieved by certain logback components. The MDC manages contextual information on a per thread basis. Typically, while starting to service a new client request, the developer will insert pertinent contextual information, such as the client id, client’s IP address, request parameters etc. into the MDC. Logback components, if appropriately configured, will automatically include this information in each log entry.

Please note that MDC as implemented by logback-classic assumes that values are placed into the MDC with moderate frequency. Also note that a child thread does not automatically inherit a copy of the mapped diagnostic context of its parent.

Logback leverages SLF4J API:

MDC And Managed Threads

https://logback.qos.ch/manual/mdc.html#managedThreads

MDCInsertingServletFilter

https://logback.qos.ch/manual/mdc.html#mis

例子

一、创建 MDCUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
import lombok.experimental.UtilityClass;
import org.slf4j.MDC;

@UtilityClass
public class MDCUtils {

public static final String TRACE_ID = "traceId";

public MDC.MDCCloseable addTraceId(String traceId) {
return MDC.putCloseable(TRACE_ID, traceId);
}

}

二、在合适的位置设置上下文:

  • 在 http 接口的拦截器
  • 在定时任务执行之前

代码如下:

1
2
3
try (MDC.MDCCloseable mdc = MDCUtils.addTraceId(...)) {
// TODO
}

TraceIdInterceptor

三、选择合适的 Appender 并加以配置,参考 Logback 配置文件样例。

参考

https://github.com/logstash/logstash-logback-encoder

细说 Java 主流日志工具库

SpringBoot + MDC 实现全链路调用日志跟踪

Java 中打印日志的正确姿势

实战总结 | 系统日志规范及最佳实践 | 阿里技术

浅析 JAVA 日志中的几则性能实践与原理解释 | 阿里技术

https://github.com/FasterXML/jackson

https://github.com/FasterXML/jackson-docs

Core modules

Core modules are the foundation on which extensions (modules) build upon. There are 3 such modules currently (as of Jackson 2.x):

  • Streaming (docs) (“jackson-core”) defines low-level streaming API, and includes JSON-specific implementations
  • Annotations (docs) (“jackson-annotations”) contains standard Jackson annotations
  • Databind (docs) (“jackson-databind”) implements data-binding (and object serialization) support on streaming package; it depends both on streaming and annotations packages

Annotations

https://github.com/FasterXML/jackson-annotations

这个页面列出了所有 Jackson 2.0 通用注解,按功能分组。

常用注解如下:

  • @JsonProperty
  • @JsonIgnore
  • @JsonIgnoreProperties
  • @JsonInclude
  • @JsonFormat
  • @JsonSerialize
  • @JsonDeserialize

Databind

For all data-binding, we need a com.fasterxml.jackson.databind.ObjectMapper instance:

1
ObjectMapper mapper = new ObjectMapper(); // create once, reuse

参考:

ObjectMapper,别再像个二货一样一直 new 了!

Configuration

https://github.com/FasterXML/jackson-databind/wiki/#reference-manual

POJO

The most common usage is to take piece of JSON, and construct a Plain Old Java Object (“POJO”) out of it.

Serialization:

1
2
3
4
5
MyValue value = mapper.readValue(new File("data.json"), MyValue.class);
// or:
MyValue value = mapper.readValue(new URL("http://some.com/api/entry.json"), MyValue.class);
// or:
MyValue value = mapper.readValue("{\"name\":\"Bob\", \"age\":13}", MyValue.class);

Deserialization:

1
2
3
4
5
mapper.writeValue(new File("result.json"), myResultObject);
// or:
byte[] jsonBytes = mapper.writeValueAsBytes(myResultObject);
// or:
String jsonString = mapper.writeValueAsString(myResultObject);

Generic Collections

You can also handle JDK Lists, Maps.

Serialization:

1
2
Map<String, Integer> scoreByName = mapper.readValue(jsonSource, Map.class);
List<String> names = mapper.readValue(jsonSource, List.class);

Deserialization:

1
2
// and can obviously write out as well
mapper.writeValue(new File("names.json"), names);

Generic Type

如果需要将 JSON 字符串反序列化为泛型,有两种方式:

方式一:TypeReference

方式一:使用 com.fasterxml.jackson.core.type.TypeReference<T>

This generic abstract class is used for obtaining full generics type information by sub-classing; it must be converted to ResolvedType implementation (implemented by JavaType from “databind” bundle) to be used. Class is based on ideas from http://gafter.blogspot.com/2006/12/super-type-tokens.html, Additional idea (from a suggestion made in comments of the article) is to require bogus implementation of Comparable (any such generic interface would do, as long as it forces a method with generic type to be implemented). to ensure that a Type argument is indeed given.

Usage is by sub-classing: here is one way to instantiate reference to generic type List<Integer>:

1
TypeReference ref = new TypeReference<List<Integer>>() { };

which can be passed to methods that accept TypeReference, or resolved using TypeFactory to obtain ResolvedType.

代码如下:

1
2
3
4
5
TypeReference<RespDTO<XxxRespDTO>> typeRef = new TypeReference<RespDTO<XxxRespDTO>>() {};
RespDTO<XxxRespDTO> resp = mapper.readValue(JSON_A, typeRef);

TypeReference<List<XxxRespDTO>> typeRef1 = new TypeReference<List<XxxRespDTO>>() {};
List<XxxRespDTO> resp2 = mapper.readValue(JSON_B, typeRef1);

方式二:JavaType

方式二:使用 com.fasterxml.jackson.databind.JavaType

Base class for type token classes used both to contain information and as keys for deserializers.

Instances can (only) be constructed by com.fasterxml.jackson.databind.type.TypeFactory.

JavaType 的继承结构如下图:

com.fasterxml.jackson.databind.JavaType

代码如下:

1
2
3
4
5
JavaType valueType = mapper.getTypeFactory().constructParametricType(RespDTO.class, XxxRespDTO.class);
RespDTO<XxxRespDTO> resp = mapper.readValue(JSON_A, valueType);

JavaType valueType2 = mapper.getTypeFactory().constructParametricType(List.class, XxxRespDTO.class);
List<XxxRespDTO> resp2 = mapper.readValue(JSON_B, valueType2);

JavaType 的调试结果如下图,其值为 JavaType 的子类 CollectionType

com.fasterxml.jackson.databind.type.CollectionType

Tree Model (JsonNode)

Tree Model can be more convenient than data-binding, especially in cases where structure is highly dynamic, or does not map nicely to Java classes.

com.fasterxml.jackson.databind.JsonNode 表示一个 JSON 树节点,可以通过 ObjectMapper#readTree 方法反序列化出来,也可以通过 JsonNode 的子类 API 自定义构建:

JsonNode

构建代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JsonNode jsonNode =
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of(
"hello",
new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key", new TextNode("value0"))),
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key", new TextNode("value1"))),
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key2", new TextNode("value2")))
)),
"test",
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of(
"key3", new TextNode("value3")))));

// {"hello":[{"key":"value0"},{"key":"value1"},{"key2":"value2"}],"test":{"key3":"value3"}}
log.info(jsonNode.toString());

JsonNode 构建完成后,可以灵活的读取其值,例如:

1
2
3
4
// [value0, value1]
log.info(jsonNode.get("hello").findValuesAsText("key").toString());
// value3
log.info(jsonNode.get("test").get("key3").asText());

也可以修改其值:

1
((ObjectNode) jsonNode).put("key", "value");

使用场景

一、对接口响应的 JSON 原文进行验签:

1
2
3
4
5
6
7
8
9
10
11
{
"response": {
"head": {
...
},
"body": {
...
}
},
"signature": "..."
}
1
2
3
4
JsonNode jsonNode = objectMapper.readTree(responseJson);
String response = jsonNode.get("response").toString();
String signature = jsonNode.get("signature").toString();
return RsaUtil.verify(response, publicKey, signature);

二、<Compare Two JSON Objects with Jackson>

using the JsonNode.equals method. The equals() method performs a full (deep) comparison.

例子

本例中,我们需要获取以下两个方法的泛型返回值中的实际类型参数 XxxRespDTOClass 类型,以用于 JSON 转换:

1
2
3
4
5
6
7
8
9
10
11
12
public interface ApiService {

/**
* 接口一
*/
RespDTO<XxxRespDTO> get(XxxReqDTO reqDTO);

/**
* 接口二
*/
RespDTO<List<XxxRespDTO>> list(XxxReqDTO reqDTO);
}

定义一个方法,用于转换 JSON:

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
private Object getObject(Method method, String json) {
Object result;

// 获取该方法的泛型返回值,然后获取其第一个实际类型参数
ParameterizedType returnType = (ParameterizedType) method.getGenericReturnType();
Type type = returnType.getActualTypeArguments()[0];

// 处理接口一的情况
if (type instanceof Class) {
Class<?> clazz = (Class<?>) type;
result = JsonUtils.fromJson(json, clazz); // ObjectMapper#readValue(String, Class<T>)
} else if (type instanceof ParameterizedType) {
ParameterizedType nestedReturnType = (ParameterizedType) type;
// 处理接口二的情况
if (nestedReturnType.getRawType() == List.class) {
Class<?> clazz = (Class<?>) nestedReturnType.getActualTypeArguments()[0];
result = JsonUtils.fromJsonToList(decryptedRespData, clazz);
} else {
throw new IllegalStateException("未实现的 JSON 解析!");
}
} else {
throw new IllegalStateException("未实现的 JSON 解析!");
}
return result;
}

这种用法常常出现在框架之中。下面来看下调试效果:

接口一

下图展示了变量 returnType 为参数化类型 ParameterizedType,其实际类型参数 typeClass 类型,值为 XxxRespDTO

JsonNode

接口二

下图展示了变量 returnType 的实际类型参数 type 与接口一为 Class 类型不同,接口二为 ParameterizedType 参数化类型,值为 List<XxxRespDTO>

常见报错

Unrecognized field, not marked as ignorable

该错误的意思是说,不能够识别的字段没有标示为可忽略。出现该问题的原因就是 JSON 中包含了目标 Java 对象没有的属性。

解决方案:

  1. 保证传入的 JSON 串不包含目标对象的没有的属性。

  2. On deserialization, @JsonIgnoreProperties(ignoreUnknown=true) ignores properties that don’t have getter/setters

  3. Deserialization Features 全局配置:

    1
    2
    // 配置该 `objectMapper` 在反序列化时,忽略目标对象没有的属性。
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

缺少默认构造方法

问题:

1
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.***.RespBody` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

解决方案:

  • POJO 加上 @NoArgsConstructor

Using Java inner classes for Jackson serialization

https://dev.to/pavel_polivka/using-java-inner-classes-for-jackson-serialization-4ef8

Google Gson

https://github.com/google/gson

1
2
3
4
5
6
7
8
9
10
11
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("time", LocalDateTime.now().toString());
jsonObject.addProperty("ip", ip);
jsonObject.addProperty("rspMillis", rspMillis);
jsonObject.addProperty("rspCode", rspCode);
jsonObject.addProperty("method", method);
jsonObject.addProperty("path", uriPath);
// Deprecated
// new JsonParser().parse(jsonTrace);
jsonObject.add("body", JsonParser.parseString(jsonTrace));
requestJsonLogger.info(jsonObject.toString());

Deserialization to Generic Type: com.google.gson.reflect.TypeToken

1
2
RespDTO<XxxRespDTO> data = new Gson().fromJson(json, new TypeToken<RespDTO<XxxRespDTO>>() {}.getType());
List<XxxRespDTO> data = new Gson().fromJson(json, new TypeToken<ArrayList<XxxRespDTO>>() {}.getType());

参考

https://www.baeldung.com/category/json/jackson/

https://github.com/qidawu/java-api-test/tree/master/src/main/java/json

规范

The Jakarta XML Binding provides an API and tools that automate the mapping between XML documents and Java objects.

规范文档:https://jakarta.ee/specifications/xml-binding/

实现

官方认证实现:https://eclipse-ee4j.github.io/jaxb-ri/

依赖下载:https://mvnrepository.com/artifact/com.sun.xml.bind/jaxb-ri,将传递依赖:

1
2
3
4
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>

示例

本文演示如何生成和解析 XML,主要涉及以下常用注解,更多注解可以自行尝试:

1
2
3
4
@XmlRootElement
@XmlElement
@XmlAttribute
@XmlValue

实体类如下:

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
// @Setter 会报错 com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 4 counts of IllegalAnnotationExceptions
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@XmlRootElement(name = "head")
class Head {
@XmlElement(name = "trade_code")
private String tradeCode;

@XmlElement(name = "trade_date")
private String tradeDate;

@XmlElement(name = "trade_time")
private String tradeTime;

@XmlElement(name = "serial_no")
private String serialNo;

@XmlElement(name = "parent")
private Parent parent;
}

@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@XmlRootElement
class Parent {
@XmlElement
private List<Child> child;
}

@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
class Child {
@XmlAttribute
private String key;

@XmlValue
private String value;
}

XML 工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Slf4j
public class XmlUtils {

private static final Map<String, JAXBContext> JAXB_CONTEXT_MAP = new HashMap<>();

/**
* 传入一个对象,生成对应的 XML
*/
public static <T> String toXml(T t) {
if (t == null) {
log.warn("[XmlUtils toXml] params is null");
return null;
}

JAXBContext jaxbContext = getJAXBContext(t.getClass());
Marshaller jaxbMarshaller;
try (StringWriter stringWriter = new StringWriter()) {
jaxbMarshaller = jaxbContext.createMarshaller();
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
jaxbMarshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
jaxbMarshaller.marshal(t, stringWriter);
return stringWriter.toString();
} catch (JAXBException | IOException e) {
log.error("[XmlUtils toXml] exception", e);
}
return null;
}

/**
* 将 XML 解析成指定对象
*/
public static <T> T unXml(String xml, Class<T> clazz) {
if (xml == null || xml.isEmpty()) {
log.warn("[XmlUtils unXml] xml is null");
return null;
}

JAXBContext jaxbContext = getJAXBContext(clazz);
try {
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
StringReader reader = new StringReader(xml);
@SuppressWarnings("unchecked")
T unmarshal = (T) unmarshaller.unmarshal(reader);
return unmarshal;
} catch (JAXBException e) {
log.error("[XmlUtils unXml] exception", e);
}
return null;
}

/**
* 根据类名称获取 JAXBContext
*/
private static JAXBContext getJAXBContext(Class clazz) {
JAXBContext jaxbContext = JAXB_CONTEXT_MAP.get(clazz.getName());
if (jaxbContext == null) {
try {
jaxbContext = JAXBContext.newInstance(clazz);
JAXB_CONTEXT_MAP.put(clazz.getName(), jaxbContext);
} catch (JAXBException e) {
log.error(e.getMessage(), e);
}
}
return jaxbContext;
}

}

生成 XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testToXml() {
LocalDateTime now = LocalDateTime.now();
Child child = Child.builder()
.key("key")
.value("value")
.build();

Parent parent = Parent.builder()
.child(Arrays.asList(child, child))
.build();

Head head = Head.builder()
.tradeCode("XT-001")
.tradeDate(now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
.tradeTime(now.format(DateTimeFormatter.ofPattern("HH:mm:ss")))
.serialNo(UUID.randomUUID().toString())
.parent(parent)
.build();
String xml = XmlUtils.toXml(head);
log.info(xml);
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<head>
<trade_code>XT-001</trade_code>
<trade_date>2020-03-19</trade_date>
<trade_time>18:39:28</trade_time>
<serial_no>114d2c0c-30a8-4275-982c-f8eba86cbadb</serial_no>
<parent>
<child key="key">value</child>
<child key="key">value</child>
</parent>
</head>

解析 XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testUnXml() {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" +
"<head>\n" +
" <trade_code>XT-001</trade_code>\n" +
" <trade_date>2020-03-19</trade_date>\n" +
" <trade_time>18:39:28</trade_time>\n" +
" <serial_no>114d2c0c-30a8-4275-982c-f8eba86cbadb</serial_no>\n" +
" <parent>\n" +
" <child key=\"key\">value</child>\n" +
" <child key=\"key\">value</child>\n" +
" </parent>\n" +
"</head>";
Head head = XmlUtils.unXml(xml, Head.class);
log.info(head.toString());
}

输出结果

1
Head(tradeCode=XT-001, tradeDate=2020-03-19, tradeTime=18:39:28, serialNo=114d2c0c-30a8-4275-982c-f8eba86cbadb, parent=Parent(child=[Child(key=key, value=value), Child(key=key, value=value)]))

参考

https://jakarta.ee/specifications/xml-binding/

https://en.wikipedia.org/wiki/Jakarta_XML_Binding

Bean Validation 规范

Bean Validation 为 JavaBean 和方法验证定义了一组元数据模型和 API 规范,常用于后端数据的声明式校验。

RoadMap

Bean Validation 规范最早在 Oracle Java EE 下维护。

2017 年 11 月,Oracle 将 Java EE 移交给 Eclipse 基金会。 2018 年 3 月 5 日,Eclipse 基金会宣布 Java EE (Enterprise Edition) 被更名为 Jakarta EE。因此 Bean Validation 规范经历了从 JavaEE Bean Validation 到 Jakarta Bean Validation 的两个阶段:

Bean Validation 1.0 (JSR 303)

Bean Validation 1.0 (JSR 303) was the first version of Java’s standard for object validation.

It was released in 2009 and is part of Java EE 6.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
</dependency>

Bean Validation 1.1 (JSR 349)

Bean Validation 1.1 (JSR 349) was finished in 2013. Changes between Bean Validation 1.0 and 1.1

It is part of Java EE 7.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>

Bean Validation 2.0 (JSR 380)

Bean Validation 2.0 (JSR 380) was finished in August 2017. Changes between Bean Validation 2.0 and 1.1

It’s part of Java EE 8 (but can of course be used with plain Java SE as the previous releases).

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

Jakarta Bean Validation 2.0

Jakarta Bean Validation 2.0 was published in August 2019. There are no changes between Jakarta Bean Validation 2.0 and Bean Validation 2.0 except for the GAV: it is now jakarta.validation:jakarta.validation-api.

It’s part of Jakarta EE 8 (but can of course be used with plain Java SE as the previous releases).

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>

Jakarta Bean Validation 3.0

Jakarta Bean Validation 3.0 was released in Wednesday, October 7, 2020.

This release is part of Jakarta EE 9.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.0</version>
</dependency>

Constraints

Constraints 约束是 Bean Validation 规范的核心。约束是通过约束注解和一系列约束验证实现的组合来定义的。约束注解可应用于类型、字段、方法、构造函数、参数、容器元素或其它约束注解。

一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。也就是说可以有多个 constraint validator 对应一个 annotation。在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。

有些时候,在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制。可以通过两种方法去实现,一种是组合现有的 constraint 来生成一个更复杂的 constraint,另外一种是开发一个全新的 constraint。

下表列出了常用 Constraints:

内置 Constraints

表 1. Bean Validation 1.x 内置的 Constraints,如下:

Constraint 详细信息
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式

Bean Validation 2.0 内置的 Constraints,参考:https://beanvalidation.org/2.0/spec/#builtinconstraints

Bean Validation 3.0 内置的 Constraints,参考官方新版规范:Jakarta Bean Validation specification

第三方 Constraints

表 2. Hibernate Validator 附加的 Constraints,更多 Constraints 详见:Additional constraints

Constraint 详细信息
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内

其它第三方 Constraints:

核心 API

核心类 Validation 作为 Bean Validation 的入口点,提供了三种引导方式。下面代码演示了以最简单的方式创建默认的 ValidatorFactory,并获取 Validator 用以验证 Java Bean。涉及的 API 如下:

  • 核心类 javax.validation.Validation

    Note:

    • The ValidatorFactory object built by the bootstrap process should be cached and shared amongst Validator consumers.
    • This class is thread-safe.
  • 接口 javax.validation.ValidatorFactory

    Factory returning initialized Validator instances.

    Implementations are thread-safe and instances are typically cached and reused.

  • 接口 javax.validation.Validator

    Validates bean instances. Implementations of this interface must be thread-safe.

代码如下:

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
import lombok.experimental.UtilityClass;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.Validator;
import java.util.Set;

/**
* JavaBean 校验器
*/
@UtilityClass
public class ValidatorUtil {

private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();

/**
* 校验参数
* @param obj 参数
* @return 校验结果
*/
public <T> Set<ConstraintViolation<T>> validate(T obj) {
return VALIDATOR.validate(obj);
}

}

Bean Validation 实现

Hibernate Validator

Hibernate 框架提供了各种子项目,如下:

Hibernate Projects

其中,子项目 Hibernate Validator 是 Bean Validation 规范的官方认证实现

要在 Maven 项目中使用 Hibernate Validator,需要添加如下依赖项:

  • 新版实现。6.x 及以上版本已完全从 Hibernate 持久化框架中剥离并被移至新 group:

    hibernate-validator is entirely separate from the persistence aspects of Hibernate. So, by adding it as a dependency, we’re not adding these persistence aspects into the project.

    1
    2
    3
    4
    5
    6
    <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
    <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.0.Final</version>
    </dependency>
  • 遗留实现,可以下载到 5.x 及之前的老版本,例如:

    1
    2
    3
    4
    5
    6
    <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
    <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.3.Final</version>
    </dependency>

下表汇总了 Hibernate Validator 各版本的传递依赖:

Hibernate Validator Java Bean Validation 规范 Expression Language (EL) 规范
7.0 series 8, 11 or 17 Jakarta EE 9 - Bean Validation 3.0 Jakarta EE 9 - Expression Language 4.0
6.2 series 8, 11 or 17 Jakarta EE 8 - Bean Validation 2.0 Jakarta EE 8 - Expression Language 3.0
6.1 series 8, 11 or 17 Jakarta EE 8 - Bean Validation 2.0 Jakarta EE 8 - Expression Language 3.0
6.0 series 8, 11 or 17 JavaEE 8 - Bean Validation 2.0 (JSR 380) Java EE 7 - Expression Language 3.0 (JSR 341)
5.0 series 6 or 7 JavaEE 7 - Bean Validation 1.1 (JSR 349) Java EE 7 - Expression Language 3.0 (JSR 341)

引入 Hibernate Validator 后,将传递依赖 Bean Validation API 规范相应的版本,无需重复引入:

Hibernate’s Jakarta Bean Validation reference implementation. This transitively pulls in the dependency to the Bean Validation API.

详见如下

使用方式

分组约束

如果我们想在新增的情况验证 id 和 name,而修改的情况验证 name 和 password,可以使用分组约束功能。

https://blog.csdn.net/win7system/article/details/51241837

组合约束

参考 Bean Validation 文档:Constraint composition

Constraint composition is useful in several ways:

  • Avoid duplication and facilitate reuse of more primitive constraints.
  • Expose primitive constraints as part of a composed constraint in the metadata API and enhance tool awareness.

Composition is done by annotating a constraint annotation with the composing constraint annotations.

compose constraints via a logical OR or NOT

参考 Hibernate 文档:Boolean composition of constraints

Jakarta Bean Validation specifies that the constraints of a composed constraint are all combined via a logical AND. This means all of the composing constraints need to return true to obtain an overall successful validation.

Hibernate Validator offers an extension to this and allows you to compose constraints via a logical OR or NOT. To do so, you have to use the @ConstraintComposition annotation and the enum CompositionType with its values AND, OR and ALL_FALSE.

自定义约束

参考 Bean Validation 文档:Constraint validation implementation

Dubbo 参数验证

https://dubbo.apache.org/zh/docs/v2.7/user/examples/parameter-validation/

Spring MVC 参数验证

参考:

Spring MVC 方法参数验证

SpringBoot 中使用 @Valid 注解 + 全局异常处理器 优雅处理参数验证

@Validated 和 @Valid 的区别

  • @Valid 由 Bean Validation 提供,由 Hibernate Validator 实现。
  • @Validated 由 Spring Validator 提供,是 @Valid 的变种,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同。

常见问题

缺少 Expression Language 依赖

以 Hibernate Validator 6.0 series 为例,查看其 pom.xml,会发现 Expression Language 依赖被声明为 provided

1
2
3
4
5
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<scope>provided</scope>
</dependency>

这表示该依赖在运行时由 Java EE container 容器提供,因此无须重复引入。但对于 Spring Boot 应用来说,则需要添加此依赖。如果缺少该依赖,则报错如下:

1
HV000183: Unable to load 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead

问题原因:缺少 Unified Expression Language (EL) 规范的实现依赖,即 Glassfish。

解决方案:

Hibernate Validator also requires an implementation of the Unified Expression Language (JSR 341) for evaluating dynamic expressions in constraint violation messages.

When your application runs in a Java EE container such as WildFly, an EL implementation is already provided by the container.

In a Java SE environment, however, you have to add an implementation as dependency to your POM file. For instance, you can add the following dependency to use the JSR 341 reference implementation:

1
2
3
4
5
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>

注意,要使用与 Hibernate Validator 匹配的 Unified Expression Language (EL) 的依赖版本。

参考:https://stackoverflow.com/questions/24386771/javax-validation-validationexception-hv000183-unable-to-load-javax-el-express

参考

https://docs.oracle.com/javaee/7/index.html

https://docs.oracle.com/javaee/7/api/index.html

https://jakarta.ee/

https://jakarta.ee/specifications/bean-validation/

https://beanvalidation.org/

https://beanvalidation.org/2.0/spec/

http://hibernate.org/validator/

baeldung

后台表单校验(JSR303)

Spring Boot参数校验以及分组校验的使用

Jakarta Annotations defines a collection of annotations representing common semantic concepts that enable a declarative style of programming that applies across a variety of Java technologies.

通过在 Java 平台中添加 JSR 175(Java 编程语言的元数据工具),我们设想各种技术将使用注解来实现声明式编程风格。如果这些技术各自为共同概念独立定义自己的注解,那将是不幸的。在 Jakarta EE 和 Java SE 组件技术中保持一致性很有价值,但在 Jakarta EE 和 Java SE 之间实现一致性也很有价值。

本规范的目的是定义一小组通用注解,这些注解可在其它规范中使用。希望这将有助于避免在不同 Jakarta EE 规范中定义的注解之间不必要的冗余或重复。这将允许我们将通用注解集中在一个地方,让技术引用此规范,而不是在多个规范中指定它们。这样,所有技术都可以使用相同版本的注解,并且跨平台使用的注解将保持一致。

这些通用注解详见如下:

Specifications Description Compatible Implementations
Jakarta Servlet A server-side API for handling HTTP requests and responses Eclipse GlassFish
Jakarta Server Pages (JSP) Defines a template engine for web applications Eclipse GlassFish
Jakarta Standard Tag Library (JSTL) Provides a set of tags to simplify the JSP development Eclipse GlassFish
Jakarta Expression Language (EL) Defines an expression language for Java applications Eclipse Expression Language
Jakarta Server Faces (JSF) MVC framework for building user interfaces for web apps Eclipse Mojarra
Jakarta MVC Standardizes the action-based model-view-controller pattern Eclipse Krazo
Jakarta RESTful Web Services (JAX-RS) API to develop web services following the REST pattern Eclipse Jersey
Jakarta Enterprise Web Services Web Services for Jakarta EE architecture Eclipse GlassFish
Jakarta WebSocket API for Server and Client Endpoints for WebSocket protocol Eclipse Tyrus
……

一点关于 EL 的历史:

Expression Language (EL) 最初受到 ECMAScript 和 XPath 表达式语言的启发。在其成立之初,参与的专家非常不愿意设计另一种表达语言,并试图使用这些语言中的每一种,但他们在不同的领域都有所欠缺。

因此,JSP Standard Tag Library (JSTL) version 1.0 (based on JSP 1.2) 首先引入了一种表达式语言,使前端页面开发者可以轻松访问和操作应用程序数据,而无需掌握如 Java、JavaScript 等编程语言相关的复杂性。

鉴于其成功,EL 随后被移入 JSP 规范(JSP 2.0/JSTL 1.1),使其在 JSP 页面中普遍可用(而不仅仅用于 JSTL 标记库的属性)。

JavaServer Faces 1.0 定义了用于构建用户界面组件的标准框架,并且构建在 JSP 1.2 技术之上。由于 JSP 1.2 技术没有集成的表达语言,并且 JSP 2.0 EL 不能满足 Faces 的所有需求,因此为 Faces 1.0 开发了一个 EL 变体。Faces 专家组试图使该语言尽可能与 JSP 2.0 兼容,但是还是有一些区别。

显然需要一种单一、统一的表达语言来满足各种 Web 层技术的需求。因此,Faces 和 JSP 专家组共同制定了统一表达式语言的规范,该规范在 JSR 245 中定义,并在 JSP 2.1 和 Faces 1.2 版本中生效。

JSP/JSTL/Faces 专家组也意识到 EL 的用处超出了他们自己的规范。3.0 规范是第一个将表达式语言定义为独立规范的 JSR,不依赖于其它技术。

参考

JSP 标签总结

JSP 标准标签库(JSTL)总结

JSP EL 表达式总结

Java Documentation

Standard Edition

Java Platform, Standard Edition Documentation

Roadmap

详见:Oracle Java SE Support Roadmap

Oracle Java SE Support Roadmap

参考:

一文详解|从JDK8飞升到JDK17,再到未来的JDK21 | 阿里技术

Java SE 8

Java Platform, Standard Edition (Java SE) Overview

Reference

Oracle has two products that implement Java Platform Standard Edition (Java SE) 8:

  • Java SE Development Kit (JDK) 8. JDK 8 is a superset of JRE 8, and contains everything that is in JRE 8, plus tools such as the compilers and debuggers necessary for developing applets and applications.

  • Java SE Runtime Environment (JRE) 8. JRE 8 provides the libraries, the Java Virtual Machine (JVM), and other components to run applets and applications written in the Java programming language. Note that the JRE includes components not required by the Java SE specification, including both standard and non-standard Java components.

The following conceptual diagram illustrates the components of Oracle’s Java SE products:

Description of Java Conceptual Diagram

JDK

一些术语:

  • JCP 是 Java Community Process(Java社区进程)的简称,社会各界组成的 Java 社区,规划和领导 Java 的发展。
  • JSR 是 Java Specification Requests(Java 规范请求)的简称,是 JCP 成员向委员会提交的 Java 发展议案,经过一系列流程后,如果通过会成为 JEP,最终会体现在未来的 Java 中。
  • JEP 是 JDK Enhancement Proposals (Java 增强提案)的简称,为了保证日后 JDK 研发能够更加顺利地进行,从 JDK 8 开始,Oracle 启用 JEP 来定义和管理纳入新版 JDK 发布范围的功能特性。JDK 的版本变化将从这些提案中选取。例如:

一些历史:参考《JDK 历代版本变化

JDK 从 1.5 版本开始,在官方的正式文档与宣传资料中已经不再使用类似“JDK 1.5”的名称,只有程序员内部使用的开发版本号(Developer Version,例如 java -version 的输出)才继续沿用 1.5、1.6 和 1.7 的版本号(JDK 10 之后又改为了采用年份加月份作为开发版本号,例如 18.3),而公开版本号(Product Version)则改为 JDK 5、JDK 6 和 JDK 7 的命名方式。

从 JDK 10 开始,每年的 3 月和 9 月各发布一个大版本,目的就是避免众多功能特性被集中捆绑到一个 JDK 版本上而引发交付风险。同时为了降低维护成本,每六个 JDK 大版本中才会被划出一个长期支持(Long Term Suppot,LTS)版本,只有 LTS 版的 JDK 能够获得为期三年的支持和更新,普通版的 JDK 就只有短短六个月的生命周期。

JDK 8 和 JDK 11 是 LTS 版本,再下一个就到 2021 年发布的 JDK 17。

在 2018 年发布的 Java 11, Oracle 已经让 OpenJDK 和 Oracle JDK 两者的二进制文件在功能上尽可能相互接近,尽管 OpenJDK 与 Oracle JDK 两者在一些选项之间仍然存在一些差异。参考:<Differences Between Oracle JDK and OpenJDK>。

JDK

根据 JEP-320 的内容,计划于 2018 年 9 月发布的 JDK 11 将不包括 Java EE 模块:JAX-WS( JSR-224 )、JAXB( JSR-222 )、JAF( JSR-925 )、Commons Annotations( JSR-250 )和 JTA( JSR-907 ),而这些模块已在 JDK 中存在了多年。计划在 JDK 11 中移除的四个 Java EE 模块最终将进入 EE4J。

常见 JDK 版本:《你该选择什么样的 JDK?

  • Oracle JDK 商业版

    OTN 协议下发行的传统的 Oracle JDK,个人可以免费使用,但若在生产环境中商用就必须付费,可以有三年时间的更新支持。

  • OpenJDK 开源版

    GPLv2+CE 协议下由 Oracle 发行的 OpenJDK,可以免费在开发、测试、生产环境中使用,但是只有半年时间的更新支持。

    由于 Oracle 不愿意在旧版本的 OpenJDK 上继续耗费资源,而 RedHat (IBM) 又乐意扩大自己在 Java 社区的影响力,因此 RedHat 代替 Oracle 成为了 OpenJDK 历史版本的维护者,接过了 OpenJDK 6、7、8、11 的管理权力和维护职责。

  • OpenJDK 的变种版本:

JRE

不需要考虑 JDK 与 JRE 的关系了

JDK 和 JRE 都是 Java 的重要组成部分,但它们的角色和用途是不同的。JDK 是 Java 开发工具包,主要用于开发 Java 应用。它包含了 JRE,同时还提供了一些额外的工具,如编译器(javac)、调试器(jdb)等。JRE 则是运行 Java 应用程序所需的环境。它包含了 Java 虚拟机(JVM)和 Java 类库,也就是 Java 应用程序运行时所需的核心类和其他支持文件。

在 JDK 8 及之前的版本中,Oracle 会提供独立的 JRE 和 JDK 供用户下载。也就是说,你可以只安装 JRE 来运行 Java 程序,也可以安装 JDK 来开发 Java 程序。

然而从 JDK 9 开始,Oracle 不再单独发布 JRE。取而代之的是 jlink 工具,可以使用这个工具来生成 定制的运行时镜像。这种方式简化了 Java 应用的部署,因你只需要分发包含你的应用和定制运行时镜像的包,不需要单独安装 JRE。

JVM

除了官方 HotSpotVM、GraalVM 实现,其它厂商实现如下:

参考:Java 虚拟机系列

Enterprise Edition

Roadmap

作为分水岭,2017 年 11 月,Oracle 将 Java EE 移交给 Eclipse 基金会。 2018 年 3 月 5 日,Eclipse 基金会宣布 Java EE (Enterprise Edition) 被更名为 Jakarta EE。

参考:《Java EE 规范重命名为 Jakarta EE

规范 创建组织 发布时间
Java EE 1.3 (JSR-058) Java Community Process
Java EE 1.4 (JSR-151) Java Community Process
Java EE 5 (JSR-244) Java Community Process 2005
Java EE 6 (JSR-316) Java Community Process 2007
Java EE 7 (JSR-342) Java Community Process May 28, 2013
Java EE 8 (JSR-366) Java Community Process Aug 21, 2017
Jakarta EE 8 Jakarta EE Platform Specification Project with guidance provided by the Jakarta EE Working Group 2019.9
Jakarta EE 9 Jakarta EE Platform Specification Project with guidance provided by the Jakarta EE Working Group 2020.9

Java EE

Java Platform, Enterprise Edition

Java EE 和 Spring 之间复杂的关系:

Spring 诞生于 2004 年,由 Rod Johnson 发起,作为对 J2EE(Java 2 Platform,Enterprise Edition)和 EJB 2 复杂性的反击。从那个时候开始,Spring 和 Java EE 之间就没有停止过竞争,并彼此影响对方:

  • Spring(以及 Hibernate)的出现刺激了 Java EE 社区,促使他们推出了 EJB 3 和 JAP 1.0。
  • Spring Batch 直接影响到了 Batch 规范(JSR 352)。
  • Spring Dependency Injection 启发了 CDI(Context and Dependency Injection)。
  • Spring 恰到好处地使用了 J2EE 和 Java EE 中的某些标准,如 Servlet、JMS 和 JPA。
  • Spring 5 宣称兼容 Java EE 8。

从 2006 年开始,Java EE 也将提升易用性和对开发者的友好放在首位,但在演进速度方面还是很慢,主要有两个原因:

  • JCP 制定规范需要很长时间:即使是一个轻量级的规范,也需要多方参与,需要更长的时间才能达成一致。
  • 实现和认证:在规范发布之后,需要几个月时间才能找到符合认证的应用服务器。

而最近,这方面的差距在加大:

  • Spring Boot 将“以约定代替配置(Convention Over Configuration)”的原则发挥到了极致,进一步提升易用性。
  • Spring Cloud 利用 Netflix 的开源组件解决了与云原生应用开发相关的问题,如服务注册、服务发现、弹性、负载均衡、监控……
  • Spring 5 将响应式编程(Reactive Programming)提升为一等公民。

Java EE 在这方面的速度要慢的多。在 2013 年发布 Java EE 7 之后,经历了一段消停期。2016 年,在社区的压力下,Oracle 才发布了一个新的路线图。

Java EE 8 发布于 2017 年 9 月,虽然人们对其期望甚高,但并非革命性的。人们还是把更多的目光投向了 Java EE 9,期望下一个版本会有更多的创新。

与此同时,Eclipse 基金会于 2016 年中启动 Microprofile.io 项目,旨在以微服务架构为基准来优化企业版 Java,以此来推动 Java EE 生态系统的发展。Microprofile 1.0 涵盖了 JAX-RS 2.0、CDI 1.2 和 JSON-P 1.0,1.2 版本于 2017 年 9 月发布,加入了更多特性,如配置、容错、JWT、度量指标和健康检测,2.0 版本有望与 Java EE 8 看齐。

Jakarta EE

Eclipse Foundation Projects

Architecture

Jakarta EE 平台的架构关系如下图所示。(请注意,此图显示了元素间的逻辑关系;它并不意味着将元素间物理关系为单独的机器、进程、地址空间或虚拟机。)

下面分别描述每个矩形及其之间的关系:

  • 容器(如 Web Container)作为 Jakarta EE 运行时环境,为 Application Components(如 Server Pages、Servlet)提供所需服务;
  • 而所提供的服务,由矩形下半部分的方框表示。例如, Web Container 为 Servlet 提供了 Bean Validation API。详见 Jakarta EE 标准服务
  • Java SE 的 API 受 Java SE 运行时环境(JRE)的支持,适用于每种类型的 Application Components。
  • 箭头表示需要访问 Jakarta EE 平台的其它部分。例如,Web Container 通过 JDBC™ API 为 Server Pages、Servlet 提供数据库访问能力。

Jakarta EE Architecture Diagram

Application Components

Jakarta EE 运行时环境定义了 Jakarta EE 产品必须支持的四种 Application Components 类型:

Application Components 描述
Application clients 通常是在台式计算机上执行的 GUI 程序。提供类似于本机应用程序的用户体验,并且可以访问 Jakarta EE 中间层的所有设施。
Applets 通常是在 Web 浏览器中执行的 GUI 组件,但也可以在支持 Applet 编程模型的各种其它应用程序或设备中执行。
Web Components (Servlets, Server Pages, Server Faces Applications, Filters, and Web Event Listeners) 通常在 Web 容器中执行,并可能响应来自 Web 客户端的 HTTP 请求。
Enterprise Beans 在支持事务的托管环境中执行。可以使用 SOAP/HTTP 协议直接提供 Web 服务。

Containers

容器为 Jakarta EE Application Components 提供运行时支持。

容器为 Application Components 提供了一套底层 Jakarta EE API 的联合视图。Jakarta EE Application Components 从不直接与其它 Jakarta EE Application Components 交互。它们使用容器的协议和方法来相互交互以及与平台服务交互。在 Application Components 和 Jakarta EE 服务之间插入一个容器,可以使该容器透明地注入该组件所需的服务,例如声明式事务管理,安全检查,资源池和状态管理。

This specification requires that containers provide a Java Compatible™ runtime environment, as defined by the Java Platform, Standard Edition, v8 specification (Java SE).

Database

The Jakarta EE platform requires a database, accessible through the JDBC API, for the storage of business data. The database is accessible from:

  • Web Components
  • Enterprise Beans
  • Application clients

Jakarta EE Standard Services

https://jakarta.ee/specifications/platform/9/jakarta-platform-spec-9.html#a84

参考

教程:

2017 年

2019 年

  • InfoQ 2019 年 Java 发展趋势报告

  • 2019 中国 Java 发展趋势报告

    Java 作为使用最为广泛的语言,最近几年还是有比较大进步的,无论从语法的易用性上还是性能上都有很大程度的提升。吸收了函数式编程的思想,lambda 表达式、Parallem stream、Var 变量等提升了开发人员的效率与代码的简洁性。ZGC 无疑是一项重大的改进,在一定程度上解决了 Java 天生的 GC 延迟问题。

    Java 的编程复杂度并没有明显的降低,比如 I/O 处理、并发 / 并⾏计算,以及类加载等等。再者是 Java 与操作系统之间的交互仍不够充分,尽管 Java 9 开始提供了不少的 API,然⽽了解和使用的群体不⾜。Java 在这方面明显不及 GO 语言。

    从语⾔层⾯来看,Java 正在向主流非 Java 语⾔融合,解决其中鸿沟的关键是语法的变化,比如 Java 8 的 Lambda 表达式 和 Java 10 的局部变量类型( var )等。个人认为这是一件好事,未来前后端不分家,相互渗透,对于彼此语言都是良性。

2020 年

2021 年

2022 年

2023 年:

从 Java 8 升级到 Java 17 踩坑全过程

从 JDK8 飞升到 JDK17,再到未来的 JDK21 | 阿里技术

升级指南之 JDK 11+ 新特性和阿里巴巴 AJDK | 阿里技术

从 JDK 9 到 19,我们帮您提炼了和云原生场景有关的能力列表 | 阿里技术

从 JDK 9 到 19,认识一个新的 Java 形态(内存篇)| 阿里技术

从 JDK 9 开始,一些关键特性 | 阿里技术

如何创建调度器?

通过工厂类 Schedulers 创建调取器:

https://projectreactor.io/docs/core/release/api/reactor/core/scheduler/Schedulers.html

Return a shared instance Return a new instance Description Notes
immediate() / No execution context at processing time, the submitted Runnable will be directly executed, effectively running them on the current Thread (can be seen as a “null object” or no-op Scheduler).
single() newSingle(...) A single, reusable thread. Note that this method reuses the same thread for all callers, until the Scheduler is disposed.
elastic() newElastic(...) An unbounded elastic thread pool. This one is no longer preferred with the introduction of Schedulers.boundedElastic(), as it has a tendency to hide backpressure problems and lead to too many threads (see below).
boundedElastic() newBoundedElastic(...) A bounded elastic thread pool. Like its predecessor elastic(), it creates new worker pools as needed and reuses idle ones. Worker pools that stay idle for too long (the default is 60s) are also disposed.
Unlike its predecessor elastic(), it has a cap on the number of backing threads it can create (default is number of CPU cores x 10). Up to 100 000 tasks submitted after the cap has been reached are enqueued and will be re-scheduled when a thread becomes available.
This is a better choice for I/O blocking work. While it is made to help with legacy blocking code if it cannot be avoided. Schedulers.boundedElastic() is a handy way to give a blocking process its own thread so that it does not tie up other resources. See How Do I Wrap a Synchronous, Blocking Call?, but doesn’t pressure the system too much with new threads.
parallel() newParallel(...) A fixed pool of workers that is tuned for parallel work. It creates as many workers as you have CPU cores.
fromExecutorService(ExecutorService) A Customize thread pool. Create a Scheduler out of any pre-existing ExecutorService

delayElements
Signals are delayed and continue on the parallel default Scheduler
Signals are delayed and continue on an user-specified Scheduler

如何使用调度器?

Reactor offers two means of switching the execution context (or Scheduler) in a reactive chain: publishOn and subscribeOn. Both take a Scheduler and let you switch the execution context to that scheduler. But the placement of publishOn in the chain matters, while the placement of subscribeOn does not. To understand that difference, you first have to remember that nothing happens until you subscribe.

Let’s have a closer look at the publishOn and subscribeOn operators:

例子一

演示流是运行在 subscribe() 方法调用的线程上,且大多数操作符继续在前一个操作符执行的线程中工作。

most operators continue working in the Thread on which the previous operator executed. Unless specified, the topmost operator (the source) itself runs on the Thread in which the subscribe() call was made. The following example runs a Mono in a new thread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// The Mono<String> is assembled in thread main.
final Mono<String> mono =
Mono.fromSupplier(() -> {
log.info("fromSupplier");
return "hello";
})
.map(msg -> {
log.info("map");
return msg + " world";
});

Thread t = new Thread(() ->
// However, it is subscribed to in thread Thread-0.
// As a consequence, all callbacks (fromSupplier, map, onNext) actually run in Thread-0
mono.subscribe(log::info)
);
t.start();
t.join();

输出结果:

1
2
3
21:02:18.436 [Thread-0] INFO FluxTest - fromSupplier
21:02:18.436 [Thread-0] INFO FluxTest - map
21:02:18.437 [Thread-0] INFO FluxTest - hello world

例子二

演示如何使用 subscribeOn 方法,简化上述例子一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CountDownLatch countDownLatch = new CountDownLatch(1);

// The Mono<String> is assembled in thread main.
Mono.fromSupplier(() -> {
log.info("fromSupplier");
return "hello";
})
.map(msg -> {
log.info("map");
return msg + " world";
})
.doOnTerminate(countDownLatch::countDown)
// However, the subscribeOn switches the whole sequence on a Thread picked from Scheduler.
.subscribeOn(Schedulers.newSingle("subscribeOn"))
.subscribe(log::info);

countDownLatch.await();

输出结果:

1
2
3
21:31:52.563 [subscribeOn-1] INFO FluxTest - fromSupplier
21:31:52.563 [subscribeOn-1] INFO FluxTest - map
21:31:52.563 [subscribeOn-1] INFO FluxTest - hello world

例子三

演示 publishOn 如何影响其后续操作符的执行线程。

publishOn takes signals from upstream and replays them downstream while executing the callback on a worker from the associated Scheduler. Consequently, it affects where the subsequent operators execute (until another publishOn is chained in), as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CountDownLatch countDownLatch = new CountDownLatch(1);

// 1、The Mono<String> is assembled in thread main.
Mono.fromSupplier(() -> {
log.info("fromSupplier");
return "hello";
})
.map(msg -> {
log.info("first map");
return msg + " world";
})
// 3、The publishOn affects where the subsequent operators execute.
.publishOn(Schedulers.newSingle("publishOn"))
.map(msg -> {
log.info("second map");
return msg + " again";
})
.doOnTerminate(countDownLatch::countDown)
// 2、However, the subscribeOn switches the whole sequence on a Thread picked from Scheduler.
.subscribeOn(Schedulers.newSingle("subscribeOn"))
.subscribe(log::info);

countDownLatch.await();

输出结果:

1
2
3
4
21:32:36.975 [subscribeOn-1] INFO FluxTest - fromSupplier
21:32:36.976 [subscribeOn-1] INFO FluxTest - first map
21:32:36.977 [publishOn-2] INFO FluxTest - second map
21:32:36.977 [publishOn-2] INFO FluxTest - hello world again

如何包装同步阻塞调用?

How Do I Wrap a Synchronous, Blocking Call?

参考

4.5. Threading and Schedulers

8. Exposing Reactor metrics

Appendix C.1. How Do I Wrap a Synchronous, Blocking Call?

Appendix C.6. How Do I Ensure Thread Affinity when I Use publishOn()?

https://www.woolha.com/tutorials/project-reactor-publishon-vs-subscribeon-difference