Dunwu Blog

大道至简,知易行难

Java 和 JSON 序列化

JSON(JavaScript Object Notation)是一种基于文本的数据交换格式。几乎所有的编程语言都有很好的库或第三方工具来提供基于 JSON 的 API 支持,因此你可以非常方便地使用任何自己喜欢的编程语言来处理 JSON 数据。

本文主要从 Java 语言的角度来讲解 JSON 的应用。

JSON 简介

JSON 是什么

JSON 起源于 1999 年的 JS 语言规范 ECMA262 的一个子集(即 15.12 章节描述了格式与解析),后来 2003 年作为一个数据格式ECMA404(很囧的序号有不有?)发布。
2006 年,作为 rfc4627 发布,这时规范增加到 18 页,去掉没用的部分,十页不到。

JSON 的应用很广泛,这里有超过 100 种语言下的 JSON 库:json.org

更多的可以参考这里,关于 json 的一切

JSON 标准

这估计是最简单标准规范之一:

  • 只有两种结构:对象内的键值对集合结构和数组,对象用 {} 表示、内部是 "key":"value",数组用 [] 表示,不同值用逗号分开
  • 基本数值有 7 个: false / null / true / object / array / number / string
  • 再加上结构可以嵌套,进而可以用来表达复杂的数据
  • 一个简单实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Image": {
"Width": 800,
"Height": 600,
"Title": "View from 15th Floor",
"Thumbnail": {
"Url": "http://www.example.com/image/481989943",
"Height": 125,
"Width": "100"
},
"IDs": [116, 943, 234, 38793]
}
}

扩展阅读:

JSON 优缺点

优点:

  • 基于纯文本,所以对于人类阅读是很友好的。
  • 规范简单,所以容易处理,开箱即用,特别是 JS 类的 ECMA 脚本里是内建支持的,可以直接作为对象使用。
  • 平台无关性,因为类型和结构都是平台无关的,而且好处理,容易实现不同语言的处理类库,可以作为多个不同异构系统之间的数据传输格式协议,特别是在 HTTP/REST 下的数据格式。

缺点:

  • 性能一般,文本表示的数据一般来说比二进制大得多,在数据传输上和解析处理上都要更影响性能。
  • 缺乏 schema,跟同是文本数据格式的 XML 比,在类型的严格性和丰富性上要差很多。XML 可以借由 XSD 或 DTD 来定义复杂的格式,并由此来验证 XML 文档是否符合格式要求,甚至进一步的,可以基于 XSD 来生成具体语言的操作代码,例如 apache xmlbeans。并且这些工具组合到一起,形成一套庞大的生态,例如基于 XML 可以实现 SOAP 和 WSDL,一系列的 ws-*规范。但是我们也可以看到 JSON 在缺乏规范的情况下,实际上有更大一些的灵活性,特别是近年来 REST 的快速发展,已经有一些 schema 相关的发展(例如理解 JSON Schema使用 JSON Schema在线 schema 测试),也有类似于 WSDL 的WADL出现。

JSON 工具

Java JSON 库

Java 中比较流行的 JSON 库有:

  • Fastjson - 阿里巴巴开发的 JSON 库,性能十分优秀。
  • Jackson - 社区十分活跃且更新速度很快。Spring 框架默认 JSON 库。
  • Gson - 谷歌开发的 JSON 库,目前功能最全的 JSON 库 。

从性能上来看,一般情况下:Fastjson > Jackson > Gson

JSON 编码指南

遵循好的设计与编码风格,能提前解决 80%的问题,个人推荐 Google JSON 风格指南。

简单摘录如下:

  • 属性名和值都是用双引号,不要把注释写到对象里面,对象数据要简洁
  • 不要随意结构化分组对象,推荐是用扁平化方式,层次不要太复杂
  • 命名方式要有意义,比如单复数表示
  • 驼峰式命名,遵循 Bean 规范
  • 使用版本来控制变更冲突
  • 对于一些关键字,不要拿来做 key
  • 如果一个属性是可选的或者包含空值或 null 值,考虑从 JSON 中去掉该属性,除非它的存在有很强的语义原因
  • 序列化枚举类型时,使用 name 而不是 value
  • 日期要用标准格式处理
  • 设计好通用的分页参数
  • 设计好异常处理

JSON API与 Google JSON 风格指南有很多可以相互参照之处。

JSON API是数据交互规范,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。

JSON API 设计用来最小化请求的数量,以及客户端与服务器间传输的数据量。在高效实现的同时,无需牺牲可读性、灵活性和可发现性。

Fastjson 应用

添加 maven 依赖

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

Fastjson API

定义 Bean

Group.java

1
2
3
4
5
6
public class Group {

private Long id;
private String name;
private List<User> users = new ArrayList<User>();
}

User.java

1
2
3
4
5
public class User {

private Long id;
private String name;
}

初始化 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Group group = new Group();
group.setId(0L);
group.setName("admin");

User guestUser = new User();
guestUser.setId(2L);
guestUser.setName("guest");

User rootUser = new User();
rootUser.setId(3L);
rootUser.setName("root");

group.addUser(guestUser);
group.addUser(rootUser);

序列化

1
2
String jsonString = JSON.toJSONString(group);
System.out.println(jsonString);

反序列化

1
Group bean = JSON.parseObject(jsonString, Group.class);

Fastjson 注解

@JSONField

扩展阅读:更多 API 使用细节可以参考:JSONField 用法,这里介绍基本用法。

可以配置在属性(setter、getter)和字段(必须是 public field)上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@JSONField(name="ID")
public int getId() {return id;}

// 配置date序列化和反序列使用yyyyMMdd日期格式
@JSONField(format="yyyyMMdd")
public Date date1;

// 不序列化
@JSONField(serialize=false)
public Date date2;

// 不反序列化
@JSONField(deserialize=false)
public Date date3;

// 按ordinal排序
@JSONField(ordinal = 2)
private int f1;

@JSONField(ordinal = 1)
private int f2;

@JSONType

JSONType.alphabetic 属性: fastjson 缺省时会使用字母序序列化,如果你是希望按照 java fields/getters 的自然顺序序列化,可以配置 JSONType.alphabetic,使用方法如下:

1
2
3
4
5
6
@JSONType(alphabetic = false)
public static class B {
public int f2;
public int f1;
public int f0;
}

Jackson 应用

扩展阅读:更多 API 使用细节可以参考 jackson-databind 官方说明

添加 maven 依赖

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>

Jackson API

序列化

1
2
3
4
5
6
7
ObjectMapper mapper = new ObjectMapper();

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

反序列化

1
2
3
4
5
6
7
ObjectMapper mapper = new ObjectMapper();

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

容器的序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Person p = new Person("Tom", 20);
Person p2 = new Person("Jack", 22);
Person p3 = new Person("Mary", 18);

List<Person> persons = new LinkedList<>();
persons.add(p);
persons.add(p2);
persons.add(p3);

Map<String, List> map = new HashMap<>();
map.put("persons", persons);

String json = null;
try {
json = mapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}

Jackson 注解

扩展阅读:更多注解使用细节可以参考 jackson-annotations 官方说明

@JsonProperty

1
2
3
4
5
6
7
8
9
10
11
public class MyBean {
private String _name;

// without annotation, we'd get "theName", but we want "name":
@JsonProperty("name")
public String getTheName() { return _name; }

// note: it is enough to add annotation on just getter OR setter;
// so we can omit it here
public void setTheName(String n) { _name = n; }
}

@JsonIgnoreProperties@JsonIgnore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// means that if we see "foo" or "bar" in JSON, they will be quietly skipped
// regardless of whether POJO has such properties
@JsonIgnoreProperties({ "foo", "bar" })
public class MyBean {
// will not be written as JSON; nor assigned from JSON:
@JsonIgnore
public String internal;

// no annotation, public field is read/written normally
public String external;

@JsonIgnore
public void setCode(int c) { _code = c; }

// note: will also be ignored because setter has annotation!
public int getCode() { return _code; }
}

@JsonCreator

1
2
3
4
5
6
7
8
9
10
11
12
public class CtorBean {
public final String name;
public final int age;

@JsonCreator // constructor can be public, private, whatever
private CtorBean(@JsonProperty("name") String name,
@JsonProperty("age") int age)
{
this.name = name;
this.age = age;
}
}

@JsonPropertyOrder

alphabetic 设为 true 表示,json 字段按自然顺序排列,默认为 false。

1
2
@JsonPropertyOrder(alphabetic = true)
public class JacksonAnnotationBean {}

Gson 应用

详细内容可以参考官方文档:Gson 用户指南

添加 maven 依赖

1
2
3
4
5
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>

Gson API

序列化

1
2
3
4
5
6
Gson gson = new Gson();
gson.toJson(1); // ==> 1
gson.toJson("abcd"); // ==> "abcd"
gson.toJson(10L); // ==> 10
int[] values = { 1 };
gson.toJson(values); // ==> [1]

反序列化

1
2
3
4
5
6
int i1 = gson.fromJson("1", int.class);
Integer i2 = gson.fromJson("1", Integer.class);
Long l1 = gson.fromJson("1", Long.class);
Boolean b1 = gson.fromJson("false", Boolean.class);
String str = gson.fromJson("\"abc\"", String.class);
String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class);

GsonBuilder

Gson 实例可以通过 GsonBuilder 来定制实例化,以控制其序列化、反序列化行为。

1
2
3
4
5
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE)
.create();

Gson 注解

@Since

@Since 用于控制对象的序列化版本。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class VersionedClass {
@Since(1.1) private final String newerField;
@Since(1.0) private final String newField;
private final String field;

public VersionedClass() {
this.newerField = "newer";
this.newField = "new";
this.field = "old";
}
}

VersionedClass versionedObject = new VersionedClass();
Gson gson = new GsonBuilder().setVersion(1.0).create();
String jsonOutput = gson.toJson(versionedObject);
System.out.println(jsonOutput);
System.out.println();

gson = new Gson();
jsonOutput = gson.toJson(versionedObject);
System.out.println(jsonOutput);

@SerializedName

@SerializedName 用于将类成员按照指定名称序列化、反序列化。示例:

1
2
3
4
5
6
7
8
9
private class SomeObject {
@SerializedName("custom_naming") private final String someField;
private final String someOtherField;

public SomeObject(String a, String b) {
this.someField = a;
this.someOtherField = b;
}
}

示例源码

示例源码:javalib-io-json

参考资料

Java 二进制序列化

简介

为什么需要二进制序列化库

原因很简单,就是 Java 默认的序列化机制(ObjectInputStreamObjectOutputStream)具有很多缺点。

不了解 Java 默认的序列化机制,可以参考:Java 序列化

Java 自身的序列化方式具有以下缺点:

  • 无法跨语言使用。这点最为致命,对于很多需要跨语言通信的异构系统来说,不能跨语言序列化,即意味着完全无法通信(彼此数据不能识别,当然无法交互了)。
  • 序列化的性能不高。序列化后的数据体积较大,这大大影响存储和传输的效率。
  • 序列化一定需要实现 Serializable 接口。
  • 需要关注 serialVersionUID

引入二进制序列化库就是为了解决这些问题,这在 RPC 应用中尤为常见。

主流序列化库简介

Protobuf

Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储
格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf
使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL
编译器,生成序列化工具类。

优点:

  • 序列化后体积相比 JSON、Hessian 小很多
  • 序列化反序列化速度很快,不需要通过反射获取类型
  • 语言和平台无关(基于 IDL),IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器
  • 消息格式升级和兼容性不错,可以做到后向兼容
  • 支持 Java, C++, Python 三种语言

缺点:

  • Protobuf 对于具有反射和动态能力的语言来说,用起来很费劲。

Thrift

Thrift 是 apache 开源项目,是一个点对点的 RPC 实现。

它具有以下特性:

  • 支持多种语言(目前支持 28 种语言,如:C++、go、Java、Php、Python、Ruby 等等)。
  • 使用了组建大型数据交换及存储工具,对于大型系统中的内部数据传输,相对于 Json 和 xml 在性能上和传输大小上都有明显的优势。
  • 支持三种比较典型的编码方式(通用二进制编码,压缩二进制编码,优化的可选字段压缩编解码)。

Hessian

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协
议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节
数也更小。

RPC 框架 Dubbo 就支持 Thrift 和 Hession。

