Dunwu Blog

大道至简,知易行难

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

参考资料

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.

参考资料

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}"}]

如何有序发送数据

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

参考资料

JMH 快速入门

基准测试简介

什么是基准测试

基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

现代软件常常都把高性能作为目标。那么,何为高性能,性能就是快,更快吗?显然,如果没有一个量化的标准,难以衡量性能的好坏。

不同的基准测试其具体内容和范围也存在很大的不同。如果是专业的性能工程师,更加熟悉的可能是类似 SPEC 提供的工业标准的系统级测试;而对于大多数 Java 开发者,更熟悉的则是范围相对较小、关注点更加细节的微基准测试(Micro-Benchmark)。何谓 Micro Benchmark 呢? 简单地说就是在 method 层面上的 benchmark,精度可以精确到 微秒级

何时需要微基准测试

微基准测试大多是 API 级别的性能测试。

微基准测试的适用场景:

  • 如果开发公共类库、中间件,会被其他模块经常调用的 API。
  • 对于性能,如响应延迟、吞吐量有严格要求的核心 API。

JMH 简介

JMH(即 Java Microbenchmark Harness),是目前主流的微基准测试框架。JMH 是由 Hotspot JVM 团队专家开发的,除了支持完整的基准测试过程,包括预热、运行、统计和报告等,还支持 Java 和其他 JVM 语言。更重要的是,它针对 Hotspot JVM 提供了各种特性,以保证基准测试的正确性,整体准确性大大优于其他框架,并且,JMH 还提供了用近乎白盒的方式进行 Profiling 等工作的能力。

为什么需要 JMH

死码消除

所谓死码,是指注释的代码,不可达的代码块,可达但不被使用的代码等等 。

常量折叠与常量传播

常量折叠 (Constant folding) 是一个在编译时期简化常数的一个过程,常数在表示式中仅仅代表一个简单的数值,就像是整数 2,若是一个变数从未被修改也可作为常数,或者直接将一个变数被明确地被标注为常数,例如下面的描述:

JMH 的注意点

  • 测试前需要预热。
  • 防止无用代码进入测试方法中。
  • 并发测试。
  • 测试结果呈现。

应用场景

  1. 当你已经找出了热点函数,而需要对热点函数进行进一步的优化时,就可以使用 JMH 对优化的效果进行定量的分析。
  2. 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性
  3. 一个函数有两种不同实现(例如 JSON 序列化/反序列化有 Jackson 和 Gson 实现),不知道哪种实现性能更好

JMH 概念

  • Iteration - iteration 是 JMH 进行测试的最小单位,包含一组 invocations。
  • Invocation - 一次 benchmark 方法调用。
  • Operation - benchmark 方法中,被测量操作的执行。如果被测试的操作在 benchmark 方法中循环执行,可以使用@OperationsPerInvocation表明循环次数,使测试结果为单次 operation 的性能。
  • Warmup - 在实际进行 benchmark 前先进行预热。因为某个函数被调用多次之后,JIT 会对其进行编译,通过预热可以使测量结果更加接近真实情况。

JMH 快速入门

添加 maven 依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>

测试代码

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
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.*;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(8)
@Fork(2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class StringBuilderBenchmark {

@Benchmark
public void testStringAdd() {
String a = "";
for (int i = 0; i < 10; i++) {
a += i;
}
// System.out.println(a);
}

@Benchmark
public void testStringBuilderAdd() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
// System.out.println(sb.toString());
}

public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(StringBuilderBenchmark.class.getSimpleName())
.output("d:/Benchmark.log")
.build();
new Runner(options).run();
}

}

执行 JMH

命令行

(1)初始化 benchmarking 工程

1
2
3
4
5
6
7
$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0

(2)构建 benchmark

1
2
cd test/
mvn clean install

(3)运行 benchmark

1
java -jar target/benchmarks.jar

执行 main 方法

执行 main 方法,耐心等待测试结果,最终会生成一个测试报告,内容大致如下;

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
# JMH version: 1.22
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58635:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringAdd

# Run progress: 0.00% complete, ETA 00:05:20
# Fork: 1 of 2
# Warmup Iteration 1: 21803.050 ops/ms
# Warmup Iteration 2: 22501.860 ops/ms
# Warmup Iteration 3: 20953.944 ops/ms
Iteration 1: 21627.645 ops/ms
Iteration 2: 21215.269 ops/ms
Iteration 3: 20863.282 ops/ms
Iteration 4: 21617.715 ops/ms
Iteration 5: 21695.645 ops/ms
Iteration 6: 21886.784 ops/ms
Iteration 7: 21986.899 ops/ms
Iteration 8: 22389.540 ops/ms
Iteration 9: 22507.313 ops/ms
Iteration 10: 22124.133 ops/ms

# Run progress: 25.00% complete, ETA 00:04:02
# Fork: 2 of 2
# Warmup Iteration 1: 22262.108 ops/ms
# Warmup Iteration 2: 21567.804 ops/ms
# Warmup Iteration 3: 21787.002 ops/ms
Iteration 1: 21598.970 ops/ms
Iteration 2: 22486.133 ops/ms
Iteration 3: 22157.834 ops/ms
Iteration 4: 22321.827 ops/ms
Iteration 5: 22477.063 ops/ms
Iteration 6: 22154.760 ops/ms
Iteration 7: 21561.095 ops/ms
Iteration 8: 22194.863 ops/ms
Iteration 9: 22493.844 ops/ms
Iteration 10: 22568.078 ops/ms