它具有以下特性:

  • 支持多种语言。如:Java、Python、C++、C#、PHP、Ruby 等。
  • 相对其他二进制序列化库较慢。

Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持:

  • Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复;
  • Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
  • Byte/Short 反序列化的时候变成 Integer。

Kryo

Kryo 是用于 Java 的快速高效的二进制对象图序列化框架。Kryo 还可以执行自动的深拷贝和浅拷贝。 这是从对象到对象的直接复制,而不是从对象到字节的复制。

它具有以下特性:

  • 速度快,序列化体积小
  • 官方不支持 Java 以外的其他语言

FST

FST 是一个 Java 实现二进制序列化库。

它具有以下特性:

  • 近乎于 100% 兼容 JDK 序列化,且比 JDK 原序列化方式快 10 倍
  • 2.17 开始与 Android 兼容
  • (可选)2.29 开始支持将任何可序列化的对象图编码/解码为 JSON(包括共享引用)

小结

了解了以上这些常见的二进制序列化库的特性。在技术选型时,我们就可以做到有的放矢。

(1)选型参考依据

对于二进制序列化库,我们的选型考量一般有以下几点:

  • 是否支持跨语言
    • 根据业务实际需求来决定。一般来说,支持跨语言,为了兼容,使用复杂度上一般会更高一些。
  • 序列化、反序列化的性能
  • 类库是否轻量化,API 是否简单易懂

(2)选型建议

  • 如果需要跨语言通信,那么可以考虑:Protobuf、Thrift、Hession。

    • thriftprotobuf - 适用于对性能敏感,对开发体验要求不高的内部系统。
    • hessian - 适用于对开发体验敏感,性能有要求的内外部系统。
  • 如果不需要跨语言通信,可以考虑:KryoFST,性能不错,且 API 十分简单。

FST 应用

引入依赖

1
2
3
4
5
<dependency>
<groupId>de.ruedigermoeller</groupId>
<artifactId>fst</artifactId>
<version>2.56</version>
</dependency>

FST API

示例:

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

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class FstDemo {

private static FSTConfiguration DEFAULT_CONFIG = FSTConfiguration.createDefaultConfiguration();

/**
* 将对象序列化为 byte 数组
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的 byte 数组
*/
public static <T> byte[] writeToBytes(T obj) {
return DEFAULT_CONFIG.asByteArray(obj);
}

/**
* 将对象序列化为 byte 数组后,再使用 Base64 编码
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的字符串
*/
public static <T> String writeToString(T obj) {
byte[] bytes = writeToBytes(obj);
return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
}

/**
* 将 byte 数组反序列化为原对象
*
* @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组
* @param clazz 原对象的类型
* @param <T> 原对象的类型
* @return 原对象
*/
public static <T> T readFromBytes(byte[] bytes, Class<T> clazz) throws IOException {
Object obj = DEFAULT_CONFIG.asObject(bytes);
if (clazz.isInstance(obj)) {
return (T) obj;
} else {
throw new IOException("derialize failed");
}
}

/**
* 将字符串反序列化为原对象,先使用 Base64 解码
*
* @param str {@link #writeToString} 方法序列化后的字符串
* @param clazz 原对象的类型
* @param <T> 原对象的类型
* @return 原对象
*/
public static <T> T readFromString(String str, Class<T> clazz) throws IOException {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
return readFromBytes(Base64.getDecoder().decode(bytes), clazz);
}

}

测试:

1
2
3
4
5
6
7
8
long begin = System.currentTimeMillis();
for (int i = 0; i < BATCH_SIZE; i++) {
TestBean oldBean = BeanUtils.initJdk8Bean();
byte[] bytes = FstDemo.writeToBytes(oldBean);
TestBean newBean = FstDemo.readFromBytes(bytes, TestBean.class);
}
long end = System.currentTimeMillis();
System.out.printf("FST 序列化/反序列化耗时:%s", (end - begin));

Kryo 应用

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.0.0-RC4</version>
</dependency>

Kryo API

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class KryoDemo {

// 每个线程的 Kryo 实例
private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();

/**
* 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化,
* 上线的同时就必须清除 Redis 里的所有缓存,
* 否则那些缓存再回来反序列化的时候,就会报错
*/
//支持对象循环引用(否则会栈溢出)
kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置

//不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册)
kryo.setRegistrationRequired(false); //默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置

//Fix the NPE bug when deserializing Collections.
((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
.setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());

return kryo;
});

/**
* 获得当前线程的 Kryo 实例
*
* @return 当前线程的 Kryo 实例
*/
public static Kryo getInstance() {
return kryoLocal.get();
}

/**
* 将对象序列化为 byte 数组
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的 byte 数组
*/
public static <T> byte[] writeToBytes(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);

Kryo kryo = getInstance();
kryo.writeObject(output, obj);
output.flush();

return byteArrayOutputStream.toByteArray();
}

/**
* 将对象序列化为 byte 数组后,再使用 Base64 编码
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的字符串
*/
public static <T> String writeToString(T obj) {
byte[] bytes = writeToBytes(obj);
return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
}

/**
* 将 byte 数组反序列化为原对象
*
* @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组
* @param clazz 原对象的类型
* @param <T> 原对象的类型
* @return 原对象
*/
@SuppressWarnings("unchecked")
public static <T> T readFromBytes(byte[] bytes, Class<T> clazz) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream);

Kryo kryo = getInstance();
return (T) kryo.readObject(input, clazz);
}

/**
* 将字符串反序列化为原对象,先使用 Base64 解码
*
* @param str {@link #writeToString} 方法序列化后的字符串
* @param clazz 原对象的类型
* @param <T> 原对象的类型
* @return 原对象
*/
public static <T> T readFromString(String str, Class<T> clazz) {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
return readFromBytes(Base64.getDecoder().decode(bytes), clazz);
}

}

测试:

1
2
3
4
5
6
7
8
long begin = System.currentTimeMillis();
for (int i = 0; i < BATCH_SIZE; i++) {
TestBean oldBean = BeanUtils.initJdk8Bean();
byte[] bytes = KryoDemo.writeToBytes(oldBean);
TestBean newBean = KryoDemo.readFromBytes(bytes, TestBean.class);
}
long end = System.currentTimeMillis();
System.out.printf("Kryo 序列化/反序列化耗时:%s", (end - begin));

Hessian 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Student student = new Student();
student.setNo(101);
student.setName("HESSIAN");
//把student对象转化为byte数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bos);
output.writeObject(student);
output.flushBuffer();
byte[] data = bos.toByteArray();
bos.close();
//把刚才序列化出来的byte数组转化为student对象
ByteArrayInputStream bis = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(bis);
Student deStudent = (Student) input.readObject();
input.close();
System.out.println(deStudent);

参考资料

Lombok 快速入门

Lombok 简介

Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 hashCode()equals()getter / setter 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。

Lombok 安装

由于 Lombok 仅在编译阶段生成代码,所以使用 Lombok 注解的源代码,在 IDE 中会被高亮显示错误,针对这个问题可以通过安装 IDE 对应的插件来解决。具体的安装方式可以参考:Setting up Lombok with Eclipse and Intellij

使 IntelliJ IDEA 支持 Lombok 方式如下:

  • Intellij 设置支持注解处理
    • 点击 File > Settings > Build > Annotation Processors
    • 勾选 Enable annotation processing
  • 安装插件
    • 点击 Settings > Plugins > Browse repositories
    • 查找 Lombok Plugin 并进行安装
    • 重启 IntelliJ IDEA
  • 将 lombok 添加到 pom 文件
1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
</dependency>

Lombok 使用

Lombok 提供注解 API 来修饰指定的类:

@Getter and @Setter

@Getter and @Setter Lombok 代码:

1
2
@Getter @Setter private boolean employed = true;
@Setter(AccessLevel.PROTECTED) private String name;

等价于 Java 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean employed = true;
private String name;

public boolean isEmployed() {
return employed;
}

public void setEmployed(final boolean employed) {
this.employed = employed;
}

protected void setName(final String name) {
this.name = name;
}

@NonNull

@NonNull Lombok 代码:

1
2
@Getter @Setter @NonNull
private List<Person> members;

等价于 Java 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@NonNull
private List<Person> members;

public Family(@NonNull final List<Person> members) {
if (members == null) throw new java.lang.NullPointerException("members");
this.members = members;
}

@NonNull
public List<Person> getMembers() {
return members;
}

public void setMembers(@NonNull final List<Person> members) {
if (members == null) throw new java.lang.NullPointerException("members");
this.members = members;
}

@ToString

@ToString Lombok 代码:

1
2
3
4
5
6
@ToString(callSuper=true,exclude="someExcludedField")
public class Foo extends Bar {
private boolean someBoolean = true;
private String someStringField;
private float someExcludedField;
}

等价于 Java 源码:

1
2
3
4
5
6
7
8
9
10
11
12
public class Foo extends Bar {
private boolean someBoolean = true;
private String someStringField;
private float someExcludedField;

@java.lang.Override
public java.lang.String toString() {
return "Foo(super=" + super.toString() +
", someBoolean=" + someBoolean +
", someStringField=" + someStringField + ")";
}
}

@EqualsAndHashCode

@EqualsAndHashCode Lombok 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@EqualsAndHashCode(callSuper=true,exclude={"address","city","state","zip"})
public class Person extends SentientBeing {
enum Gender { Male, Female }

@NonNull private String name;
@NonNull private Gender gender;

private String ssn;
private String address;
private String city;
private String state;
private String zip;
}

等价于 Java 源码:

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
public class Person extends SentientBeing {

enum Gender {
/*public static final*/ Male /* = new Gender() */,
/*public static final*/ Female /* = new Gender() */;
}
@NonNull
private String name;
@NonNull
private Gender gender;
private String ssn;
private String address;
private String city;
private String state;
private String zip;

@java.lang.Override
public boolean equals(final java.lang.Object o) {
if (o == this) return true;
if (o == null) return false;
if (o.getClass() != this.getClass()) return false;
if (!super.equals(o)) return false;
final Person other = (Person)o;
if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
if (this.gender == null ? other.gender != null : !this.gender.equals(other.gender)) return false;
if (this.ssn == null ? other.ssn != null : !this.ssn.equals(other.ssn)) return false;
return true;
}

@java.lang.Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = result * PRIME + super.hashCode();
result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
result = result * PRIME + (this.gender == null ? 0 : this.gender.hashCode());
result = result * PRIME + (this.ssn == null ? 0 : this.ssn.hashCode());
return result;
}
}

@Data

@Data Lombok 代码:

1
2
3
4
5
6
@Data(staticConstructor="of")
public class Company {
private final Person founder;
private String name;
private List<Person> employees;
}

等价于 Java 源码:

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
public class Company {
private final Person founder;
private String name;
private List<Person> employees;

private Company(final Person founder) {
this.founder = founder;
}

public static Company of(final Person founder) {
return new Company(founder);
}

public Person getFounder() {
return founder;
}

public String getName() {
return name;
}

public void setName(final String name) {
this.name = name;
}

public List<Person> getEmployees() {
return employees;
}

public void setEmployees(final List<Person> employees) {
this.employees = employees;
}

@java.lang.Override
public boolean equals(final java.lang.Object o) {
if (o == this) return true;
if (o == null) return false;
if (o.getClass() != this.getClass()) return false;
final Company other = (Company)o;
if (this.founder == null ? other.founder != null : !this.founder.equals(other.founder)) return false;
if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
if (this.employees == null ? other.employees != null : !this.employees.equals(other.employees)) return false;
return true;
}

@java.lang.Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = result * PRIME + (this.founder == null ? 0 : this.founder.hashCode());
result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode());
return result;
}

@java.lang.Override
public java.lang.String toString() {
return "Company(founder=" + founder + ", name=" + name + ", employees=" + employees + ")";
}
}

@Cleanup

@Cleanup Lombok 代码:

1
2
3
4
5
6
7
8
9
public void testCleanUp() {
try {
@Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(new byte[] {'Y','e','s'});
System.out.println(baos.toString());
} catch (IOException e) {
e.printStackTrace();
}
}

等价于 Java 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void testCleanUp() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(new byte[]{'Y', 'e', 's'});
System.out.println(baos.toString());
} finally {
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

@Synchronized

@Synchronized Lombok 代码:

1
2
3
4
5
6
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

@Synchronized
public String synchronizedFormat(Date date) {
return format.format(date);
}

等价于 Java 源码:

1
2
3
4
5
6
7
8
private final java.lang.Object $lock = new java.lang.Object[0];
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

public String synchronizedFormat(Date date) {
synchronized ($lock) {
return format.format(date);
}
}

@SneakyThrows

@SneakyThrows Lombok 代码:

1
2
3
4
@SneakyThrows
public void testSneakyThrows() {
throw new IllegalAccessException();
}

等价于 Java 源码:

1
2
3
4
5
6
7
public void testSneakyThrows() {
try {
throw new IllegalAccessException();
} catch (java.lang.Throwable $ex) {
throw lombok.Lombok.sneakyThrow($ex);
}
}

示例源码

示例源码:javalib-bean

Lombok 使用注意点

谨慎使用 @Builder

在类上标注了 @Data@Builder 注解的时候,编译时,lombok 优化后的 Class 中会没有默认的构造方法。在反序列化的时候,没有默认构造方法就可能会报错。

【示例】使用 @Builder 不当导致 json 反序列化失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@Builder
public class BuilderDemo01 {

private String name;

public static void main(String[] args) throws JsonProcessingException {
BuilderDemo01 demo01 = BuilderDemo01.builder().name("demo01").build();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(demo01);
BuilderDemo01 expectDemo01 = mapper.readValue(json, BuilderDemo01.class);
System.out.println(expectDemo01.toString());
}

}

运行时会抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `io.github.dunwu.javatech.bean.lombok.BuilderDemo01` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"name":"demo01"}"; line: 1, column: 2]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1432)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1062)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3182)
at io.github.dunwu.javatech.bean.lombok.BuilderDemo01.main(BuilderDemo01.java:22)

【示例】使用 @Builder 正确方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BuilderDemo02 {

private String name;

public static void main(String[] args) throws JsonProcessingException {
BuilderDemo02 demo02 = BuilderDemo02.builder().name("demo01").build();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(demo02);
BuilderDemo02 expectDemo02 = mapper.readValue(json, BuilderDemo02.class);
System.out.println(expectDemo02.toString());
}

}

@Data 注解和继承

使用 @Data 注解时,则有了 @EqualsAndHashCode 注解,那么就会在此类中存在 equals(Object other)hashCode() 方法,且不会使用父类的属性,这就导致了可能的问题。比如,有多个类有相同的部分属性,把它们定义到父类中,恰好 id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,这是因为:lombok 自动生成的 equals(Object other)hashCode() 方法判定为相等,从而导致和预期不符。

修复此问题的方法很简单:

  • 使用 @Data 时,加上 @EqualsAndHashCode(callSuper=true) 注解。
  • 使用 @Getter @Setter @ToString 代替 @Data 并且自定义 equals(Object other)hashCode() 方法。

【示例】测试 @Data@EqualsAndHashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Data
@ToString(exclude = "age")
@EqualsAndHashCode(exclude = { "age", "sex" })
public class Person {

protected String name;

protected Integer age;

protected String sex;

}

@Data
@EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" })
public class EqualsAndHashCodeDemo extends Person {

@NonNull
private String name;

@NonNull
private Gender gender;

private String ssn;

private String address;

private String city;

private String state;

private String zip;

public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender) {
this.name = name;
this.gender = gender;
}

public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender,
String ssn, String address, String city, String state, String zip) {
this.name = name;
this.gender = gender;
this.ssn = ssn;
this.address = address;
this.city = city;
this.state = state;
this.zip = zip;
}

public enum Gender {
Male,
Female
}

}

@Test
@DisplayName("测试 @EqualsAndHashCode")
public void testEqualsAndHashCodeDemo() {
EqualsAndHashCodeDemo demo1 =
new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "xxx", "xxx", "xxx", "xxx");
EqualsAndHashCodeDemo demo2 =
new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "ooo", "ooo", "ooo", "ooo");
Assertions.assertEquals(demo1, demo2);

Person person = new Person();
person.setName("张三");
person.setAge(20);
person.setSex("男");

Person person2 = new Person();
person2.setName("张三");
person2.setAge(18);
person2.setSex("男");

Person person3 = new Person();
person3.setName("李四");
person3.setAge(20);
person3.setSex("男");

Assertions.assertEquals(person2, person);
Assertions.assertNotEquals(person3, person);
}

上面的单元测试可以通过,但如果将 @EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" }) 注掉就会报错。

参考资料

Dozer 快速入门

这篇文章是本人在阅读 Dozer 官方文档(5.5.1 版本,官网已经一年多没更新了)的过程中,整理下来我认为比较基础的应用场景。

本文中提到的例子应该能覆盖 JavaBean 映射的大部分场景,希望对你有所帮助。

简介

Dozer 是什么?

Dozer 是一个 JavaBean 映射工具库。

它支持简单的属性映射,复杂类型映射,双向映射,隐式显式的映射,以及递归映射。

它支持三种映射方式:注解、API、XML。

它是开源的,遵从Apache 2.0 协议

安装

引入 jar 包

maven 方式

如果你的项目使用 maven,添加以下依赖到你的 pom.xml 即可:

1
2
3
4
5
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.4.0</version>
</dependency>

非 maven 方式

如果你的项目不使用 maven,那就只能发扬不怕苦不怕累的精神了。

使用 Dozer 需要引入 Dozer 的 jar 包以及其依赖的第三方 jar 包。

Eclipse 插件

Dozer 有插件可以在 Eclipse 中使用(不知道是否好用,反正我没用过)

插件地址: http://dozer.sourceforge.net/eclipse-plugin

使用

将 Dozer 引入到工程中后,我们就可以来小试一番了。

实践出真知,先以一个最简单的例子来展示 Dozer 映射的处理过程。

准备

我们先准备两个要互相映射的类

NotSameAttributeA.java

1
2
3
4
5
6
7
public class NotSameAttributeA {
private long id;
private String name;
private Date date;

// 省略getter/setter
}

NotSameAttributeB.java

1
2
3
4
5
6
7
public class NotSameAttributeB {
private long id;
private String value;
private Date date;

// 省略getter/setter
}

这两个类存在属性名不完全相同的情况:name 和 value。

Dozer 的配置

为什么要有映射配置?

如果要映射的两个对象有完全相同的属性名,那么一切都很简单。

只需要直接使用 Dozer 的 API 即可:

1
2
3
Mapper mapper = new DozerBeanMapper();
DestinationObject destObject =
mapper.map(sourceObject, DestinationObject.class);

但实际映射时,往往存在属性名不同的情况。

所以,你需要一些配置来告诉 Dozer 应该转换什么,怎么转换。

注:官网着重建议:在现实应用中,最好不要每次映射对象时都创建一个Mapper实例来工作,这样会产生不必要的开销。如果你不使用 IoC 容器(如:spring)来管理你的项目,那么,最好将Mapper定义为单例模式。

映射配置文件

src/test/resources目录下添加dozer/dozer-mapping.xml文件。
<mapping>标签中允许你定义<class-a><class-b>,对应着相互映射的类。
<field>标签里定义要映射的特殊属性。需要注意<a><class-a>对应,<b><class-b>对应,聪明的你,猜也猜出来了吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozer.sourceforge.net
http://dozer.sourceforge.net/schema/beanmapping.xsd">
<mapping date-format="yyyy-MM-dd">
<class-a>org.zp.notes.spring.common.dozer.vo.NotSameAttributeA</class-a>
<class-b>org.zp.notes.spring.common.dozer.vo.NotSameAttributeB</class-b>
<field>
<a>name</a>
<b>value</b>
</field>
</mapping>
</mappings>

与 Spring 整合

配置 DozerBeanMapperFactoryBean

src/test/resources目录下添加spring/spring-dozer.xml文件。

Dozer 与 Spring 的整合很便利,你只需要声明一个DozerBeanMapperFactoryBean
将所有的 dozer 映射配置文件作为属性注入到mappingFiles
DozerBeanMapperFactoryBean会加载这些规则。

spring-dozer.xml 文件范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"
default-autowire="byName" default-lazy-init="false">

<bean id="mapper" class="org.dozer.spring.DozerBeanMapperFactoryBean">
<property name="mappingFiles">
<list>
<value>classpath*:dozer/dozer-mapping.xml</value>
</list>
</property>
</bean>
</beans>

自动装配

至此,万事具备,你只需要自动装配mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring/spring-dozer.xml"})
@TransactionConfiguration(defaultRollback = false)
public class DozerTest extends TestCase {
@Autowired
Mapper mapper;

@Test
public void testNotSameAttributeMapping() {
NotSameAttributeA src = new NotSameAttributeA();
src.setId(007);
src.setName("邦德");
src.setDate(new Date());

NotSameAttributeB desc = mapper.map(src, NotSameAttributeB.class);
Assert.assertNotNull(desc);
}
}

运行一下单元测试,绿灯通过。

Dozer 支持的数据类型转换

Dozer 可以自动做数据类型转换。当前,Dozer 支持以下数据类型转换(都是双向的)

  • Primitive to Primitive Wrapper

    原型(int、long 等)和原型包装类(Integer、Long)

  • Primitive to Custom Wrapper

    原型和定制的包装

  • Primitive Wrapper to Primitive Wrapper

    原型包装类和包装类

  • Primitive to Primitive

    原型和原型

  • Complex Type to Complex Type

    复杂类型和复杂类型

  • String to Primitive

    字符串和原型

  • String to Primitive Wrapper

    字符串和原型包装类

  • String to Complex Type if the Complex Type contains a String constructor

    字符串和有字符串构造器的复杂类型(类)

  • String to Map

    字符串和 Map

  • Collection to Collection

    集合和集合

  • Collection to Array

    集合和数组

  • Map to Complex Type

    Map 和复杂类型

  • Map to Custom Map Type

    Map 和定制 Map 类型

  • Enum to Enum

    枚举和枚举

  • Each of these can be mapped to one another: java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar, java.util.GregorianCalendar

    这些时间相关的常见类可以互换:java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar, java.util.GregorianCalendar

  • String to any of the supported Date/Calendar Objects.

    字符串和支持 Date/Calendar 的对象

  • Objects containing a toString() method that produces a long representing time in (ms) to any supported Date/Calendar object.

    如果一个对象的 toString()方法返回的是一个代表 long 型的时间数值(单位:ms),就可以和任何支持 Date/Calendar 的对象转换。

Dozer 的映射配置

在前面的简单例子中,我们体验了一把 Dozer 的映射流程。但是两个类进行映射,有很多复杂的情况,相应的,你也需要一些更复杂的配置。

Dozer 有三种映射配置方式:

  • 注解方式
  • API 方式
  • XML 方式

用注解来配置映射

Dozer 5.3.2版本开始支持注解方式配置映射(只有一个注解:@Mapping)。可以应对一些简单的映射处理,复杂的就玩不转了。

看一下@Mapping的声明就可以知道,这个注解只能用于元素和方法。

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Mapping {
String value() default "";
}

让我们来试试吧:

TargetBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SourceBean {

private Long id;

private String name;

@Mapping("binaryData")
private String data;

@Mapping("pk")
public Long getId() {
return this.id;
}

//其余getter/setter方法略
}

TargetBean.java

1
2
3
4
5
6
7
8
9
10
public class TargetBean {

private String pk;

private String name;

private String binaryData;

//getter/setter方法略
}

定义了两个相互映射的 Java 类,只需要在源类中用@Mapping标记和目标类中对应的属性就可以了。

1
2
3
4
5
6
7
8
9
10
@Test
public void testAnnotationMapping() {
SourceBean src = new SourceBean();
src.setId(7L);
src.setName("邦德");
src.setData("00000111");

TargetBean desc = mapper.map(src, TargetBean.class);
Assert.assertNotNull(desc);
}

测试一下,绿灯通过。

官方文档说,虽然当前版本(文档的版本对应 Dozer 5.5.1)仅支持@Mapping,但是在未来的发布版本会提供其他的注解功能,那就敬请期待吧(再次吐槽一下:一年多没更新了)。

用 API 来配置映射

个人觉得这种方式比较麻烦,不推荐,也不想多做介绍,就是这么任性。

用 XML 来配置映射

需要强调的是:如果两个类的所有属性都能很好的互转,可以你中有我,我中有你,不分彼此,那么就不要画蛇添足的在 xml 中去声明映射规则了。

属性名不同时的映射(Basic Property Mapping)

Dozer 会自动映射属性名相同的属性,所以不必添加在 xml 文件中。