Result "io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringAdd":
21996.435 ±(99.9%) 412.955 ops/ms [Average]
(min, avg, max) = (20863.282, 21996.435, 22568.078), stdev = 475.560
CI (99.9%): [21583.480, 22409.390] (assumes normal distribution)


# JMH version: 1.22
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58635:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringBuilderAdd

# Run progress: 50.00% complete, ETA 00:02:41
# Fork: 1 of 2
# Warmup Iteration 1: 241500.886 ops/ms
# Warmup Iteration 2: 134206.032 ops/ms
# Warmup Iteration 3: 86907.846 ops/ms
Iteration 1: 86143.339 ops/ms
Iteration 2: 74725.356 ops/ms
Iteration 3: 72316.121 ops/ms
Iteration 4: 77319.716 ops/ms
Iteration 5: 83469.256 ops/ms
Iteration 6: 87712.360 ops/ms
Iteration 7: 79421.899 ops/ms
Iteration 8: 80867.839 ops/ms
Iteration 9: 82619.163 ops/ms
Iteration 10: 87026.928 ops/ms

# Run progress: 75.00% complete, ETA 00:01:20
# Fork: 2 of 2
# Warmup Iteration 1: 228342.337 ops/ms
# Warmup Iteration 2: 124737.248 ops/ms
# Warmup Iteration 3: 82598.851 ops/ms
Iteration 1: 86877.318 ops/ms
Iteration 2: 89388.624 ops/ms
Iteration 3: 88523.558 ops/ms
Iteration 4: 87547.332 ops/ms
Iteration 5: 88376.087 ops/ms
Iteration 6: 88848.837 ops/ms
Iteration 7: 85998.124 ops/ms
Iteration 8: 86796.998 ops/ms
Iteration 9: 87994.726 ops/ms
Iteration 10: 87784.453 ops/ms


Result "io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringBuilderAdd":
84487.902 ±(99.9%) 4355.525 ops/ms [Average]
(min, avg, max) = (72316.121, 84487.902, 89388.624), stdev = 5015.829
CI (99.9%): [80132.377, 88843.427] (assumes normal distribution)


# Run complete. Total time: 00:05:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark Mode Cnt Score Error Units
StringBuilderBenchmark.testStringAdd thrpt 20 21996.435 ± 412.955 ops/ms
StringBuilderBenchmark.testStringBuilderAdd thrpt 20 84487.902 ± 4355.525 ops/ms

JMH API

下面来了解一下 jmh 常用 API

@BenchmarkMode

基准测试类型。这里选择的是 Throughput 也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。

  • Throughput - 整体吞吐量,例如“1 秒内可以执行多少次调用”。
  • AverageTime - 调用的平均时间,例如“每次调用平均耗时 xxx 毫秒”。
  • SampleTime - 随机取样,最后输出取样结果的分布,例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”
  • SingleShotTime - 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。
  • All - 所有模式

@Warmup

上面我们提到了,进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数 iterations 也就非常好理解了,就是预热轮数。

为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement

度量,其实就是一些基本的测试参数。

  • iterations - 进行测试的轮次
  • time - 每轮进行的时长
  • timeUnit - 时长单位

都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。

@Threads

每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为 cpu 乘以 2。

@Fork

进行 fork 的次数。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。

@OutputTimeUnit

这个比较简单了,基准测试结果的时间类型。一般选择秒、毫秒、微秒。

@Benchmark

方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@Param

属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。

@Setup

方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。

@TearDown

方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。

@State

当使用 @Setup 参数的时候,必须在类上加这个参数,不然会提示无法运行。

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。

  • Thread - 该状态为每个线程独享。
  • Group - 该状态为同一个组里面所有线程共享。
  • Benchmark - 该状态在所有线程间共享。

关于 State 的用法,官方的 code sample 里有比较好的例子

参考资料

细说 Java 主流日志工具库

在项目开发中,为了跟踪代码的运行情况,常常要使用日志来记录信息。

在 Java 世界,有很多的日志工具库来实现日志功能,避免了我们重复造轮子。

我们先来逐一了解一下主流日志工具。

日志框架

java.util.logging (JUL)

JDK1.4 开始,通过 java.util.logging 提供日志功能。

它能满足基本的日志需要,但是功能没有 Log4j 强大,而且使用范围也没有 Log4j 广泛。

Log4j

Log4j 是 apache 的一个开源项目,创始人 Ceki Gulcu。

Log4j 应该说是 Java 领域资格最老,应用最广的日志工具。从诞生之日到现在一直广受业界欢迎。

Log4j 是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX 系统日志等。

Log4j 中有三个主要组成部分:

  • loggers - 负责捕获记录信息。
  • appenders - 负责发布日志信息,以不同的首选目的地。
  • layouts - 负责格式化不同风格的日志信息。

官网地址

Logback

Logback 是由 log4j 创始人 Ceki Gulcu 设计的又一个开源日记组件,目标是替代 log4j。

logback 当前分成三个模块:logback-corelogback-classiclogback-access

  • logback-core - 是其它两个模块的基础模块。
  • logback-classic - 是 log4j 的一个 改良版本。此外 logback-classic 完整实现 SLF4J API 使你可以很方便地更换成其它日记系统如 log4j 或 JDK14 Logging。
  • logback-access - 访问模块与 Servlet 容器集成提供通过 Http 来访问日记的功能。

官网地址

Log4j2

官网地址

按照官方的说法,Log4j2 是 Log4j 和 Logback 的替代。