1
2
3
4
<field>
<a>one</a>
<b>onePrime</b>
</field>

字符串和日期映射(String to Date Mapping)

字符串在和日期进行映射时,允许用户指定日期的格式。

格式的设置分为三个作用域级别:

属性级别

对当前属性有效(这个属性必须是日期字符串)

1
2
3
4
<field>
<a date-format="MM/dd/yyyy HH:mm:ss:SS">dateString</a>
<b>dateObject</b>
</field>

类级别

对这个类中的所有日期相关的属性有效

1
2
3
4
5
6
7
8
<mapping date-format="MM-dd-yyyy HH:mm:ss">
<class-a>org.dozer.vo.TestObject</class-a>
<class-b>org.dozer.vo.TestObjectPrime</class-b>
<field>
<a>dateString</a>
<b>dateObject</b>
</field>
</mapping>

全局级别

对整个文件中的所有日期相关的属性有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mappings>
<configuration>
<date-format>MM/dd/yyyy HH:mm</date-format>
</configuration>

<mapping wildcard="true">
<class-a>org.dozer.vo.TestObject</class-a>
<class-b>org.dozer.vo.TestObjectPrime</class-b>
<field>
<a>dateString</a>
<b>dateObject</b>
</field>
</mapping>
</mappings>

集合和数组映射(Collection and Array Mapping)

Dozer 可以自动处理以下类型的双向转换。

  • List to List
  • List to Array
  • Array to Array
  • Set to Set
  • Set to Array
  • Set to List

使用 hint

如果使用泛型或数组,没有必要使用 hint。

如果不使用泛型或数组。在处理集合或数组之间的转换时,你需要用hint指定目标列表的数据类型。

若你不指定hint,Dozer 将认为目标集合和源集合的类型是一致的。

使用 Hints 的范例:

1
2
3
4
5
<field>
<a>hintList</a>
<b>hintList</b>
<b-hint>org.dozer.vo.TheFirstSubClassPrime</b-hint>
</field>

累计映射和非累计映射(Cumulative vs. Non-Cumulative List Mapping)

如果你要转换的目标类已经初始化,你可以选择让 Dozer 添加或更新对象到你的集合中。

而这取决于relationship-type配置,默认是累计。

它的设置有作用域级别:

  • 全局级
1
2
3
4
5
<mappings>
<configuration>
<relationship-type>non-cumulative</relationship-type>
</configuration>
</mappings>
  • 类级别
1
2
3
4
5
<mappings>
<mapping relationship-type="non-cumulative">
<!-- 省略 -->
</mapping>
</mappings>
  • 属性级别
1
2
3
4
5
6
<field relationship-type="cumulative">
<a>hintList</a>
<b>hintList</b>
<a-hint>org.dozer.vo.TheFirstSubClass</a-hint>
<b-hint>org.dozer.vo.TheFirstSubClassPrime</b-hint>
</field>

移动孤儿(Removing Orphans)

这里的孤儿是指目标集合中存在,但是源集合中不存在的元素。

你可以使用remove-orphans开关来选择是否移除这样的元素。

1
2
3
4
<field remove-orphans="true">
<a>srcList</a>
<b>destList</b>
</field>

深度映射(Deep Mapping)

所谓深度映射,是指允许你指定属性的属性(比如一个类的属性本身也是一个类)。举例来说

Source.java

1
2
3
4
public class Source {
private long id;
private String info;
}

Dest.java

1
2
3
4
public class Dest {
private long id;
private Info info;
}
1
2
3
public class Info {
private String content;
}

映射规则

1
2
3
4
5
6
7
8
<mapping>
<class-a>org.zp.notes.spring.common.dozer.vo.Source</class-a>
<class-b>org.zp.notes.spring.common.dozer.vo.Dest</class-b>
<field>
<a>info</a>
<b>info.content</b>
</field>
</mapping>

排除属性(Excluding Fields)

就像任何团体都有捣乱分子,类之间转换时也有想要排除的因子。

如何在做类型转换时,自动排除一些属性,Dozer 提供了几种方法,这里只介绍一种比较通用的方法。

更多详情参考官网

field-exclude 可以排除不需要映射的属性。

1
2
3
4
<field-exclude>
<a>fieldToExclude</a>
<b>fieldToExclude</b>
</field-exclude>

单向映射(One-Way Mapping)

注:本文的映射方式,无特殊说明,都是双向映射的。

有的场景可能希望转换过程不可逆,即单向转换。

单向转换可以通过使用one-way来开启

类级别

1
2
3
4
5
6
7
8
<mapping type="one-way">
<class-a>org.dozer.vo.TestObjectFoo</class-a>
<class-b>org.dozer.vo.TestObjectFooPrime</class-b>
<field>
<a>oneFoo</a>
<b>oneFooPrime</b>
</field>
</mapping>

属性级别

1
2
3
4
5
6
7
8
9
10
11
12
<mapping>
<class-a>org.dozer.vo.TestObjectFoo2</class-a>
<class-b>org.dozer.vo.TestObjectFooPrime2</class-b>
<field type="one-way">
<a>oneFoo2</a>
<b>oneFooPrime2</b>
</field>

<field type="one-way">
<a>oneFoo3.prime</a>
<b>oneFooPrime3</b>
</field>

全局配置(Global Configuration)

全局配置用来设置全局的配置信息。此外,任何定制转换都是在这里定义的。

全局配置都是可选的。

  • <date-format>表示日期格式
  • <stop-on-errors>错误处理开关
  • <wildcard>通配符
  • <trim-strings>裁剪字符串开关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<configuration >

<date-format>MM/dd/yyyy HH:mm</date-format>
<stop-on-errors>true</stop-on-errors>
<wildcard>true</wildcard>
<trim-strings>false</trim-strings>

<custom-converters> <!-- these are always bi-directional -->
<converter type="org.dozer.converters.TestCustomConverter" >
<class-a>org.dozer.vo.TestCustomConverterObject</class-a>
<class-b>another.type.to.Associate</class-b>
</converter>

</custom-converters>
</configuration>

全局配置的作用是帮助你少配置一些参数,如果个别类的映射规则需要变更,你可以 mapping 中覆盖它。

覆盖的范例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mapping date-format="MM-dd-yyyy HH:mm:ss">
<!-- 省略 -->
</mapping>

<mapping wildcard="false">
<!-- 省略 -->
</mapping>

<mapping stop-on-errors="false">
<!-- 省略 -->
</mapping>

<mapping trim-strings="true">
<!-- 省略 -->
</mapping>

定制转换(Custom Converters)

如果 Dozer 默认的转换规则不能满足实际需要,你可以选择定制转换。

定制转换通过配置 XML 来告诉 Dozer 如何去转换两个指定的类。当 Dozer 转换这两个指定类的时候,会调用你的映射规则去替换标准映射规则。

为了让 Dozer 识别,你必须实现org.dozer.CustomConverter接口。否则,Dozer 会抛异常。

具体做法:

(1) 创建一个类实现org.dozer.CustomConverter接口。

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
public class TestCustomConverter implements CustomConverter {

public Object convert(Object destination, Object source,
Class destClass, Class sourceClass) {
if (source == null) {
return null;
}
CustomDoubleObject dest = null;
if (source instanceof Double) {
// check to see if the object already exists
if (destination == null) {
dest = new CustomDoubleObject();
} else {
dest = (CustomDoubleObject) destination;
}
dest.setTheDouble(((Double) source).doubleValue());
return dest;
} else if (source instanceof CustomDoubleObject) {
double sourceObj =
((CustomDoubleObject) source).getTheDouble();
return new Double(sourceObj);
} else {
throw new MappingException("Converter TestCustomConverter "
+ "used incorrectly. Arguments passed in were:"
+ destination + " and " + source);
}
}

(2) 在 xml 中引用定制的映射规则

引用定制的映射规则也是分级的,你可以酌情使用。

  • 全局级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozer.sourceforge.net
http://dozer.sourceforge.net/schema/beanmapping.xsd">
<configuration>
<!-- 总是双向转换的 -->
<custom-converters>
<converter type="org.dozer.converters.TestCustomConverter" >
<class-a>org.dozer.vo.CustomDoubleObject</class-a>
<class-b>java.lang.Double</class-b>
</converter>

<!-- You are responsible for mapping everything between
ClassA and ClassB -->
<converter
type="org.dozer.converters.TestCustomHashMapConverter" >
<class-a>org.dozer.vo.TestCustomConverterHashMapObject</class-a>
<class-b>org.dozer.vo.TestCustomConverterHashMapPrimeObject</class-b>
</converter>
</custom-converters>
</configuration>
</mappings>
  • 属性级
1
2
3
4
5
6
7
8
9
<mapping>
<class-a>org.dozer.vo.SimpleObj</class-a>
<class-b>org.dozer.vo.SimpleObjPrime2</class-b>
<field custom-converter=
"org.dozer.converters.TestCustomConverter">
<a>field1</a>
<b>field1Prime</b>
</field>
</mapping>

映射的继承(Inheritance Mapping)

Dozer 支持映射规则的继承机制。

属性如果有着相同的名字则不需要在 xml 中配置,除非使用了hint

我们来看一个例子

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
<mapping>
<class-a>org.dozer.vo.SuperClass</class-a>
<class-b>org.dozer.vo.SuperClassPrime</class-b>

<field>
<a>superAttribute</a>
<b>superAttr</b>
</field>
</mapping>

<mapping>
<class-a>org.dozer.vo.SubClass</class-a>
<class-b>org.dozer.vo.SubClassPrime</class-b>

<field>
<a>attribute</a>
<b>attributePrime</b>
</field>
</mapping>

<mapping>
<class-a>org.dozer.vo.SubClass2</class-a>
<class-b>org.dozer.vo.SubClassPrime2</class-b>

<field>
<a>attribute2</a>
<b>attributePrime2</b>
</field>
</mapping>

在上面的例子中 SubClass、SubClass2 是 SuperClass 的子类;

SubClassPrime 和 SubClassPrime2 是 SuperClassPrime 的子类。

superAttribute 和 superAttr 的映射规则会被子类所继承,所以不必再重复的在子类中去声明。

参考

Dozer 官方文档 | Dozer 源码地址

Freemark 快速入门

FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML 网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件。

Freemark 简介

Freemark 模板编写为 FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像 PHP 那样成熟的编程语言。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

img

这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML 设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。

Freemark 模板一句话概括就是:**_模板 + 数据模型 = 输出_**

总体结构

  • 文本:文本会照着原样来输出。
  • 插值:这部分的输出会被计算的值来替换。插值由 ${ and } 所分隔(或者 #{ and },这种风格已经不建议再使用了;点击查看更多)。
  • FTL 标签:FTL 标签和 HTML 标签很相似,但是它们却是给 FreeMarker 的指示, 而且不会打印在输出内容中。
  • 注释:注释和 HTML 的注释也很相似,但它们是由 <#---->来分隔的。注释会被 FreeMarker 直接忽略, 更不会在输出内容中显示。

img

🔔 注意:

  • FTL 是区分大小写的。
  • 插值 仅仅可以在 文本 中使用。
  • FTL 标签 不可以在其他 FTL 标签插值 中使用。
  • 注释 可以放在 FTL 标签插值 中。

指令

FTL 指令有两种类型: 预定义指令用户自定义指令。 对于用户自定义的指令使用 @ 来代替 #

🔔 注意:

  • FreeMarker 仅仅关心 FTL 标签的嵌套而不关心 HTML 标签的嵌套。 它只会把 HTML 看做是文本,不会来解释 HTML。
  • 如果你尝试使用一个不存在的指令(比如,输错了指令的名称), FreeMarker 就会拒绝执行模板,同时抛出错误信息。
  • FreeMarker 会忽略 FTL 标签中多余的 空白标记

表达式

以下为快速浏览清单,如果需要了解更多细节,请参考这里

变量

注意:变量 仅仅文本区 (比如 <h1>Hello ${name}!</h1>) 和 字符串 中起作用。

正确示例:

1
2
<#include "/footer/${company}.html">
<#if big>...</#if>

错误示例:

1
2
<#if ${big}>...</#if>
<#if "${big}">...</#if>

数据类型

Freemark 支持的类型有:

标量

字符串

1
2
3
${"Hello ${user}"}
${"I can escape with \\ ${user}"}
${r"Now I can read dollar signs $"}

输出:

1
2
3
Hello deister
I can escape with \ deister
Now I can read dollar signs $

数字

布尔值

日期/时间 (日期,时间或日期时间)

容器

  • 哈希表
  • 序列
  • 集合

子程序

其它

转义符

FTL 支持的所有转义字符:

转义序列 含义
\" 引号 (u0022)
\' 单引号(又称为撇号) (u0027)
\{ 起始花括号:{
\\ 反斜杠 (u005C)
\n 换行符 (u000A)
\r 回车 (u000D)
\t 水平制表符(又称为 tab) (u0009)
\b 退格 (u0008)
\f 换页 (u000C)
\l 小于号:<
\g 大于号:>
\a &符:&
\xCode 字符的 16 进制 Unicode 码 (UCS 码)

参考资料

Velocity 快速入门

Velocity (简称 VTL)是一个基于 Java 的模版引擎。它允许 web 页面设计者引用 JAVA 代码预定义的方法。Web 设计者可以根据 MVC 模式和 JAVA 程序员并行工作,这意味着 Web 设计者可以单独专注于设计良好的站点,而程序员则可单独专注于编写底层代码。Velocity 将 Java 代码从 web 页面中分离出来,使站点在长时间运行后仍然具有很好的可维护性,并提供了一个除 JSP 和 PHP 之外的可行的被选方案。

注释

单行注释以##开始,并在本行结束。

1
## This is a single line comment.

多行注释,以 # 开始并以 # 结束可以处理这种情况。

1
2
3
4
5
#*
Thus begins a multi-line comment. Online visitors won't
see this text because the Velocity Templating Engine will
ignore it.
*#

注释块 ,可以用来存储诸如文档作者、版本信息等。

1
2
3
4
5
6
7
8
#**
This is a VTL comment block and
may be used to store such information
as the document author and versioning
information:
@author
@version 5
*#

引用

VTL 中有三种类型的引用:变量,属性和方法。

变量

变量(Variables)的简略标记是有一个前导 $ 字符后跟一个 VTL 标识符(Identifier.)组成。一个 VTL 标识符必须以一个字母开始(a .. z 或 A .. Z)。

剩下的字符将由以下类型的字符组成:

  • 字母 (a .. z, A .. Z)
  • 数字 (0 .. 9)
  • 连字符(“-“)
  • 下划线 (“_“)

示例:有效变量

1
2
3
4
5
6
7
8
9
## 有效变量变量名
$foo
$mudSlinger
$mud-slinger
$mud_slinger
$mudSlinger1

## 给变量赋值
#set( $foo = "bar" )

属性

VTL 引用的第二种元素是属性,而属性具有独特的格式。属性的简略标记识前导符 $ 后跟一个 VTL 标识符,在后跟一个点号(“.”)最后又是一个 VTL 标识符。

示例:有效属性

1
2
$customer.Address
$purchase.Total

方法

方法在 JAVA 代码中定义,并作一些有用的事情,比如运行一个计算器或者作出一个决定。方法是实际上也是引用,由前导符 $ 后跟一个 VTL 标识符,后跟一个 VTL 方法体(Method Body)。 VTL 方法体由一个 VTL 标识符后跟一个左括号,再跟可选的参数列表,最后是右括号。

示例:有效方法

1
2
3
4
$customer.getAddress()
$purchase.getTotal()
$page.setTitle( "My Home Page" )
$person.setAttributes( ["Strange", "Weird", "Excited"] )

赋值

#set 指令用来为引用设置相应的值。值可以被值派给变量引用或者是属性引用,而且赋值要在括号里括起来。

1
2
3
4
5
6
#set( $monkey = $bill ) ## variable reference
#set( $monkey.Friend = "monica" ) ## string literal
#set( $monkey.Blame = $whitehouse.Leak ) ## property reference
#set( $monkey.Plan = $spindoctor.weave($web) ) ## method reference
#set( $monkey.Number = 123 ) ##number literal
#set( $monkey.Say = ["Not", $my, "fault"] ) ## ArrayList

字符串

使用 #set 指令时,括在双引号中的字面字符串将解析和重新解释 。 然而,当字面字符串括在单引号中时,不被解析:

示例:

1
2
3
4
#set( $foo = "bar" )
$foo
#set( $blargh = '$foo' )
$blargh

输出:

1
2
Bar
$foo

条件

VTL 使用 #If#elseif#else 指令做条件语句控制。

示例:

1
2
3
4
5
6
7
8
9
#if( $foo < 10 )
<strong>Go North</strong>
#elseif( $foo == 10 )
<strong>Go East</strong>
#elseif( $bar == 6 )
<strong>Go South</strong>
#else
<strong>Go West</strong>
#end

逻辑

VTL 支持与(&&)、或(||)、非(!)逻辑判断。

示例:

1
2
3
4
5
6
7
8
9
10
11
#if( $foo && $bar )
<strong> This AND that</strong>
#end

#if( $foo || $bar )
<strong>This or That</strong>
#end

#if( !$foo )
<strong>NOT that</strong>
#end

循环

VTL 通过 #foreach 支持循环

1
2
3
4
5
<ul>
#foreach( $product in $allProducts )
<li>$product</li>
#end
</ul>

包含

VTL 通过 #include 来导入其他文件。

示例:

1
2
3
4
5
#include( "one.txt" )

#include( "one.gif","two.txt","three.htm" )

#include( "greetings.txt", $seasonalstock )

解析

VTL 通过 #parse 导入其他 vm 文件。

1
2
3
4
5
6
7
$count
#set( $count = $count - 1 )
#if( $count > 0 )
#parse( "parsefoo.vm" )
#else
All done with parsefoo.vm!
#end

停止

VTL 使用 #stop 停止模板引擎的执行,并返回。这通常用作调试。

1
#stop ##

VTL 使用 #macro#end 配合来定义宏,以此实现自定义指令。

示例一:

1
2
3
4
5
6
7
## 定义宏
#macro( d )
<tr><td></td></tr>
#end

## 使用宏
#d()

示例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
## 定义宏
#macro( tablerows $color $somelist )
#foreach( $something in $somelist )
<tr><td bgcolor=$color>$something</td></tr>
#end
#end

## 使用宏
#set( $greatlakes = ["Superior","Michigan","Huron","Erie","Ontario"] )
#set( $color = "blue" )
<table>
#tablerows( $color $greatlakes )
</table>

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<table>
<tr>
<td bgcolor="blue">Superior</td>
</tr>
<tr>
<td bgcolor="blue">Michigan</td>
</tr>
<tr>
<td bgcolor="blue">Huron</td>
</tr>
<tr>
<td bgcolor="blue">Erie</td>
</tr>
<tr>
<td bgcolor="blue">Ontario</td>
</tr>
</table>

转义

VTL 使用 \ 符号来进行字符转义。

示例一

1
2
3
4
## The following line defines $email in this template:
#set( $email = "foo" )
$email
\$email

输出:

1
2
foo
$email

语义要点

Velocity 有一些语义要点,容易产生歧义,这里归纳一下。

(1)Velocity 的行为并不受空格的影响

示例:以下三种写法效果一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 写法一
Send me #set($foo = ["$10 and ","a cake"])#foreach($a in $foo)$a #end please.

## 写法二
Send me
#set( $foo = ["$10 and ","a cake"] )
#foreach( $a in $foo )
$a
#end
please.

## 写法三
Send me
#set($foo = ["$10 and ","a cake"])
#foreach ($a in $foo )$a
#end please.

参考资料

Thymeleaf 快速入门

标准方言

标准方言是指 Thymeleaf 定义了一组功能,这些功能应该足以满足大多数情况。可以识别这些标准方言在模板中的使用,因为它将包含以th前缀开头的属性,如<span th:text="...">

表达式

${...} : 变量表达式。

*{...} : 选择表达式。

#{...} : 消息 (i18n) 表达式。

@{...} : 链接 (URL) 表达式。

~{...} : 片段表达式。

变量表达式

变量表达式是 OGNL 表达式 - 如果将 Thymeleaf 与 Spring - 集成在上下文变量上(也称为 Spring 术语中的模型属性),则为 Spring EL。 它们看起来像这样:

1
${session.user.name}

它们作为属性值或作为它们的一部分,取决于属性:

1
<span th:text="${book.author.name}"></span>

上面的表达式与下面是相同的(在 OGNL 和 SpringEL 中):

1
((Book)context.getVariable("book")).getAuthor().getName()

但是不仅在涉及输出的场景中找到变量表达式,而且还可以使用更复杂的处理方式,如:条件,迭代…等等。

1
<li th:each="book : ${books}"></li>

这里${books}从上下文中选择名为books的变量,并在th:each中使用循环将其评估为迭代器。

选择表达式

选择表达式就像变量表达式一样,它们不是整个上下文变量映射上执行,而是在先前选择的对象。 它们看起来像这样:

1
*{customer.name}

它们所作用的对象由th:object属性指定:

1
2
3
4
5
<div th:object="${book}">
...
<span th:text="*{title}">...</span>
...
</div>

所以这相当于:

1
2
3
4
5
6
{
// th:object="${book}"
final Book selection = (Book) context.getVariable("book");
// th:text="*{title}"
output(selection.getTitle());
}

消息(i18n)表达式

消息表达式(通常称为文本外部化,国际化或 i18n)允许从外部源(如:.properties)文件中检索特定于语言环境的消息,通过键来引用这引用消息。

在 Spring 应用程序中,它将自动与 Spring 的 MessageSource 机制集成。如下 -

1
2
#{main.title}
#{message.entrycreated(${entryId})}

以下是在模板中使用它们的方式:

1
2
3
4
5
6
<table>
...
<th th:text="#{header.address.city}">...</th>
<th th:text="#{header.address.country}">...</th>
...
</table>

请注意,如果希望消息键由上下文变量的值确定,或者希望将变量指定为参数,则可以在消息表达式中使用变量表达式:

1
#{${config.adminWelcomeKey}(${session.user.name})} Jsp

链接(URL)表达式

链接表达式在构建 URL 并向其添加有用的上下文和会话信息(通常称为 URL 重写的过程)。
因此,对于部署在 Web 服务器的/myapp上下文中的 Web 应用程序,可以使用以下表达式:

1
<a th:href="@{/order/list}">...</a>

可以转换成如下的东西:

1
<a href="/myapp/order/list">...</a>

甚至,如果需要保持会话,并且 cookie 未启用(或者服务器还不知道),那么生成的格式为:

1
<a href="/myapp/order/list;jsessionid=s2ds3fa31abd241e2a01932">...</a> HTML

网址也可以带参数,如下所示:

1
<a th:href="@{/order/details(id=${orderId},type=${orderType})}">...</a>

这将产生类似以下的结果 -

1
2
<!-- 注意&符号会在标签属性中进行HTML转义... -->
<a href="/myapp/order/details?id=23&type=online">...</a>

链接表达式可以是相对的,在这种情况下,应用程序上下文将不会被加到 URL 的前面:

1
<a th:href="@{../documents/report}">...</a>

也是服务器相对的(同样,没有应用程序上下文的前缀):

1
<a th:href="@{~/contents/main}">...</a>

和协议相关(就像绝对 URL 一样,但浏览器将使用与正在显示的页面相同的 HTTP 或 HTTPS 协议):

1
<a th:href="@{//static.mycompany.com/res/initial}">...</a>

当然,链接表达式也可以是绝对的:

1
<a th:href="@{http://www.mycompany.com/main}">...</a>

但是绝对(或协议相对)URL ,在 Thymeleaf 链接表达式中应该添加什么值? 很简单:由响应过滤器定义 URL 重写:在基于 Servlet 的 Web 应用程序中,对于每个输出的 URL(上下文相对,相对,绝对…),在显示 URL 之前,Thymeleaf 总是调用HttpServletResponse.encodeUrl(...)机制。 这意味着一个过滤器可以通过包装 HttpServletResponse 对象来为应用程序执行自定义的 URL 重写。

片段表达式

片段表达式是一种简单的方法用来表示标记的片段并将其移动到模板中。 由于这些表达式,片段可以被复制,传递给其他模板的参数等等。

最常见的是使用th:insertth:replace来插入片段:

1
<div th:insert="~{commons :: main}">...</div>

但是它们可以在任何地方使用,就像任何其他变量一样:

1
2
3
<div th:with="frag=~{footer :: #main/text()}">
<p th:insert="${frag}"></p>
</div>

片段表达式可以有参数。

表达式预处理

关于表达式的最后一件事是知道表达式预处理,在__之间指定,如下所示:

1
#{selection.__${sel.code}__}

上面代码中,第一个被执行的变量表达式是:${sel.code},并且将使用它的结果作为表达式的一部分(假设${sel.code}的结果为:ALL),在此处执行国际化的情况下(这将查找与关键selection.ALL消息)。

文字和操作

有很多类型的文字和操作可用,它们分别如下:

  • 文字
    • 文本文字,例如:'one text', 'Another one!',
    • 数字文字,例如:0,10, 314, 31.01, 112.83,
    • 布尔文字,例如:true,false
    • Null 文字,例如:Null
    • 文字标记,例如:one, sometext, main,
  • 文本操作:
    • 字符串连接:+
    • 文字替换:|The name is ${name}|
  • 算术运算:
    • 二进制操作:+, -, *, /, %
    • 减号(一元运算符):-
  • 布尔运算:
    • 二进制运算符,and,or
    • 布尔否定(一元运算符):!,not
  • 比较和相等:
    • 比较运算符:>,<,>=,<=(gt,lt,ge,le)
    • 相等运算符:==, != (eq, ne)
  • 条件操作符:
    • If-then:(if) ? (then)
    • If-then-else:(if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)

基本属性

下面来看看标准方言中的几个最基本的属性。 从th:文本开始,它代替了标签的主体:

1
<p th:text="#{msg.welcome}">Welcome everyone!</p>

现在,th:each重复它所在元素的次数,由它的表达式返回的数组或列表所指定的次数,为迭代元素创建一个内部变量,其语法与 Java 的 foreach 表达式相同:

1
2
3
<li th:each="book : ${books}" th:text="${book.title}">
En las Orillas del Sar
</li>

最后,Thymeleaf 为特定的 XHTML 和 HTML5 属性提供了许多th属性,这些属性只评估它们的表达式,并将这些属性的值设置为结果。

1
2
3
4
<form th:action="@{/createOrder}">
<input type="button" th:value="#{form.submit}" />
<a th:href="@{/admin/users}"></a>
</form>

标准 URL

Thymeleaf 标准方言(称为 Standard 和 SpringStandard)提供了一种在 Web 应用程序中轻松创建 URL 的方法,以便它们包含任何所需的 URL 工件。 这是通过连接表达方式来完成的,这是一种类似于 Thymeleaf 标准的表现:@{...}

绝对网址

绝对 URL 用于创建到其他服务器的链接。它们需要指定一个协议名称(http://https://)开头。

1
<a th:href="@{https://www.yiibai.com/thymeleaf/}"></a>

上面链接不会被修改,除非在服务器上配置了 URL 重写过滤器,并在HttpServletResponse.encodeUrl(...)方法中执行修改。最后生成的 HTML 代码如下:

1
<a href="https://www.yiibai.com/thymeleaf/"></a>

上下文相关 URL

最常用的 URL 类型是上下文相关的。 这些 URL 是一旦安装在服务器上,就会与 Web 应用程序根相关联 URL。 例如,如果将一个名称为myapp.war的文件部署到一个 Tomcat 服务器中,那么应用程序一般是通过 URL:http://localhost:8080/myapp来访问,myapp就是上下文名称。

与上下文相关的 URL 以/字符开头:

1
<a th:href="@{/order/list}"></a>

如果应用程序访问 URL 为:http://localhost:8080/myapp,则此 URL 将输出:

1
<a href="/myapp/order/list"></a>

与服务器相关 URL

服务器相关的 URL 与上下文相关的 URL 非常相似,只是它们不假定 URL 要链接到应用程序上下文中的资源,因此允许链接到同一服务器中的不同上下文:

1
<a th:href="@{~/billing-app/showDetails.html}"></a>

当前应用程序的上下文将被忽略,因此尽管应用程序部署在http:// localhost:8080 / myapp,但该 URL 将输出:

1
<a href="/billing-app/showDetails.html"></a>

协议相关 URL

与协议相关的 URL 实际上是绝对的 URL,它将保持用于显示当前页面的协议(HTTP,HTTPS)。 它们通常用于包括样式,脚本等外部资源:

1
2
3
<script th:src="@{//scriptserver.example.net/myscript.js}">
...
</script>

它将呈现与上面一致的 URL(URL 重写除外),如:

1
2
3
<script src="//scriptserver.example.net/myscript.js">
...
</script>

添加参数

如何向使用@{...}表达式创建的 URL 添加参数? 这也很简单:

1
<a th:href="@{/order/details(id=3)}"></a>

上面示例代码,最终将输出为:

1
<a href="/order/details?id=3"></a>

也可以添加几个参数,用逗号分隔它们:

1
<a th:href="@{/order/details(id=3,action='show_all')}"></a>

上面代码将输出结果为:

1
2
<!-- 注意&符号在标签属性中进行HTML转义... -->
<a href="/order/details?id=3&action=show_all"></a>

还可以使用正常参数的路径变量的形式包含参数,但在 URL 的路径中指定一个占位符:

1
<a th:href="@{/order/{id}/details(id=3,action='show_all')}"></a>

上面输出结果为:

1
<a href="/order/3/details?action=show_all"></a>

网址片段标识符

片段标识符可以包含在 URL 中,包含参数和不包含参数。 它们将始终包含在网址的基础上,参考以下代码:

1
<a th:href="@{/home#all_info(action='show')}"></a>

执行输出结果如下 -

1
<a href="/home?action=show#all_info">

URL 重写

Thymeleaf 允许在应用程序中配置 URL 重写过滤器,它通过调用 Thymeleaf 模板生成的每个 URL 的 Servlet API 的javax.servlet.http.HttpServletResponse类中的response.encodeURL()方法来实现。

下面在 Java Web 应用程序中支持 URL 重写操作的标准方式,并允许 URL:

  • 自动检测用户是否启用了 Cookie,如果未启用或者如果它是第一个请求并且 cookie 配置仍未知。则将;jsessionid=...片段添加到 URL。
  • 在需要时自动将代理配置应用于 URL。
  • 使用不同的 CDN 设置,以便链接到分布在多个服务器中的内容。

URL 其它属性

不要以为在@{...}表达式中只有th:href属性来表示 URL 。 事实上,它们可以像变量表达式(${...})或消息外部化/国际化(#{...})一样用于任何地方。

例如,表单提交时,可使用以下写法 -

1
<form th:action="@{/order/processOrder}"></form>

或作为其他表达的一部分。 如下作为外部化/国际化字符串的参数:

1
2
3
<p
th:text="#{orders.explanation('3', @{/order/details(id=3,action='show_all')})}"
></p>

在 URL 中使用表达式

下面来看看,如下所示的 URL 表达式:

1
<a th:href="@{/order/details(id=3,action='show_all')}"></a>

3'show_all'都不能是文字值,因为只有在运行时才能知道它们的值,怎么办?

1
2
3
<a
th:href="@{/order/details(id=${order.id},action=(${user.admin} ? 'show_all' : 'show_public'))}"
></a>

下面看看另一个 URL 表达式,如下所示:

1
<a th:href="@{/order/details(id=${order.id})}"></a>

它其实是下面 URL 的一个快捷方式:

1
<a th:href="@{'/order/details'(id=${order.id})}"></a>

这意味着 URL 基本身可以被指定为一个表达式,例如一个变量表达式:

1
<a th:href="@{${detailsURL}(id=${order.id})}"></a>

或外部化/国际化的文本:

1
<a th:href="@{#{orders.details.localized_url}(id=${order.id})}"></a>

甚至可以使用复杂的表达式,包括条件表达式,例如:

1
2
3
<a
th:href="@{(${user.admin}? '/admin/home' : ${user.homeUrl})(id=${order.id})}"
></a>

如果要更清洁,那么可以使用th:with :

1
2
3
4
<a
th:with="baseUrl=(${user.admin}? '/admin/home' : ${user.homeUrl})"
th:href="@{${baseUrl}(id=${order.id})}"
></a>

又或者 -

1
2
3
4
5
<div th:with="baseUrl=(${user.admin}? '/admin/home' : ${user.homeUrl})">
...
<a th:href="@{${baseUrl}(id=${order.id})}">...</a>
...
</div>

扩展

TODO

参考资料

JUnit5 快速入门

JUnit5 简介

与以前的 JUnit 版本不同,JUnit 5 由来自三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform 是在 JVM 上启动测试框架的基础。它还定义了用于开发在平台上运行的测试框架的 TestEngine API。此外,该平台还提供了一个控制台启动器,用于从命令行启动平台,并提供 JUnit 平台套件引擎,用于使用平台上的一个或多个测试引擎运行自定义测试套件。

JUnit Jupiter 是编程模型和扩展模型的组合,用于在 JUnit 5 中编写测试和扩展。Jupiter 子项目提供了一个 测试引擎(TestEngine )用于在平台上运行基于 Jupiter 的测试。

JUnit Vintage 提供了一个测试引擎(TestEngine ),用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它要求 JUnit 4.12 或更高版本。

JUnit 5 在运行时需要 Java 8(或更高版本)。

JUnit5 安装

在 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
<properties>
<junit.jupiter.version>5.3.2</junit.jupiter.version>
</properties>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

组件间依赖关系:

img

JUnit5 注解

Annotation Description
@Test Denotes that a method is a test method. Unlike JUnit 4’s @Test annotation, this annotation does not declare any attributes, since test extensions in JUnit Jupiter operate based on their own dedicated annotations. Such methods are inherited unless they are overridden.
@ParameterizedTest Denotes that a method is a parameterized test. Such methods are inherited unless they are overridden.
@RepeatedTest Denotes that a method is a test template for a repeated test. Such methods are inherited unless they are overridden.
@TestFactory Denotes that a method is a test factory for dynamic tests. Such methods are inherited unless they are overridden.
@TestInstance Used to configure the test instance lifecycle for the annotated test class. Such annotations are inherited.
@TestTemplate Denotes that a method is a template for test cases designed to be invoked multiple times depending on the number of invocation contexts returned by the registered providers. Such methods are inherited unless they are overridden.
@DisplayName Declares a custom display name for the test class or test method. Such annotations are not inherited.
@BeforeEach Denotes that the annotated method should be executed before each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory method in the current class; analogous to JUnit 4’s @Before. Such methods are inherited unless they are overridden.
@AfterEach Denotes that the annotated method should be executed after each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory method in the current class; analogous to JUnit 4’s @After. Such methods are inherited unless they are overridden.
@BeforeAll Denotes that the annotated method should be executed before all @Test, @RepeatedTest, @ParameterizedTest, and @TestFactory methods in the current class; analogous to JUnit 4’s @BeforeClass. Such methods are inherited (unless they are hidden or overridden) and must be static (unless the “per-class” test instance lifecycle is used).
@AfterAll Denotes that the annotated method should be executed after all @Test, @RepeatedTest, @ParameterizedTest, and @TestFactory methods in the current class; analogous to JUnit 4’s @AfterClass. Such methods are inherited (unless they are hidden or overridden) and must be static (unless the “per-class” test instance lifecycle is used).
@Nested Denotes that the annotated class is a nested, non-static test class. @BeforeAll and @AfterAllmethods cannot be used directly in a @Nested test class unless the “per-class” test instance lifecycle is used. Such annotations are not inherited.
@Tag Used to declare tags for filtering tests, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level.
@Disabled Used to disable a test class or test method; analogous to JUnit 4’s @Ignore. Such annotations are not inherited.
@ExtendWith Used to register custom extensions. Such annotations are inherited.

JUnit5 示例

我将一部分官方示例放在了我的个人项目中,可以直接下载测试。

示例源码路径:https://github.com/dunwu/java-tutorial/tree/master/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5

基本的单元测试类和方法

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
import org.junit.jupiter.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class Junit5StandardTests {

private static final Logger LOGGER = LoggerFactory.getLogger(Junit5StandardTests.class);

@BeforeAll
static void beforeAll() {
LOGGER.info("call beforeAll()");
}

@BeforeEach
void beforeEach() {
LOGGER.info("call beforeEach()");
}

@Test
void succeedingTest() {
LOGGER.info("call succeedingTest()");
}

@Test
void failingTest() {
LOGGER.info("call failingTest()");
// fail("a failing test");
}

@Test
@Disabled("for demonstration purposes")
void skippedTest() {
LOGGER.info("call skippedTest()");
// not executed
}

@AfterEach
void afterEach() {
LOGGER.info("call afterEach()");
}

@AfterAll
static void afterAll() {
LOGGER.info("call afterAll()");
}
}

定制测试类和方法的显示名称

支持普通字符、特殊符号、emoji

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("A special test case")
class JunitDisplayNameDemo {

@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() { }

@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() { }

@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() { }
}

断言(Assertions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsDemo {

private static Person person;

@BeforeAll
public static void beforeAll() {
person = new Person("John", "Doe");
}

@Test
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "The optional assertion message is now the last parameter.");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}

@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and any
// failures will be reported together.
assertAll("person", () -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName()));
}

@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties", () -> {
String firstName = person.getFirstName();
assertNotNull(firstName);

// Executed only if the previous assertion is valid.
assertAll("first name", () -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n")));
}, () -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);

// Executed only if the previous assertion is valid.
assertAll("last name", () -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e")));
});
}

@Test
void exceptionTesting() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}