Log4j2 架构:

img

Log4j vs Logback vs Log4j2

按照官方的说法,Log4j2 大大优于 Log4j 和 Logback。

那么,Log4j2 相比于先问世的 Log4j 和 Logback,它具有哪些优势呢?

  1. Log4j2 旨在用作审计日志记录框架。 Log4j 1.x 和 Logback 都会在重新配置时丢失事件。 Log4j 2 不会。在 Logback 中,Appender 中的异常永远不会对应用程序可见。在 Log4j 中,可以将 Appender 配置为允许异常渗透到应用程序。
  2. Log4j2 在多线程场景中,异步 Loggers 的吞吐量比 Log4j 1.x 和 Logback 高 10 倍,延迟低几个数量级。
  3. Log4j2 对于独立应用程序是无垃圾的,对于稳定状态日志记录期间的 Web 应用程序来说是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应时间性能。
  4. Log4j2 使用插件系统,通过添加新的 Appender、Filter、Layout、Lookup 和 Pattern Converter,可以非常轻松地扩展框架,而无需对 Log4j 进行任何更改。
  5. 由于插件系统配置更简单。配置中的条目不需要指定类名。
  6. 支持自定义日志等级
  7. 支持 lambda 表达式
  8. 支持消息对象
  9. Log4j 和 Logback 的 Layout 返回的是字符串,而 Log4j2 返回的是二进制数组,这使得它能被各种 Appender 使用。
  10. Syslog Appender 支持 TCP 和 UDP 并且支持 BSD 系统日志。
  11. Log4j2 利用 Java5 并发特性,尽量小粒度的使用锁,减少锁的开销。

日志门面

何谓日志门面?

日志门面是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。

common-logging

common-logging 是 apache 的一个开源项目。也称Jakarta Commons Logging,缩写 JCL

common-logging 的功能是提供日志功能的 API 接口,本身并不提供日志的具体实现(当然,common-logging 内部有一个 Simple logger 的简单实现,但是功能很弱,直接忽略),而是在运行时动态的绑定日志实现组件来工作(如 log4j、java.util.loggin)。

官网地址

slf4j

全称为 Simple Logging Facade for Java,即 java 简单日志门面。

什么,作者又是 Ceki Gulcu!这位大神写了 Log4j、Logback 和 slf4j,专注日志组件开发五百年,一直只能超越自己。

类似于 Common-Logging,slf4j 是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。但是,slf4j 在编译时静态绑定真正的 Log 库。使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。

官网地址

img

common-logging vs slf4j

slf4j 库类似于 Apache Common-Logging。但是,他在编译时静态绑定真正的日志库。这点似乎很麻烦,其实也不过是导入桥接 jar 包而已。

slf4j 一大亮点是提供了更方便的日志记录方式:

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

1
logger.debug("id: {}, name: {} ", id, name);

总结

综上所述,使用 slf4j + Logback 可谓是目前最理想的日志解决方案了。

接下来,就是如何在项目中实施了。

实施日志解决方案

使用日志解决方案基本可分为三步:

  1. 引入 jar 包
  2. 配置
  3. 使用 API

常见的各种日志解决方案的第 2 步和第 3 步基本一样,实施上的差别主要在第 1 步,也就是使用不同的库。

引入 jar 包

这里首选推荐使用 slf4j + logback 的组合。

如果你习惯了 common-logging,可以选择 common-logging+log4j。

强烈建议不要直接使用日志实现组件(logback、log4j、java.util.logging),理由前面也说过,就是无法灵活替换日志库。

还有一种情况:你的老项目使用了 common-logging,或是直接使用日志实现组件。如果修改老的代码,工作量太大,需要兼容处理。在下文,都将看到各种应对方法。

注:据我所知,当前仍没有方法可以将 slf4j 桥接到 common-logging。如果我孤陋寡闻了,请不吝赐教。

slf4j 直接绑定日志组件

slf4j + logback

添加依赖到 pom.xml 中即可。

logback-classic-1.0.13.jar 会自动将 slf4j-api-1.7.21.jarlogback-core-1.0.13.jar 也添加到你的项目中。

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

slf4j + log4j

添加依赖到 pom.xml 中即可。

slf4j-log4j12-1.7.21.jar 会自动将 slf4j-api-1.7.21.jarlog4j-1.2.17.jar 也添加到你的项目中。

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>

slf4j + java.util.logging

添加依赖到 pom.xml 中即可。

slf4j-jdk14-1.7.21.jar 会自动将 slf4j-api-1.7.21.jar 也添加到你的项目中。

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.21</version>
</dependency>

slf4j 兼容非 slf4j 日志组件

在介绍解决方案前,先提一个概念——桥接

什么是桥接呢

假如你正在开发应用程序所调用的组件当中已经使用了 common-logging,这时你需要 jcl-over-slf4j.jar 把日志信息输出重定向到 slf4j-api,slf4j-api 再去调用 slf4j 实际依赖的日志组件。这个过程称为桥接。下图是官方的 slf4j 桥接策略图:

img

从图中应该可以看出,无论你的老项目中使用的是 common-logging 或是直接使用 log4j、java.util.logging,都可以使用对应的桥接 jar 包来解决兼容问题。

slf4j 兼容 common-logging

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.12</version>
</dependency>

slf4j 兼容 log4j

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.12</version>
</dependency>

slf4j 兼容 java.util.logging

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.12</version>
</dependency>

spring 集成 slf4j