@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}

@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}

@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}

@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}

@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}

private static String greeting() {
return "Hello, World!";
}

}

假想(Assumptions)

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
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;

import org.junit.jupiter.api.Test;

class AssumptionsDemo {

@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}

@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}

@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, 2);
});

// perform these assertions in all environments
assertEquals("a string", "a string");
}

}

禁用

禁用单元测试类示例:

1
2
3
4
5
6
7
8
9
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}

禁用单元测试方法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class DisabledTestsDemo {

@Disabled
@Test
void testWillBeSkipped() {
}

@Test
void testWillBeExecuted() {
}
}

测试条件

操作系统条件

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
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
// ...
}

@TestOnMac
void testOnMac() {
// ...
}

@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
// ...
}

@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
// ...
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}

Java 运行时版本条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// ...
}

@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
// ...
}

@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
// ...
}

系统属性条件

1
2
3
4
5
6
7
8
9
10
11
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// ...
}

@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
// ...
}

嵌套测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.EmptyStackException;
import java.util.Stack;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, () -> stack.pop());
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, () -> stack.peek());
}

@Nested
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

重复测试

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
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.logging.Logger;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;

class RepeatedTestsDemo {

private Logger logger = // ...

@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
logger.info(String.format("About to execute repetition %d of %d for %s", //
currentRepetition, totalRepetitions, methodName));
}