做 java web 开发,基本离不开 spring 框架。很遗憾,spring 使用的日志解决方案是 common-logging + log4j。

所以,你需要一个桥接 jar 包:_logback-ext-spring_。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.logback-extensions</groupId>
<artifactId>logback-ext-spring</artifactId>
<version>0.1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.12</version>
</dependency>

common-logging 绑定日志组件

common-logging + log4j

添加依赖到 pom.xml 中即可。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

使用 API

slf4j 用法

使用 slf4j 的 API 很简单。使用LoggerFactory初始化一个Logger实例,然后调用 Logger 对应的打印等级函数就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
private static final Logger log = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
String msg = "print log, current level: {}";
log.trace(msg, "trace");
log.debug(msg, "debug");
log.info(msg, "info");
log.warn(msg, "warn");
log.error(msg, "error");
}
}

common-logging 用法

common-logging 用法和 slf4j 几乎一样,但是支持的打印等级多了一个更高级别的:fatal

此外,common-logging 不支持{}替换参数,你只能选择拼接字符串这种方式了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JclTest {
private static final Log log = LogFactory.getLog(JclTest.class);

public static void main(String[] args) {
String msg = "print log, current level: ";
log.trace(msg + "trace");
log.debug(msg + "debug");
log.info(msg + "info");
log.warn(msg + "warn");
log.error(msg + "error");
log.fatal(msg + "fatal");
}
}

log4j2 配置

log4j2 基本配置形式如下:

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"?>;
<Configuration>
<Properties>
<Property name="name1">value</property>
<Property name="name2" value="value2"/>
</Properties>
<Filter type="type" ... />
<Appenders>
<Appender type="type" name="name">
<Filter type="type" ... />
</Appender>
...
</Appenders>
<Loggers>
<Logger name="name1">
<Filter type="type" ... />
</Logger>
...
<Root level="level">
<AppenderRef ref="name"/>
</Root>
</Loggers>
</Configuration>

配置示例:

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
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" strict="true" name="XMLConfigTest"
packages="org.apache.logging.log4j.test">
<Properties>
<Property name="filename">target/test.log</Property>
</Properties>
<Filter type="ThresholdFilter" level="trace"/>

<Appenders>
<Appender type="Console" name="STDOUT">
<Layout type="PatternLayout" pattern="%m MDC%X%n"/>
<Filters>
<Filter type="MarkerFilter" marker="FLOW" onMatch="DENY" onMismatch="NEUTRAL"/>
<Filter type="MarkerFilter" marker="EXCEPTION" onMatch="DENY" onMismatch="ACCEPT"/>
</Filters>
</Appender>
<Appender type="Console" name="FLOW">
<Layout type="PatternLayout" pattern="%C{1}.%M %m %ex%n"/><!-- class and line number -->
<Filters>
<Filter type="MarkerFilter" marker="FLOW" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
<Filter type="MarkerFilter" marker="EXCEPTION" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</Appender>
<Appender type="File" name="File" fileName="${filename}">
<Layout type="PatternLayout">
<Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
</Layout>
</Appender>
</Appenders>

<Loggers>
<Logger name="org.apache.logging.log4j.test1" level="debug" additivity="false">
<Filter type="ThreadContextMapFilter">
<KeyValuePair key="test" value="123"/>
</Filter>
<AppenderRef ref="STDOUT"/>
</Logger>

<Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
<AppenderRef ref="File"/>
</Logger>

<Root level="trace">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>

</Configuration>

logback 配置

<configuration>

  • 作用:<configuration> 是 logback 配置文件的根元素。
  • 要点
    • 它有 <appender><logger><root> 三个子元素。

img

<appender>

  • 作用:将记录日志的任务委托给名为 appender 的组件。
  • 要点
    • 可以配置零个或多个。
    • 它有 <file><filter><layout><encoder> 四个子元素。
  • 属性
    • name:设置 appender 名称。
    • class:设置具体的实例化类。

<file>

  • 作用:设置日志文件路径。

<filter>

  • 作用:设置过滤器。
  • 要点
    • 可以配置零个或多个。

<layout>

  • 作用:设置 appender。
  • 要点
    • 可以配置零个或一个。
  • 属性
    • class:设置具体的实例化类。

<encoder>

  • 作用:设置编码。
  • 要点
    • 可以配置零个或多个。
  • 属性
    • class:设置具体的实例化类。

img

<logger>

  • 作用:设置 logger。
  • 要点
    • 可以配置零个或多个。
  • 属性
    • name
    • level:设置日志级别。不区分大小写。可选值:TRACE、DEBUG、INFO、WARN、ERROR、ALL、OFF。
    • additivity:可选值:true 或 false。

<appender-ref>

  • 作用:appender 引用。
  • 要点
    • 可以配置零个或多个。

<root>

  • 作用:设置根 logger。
  • 要点
    • 只能配置一个。
    • 除了 level,不支持任何属性。level 属性和 <logger> 中的相同。
    • 有一个子元素 <appender-ref>,与 <logger> 中的相同。

完整的 logback.xml 参考示例

在下面的配置文件中,我为自己的项目代码(根目录:org.zp.notes.spring)设置了五种等级:

TRACE、DEBUG、INFO、WARN、ERROR,优先级依次从低到高。

因为关注 spring 框架本身的一些信息,我增加了专门打印 spring WARN 及以上等级的日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?xml version="1.0" encoding="UTF-8" ?>

<!-- logback中一共有5种有效级别,分别是TRACE、DEBUG、INFO、WARN、ERROR,优先级依次从低到高 -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">

<property name="DIR_NAME" value="spring-helloworld"/>

<!-- 将记录日志打印到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<!-- RollingFileAppender begin -->
<appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/all.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>30MB</maxFileSize>
</triggeringPolicy>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/trace.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="SPRING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/springframework.%d{yyyy-MM-dd}.log
</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>
<!-- RollingFileAppender end -->

<!-- logger begin -->
<!-- 本项目的日志记录,分级打印 -->
<logger name="org.zp.notes.spring" level="TRACE" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="WARN"/>
<appender-ref ref="INFO"/>
<appender-ref ref="DEBUG"/>
<appender-ref ref="TRACE"/>
</logger>

<!-- SPRING框架日志 -->
<logger name="org.springframework" level="WARN" additivity="false">
<appender-ref ref="SPRING"/>
</logger>

<root level="TRACE">
<appender-ref ref="ALL"/>
</root>
<!-- logger end -->

</configuration>

log4j 配置

完整的 log4j.xml 参考示例

log4j 的配置文件一般有 xml 格式或 properties 格式。这里为了和 logback.xml 做个对比,就不介绍 properties 了,其实也没太大差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>

<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern"
value="%d{yyyy-MM-dd HH:mm:ss,SSS\} [%-5p] [%t] %c{36\}.%M - %m%n"/>
</layout>

<!--过滤器设置输出的级别-->
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<param name="levelMin" value="debug"/>
<param name="levelMax" value="fatal"/>
<param name="AcceptOnMatch" value="true"/>
</filter>
</appender>


<appender name="ALL" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="${user.dir}/logs/spring-common/jcl/all"/>
<param name="Append" value="true"/>
<!-- 每天重新生成日志文件 -->
<param name="DatePattern" value="'-'yyyy-MM-dd'.log'"/>
<!-- 每小时重新生成日志文件 -->
<!--<param name="DatePattern" value="'-'yyyy-MM-dd-HH'.log'"/>-->
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern"
value="%d{yyyy-MM-dd HH:mm:ss,SSS\} [%-5p] [%t] %c{36\}.%M - %m%n"/>
</layout>
</appender>

<!-- 指定logger的设置,additivity指示是否遵循缺省的继承机制-->
<logger name="org.zp.notes.spring" additivity="false">
<level value="error"/>
<appender-ref ref="STDOUT"/>
<appender-ref ref="ALL"/>
</logger>

<!-- 根logger的设置-->
<root>
<level value="warn"/>
<appender-ref ref="STDOUT"/>
</root>
</log4j:configuration>

参考

Reflections 快速入门

引入 pom

1
2
3
4
5
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.11</version>
</dependency>

典型应用

1
2
3
Reflections reflections = new Reflections("my.project");
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(SomeAnnotation.class);

使用

基本上,使用 Reflections 首先使用 urls 和 scanners 对其进行实例化

1
2
3
4
5
6
7
8
9
10
//scan urls that contain 'my.package', include inputs starting with 'my.package', use the default scanners
Reflections reflections = new Reflections("my.package");

//or using ConfigurationBuilder
new Reflections(new ConfigurationBuilder()
.setUrls(ClasspathHelper.forPackage("my.project.prefix"))
.setScanners(new SubTypesScanner(),
new TypeAnnotationsScanner().filterResultsBy(optionalFilter), ...),
.filterInputsBy(new FilterBuilder().includePackage("my.project.prefix"))
...);

然后,使用方便的查询方法

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
// 子类型扫描
Set<Class<? extends Module>> modules =
reflections.getSubTypesOf(com.google.inject.Module.class);
// 类型注解扫描
Set<Class<?>> singletons =
reflections.getTypesAnnotatedWith(javax.inject.Singleton.class);
// 资源扫描
Set<String> properties =
reflections.getResources(Pattern.compile(".*\\.properties"));
// 方法注解扫描
Set<Method> resources =
reflections.getMethodsAnnotatedWith(javax.ws.rs.Path.class);
Set<Constructor> injectables =
reflections.getConstructorsAnnotatedWith(javax.inject.Inject.class);
// 字段注解扫描
Set<Field> ids =
reflections.getFieldsAnnotatedWith(javax.persistence.Id.class);
// 方法参数扫描
Set<Method> someMethods =
reflections.getMethodsMatchParams(long.class, int.class);
Set<Method> voidMethods =
reflections.getMethodsReturn(void.class);
Set<Method> pathParamMethods =
reflections.getMethodsWithAnyParamAnnotated(PathParam.class);
// 方法参数名扫描
List<String> parameterNames =
reflections.getMethodParamNames(Method.class)
// 方法使用扫描
Set<Member> usages =
reflections.getMethodUsages(Method.class)

说明:

  • 如果未配置扫描程序,则将使用默认值 - SubTypesScanner 和 TypeAnnotationsScanner。
  • 还可以配置类加载器,它将用于从名称中解析运行时类。
  • Reflection 默认情况下会扩展超类型。 这解决了传输 URL 不被扫描的一些问题。

ReflectionUtils

1
2
3
4
5
6
7
8
9
10
11
import static org.reflections.ReflectionUtils.*;

Set<Method> getters = getAllMethods(someClass,
withModifier(Modifier.PUBLIC), withPrefix("get"), withParametersCount(0));

//or
Set<Method> listMethodsFromCollectionToBoolean =
getAllMethods(List.class,
withParametersAssignableTo(Collection.class), withReturnType(boolean.class));