@RepeatedTest(10)
void repeatedTest() {
// ...
}

@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertEquals(5, repetitionInfo.getTotalRepetitions());
}

@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
}

@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1");
}

@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
// ...
}

}

参数化测试

1
2
3
4
5
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(isPalindrome(candidate));
}

参考资料

Mockito 快速入门

Mockito 是一个针对 Java 的 mock 框架。

预备知识

如果需要往下学习,你需要先理解 Junit 框架中的单元测试。

如果你不熟悉 JUnit,请看 Junit 教程

使用 mock 对象来进行测试

单元测试的目标和挑战

单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。

测试类的分类

  • dummy object 做为参数传递给方法但是绝对不会被使用。譬如说,这种测试类内部的方法不会被调用,或者是用来填充某个方法的参数。
  • Fake 是真正接口或抽象类的实现体,但给对象内部实现很简单。譬如说,它存在内存中而不是真正的数据库中。(译者注:Fake 实现了真正的逻辑,但它的存在只是为了测试,而不适合于用在产品中。)
  • stub 类是依赖类的部分方法实现,而这些方法在你测试类和接口的时候会被用到,也就是说 stub 类在测试中会被实例化。stub 类会回应任何外部测试的调用。stub 类有时候还会记录调用的一些信息。
  • mock object 是指类或者接口的模拟实现,你可以自定义这个对象中某个方法的输出结果。

测试替代技术能够在测试中模拟测试类以外对象。因此你可以验证测试类是否响应正常。譬如说,你可以验证在 Mock 对象的某一个方法是否被调用。这可以确保隔离了外部依赖的干扰只测试测试类。

我们选择 Mock 对象的原因是因为 Mock 对象只需要少量代码的配置。

Mock 对象的产生

你可以手动创建一个 Mock 对象或者使用 Mock 框架来模拟这些类,Mock 框架允许你在运行时创建 Mock 对象并且定义它的行为。

一个典型的例子是把 Mock 对象模拟成数据的提供者。在正式的生产环境中它会被实现用来连接数据源。但是我们在测试的时候 Mock 对象将会模拟成数据提供者来确保我们的测试环境始终是相同的。

Mock 对象可以被提供来进行测试。因此,我们测试的类应该避免任何外部数据的强依赖。

通过 Mock 对象或者 Mock 框架,我们可以测试代码中期望的行为。譬如说,验证只有某个存在 Mock 对象的方法是否被调用了。

使用 Mockito 生成 Mock 对象

Mockito 是一个流行 mock 框架,可以和 JUnit 结合起来使用。Mockito 允许你创建和配置 mock 对象。使用 Mockito 可以明显的简化对外部依赖的测试类的开发。

一般使用 Mockito 需要执行下面三步

  1. 模拟并替换测试代码中外部依赖
  2. 执行测试代码
  3. 验证测试代码是否被正确的执行 0

为自己的项目添加 Mockito 依赖

在 Gradle 添加 Mockito 依赖

如果你的项目使用 Gradle 构建,将下面代码加入 Gradle 的构建文件中为自己项目添加 Mockito 依赖

1
2
repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:2.0.57-beta" }

在 Maven 添加 Mockito 依赖

需要在 Maven 声明依赖,您可以在 http://search.maven.org 网站中搜索 g:"org.mockito", a:"mockito-core" 来得到具体的声明方式。

在 Eclipse IDE 使用 Mockito

Eclipse IDE 支持 Gradle 和 Maven 两种构建工具,所以在 Eclipse IDE 添加依赖取决你使用的是哪一个构建工具。

以 OSGi 或者 Eclipse 插件形式添加 Mockito 依赖

在 Eclipse RCP 应用依赖通常可以在 p2 update 上得到。Orbit 是一个很好的第三方仓库,我们可以在里面寻找能在 Eclipse 上使用的应用和插件。

Orbit 仓库地址:http://download.eclipse.org/tools/orbit/downloads

使用 Mockito API

静态引用

如果在代码中静态引用了org.mockito.Mockito.*;,那你你就可以直接调用静态方法和静态变量而不用创建对象,譬如直接调用 mock() 方法。

使用 Mockito 创建和配置 mock 对象

除了上面所说的使用 mock() 静态方法外,Mockito 还支持通过 @Mock 注解的方式来创建 mock 对象。

如果你使用注解,那么必须要实例化 mock 对象。Mockito 在遇到使用注解的字段的时候,会调用MockitoAnnotations.initMocks(this) 来初始化该 mock 对象。另外也可以通过使用@RunWith(MockitoJUnitRunner.class)来达到相同的效果。

通过下面的例子我们可以了解到使用@Mock 的方法和MockitoRule规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import static org.mockito.Mockito.*;

public class MockitoTest {

@Mock
MyDatabase databaseMock; (1)

@Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); (2)

@Test
public void testQuery() {
ClassToTest t = new ClassToTest(databaseMock); (3)
boolean check = t.query("* from t"); (4)
assertTrue(check); (5)
verify(databaseMock).query("* from t"); (6)
}
}
  1. 告诉 Mockito 模拟 databaseMock 实例
  2. Mockito 通过 @mock 注解创建 mock 对象
  3. 使用已经创建的 mock 初始化这个类
  4. 在测试环境下,执行测试类中的代码
  5. 使用断言确保调用的方法返回值为 true
  6. 验证 query 方法是否被 MyDatabase 的 mock 对象调用

配置 mock

当我们需要配置某个方法的返回值的时候,Mockito 提供了链式的 API 供我们方便的调用

when(….).thenReturn(….)可以被用来定义当条件满足时函数的返回值,如果你需要定义多个返回值,可以多次定义。当你多次调用函数的时候,Mockito 会根据你定义的先后顺序来返回返回值。Mocks 还可以根据传入参数的不同来定义不同的返回值。譬如说你的函数可以将anyString 或者 anyInt作为输入参数,然后定义其特定的放回值。

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
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@Test
public void test1() {
// 创建 mock
MyClass test = Mockito.mock(MyClass.class);

// 自定义 getUniqueId() 的返回值
when(test.getUniqueId()).thenReturn(43);

// 在测试中使用mock对象
assertEquals(test.getUniqueId(), 43);
}

// 返回多个值
@Test
public void testMoreThanOneReturnValue() {
Iterator i= mock(Iterator.class);
when(i.next()).thenReturn("Mockito").thenReturn("rocks");
String result=i.next()+" "+i.next();
// 断言
assertEquals("Mockito rocks", result);
}

// 如何根据输入来返回值
@Test
public void testReturnValueDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo("Mockito")).thenReturn(1);
when(c.compareTo("Eclipse")).thenReturn(2);
// 断言
assertEquals(1,c.compareTo("Mockito"));
}

// 如何让返回值不依赖于输入
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(anyInt())).thenReturn(-1);
// 断言
assertEquals(-1 ,c.compareTo(9));
}

// 根据参数类型来返回值
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(isA(Todo.class))).thenReturn(0);
// 断言
Todo todo = new Todo(5);
assertEquals(todo ,c.compareTo(new Todo(1)));
}

对于无返回值的函数,我们可以使用doReturn(…).when(…).methodCall来获得类似的效果。例如我们想在调用某些无返回值函数的时候抛出异常,那么可以使用doThrow 方法。如下面代码片段所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

// 下面测试用例描述了如何使用doThrow()方法

@Test(expected=IOException.class)
public void testForIOException() {
// 创建并配置 mock 对象
OutputStream mockStream = mock(OutputStream.class);
doThrow(new IOException()).when(mockStream).close();

// 使用 mock
OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);
streamWriter.close();
}

验证 mock 对象方法是否被调用

Mockito 会跟踪 mock 对象里面所有的方法和变量。所以我们可以用来验证函数在传入特定参数的时候是否被调用。这种方式的测试称行为测试,行为测试并不会检查函数的返回值,而是检查在传入正确参数时候函数是否被调用。

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 static org.mockito.Mockito.*;

@Test
public void testVerify() {
// 创建并配置 mock 对象
MyClass test = Mockito.mock(MyClass.class);
when(test.getUniqueId()).thenReturn(43);

// 调用mock对象里面的方法并传入参数为12
test.testing(12);
test.getUniqueId();
test.getUniqueId();

// 查看在传入参数为12的时候方法是否被调用
verify(test).testing(Matchers.eq(12));

// 方法是否被调用两次
verify(test, times(2)).getUniqueId();

// 其他用来验证函数是否被调用的方法
verify(mock, never()).someMethod("never called");
verify(mock, atLeastOnce()).someMethod("called at least once");
verify(mock, atLeast(2)).someMethod("called at least twice");
verify(mock, times(5)).someMethod("called five times");
verify(mock, atMost(3)).someMethod("called at most 3 times");
}

使用 Spy 封装 java 对象

@Spy 或者spy()方法可以被用来封装 java 对象。被封装后,除非特殊声明(打桩 _stub_),否则都会真正的调用对象里面的每一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
import static org.mockito.Mockito.*;

// Lets mock a LinkedList
List list = new LinkedList();
List spy = spy(list);

// 可用 doReturn() 来打桩
doReturn("foo").when(spy).get(0);

// 下面代码不生效
// 真正的方法会被调用
// 将会抛出 IndexOutOfBoundsException 的异常,因为 List 为空
when(spy.get(0)).thenReturn("foo");

方法verifyNoMoreInteractions()允许你检查没有其他的方法被调用了。

使用 @InjectMocks 在 Mockito 中进行依赖注入

我们也可以使用@InjectMocks 注解来创建对象,它会根据类型来注入对象里面的成员方法和变量。假定我们有 ArticleManager 类

1
2
3
4
5
6
7
8
9
10
public class ArticleManager {
private User user;
private ArticleDatabase database;

ArticleManager(User user) {
this.user = user;
}

void setDatabase(ArticleDatabase database) { }
}