Set<Field> fields = getAllFields(SomeClass.class, withAnnotation(annotation), withTypeAssignableTo(type));

JavaMail 快速入门

简介

邮件相关的标准

厂商所提供的 JavaMail 服务程序可以有选择地实现某些邮件协议,常见的邮件协议包括:

  • SMTP(Simple Mail Transfer Protocol) :即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。
  • POP3(Post Office Protocol - Version 3) :即邮局协议版本 3 ,用于接收电子邮件的标准协议。
  • IMAP(Internet Mail Access Protocol) :即 Internet 邮件访问协议。是 POP3 的替代协议。

这三种协议都有对应 SSL 加密传输的协议,分别是 SMTPSPOP3SIMAPS

MIME(Multipurpose Internet Mail Extensions) :即多用途因特网邮件扩展标准。它不是邮件传输协议。但对传输内容的消息、附件及其它的内容定义了格式。

JavaMail 简介

JavaMail 是由 Sun 发布的用来处理 email 的 API 。它并没有包含在 Java SE 中,而是作为 Java EE 的一部分。

  • mail.jar :此 JAR 文件包含 JavaMail API 和 Sun 提供的 SMTP 、 IMAP 和 POP3 服务提供程序;
  • activation.jar :此 JAR 文件包含 JAF API 和 Sun 的实现。

JavaMail 包中用于处理电子邮件的核心类是: PropertiesSessionMessageAddressAuthenticatorTransportStore 等。

邮件传输过程

如上图,电子邮件的处理步骤如下:

  1. 创建一个 Session 对象。
  2. Session 对象创建一个 Transport 对象 /Store 对象,用来发送 / 保存邮件。
  3. Transport 对象 /Store 对象连接邮件服务器。
  4. Transport 对象 /Store 对象创建一个 Message 对象 ( 也就是邮件内容 ) 。
  5. Transport 对象发送邮件; Store 对象获取邮箱的邮件。

Message 结构

  • MimeMessage 类:代表整封邮件。
  • MimeBodyPart 类:代表邮件的一个 MIME 信息。
  • MimeMultipart 类:代表一个由多个 MIME 信息组合成的组合 MIME 信息。

img

JavaMail 的核心类

JavaMail 对收发邮件进行了高级的抽象,形成了一些关键的的接口和类,它们构成了程序的基础,下面我们分别来了解一下这些最常见的对象。

java.util.Properties 类(属性对象)

java.util.Properties 类代表一组属性集合。

它的每一个键和值都是 String 类型。

由于 JavaMail 需要和邮件服务器进行通信,这就要求程序提供许多诸如服务器地址、端口、用户名、密码等信息, JavaMail 通过 Properties 对象封装这些属性信息。

例: 如下面的代码封装了几个属性信息:

1
2
3
4
5
Properties prop = new Properties();
prop.setProperty("mail.debug", "true");
prop.setProperty("mail.host", "[email protected]");
prop.setProperty("mail.transport.protocol", "smtp");
prop.setProperty("mail.smtp.auth", "true");

针对不同的的邮件协议, JavaMail 规定了服务提供者必须支持一系列属性,

下表是一些常见属性(属性值都以 String 类型进行设置,属性类型栏仅表示属性是如何被解析的):

关键词 类型 描述
mail.debug boolean debug 开关。
mail.host String 指定发送、接收邮件的默认邮箱服务器。
mail.store.protocol String 指定接收邮件的协议。
mail.transport.protocol String 指定发送邮件的协议。
mail.debug.auth boolean debug 输出中是否包含认证命令。默认是 false 。

详情请参考官方 API 文档:

https://javamail.java.net/nonav/docs/api/

javax.mail.Session 类(会话对象)

Session 表示一个邮件会话。

Session 的主要作用包括两个方面:

  • 接收各种配置属性信息:通过 Properties 对象设置的属性信息;
  • 初始化 JavaMail 环境:根据 JavaMail 的配置文件,初始化 JavaMail 环境,以便通过 Session 对象创建其他重要类的实例。

JavaMail 在 Jar 包的 META-INF 目录下,通过以下文件提供了基本配置信息,以便 session 能够根据这个配置文件加载提供者的实现类:

  • javamail.default.providers
  • javamail.default.address.map

img

例:

1
2
3
Properties props = new Properties();
props.setProperty("mail.transport.protocol", "smtp");
Session session = Session.getInstance(props);

javax.mail.Transport 类(邮件传输)

邮件操作只有发送或接收两种处理方式。

JavaMail 将这两种不同操作描述为传输( javax.mail.Transport )和存储( javax.mail.Store ),传输对应邮件的发送,而存储对应邮件的接收。

  • getTransport - Session 类中的 **getTransport()**有多个重载方法,可以用来创建 Transport 对象。
  • connect - 如果设置了认证命令—— mail.smtp.auth ,那么使用 Transport 类的 connect 方法连接服务器时,则必须加上用户名和密码。
  • sendMessage - Transport 类的 sendMessage 方法用来发送邮件消息。
  • close - Transport 类的 close 方法用来关闭和邮件服务器的连接。

javax.mail.Store 类(邮件存储 )

  • getStore - Session 类中的 getStore () 有多个重载方法,可以用来创建 Store 对象。
  • connect - 如果设置了认证命令—— mail.smtp.auth ,那么使用 Store 类的 connect 方法连接服务器时,则必须加上用户名和密码。
  • getFolder - Store 类的 getFolder 方法可以 获取邮箱内的邮件夹 Folder 对象
  • close - Store 类的 close 方法用来关闭和邮件服务器的连接。

javax.mail.Message 类(消息对象)

  • javax.mail.Message - 是个抽象类,只能用子类去实例化,多数情况下为 javax.mail.internet.MimeMessage
  • MimeMessage - 代表 MIME 类型的电子邮件消息。

要创建一个 Message ,需要将 Session 对象传递给 MimeMessage 构造器:

1
MimeMessage message = new MimeMessage(session);

注意:还存在其它构造器,如用按 RFC822 格式的输入流来创建消息。

  • setFrom - 设置邮件的发件人
  • setRecipient - 设置邮件的发送人、抄送人、密送人

三种预定义的地址类型是:

  • Message.RecipientType.TO - 收件人
  • Message.RecipientType.CC - 抄送人
  • Message.RecipientType.BCC - 密送人
  • setSubject - 设置邮件的主题
  • setContent - 设置邮件内容
  • setText - 如果邮件内容是纯文本,可以使用此接口设置文本内容。

javax.mail.Address 类(地址)

一旦您创建了 Session 和 Message ,并将内容填入消息后,就可以用 Address 确定信件地址了。和 Message 一样, Address 也是个抽象类。您用的是 javax.mail.internet.InternetAddress 类。

若创建的地址只包含电子邮件地址,只要传递电子邮件地址到构造器就行了。

例:

1
Address address = new InternetAddress("[email protected]");

Authenticator 类(认证者)

与 java.net 类一样, JavaMail API 也可以利用 Authenticator 通过用户名和密码访问受保护的资源。对于 JavaMail API 来说,这些资源就是邮件服务器。Authenticator 在 javax.mail 包中,而且它和 java.net 中同名的类 Authenticator 不同。两者并不共享同一个 Authenticator ,因为 JavaMail API 用于 Java 1.1 ,它没有 java.net 类别。

要使用 Authenticator ,先创建一个抽象类的子类,并从 getPasswordAuthentication() 方法中返回 PasswordAuthentication 实例。创建完成后,您必需向 session 注册 Authenticator 。然后,在需要认证的时候,就会通知 Authenticator 。您可以弹出窗口,也可以从配置文件中(虽然没有加密是不安全的)读取用户名和密码,将它们作为 PasswordAuthentication 对象返回给调用程序。

例:

1
2
3
Properties props = new Properties();
Authenticator auth = new MyAuthenticator();
Session session = Session.getDefaultInstance(props, auth);

实例

发送文本邮件

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
public static void main(String[] args) throws Exception {
Properties prop = new Properties();
prop.setProperty("mail.debug", "true");
prop.setProperty("mail.host", MAIL_SERVER_HOST);
prop.setProperty("mail.transport.protocol", "smtp");
prop.setProperty("mail.smtp.auth", "true");

// 1、创建session
Session session = Session.getInstance(prop);
Transport ts = null;

// 2、通过session得到transport对象
ts = session.getTransport();

// 3、连上邮件服务器
ts.connect(MAIL_SERVER_HOST, USER, PASSWORD);

// 4、创建邮件
MimeMessage message = new MimeMessage(session);

// 邮件消息头
message.setFrom(new InternetAddress(MAIL_FROM)); // 邮件的发件人
message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO)); // 邮件的收件人
message.setRecipient(Message.RecipientType.CC, new InternetAddress(MAIL_CC)); // 邮件的抄送人
message.setRecipient(Message.RecipientType.BCC, new InternetAddress(MAIL_BCC)); // 邮件的密送人
message.setSubject("测试文本邮件"); // 邮件的标题

// 邮件消息体
message.setText("天下无双。");

// 5、发送邮件
ts.sendMessage(message, message.getAllRecipients());
ts.close();
}

发送 HTML 格式的邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static void main(String[] args) throws Exception {
Properties prop = new Properties();
prop.setProperty("mail.debug", "true");
prop.setProperty("mail.host", MAIL_SERVER_HOST);
prop.setProperty("mail.transport.protocol", "smtp");
prop.setProperty("mail.smtp.auth", "true");

// 1、创建session
Session session = Session.getInstance(prop);
Transport ts = null;

// 2、通过session得到transport对象
ts = session.getTransport();

// 3、连上邮件服务器
ts.connect(MAIL_SERVER_HOST, USER, PASSWORD);

// 4、创建邮件
MimeMessage message = new MimeMessage(session);

// 邮件消息头
message.setFrom(new InternetAddress(MAIL_FROM)); // 邮件的发件人
message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO)); // 邮件的收件人
message.setRecipient(Message.RecipientType.CC, new InternetAddress(MAIL_CC)); // 邮件的抄送人
message.setRecipient(Message.RecipientType.BCC, new InternetAddress(MAIL_BCC)); // 邮件的密送人
message.setSubject("测试HTML邮件"); // 邮件的标题

String htmlContent = "<h1>Hello</h1>" + "<p>显示图片<img src='cid:abc.jpg'>1.jpg</p>";
MimeBodyPart text = new MimeBodyPart();
text.setContent(htmlContent, "text/html;charset=UTF-8");
MimeBodyPart image = new MimeBodyPart();
DataHandler dh = new DataHandler(new FileDataSource("D:\\05_Datas\\图库\\吉他少年背影.png"));
image.setDataHandler(dh);
image.setContentID("abc.jpg");

// 描述数据关系
MimeMultipart mm = new MimeMultipart();
mm.addBodyPart(text);
mm.addBodyPart(image);
mm.setSubType("related");
message.setContent(mm);
message.saveChanges();