这个类会被 Mockito 构造,而类的成员方法和变量都会被 mock 对象所代替,正如下面的代码片段所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest {

@Mock ArticleCalculator calculator;
@Mock ArticleDatabase database;
@Most User user;

@Spy private UserProvider userProvider = new ConsumerUserProvider();

@InjectMocks private ArticleManager manager; (1)

@Test public void shouldDoSomething() {
// 假定 ArticleManager 有一个叫 initialize() 的方法被调用了
// 使用 ArticleListener 来调用 addListener 方法
manager.initialize();

// 验证 addListener 方法被调用
verify(database).addListener(any(ArticleListener.class));
}
}
  1. 创建 ArticleManager 实例并注入 Mock 对象

更多的详情可以查看 http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html

捕捉参数

ArgumentCaptor类允许我们在 verification 期间访问方法的参数。得到方法的参数后我们可以使用它进行测试。

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
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.util.Arrays;
import java.util.List;

import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

public class MockitoTests {
@Rule
public MockitoRule rule = MockitoJUnit.rule();

@Captor
private ArgumentCaptor<List<String>> captor;

@Test
public final void shouldContainCertainListItem() {
List<String> asList = Arrays.asList("someElement_test", "someElement");
final List<String> mockedList = mock(List.class);
mockedList.addAll(asList);

verify(mockedList).addAll(captor.capture());
final List<String> capturedArgument = captor.getValue();
assertThat(capturedArgument, hasItem("someElement"));
}
}

Mockito 的限制

Mockito 当然也有一定的限制。而下面三种数据类型则不能够被测试

  • final classes
  • anonymous classes
  • primitive types

在 Android 中使用 Mockito

在 Android 中的 Gradle 构建文件中加入 Mockito 依赖后就可以直接使用 Mockito 了。若想使用 Android Instrumented tests 的话,还需要添加 dexmaker 和 dexmaker-mockito 依赖到 Gradle 的构建文件中。(需要 Mockito 1.9.5 版本以上)

1
2
3
4
5
6
7
8
9
dependencies {
testCompile 'junit:junit:4.12'
// Mockito unit test 的依赖
testCompile 'org.mockito:mockito-core:1.+'
// Mockito Android instrumentation tests 的依赖
androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
}

实例:使用 Mockito 写一个 Instrumented Unit Test

创建一个测试的 Android 应用

创建一个包名为com.vogella.android.testing.mockito.contextmock的 Android 应用,添加一个静态方法 ,方法里面创建一个包含参数的 Intent,如下代码所示:

1
2
3
4
5
6
7
public static Intent createQuery(Context context, String query, String value) {
// 简单起见,重用MainActivity
Intent i = new Intent(context, MainActivity.class);
i.putExtra("QUERY", query);
i.putExtra("VALUE", value);
return i;
}

在 app/build.gradle 文件中添加 Mockito 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dependencies {
// Mockito 和 JUnit 的依赖
// instrumentation unit tests on the JVM
androidTestCompile 'junit:junit:4.12'
androidTestCompile 'org.mockito:mockito-core:2.0.57-beta'
androidTestCompile 'com.android.support.test:runner:0.3'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

// Mockito 和 JUnit 的依赖
// tests on the JVM
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.+'

}

创建测试

使用 Mockito 创建一个单元测试来验证在传递正确 extra data 的情况下,intent 是否被触发。

因此我们需要使用 Mockito 来 mock 一个Context对象,如下代码所示:

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
package com.vogella.android.testing.mockitocontextmock;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class TextIntentCreation {

@Test
public void testIntentShouldBeCreated() {
Context context = Mockito.mock(Context.class);
Intent intent = MainActivity.createQuery(context, "query", "value");
assertNotNull(intent);
Bundle extras = intent.getExtras();
assertNotNull(extras);
assertEquals("query", extras.getString("QUERY"));
assertEquals("value", extras.getString("VALUE"));
}
}

实例:使用 Mockito 创建一个 mock 对象

目标

创建一个 Api,它可以被 Mockito 来模拟并做一些工作

创建一个 Twitter API 的例子

实现 TwitterClient类,它内部使用到了 ITweet 的实现。但是ITweet实例很难得到,譬如说他需要启动一个很复杂的服务来得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface ITweet {

String getMessage();
}


public class TwitterClient {

public void sendTweet(ITweet tweet) {
String message = tweet.getMessage();

// send the message to Twitter
}
}

模拟 ITweet 的实例

为了能够不启动复杂的服务来得到 ITweet,我们可以使用 Mockito 来模拟得到该实例。

1
2
3
4
5
6
7
8
9
10
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();

ITweet iTweet = mock(ITweet.class);

when(iTweet.getMessage()).thenReturn("Using mockito is great");

twitterClient.sendTweet(iTweet);
}

现在 TwitterClient 可以使用 ITweet 接口的实现,当调用 getMessage() 方法的时候将会打印 “Using Mockito is great” 信息。

验证方法调用

确保 getMessage() 方法至少调用一次。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();

ITweet iTweet = mock(ITweet.class);

when(iTweet.getMessage()).thenReturn("Using mockito is great");

twitterClient.sendTweet(iTweet);

verify(iTweet, atLeastOnce()).getMessage();
}

验证

运行测试,查看代码是否测试通过。

模拟静态方法

使用 Powermock 来模拟静态方法

因为 Mockito 不能够 mock 静态方法,因此我们可以使用 Powermock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.net.InetAddress;
import java.net.UnknownHostException;

public final class NetworkReader {
public static String getLocalHostname() {
String hostname = "";
try {
InetAddress addr = InetAddress.getLocalHost();
// Get hostname
hostname = addr.getHostName();
} catch ( UnknownHostException e ) {
}
return hostname;
}
}

我们模拟了 NetworkReader 的依赖,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;

@RunWith( PowerMockRunner.class )
@PrepareForTest( NetworkReader.class )
public class MyTest {

// 测试代码

@Test
public void testSomething() {
mockStatic( NetworkUtil.class );
when( NetworkReader.getLocalHostname() ).andReturn( "localhost" );

// 与 NetworkReader 协作的测试
}

用封装的方法代替 Powermock

有时候我们可以在静态方法周围包含非静态的方法来达到和 Powermock 同样的效果。

1
2
3
4
5
class FooWraper {
void someMethod() {
Foo.someStaticMethod()
}
}

引用和引申

JMeter 快速入门

Jmeter 是一款基于 Java 开发的功能和性能测试软件。

🎁 本文编辑时的最新版本为:5.1.1

简介

Jmeter 是一款使用 Java 开发的功能和性能测试软件。

特性

Jmeter 能够加载和性能测试许多不同的应用程序/服务器/协议类型:

  • 网络 - HTTP,HTTPS(Java,NodeJS,PHP,ASP.NET 等)
  • SOAP / REST Web 服务
  • FTP 文件
  • 通过 JDBC 的数据库
  • LDAP
  • 通过 JMS 的面向消息的中间件(MOM)
  • 邮件-SMTP(S),POP3(S)和 IMAP(S)
  • 本机命令或 Shell 脚本
  • TCP 协议
  • Java 对象

工作流

Jmeter 的工作原理是仿真用户向服务器发送请求,并收集服务器应答信息并计算统计信息。

Jmeter 的工作流如下图所示:

img

主要元素

Jmeter 的主要元素如下:

  • 测试计划(Test Plan) - 可以将测试计划视为 JMeter 的测试脚本 。测试计划由测试元素组成,例如线程组,逻辑控制器,样本生成控制器,监听器,定时器,断言和配置元素。
  • 线程组(Thread Group) - 线程组的作用是:模拟大量用户负载的运行场景。
    • 设置线程数
    • 设置加速期
    • 设置执行测试的次数
  • 控制器(Controllers) - 可以分为两大类:
    • 采样器(Sampler) - 采样器的作用是模拟用户对目标服务器发送请求。 采样器是必须将组件添加到测试计划中的,因为它只能让 JMeter 知道需要将哪种类型的请求发送到服务器。 请求可以是 HTTP,HTTP(s),FTP,TCP,SMTP,SOAP 等。
    • 逻辑控制器 - 逻辑控制器的作用是:控制多个请求发送的循环次数及顺序等。
  • 监听器(Listeners) - 监听器的作用是:收集测试结果信息。如查看结果树、汇总报告等。
  • 计时器(Timers) - 计时器的作用是:控制多个请求发送的时间频次。
  • 配置元素(Configuration Elements) - 配置元素的工作与采样器的工作类似。但是,它不发送请求,而是提供预备的数据等,如 CSV、函数助手。
  • 预处理器元素(Pre-Processor Elements) - 预处理器元素在采样器发出请求之前执行,如果预处理器附加到采样器元素,那么它将在该采样器元素运行之前执行。预处理器元素用于在运行之前准备环境及参数。
  • 后处理器元素(Post-Processor Elements) - 后处理器元素是在发送采样器请求之后执行的元素,常用于处理响应数据。

img

📌 提示:

Jmeter 元素的数量关系大致如下:

  1. 脚本中最多只能有一个测试计划。
  2. 测试计划中至少要有一个线程组。
  3. 线程组中至少要有一个取样器。
  4. 线程组中至少要有一个监听器。

安装

环境要求

  • 必要的。Jmeter 基于 JDK8 开发,所以必须运行在 JDK8 环境。

    • JDK8
  • 可选的。有些 jar 包不是 Jmeter 提供的,如果需要相应的功能,需要自行下载并置于 lib 目录。

下载

进入 Jmeter 官网下载地址 选择需要版本进行下载。

启动

解压 Jmeter 压缩包,进入 bin 目录

Unix 类系统运行 jmeter ;Windows 系统运行 jmeter.bat

image-20191024104517721

使用

创建测试计划

🔔 注意:

  • 在运行整个测试计划之前,应保存测试计划。

  • JMeter 的测试计划以 .jmx 扩展文件的形式保存。

创建线程组

  • 在“测试计划”上右键 【添加】=>【线程(用户)】=>【线程组】。

  • 设置线程数和循环次数

image-20191024105545736

配置原件

  • 在新建的线程组上右键 【添加】=>【配置元件】=>【HTTP 请求默认值】。

  • 填写协议、服务器名称或 IP、端口号

image-20191024110016264

构造 HTTP 请求

  • 在“线程组”上右键 【添加-】=>【取样器】=>【HTTP 请求】。

  • 填写协议、服务器名称或 IP、端口号(如果配置了 HTTP 请求默认值可以忽略)

  • 填写方法、路径

  • 填写参数、消息体数据、文件上传

image-20191024110953063

添加 HTTP 请求头

  • 在“线程组”上右键 【添加】=>【配置元件】=>【HTTP 信息头管理器】
  • 由于我的测试例中传输的数据为 json 形式,所以设置键值对 Content-Typeapplication/json

image-20191024111825226

添加断言

  • 在“线程组”上右键 【添加】=>【断言】=>【 响应断言 】
  • 在我的案例中,以 HTTP 应答状态码为 200 来判断请求是否成功

image-20191024112335130

添加察看结果树

  • 在“线程组”上右键 【添加】=>【监听器】=>【察看结果树】
  • 直接点击运行,就可以查看测试结果

image-20191024113849270

添加汇总报告

  • 在“线程组”上右键 【添加】=>【监听器】=>【汇总报告】
  • 直接点击运行,就可以查看测试结果

image-20191024114016424

保存测试计划

执行测试计划前,GUI 会提示先保存配置为 jmx 文件。

执行测试计划

官方建议不要直接使用 GUI 来执行测试计划,这种模式指适用于创建测试计划和 debug。

执行测试计划应该使用命令行模式,语法形式如下:

1
jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]

执行测试计划后,在 -e -o 参数后指定的 web 报告目录下,可以找到测试报告内容。在浏览器中打开 index.html 文件,可以看到如下报告:

image-20191024120233058

问题

如何读取本地 txt/csv 文件作为请求参数

参考:Jmeter 读取本地 txt/csv 文件作为请求参数,实现接口自动化

(1)依次点击【添加】=>【配置元件】=>【CSV 数据文件设置】

配置如下所示:

image-20191127175820747

重要配置说明(其他配置根据实际情况填):

  • 文件名:输入需要导入的数据文件位置。
  • 文件编码:设为 UTF-8,避免乱码。
  • 变量名称:使用 , 分隔输入变量列表。如截图中设置了两个变量 ab

(2)在 HTTP 请求的消息体数据中配置参数

1
[{"a":"${a}","b":"${b}"}]

如何有序发送数据

依次点击【添加】=>【逻辑控制器】=>【事务控制器】

参考资料