// 5、发送邮件
ts.sendMessage(message, message.getAllRecipients());
ts.close();
}

发送带附件的邮件

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
public static void main(String[] args) throws Exception {
Properties prop = new Properties();
prop.setProperty("mail.debug", "true");
prop.setProperty("mail.host", MAIL_SERVER_HOST);
prop.setProperty("mail.transport.protocol", "smtp");
prop.setProperty("mail.smtp.auth", "true");

// 1、创建session
Session session = Session.getInstance(prop);

// 2、通过session得到transport对象
Transport ts = session.getTransport();

// 3、连上邮件服务器
ts.connect(MAIL_SERVER_HOST, USER, PASSWORD);

// 4、创建邮件
MimeMessage message = new MimeMessage(session);

// 邮件消息头
message.setFrom(new InternetAddress(MAIL_FROM)); // 邮件的发件人
message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO)); // 邮件的收件人
message.setRecipient(Message.RecipientType.CC, new InternetAddress(MAIL_CC)); // 邮件的抄送人
message.setRecipient(Message.RecipientType.BCC, new InternetAddress(MAIL_BCC)); // 邮件的密送人
message.setSubject("测试带附件邮件"); // 邮件的标题

MimeBodyPart text = new MimeBodyPart();
text.setContent("邮件中有两个附件。", "text/html;charset=UTF-8");

// 描述数据关系
MimeMultipart mm = new MimeMultipart();
mm.setSubType("related");
mm.addBodyPart(text);
String[] files = {
"D:\\00_Temp\\temp\\1.jpg", "D:\\00_Temp\\temp\\2.png"
};

// 添加邮件附件
for (String filename : files) {
MimeBodyPart attachPart = new MimeBodyPart();
attachPart.attachFile(filename);
mm.addBodyPart(attachPart);
}

message.setContent(mm);
message.saveChanges();

// 5、发送邮件
ts.sendMessage(message, message.getAllRecipients());
ts.close();
}

获取邮箱中的邮件

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
 public static void main(String[] args) throws Exception {

// 创建一个有具体连接信息的Properties对象
Properties prop = new Properties();
prop.setProperty("mail.debug", "true");
prop.setProperty("mail.store.protocol", "pop3");
prop.setProperty("mail.pop3.host", MAIL_SERVER_HOST);

// 1、创建session
Session session = Session.getInstance(prop);

// 2、通过session得到Store对象
Store store = session.getStore();

// 3、连上邮件服务器
store.connect(MAIL_SERVER_HOST, USER, PASSWORD);

// 4、获得邮箱内的邮件夹
Folder folder = store.getFolder("inbox");
folder.open(Folder.READ_ONLY);

// 获得邮件夹Folder内的所有邮件Message对象
Message[] messages = folder.getMessages();
for (int i = 0; i < messages.length; i++) {
String subject = messages[i].getSubject();
String from = (messages[i].getFrom()[0]).toString();
System.out.println("第 " + (i + 1) + "封邮件的主题:" + subject);
System.out.println("第 " + (i + 1) + "封邮件的发件人地址:" + from);
}

// 5、关闭
folder.close(false);
store.close();
}

转发邮件

例:获取指定邮件夹下的第一封邮件并转发

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
 public static void main(String[] args) throws Exception {
Properties prop = new Properties();
prop.put("mail.store.protocol", "pop3");
prop.put("mail.pop3.host", MAIL_SERVER_POP3);
prop.put("mail.pop3.starttls.enable", "true");
prop.put("mail.smtp.auth", "true");
prop.put("mail.smtp.host", MAIL_SERVER_SMTP);

// 1、创建session
Session session = Session.getDefaultInstance(prop);

// 2、读取邮件夹
Store store = session.getStore("pop3");
store.connect(MAIL_SERVER_POP3, USER, PASSWORD);
Folder folder = store.getFolder("inbox");
folder.open(Folder.READ_ONLY);

// 获取邮件夹中第1封邮件信息
Message[] messages = folder.getMessages();
if (messages.length <= 0) {
return;
}
Message message = messages[0];

// 打印邮件关键信息
String from = InternetAddress.toString(message.getFrom());
if (from != null) {
System.out.println("From: " + from);
}

String replyTo = InternetAddress.toString(message.getReplyTo());
if (replyTo != null) {
System.out.println("Reply-to: " + replyTo);
}

String to = InternetAddress.toString(message.getRecipients(Message.RecipientType.TO));
if (to != null) {
System.out.println("To: " + to);
}

String subject = message.getSubject();
if (subject != null) {
System.out.println("Subject: " + subject);
}

Date sent = message.getSentDate();
if (sent != null) {
System.out.println("Sent: " + sent);
}

// 设置转发邮件信息头
Message forward = new MimeMessage(session);
forward.setFrom(new InternetAddress(MAIL_FROM));
forward.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO));
forward.setSubject("Fwd: " + message.getSubject());

// 设置转发邮件内容
MimeBodyPart bodyPart = new MimeBodyPart();
bodyPart.setContent(message, "message/rfc822");

Multipart multipart = new MimeMultipart();
multipart.addBodyPart(bodyPart);
forward.setContent(multipart);
forward.saveChanges();

Transport ts = session.getTransport("smtp");
ts.connect(USER, PASSWORD);
ts.sendMessage(forward, forward.getAllRecipients());

folder.close(false);
store.close();
ts.close();
System.out.println("message forwarded successfully....");
}