Dunwu Blog

大道至简,知易行难

Jsoup 快速入门

简介

jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 JQuery 的操作方法来取出和操作数据。

jsoup 工作的流程主要如下:

  1. 从一个 URL,文件或字符串中解析 HTML,并加载为一个 Document 对象。
  2. 使用 DOM 或 CSS 选择器来取出数据;
  3. 可操作 HTML 元素、属性、文本。

jsoup 是基于 MIT 协议发布的,可放心使用于商业项目。

加载

从 HTML 字符串加载一个文档

使用静态 Jsoup.parse(String html) 方法或 Jsoup.parse(String html, String baseUri) 示例代码:

1
2
3
String html = "<html><head><title>First parse</title></head>"
+ "<body><p>Parsed HTML into a doc.</p></body></html>";
Document doc = Jsoup.parse(html);

说明

parse(String html, String baseUri) 这方法能够将输入的 HTML 解析为一个新的文档 (Document),参数 baseUri 是用来将相对 URL 转成绝对 URL,并指定从哪个网站获取文档。如这个方法不适用,你可以使用 parse(String html) 方法来解析成 HTML 字符串如上面的示例。

只要解析的不是空字符串,就能返回一个结构合理的文档,其中包含(至少) 一个 head 和一个 body 元素。

一旦拥有了一个 Document,你就可以使用 Document 中适当的方法或它父类 ElementNode中的方法来取得相关数据。

解析一个 body 片断

问题

假如你有一个 HTML 片断 (比如. 一个 div 包含一对 p 标签; 一个不完整的 HTML 文档) 想对它进行解析。这个 HTML 片断可以是用户提交的一条评论或在一个 CMS 页面中编辑 body 部分。

办法

使用Jsoup.parseBodyFragment(String html)方法.

1
2
3
String html = "<div><p>Lorem ipsum.</p>";
Document doc = Jsoup.parseBodyFragment(html);
Element body = doc.body();

说明

parseBodyFragment 方法创建一个空壳的文档,并插入解析过的 HTML 到body元素中。假如你使用正常的 Jsoup.parse(String html) 方法,通常你也可以得到相同的结果,但是明确将用户输入作为 body 片段处理,以确保用户所提供的任何糟糕的 HTML 都将被解析成 body 元素。

Document.body() 方法能够取得文档 body 元素的所有子元素,与 doc.getElementsByTag("body")相同。

保证安全 Stay safe

假如你可以让用户输入 HTML 内容,那么要小心避免跨站脚本攻击。利用基于 Whitelist 的清除器和 clean(String bodyHtml, Whitelist whitelist)方法来清除用户输入的恶意内容。

从 URL 加载一个文档

使用 Jsoup.connect(String url)方法

1
Document doc = Jsoup.connect("http://example.com/").get();

说明

connect(String url) 方法创建一个新的 Connection, 和 get() 取得和解析一个 HTML 文件。如果从该 URL 获取 HTML 时发生错误,便会抛出 IOException,应适当处理。

Connection 接口还提供一个方法链来解决特殊请求,具体如下:

1
2
3
4
5
6
Document doc = Jsoup.connect("http://example.com")
.data("query", "Java")
.userAgent("Mozilla")
.cookie("auth", "token")
.timeout(3000)
.post();

从一个文件加载一个文档

可以使用静态 Jsoup.parse(File in, String charsetName, String baseUri) 方法

1
2
File input = new File("/tmp/input.html");
Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/");

说明

parse(File in, String charsetName, String baseUri) 这个方法用来加载和解析一个 HTML 文件。如在加载文件的时候发生错误,将抛出 IOException,应作适当处理。

baseUri 参数用于解决文件中 URLs 是相对路径的问题。如果不需要可以传入一个空的字符串。

另外还有一个方法parse(File in, String charsetName) ,它使用文件的路径做为 baseUri。 这个方法适用于如果被解析文件位于网站的本地文件系统,且相关链接也指向该文件系统。

解析

使用 DOM 方法来遍历一个文档

问题

你有一个 HTML 文档要从中提取数据,并了解这个 HTML 文档的结构。

方法

将 HTML 解析成一个Document之后,就可以使用类似于 DOM 的方法进行操作。示例代码:

1
2
3
4
5
6
7
8
9
File input = new File("/tmp/input.html");
Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/");

Element content = doc.getElementById("content");
Elements links = content.getElementsByTag("a");
for (Element link : links) {
String linkHref = link.attr("href");
String linkText = link.text();
}

说明

Elements 这个对象提供了一系列类似于 DOM 的方法来查找元素,抽取并处理其中的数据。

具体如下:

查找元素

  • getElementById(String id)
  • getElementsByTag(String tag)
  • getElementsByClass(String className)
  • getElementsByAttribute(String key) (and related methods)
  • Element siblings: siblingElements(), firstElementSibling(), lastElementSibling();nextElementSibling(), previousElementSibling()
  • Graph: parent(), children(), child(int index)

元素数据

  • attr(String key)获取属性attr(String key, String value)设置属性
  • attributes()获取所有属性
  • id(), className() and classNames()
  • text()获取文本内容text(String value) 设置文本内容
  • html()获取元素内 HTMLhtml(String value)设置元素内的 HTML 内容
  • outerHtml()获取元素外 HTML 内容
  • data()获取数据内容(例如:script 和 style 标签)
  • tag() and tagName()

操作 HTML 和文本

  • append(String html), prepend(String html)
  • appendText(String text), prependText(String text)
  • appendElement(String tagName), prependElement(String tagName)
  • html(String value)

使用选择器语法来查找元素

问题

你想使用类似于 CSS 或 jQuery 的语法来查找和操作元素。

方法

可以使用Element.select(String selector)Elements.select(String selector) 方法实现:

1
2
3
4
5
6
7
8
9
10
11
File input = new File("/tmp/input.html");
Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/");

Elements links = doc.select("a[href]"); //带有href属性的a元素
Elements pngs = doc.select("img[src$=.png]");
//扩展名为.png的图片

Element masthead = doc.select("div.masthead").first();
//class等于masthead的div标签

Elements resultLinks = doc.select("h3.r > a"); //在h3元素之后的a元素

说明

jsoup elements 对象支持类似于CSS (或jquery)的选择器语法,来实现非常强大和灵活的查找功能。.

这个select 方法在Document, Element,或Elements对象中都可以使用。且是上下文相关的,因此可实现指定元素的过滤,或者链式选择访问。

Select 方法将返回一个Elements集合,并提供一组方法来抽取和处理结果。

Selector 选择器概述

  • tagname: 通过标签查找元素,比如:a
  • ns|tag: 通过标签在命名空间查找元素,比如:可以用 fb|name 语法来查找 `` 元素
  • #id: 通过 ID 查找元素,比如:#logo
  • .class: 通过 class 名称查找元素,比如:.masthead
  • [attribute]: 利用属性查找元素,比如:[href]
  • [^attr]: 利用属性名前缀来查找元素,比如:可以用[^data-] 来查找带有 HTML5 Dataset 属性的元素
  • [attr=value]: 利用属性值来查找元素,比如:[width=500]
  • [attr^=value], [attr$=value], [attr*=value]: 利用匹配属性值开头、结尾或包含属性值来查找元素,比如:[href*=/path/]
  • [attr\~=regex]: 利用属性值匹配正则表达式来查找元素,比如: img[src\~=(?i)\.(png|jpe?g)]
  • *: 这个符号将匹配所有元素

Selector 选择器组合使用

  • el##id: 元素+ID,比如: div##logo
  • el.class: 元素+class,比如: div.masthead
  • el[attr]: 元素+class,比如: a[href]
  • 任意组合,比如:a[href].highlight
  • ancestor child: 查找某个元素下子元素,比如:可以用.body p 查找在”body”元素下的所有p元素
  • parent > child: 查找某个父元素下的直接子元素,比如:可以用div.content > p 查找 p 元素,也可以用body > * 查找 body 标签下所有直接子元素
  • siblingA + siblingB: 查找在 A 元素之前第一个同级元素 B,比如:div.head + div
  • siblingA \~ siblingX: 查找 A 元素之前的同级 X 元素,比如:h1 \~ p
  • el, el, el:多个选择器组合,查找匹配任一选择器的唯一元素,例如:div.masthead, div.logo

伪选择器 selectors

  • :lt(n): 查找哪些元素的同级索引值(它的位置在 DOM 树中是相对于它的父节点)小于 n,比如:td:lt(3) 表示小于三列的元素
  • :gt(n):查找哪些元素的同级索引值大于n``,比如div p:gt(2)表示哪些 div 中有包含 2 个以上的 p 元素
  • :eq(n): 查找哪些元素的同级索引值与n相等,比如:form input:eq(1)表示包含一个 input 标签的 Form 元素
  • :has(seletor): 查找匹配选择器包含元素的元素,比如:div:has(p)表示哪些 div 包含了 p 元素
  • :not(selector): 查找与选择器不匹配的元素,比如: div:not(.logo) 表示不包含 class=logo 元素的所有 div 列表
  • :contains(text): 查找包含给定文本的元素,搜索不区分大不写,比如: p:contains(jsoup)
  • :containsOwn(text): 查找直接包含给定文本的元素
  • :matches(regex): 查找哪些元素的文本匹配指定的正则表达式,比如:div:matches((?i)login)
  • :matchesOwn(regex): 查找自身包含文本匹配指定正则表达式的元素
  • 注意:上述伪选择器索引是从 0 开始的,也就是说第一个元素索引值为 0,第二个元素 index 为 1 等

可以查看Selector API 参考来了解更详细的内容

从元素抽取属性,文本和 HTML

问题

在解析获得一个 Document 实例对象,并查找到一些元素之后,你希望取得在这些元素中的数据。

方法

  • 要取得一个属性的值,可以使用Node.attr(String key) 方法
  • 对于一个元素中的文本,可以使用Element.text()方法
  • 对于要取得元素或属性中的 HTML 内容,可以使用Element.html(), 或 Node.outerHtml()方法

示例:

1
2
3
4
5
6
7
8
9
10
11
String html = "<p>An <a href='http://example.com/'><b>example</b></a> link.</p>";
Document doc = Jsoup.parse(html);//解析HTML字符串返回一个Document实现
Element link = doc.select("a").first();//查找第一个a元素

String text = doc.body().text(); // "An example link"//取得字符串中的文本
String linkHref = link.attr("href"); // "http://example.com/"//取得链接地址
String linkText = link.text(); // "example""//取得链接地址中的文本

String linkOuterH = link.outerHtml();
// "<a href="http://example.com"><b>example</b></a>"
String linkInnerH = link.html(); // "<b>example</b>"//取得链接内的html内容

说明

上述方法是元素数据访问的核心办法。此外还其它一些方法可以使用:

  • Element.id()
  • Element.tagName()
  • Element.className() and Element.hasClass(String className)

这些访问器方法都有相应的 setter 方法来更改数据

参见

处理 URLs

问题

你有一个包含相对 URLs 路径的 HTML 文档,需要将这些相对路径转换成绝对路径的 URLs。

方法

  1. 在你解析文档时确保有指定base URI,然后
  2. 使用 abs: 属性前缀来取得包含base URI的绝对路径。代码如下:
1
2
3
4
5
6
Document doc = Jsoup.connect("http://www.open-open.com").get();

Element link = doc.select("a").first();
String relHref = link.attr("href"); // == "/"
String absHref = link.attr("abs:href"); // "http://www.open-open.com/"

说明

在 HTML 元素中,URLs 经常写成相对于文档位置的相对路径: <a href="/download">...</a>. 当你使用 Node.attr(String key) 方法来取得 a 元素的 href 属性时,它将直接返回在 HTML 源码中指定定的值。

假如你需要取得一个绝对路径,需要在属性名前加 abs: 前缀。这样就可以返回包含根路径的 URL 地址attr("abs:href")

因此,在解析 HTML 文档时,定义 base URI 非常重要。

如果你不想使用abs: 前缀,还有一个方法能够实现同样的功能 Node.absUrl(String key)

数据修改

设置属性的值

问题

在你解析一个 Document 之后可能想修改其中的某些属性值,然后再保存到磁盘或都输出到前台页面。

方法

可以使用属性设置方法 Element.attr(String key, String value), 和 Elements.attr(String key, String value).

假如你需要修改一个元素的 class 属性,可以使用 Element.addClass(String className)Element.removeClass(String className) 方法。

Elements 提供了批量操作元素属性和 class 的方法,比如:要为 div 中的每一个 a 元素都添加一个rel="nofollow" 可以使用如下方法:

1
2
doc.select("div.comments a").attr("rel", "nofollow");

说明

Element中的其它方法一样,attr 方法也是返回当 Element (或在使用选择器是返回 Elements集合)。这样能够很方便使用方法连用的书写方式。比如:

1
doc.select("div.masthead").attr("title", "jsoup").addClass("round-box");

设置一个元素的 HTML 内容

问题

你需要一个元素中的 HTML 内容

方法

可以使用Element中的 HTML 设置方法具体如下:

1
2
3
4
5
6
7
8
9
Element div = doc.select("div").first(); // <div></div>
div.html("<p>lorem ipsum</p>"); // <div><p>lorem ipsum</p></div>
div.prepend("<p>First</p>");//在div前添加html内容
div.append("<p>Last</p>");//在div之后添加html内容
// 添完后的结果: <div><p>First</p><p>lorem ipsum</p><p>Last</p></div>

Element span = doc.select("span").first(); // <span>One</span>
span.wrap("<li><a href='http://example.com/'></a></li>");
// 添完后的结果: <li><a href="http://example.com"><span>One</span></a></li>

说明

  • Element.html(String html) 这个方法将先清除元素中的 HTML 内容,然后用传入的 HTML 代替。
  • Element.prepend(String first)Element.append(String last) 方法用于在分别在元素内部 HTML 的前面和后面添加 HTML 内容
  • Element.wrap(String around) 对元素包裹一个外部 HTML 内容。

参见

可以查看 API 参考文档中 Element.prependElement(String tag)Element.appendElement(String tag) 方法来创建新的元素并作为文档的子元素插入其中。

设置元素的文本内容

问题

你需要修改一个 HTML 文档中的文本内容

方法

可以使用Element的设置方法::

1
2
3
4
5
Element div = doc.select("div").first(); // <div></div>
div.text("five > four"); // <div>five &gt; four</div>
div.prepend("First ");
div.append(" Last");
// now: <div>First five &gt; four Last</div>

说明

文本设置方法与 HTML setter 方法一样:

  • Element.text(String text) 将清除一个元素中的内部 HTML 内容,然后提供的文本进行代替
  • Element.prepend(String first)Element.append(String last) 将分别在元素的内部 html 前后添加文本节点。

对于传入的文本如果含有像 <, > 等这样的字符,将以文本处理,而非 HTML。

HTML 清理

消除不受信任的 HTML (来防止 XSS 攻击)

问题

在做网站的时候,经常会提供用户评论的功能。有些不怀好意的用户,会搞一些脚本到评论内容中,而这些脚本可能会破坏整个页面的行为,更严重的是获取一些机要信息,此时需要清理该 HTML,以避免跨站脚本cross-site scripting攻击(XSS)。

方法

使用 jsoup HTML Cleaner 方法进行清除,但需要指定一个可配置的 Whitelist

1
2
3
4
String unsafe =
"<p><a href='http://example.com/' onclick='stealCookies()'>Link</a></p>";
String safe = Jsoup.clean(unsafe, Whitelist.basic());
// now: <p><a href="http://example.com/" rel="nofollow">Link</a></p>

说明

XSS 又叫 CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览该页之时,嵌入其中 Web 里面的 html 代码会被执行,从而达到恶意攻击用户的特殊目的。XSS 属于被动式的攻击,因为其被动且不好利用,所以许多人常忽略其危害性。所以我们经常只让用户输入纯文本的内容,但这样用户体验就比较差了。

一个更好的解决方法就是使用一个富文本编辑器 WYSIWYG 如 CKEditorTinyMCE。这些可以输出 HTML 并能够让用户可视化编辑。虽然他们可以在客户端进行校验,但是这样还不够安全,需要在服务器端进行校验并清除有害的 HTML 代码,这样才能确保输入到你网站的 HTML 是安全的。否则,攻击者能够绕过客户端的 Javascript 验证,并注入不安全的 HMTL 直接进入您的网站。

jsoup 的 whitelist 清理器能够在服务器端对用户输入的 HTML 进行过滤,只输出一些安全的标签和属性。

jsoup 提供了一系列的 Whitelist 基本配置,能够满足大多数要求;但如有必要,也可以进行修改,不过要小心。

这个 cleaner 非常好用不仅可以避免 XSS 攻击,还可以限制用户可以输入的标签范围。

参见

  • 参阅XSS cheat sheet ,有一个例子可以了解为什么不能使用正则表达式,而采用安全的 whitelist parser-based 清理器才是正确的选择。
  • 参阅Cleaner ,了解如何返回一个 Document 对象,而不是字符串
  • 参阅Whitelist,了解如何创建一个自定义的 whitelist
  • nofollow 链接属性了解

参考

ZXing 快速入门

简介

ZXing 是一个开源 Java 类库用于解析多种格式的 1D/2D 条形码。目标是能够对 QR 编码、Data Matrix、UPC 的 1D 条形码进行解码。 其提供了多种平台下的客户端包括:J2ME、J2SE 和 Android。

官网:ZXing github 仓库

实战

本例演示如何在一个非 android 的 Java 项目中使用 ZXing 来生成、解析二维码图片。

安装

maven 项目只需引入依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>

如果非 maven 项目,就去官网下载发布版本:下载地址

生成二维码图片

ZXing 生成二维码图片有以下步骤:

  1. com.google.zxing.MultiFormatWriter 根据内容以及图像编码参数生成图像 2D 矩阵。
  2. com.google.zxing.client.j2se.MatrixToImageWriter 根据图像矩阵生成图片文件或图片缓存 BufferedImage
1
2
3
4
5
6
7
8
9
public void encode(String content, String filepath) throws WriterException, IOException {
int width = 100;
int height = 100;
Map<EncodeHintType, Object> encodeHints = new HashMap<EncodeHintType, Object>();
encodeHints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, encodeHints);
Path path = FileSystems.getDefault().getPath(filepath);
MatrixToImageWriter.writeToPath(bitMatrix, "png", path);
}

解析二维码图片

ZXing 解析二维码图片有以下步骤:

  1. 使用 javax.imageio.ImageIO 读取图片文件,并存为一个 java.awt.image.BufferedImage 对象。

  2. java.awt.image.BufferedImage 转换为 ZXing 能识别的 com.google.zxing.BinaryBitmap 对象。

  3. com.google.zxing.MultiFormatReader 根据图像解码参数来解析 com.google.zxing.BinaryBitmap

1
2
3
4
5
6
7
8
9
10
public String decode(String filepath) throws IOException, NotFoundException {
BufferedImage bufferedImage = ImageIO.read(new FileInputStream(filepath));
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
Binarizer binarizer = new HybridBinarizer(source);
BinaryBitmap bitmap = new BinaryBitmap(binarizer);
HashMap<DecodeHintType, Object> decodeHints = new HashMap<DecodeHintType, Object>();
decodeHints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
Result result = new MultiFormatReader().decode(bitmap, decodeHints);
return result.getText();
}

完整参考示例:测试例代码

以下是一个生成的二维码图片示例:

img

参考

ZXing github 仓库

Thumbnailator 快速入门

简介

Thumbnailator 是一个开源的 Java 项目,它提供了非常简单的 API 来对图片进行缩放、旋转以及加水印的处理。

有多简单呢?简单到一行代码就可以完成图片处理。形式如下:

1
2
3
4
Thumbnails.of(new File("path/to/directory").listFiles())
.size(640, 480)
.outputFormat("jpg")
.toFiles(Rename.PREFIX_DOT_THUMBNAIL);

当然,Thumbnailator 还有一些使用细节,下面我会一一道来。

核心 API

Thumbnails

Thumbnails 是使用 Thumbnailator 创建缩略图的主入口。

它提供了一组初始化 Thumbnails.Builder 的接口。

先看下这组接口的声明:

1
2
3
4
5
6
7
8
9
10
11
12
// 可变长度参数列表
public static Builder<File> of(String... files) {...}
public static Builder<File> of(File... files) {...}
public static Builder<URL> of(URL... urls) {...}
public static Builder<? extends InputStream> of(InputStream... inputStreams) {...}
public static Builder<BufferedImage> of(BufferedImage... images) {...}
// 迭代器(所有实现 Iterable 接口的 Java 对象都可以,当然也包括 List、Set)
public static Builder<File> fromFilenames(Iterable<String> files) {...}
public static Builder<File> fromFiles(Iterable<File> files) {...}
public static Builder<URL> fromURLs(Iterable<URL> urls) {...}
public static Builder<InputStream> fromInputStreams(Iterable<? extends InputStream> inputStreams) {...}
public static Builder<BufferedImage> fromImages(Iterable<BufferedImage> images) {...}

很显然,Thumbnails 允许通过传入文件名、文件、网络图的 URL、图片流、图片缓存多种方式来初始化构造器

因此,你可以根据实际需求来灵活的选择图片的输入方式。

需要注意一点:如果输入是多个对象(无论你是直接输入容器对象或使用可变参数方式传入多个对象),则输出也必须选用输出多个对象的方式,否则会报异常。

Thumbnails.Builder

Thumbnails.BuilderThumbnails 的内部静态类。它用于设置生成缩略图任务的相关参数。

注:Thumbnails.Builder 的构造函数是私有函数。所以,它只允许通过 Thumbnails 的实例化函数来进行初始化。

设置参数的函数

Thumbnails.Builder 提供了一组函数链形式的接口来设置缩放图参数。

以设置大小函数为例:

1
2
3
4
5
6
7
8
9
10
11
public Builder<T> size(int width, int height)
{
updateStatus(Properties.SIZE, Status.ALREADY_SET);
updateStatus(Properties.SCALE, Status.CANNOT_SET);

validateDimensions(width, height);
this.width = width;
this.height = height;

return this;
}

通过返回 this 指针,使得设置参数函数可以以链式调用的方式来使用,形式如下:

1
2
3
4
5
6
Thumbnails.of(new File("original.jpg"))
.size(160, 160)
.rotate(90)
.watermark(Positions.BOTTOM_RIGHT, ImageIO.read(new File("watermark.png")), 0.5f)
.outputQuality(0.8)
.toFile(new File("image-with-watermark.jpg"));

好处,不言自明:那就是大大简化了代码。

输出函数

Thumbnails.Builder 提供了一组重载函数来输出生成的缩放图。

函数声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 返回图片缓存
public List<BufferedImage> asBufferedImages() throws IOException {...}
public BufferedImage asBufferedImage() throws IOException {...}
// 返回文件列表
public List<File> asFiles(Iterable<File> iterable) throws IOException {...}
public List<File> asFiles(Rename rename) throws IOException {...}
public List<File> asFiles(File destinationDir, Rename rename) throws IOException {...}
// 创建文件
public void toFile(File outFile) throws IOException {...}
public void toFile(String outFilepath) throws IOException {...}
public void toFiles(Iterable<File> iterable) throws IOException {...}
public void toFiles(Rename rename) throws IOException {...}
public void toFiles(File destinationDir, Rename rename) throws IOException {...}
// 创建输出流
public void toOutputStream(OutputStream os) throws IOException {...}
public void toOutputStreams(Iterable<? extends OutputStream> iterable) throws IOException {...}

工作流

Thumbnailator 的工作步骤十分简单,可分为三步:

  1. 输入Thumbnails 根据输入初始化构造器—— Thumbnails.Builder

  2. 设置Thumbnails.Builder 设置缩放图片的参数。

  3. 输出Thumbnails.Builder 输出图片文件或图片流。

更多详情可以参考: Thumbnailator 官网 javadoc

实战

前文介绍了 Thumbnailator 的核心 API,接下来我们就可以通过实战来看看 Thumbnailator 究竟可以做些什么。

Thumbnailator 生成什么样的图片,是根据设置参数来决定的。

安装

maven 项目中引入依赖:

1
2
3
4
5
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>[0.4, 0.5)</version>
</dependency>

图片缩放

Thumbnails.Buildersize 函数可以设置新图片精确的宽度和高度,也可以用 scale 函数设置缩放比例。

1
2
3
4
5
6
7
8
9
10
11
Thumbnails.of("oldFile.png")
.size(16, 16)
.toFile("newFile_16_16.png");

Thumbnails.of("oldFile.png")
.scale(2.0)
.toFile("newFile_scale_2.0.png");

Thumbnails.of("oldFile.png")
.scale(1.0, 0.5)
.toFile("newFile_scale_1.0_0.5.png");

oldFile.png

img

newFile_scale_1.0_0.5.png

img

图片旋转

Thumbnails.Buildersize 函数可以设置新图片的旋转角度。

1
2
3
4
5
6
7
8
9
Thumbnails.of("oldFile.png")
.scale(0.8)
.rotate(90)
.toFile("newFile_rotate_90.png");

Thumbnails.of("oldFile.png")
.scale(0.8)
.rotate(180)
.toFile("newFile_rotate_180.png");

newFile_rotate_90.png

img

加水印

Thumbnails.Builderwatermark 函数可以为图片添加水印图片。第一个参数是水印的位置;第二个参数是水印图片的缓存数据;第三个参数是透明度。

1
2
3
4
5
BufferedImage watermarkImage = ImageIO.read(new File("wartermarkFile.png"));
Thumbnails.of("oldFile.png")
.scale(0.8)
.watermark(Positions.BOTTOM_LEFT, watermarkImage, 0.5f)
.toFile("newFile_watermark.png");

wartermarkFile.png

img

newFile_watermark.png

img

批量处理图片

下面以批量给图片加水印来展示一下如何处理多个图片文件。

1
2
3
4
5
6
7
BufferedImage watermarkImage = ImageIO.read(new File("wartermarkFile.png"));

File destinationDir = new File("D:\\watermark\\");
Thumbnails.of("oldFile.png", "oldFile2.png")
.scale(0.8)
.watermark(Positions.BOTTOM_LEFT, watermarkImage, 0.5f)
.toFiles(destinationDir, Rename.PREFIX_DOT_THUMBNAIL);

需要参考完整测试例代码请 点击这里

参考

Thumbnailator 官方示例文档

MyBatis 快速入门

MyBatis 的前身就是 iBatis ,是一个作用在数据持久层的对象关系映射(Object Relational Mapping,简称 ORM)框架。

img

Mybatis 简介

img

什么是 MyBatis

MyBatis 是一款持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

MyBatis vs. Hibernate

MyBatis 和 Hibernate 都是 Java 世界中比较流行的 ORM 框架。我们应该了解其各自的优势,根据项目的需要去抉择在开发中使用哪个框架。

Mybatis 优势

  • MyBatis 可以进行更为细致的 SQL 优化,可以减少查询字段。
  • MyBatis 容易掌握,而 Hibernate 门槛较高。

Hibernate 优势

  • Hibernate 的 DAO 层开发比 MyBatis 简单,Mybatis 需要维护 SQL 和结果映射。
  • Hibernate 对对象的维护和缓存要比 MyBatis 好,对增删改查的对象的维护要方便。
  • Hibernate 数据库移植性很好,MyBatis 的数据库移植性不好,不同的数据库需要写不同 SQL。
  • Hibernate 有更好的二级缓存机制,可以使用第三方缓存。MyBatis 本身提供的缓存机制不佳。

快速入门

这里,我将以一个入门级的示例来演示 Mybatis 是如何工作的。

注:本文后面章节中的原理、源码部分也将基于这个示例来进行讲解。

数据库准备

在本示例中,需要针对一张用户表进行 CRUD 操作。其数据模型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE IF NOT EXISTS user (
id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',
name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名',
age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄',
address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址',
email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件',
PRIMARY KEY (id)
) COMMENT = '用户表';

INSERT INTO user (name, age, address, email)
VALUES ('张三', 18, '北京', 'xxx@163.com');
INSERT INTO user (name, age, address, email)
VALUES ('李四', 19, '上海', 'xxx@163.com');

添加 Mybatis

如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中:

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>

Mybatis 配置

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。

本示例中只是给出最简化的配置。

【示例】mybatis-config.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url"
value="jdbc:mysql://127.0.0.1:3306/spring_tutorial?serverTimezone=UTC" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/mapper/UserMapper.xml" />
</mappers>
</configuration>

说明:上面的配置文件中仅仅指定了数据源连接方式和 User 表的映射配置文件。

Mapper

Mapper.xml

个人理解,Mapper.xml 文件可以看做是 Mybatis 的 JDBC SQL 模板。

【示例】UserMapper.xml 文件

下面是一个通过 Mybatis Generator 自动生成的完整的 Mapper 文件。

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.github.dunwu.spring.orm.mapper.UserMapper">
<resultMap id="BaseResultMap" type="io.github.dunwu.spring.orm.entity.User">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="address" jdbcType="VARCHAR" property="address" />
<result column="email" jdbcType="VARCHAR" property="email" />
</resultMap>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from user
where id = #{id,jdbcType=BIGINT}
</delete>
<insert id="insert" parameterType="io.github.dunwu.spring.orm.entity.User">
insert into user (id, name, age,
address, email)
values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER},
#{address,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR})
</insert>
<update id="updateByPrimaryKey" parameterType="io.github.dunwu.spring.orm.entity.User">
update user
set name = #{name,jdbcType=VARCHAR},
age = #{age,jdbcType=INTEGER},
address = #{address,jdbcType=VARCHAR},
email = #{email,jdbcType=VARCHAR}
where id = #{id,jdbcType=BIGINT}
</update>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select id, name, age, address, email
from user
where id = #{id,jdbcType=BIGINT}
</select>
<select id="selectAll" resultMap="BaseResultMap">
select id, name, age, address, email
from user
</select>
</mapper>

Mapper.java

Mapper.java 文件是 Mapper.xml 对应的 Java 对象。

【示例】UserMapper.java 文件

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

int deleteByPrimaryKey(Long id);

int insert(User record);

User selectByPrimaryKey(Long id);

List<User> selectAll();

int updateByPrimaryKey(User record);

}

对比 UserMapper.java 和 UserMapper.xml 文件,不难发现:

UserMapper.java 中的方法和 UserMapper.xml 的 CRUD 语句元素( <insert><delete><update><select>)存在一一对应关系。

在 Mybatis 中,正是通过方法的全限定名,将二者绑定在一起。

数据实体.java

【示例】User.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
public class User {
private Long id;

private String name;

private Integer age;

private String address;

private String email;

}

<insert><delete><update><select>parameterType 属性以及 <resultMap>type 属性都可能会绑定到数据实体。这样就可以把 JDBC 操作的输入输出和 JavaBean 结合起来,更加方便、易于理解。

测试程序

【示例】MybatisDemo.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MybatisDemo {

public static void main(String[] args) throws Exception {
// 1. 加载 mybatis 配置文件,创建 SqlSessionFactory
// 注:在实际的应用中,SqlSessionFactory 应该是单例
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);

// 2. 创建一个 SqlSession 实例,进行数据库操作
SqlSession sqlSession = factory.openSession();

// 3. Mapper 映射并执行
Long params = 1L;
List<User> list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params);
for (User user : list) {
System.out.println("user name: " + user.getName());
}
// 输出:user name: 张三
}

}

说明:

SqlSession 接口是 Mybatis API 核心中的核心,它代表了 Mybatis 和数据库的一次完整会话。

  • Mybatis 会解析配置,并根据配置创建 SqlSession
  • 然后,Mybatis 将 Mapper 映射为 SqlSession,然后传递参数,执行 SQL 语句并获取结果。

Mybatis xml 配置

配置的详细内容请参考:“ Mybatis 官方文档之配置 ” 。

MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。主要配置项有以下:

Mybatis xml 映射器

SQL XML 映射文件详细内容请参考:“ Mybatis 官方文档之 XML 映射文件 ”。

XML 映射文件只有几个顶级元素:

  • cache – 对给定命名空间的缓存配置。
  • cache-ref – 对其他命名空间缓存配置的引用。
  • resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
  • parameterMap – 已被废弃!老式风格的参数映射。更好的办法是使用内联参数,此元素可能在将来被移除。文档中不会介绍此元素。
  • sql – 可被其他语句引用的可重用语句块。
  • insert – 映射插入语句
  • update – 映射更新语句
  • delete – 映射删除语句
  • select – 映射查询语句

Mybatis 扩展

Mybatis 类型处理器

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。

从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API) 。

你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ExampleTypeHandler.java
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {

@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}

@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}

@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}

@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}
1
2
3
4
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。

Mybatis 插件

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
1
2
3
4
5
6
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。

参考资料

Mybatis 原理

Mybatis 的前身就是 iBatis ,是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。本文以一个 Mybatis 完整示例为切入点,结合 Mybatis 底层源码分析,图文并茂的讲解 Mybatis 的核心工作机制。

Mybatis 完整示例

这里,我将以一个入门级的示例来演示 Mybatis 是如何工作的。

注:本文后面章节中的原理、源码部分也将基于这个示例来进行讲解。

完整示例源码地址

数据库准备

在本示例中,需要针对一张用户表进行 CRUD 操作。其数据模型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE IF NOT EXISTS user (
id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',
name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名',
age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄',
address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址',
email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件',
PRIMARY KEY (id)
) COMMENT = '用户表';

INSERT INTO user (name, age, address, email)
VALUES ('张三', 18, '北京', 'xxx@163.com');
INSERT INTO user (name, age, address, email)
VALUES ('李四', 19, '上海', 'xxx@163.com');

添加 Mybatis

如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中:

1
2
3
4
5
<dependency>
<groupId>org.Mybatis</groupId>
<artifactId>Mybatis</artifactId>
<version>x.x.x</version>
</dependency>

Mybatis 配置

XML 配置文件中包含了对 Mybatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。

本示例中只是给出最简化的配置。

【示例】Mybatis-config.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE configuration PUBLIC "-//Mybatis.org//DTD Config 3.0//EN"
"http://Mybatis.org/dtd/Mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url"
value="jdbc:mysql://127.0.0.1:3306/spring_tutorial?serverTimezone=UTC" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="Mybatis/mapper/UserMapper.xml" />
</mappers>
</configuration>

说明:上面的配置文件中仅仅指定了数据源连接方式和 User 表的映射配置文件。

Mapper

Mapper.xml

个人理解,Mapper.xml 文件可以看做是 Mybatis 的 JDBC SQL 模板。

【示例】UserMapper.xml 文件

下面是一个通过 Mybatis Generator 自动生成的完整的 Mapper 文件。

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//Mybatis.org//DTD Mapper 3.0//EN" "http://Mybatis.org/dtd/Mybatis-3-mapper.dtd">
<mapper namespace="io.github.dunwu.spring.orm.mapper.UserMapper">
<resultMap id="BaseResultMap" type="io.github.dunwu.spring.orm.entity.User">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="address" jdbcType="VARCHAR" property="address" />
<result column="email" jdbcType="VARCHAR" property="email" />
</resultMap>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from user
where id = #{id,jdbcType=BIGINT}
</delete>
<insert id="insert" parameterType="io.github.dunwu.spring.orm.entity.User">
insert into user (id, name, age,
address, email)
values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER},
#{address,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR})
</insert>
<update id="updateByPrimaryKey" parameterType="io.github.dunwu.spring.orm.entity.User">
update user
set name = #{name,jdbcType=VARCHAR},
age = #{age,jdbcType=INTEGER},
address = #{address,jdbcType=VARCHAR},
email = #{email,jdbcType=VARCHAR}
where id = #{id,jdbcType=BIGINT}
</update>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select id, name, age, address, email
from user
where id = #{id,jdbcType=BIGINT}
</select>
<select id="selectAll" resultMap="BaseResultMap">
select id, name, age, address, email
from user
</select>
</mapper>

Mapper.java

Mapper.java 文件是 Mapper.xml 对应的 Java 对象。

【示例】UserMapper.java 文件

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

int deleteByPrimaryKey(Long id);

int insert(User record);

User selectByPrimaryKey(Long id);

List<User> selectAll();

int updateByPrimaryKey(User record);

}

对比 UserMapper.java 和 UserMapper.xml 文件,不难发现:

UserMapper.java 中的方法和 UserMapper.xml 的 CRUD 语句元素( <insert><delete><update><select>)存在一一对应关系。

在 Mybatis 中,正是通过方法的全限定名,将二者绑定在一起。

数据实体.java

【示例】User.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
public class User {
private Long id;

private String name;

private Integer age;

private String address;

private String email;

}

<insert><delete><update><select>parameterType 属性以及 <resultMap>type 属性都可能会绑定到数据实体。这样就可以把 JDBC 操作的输入输出和 JavaBean 结合起来,更加方便、易于理解。

测试程序

【示例】MybatisDemo.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MybatisDemo {

public static void main(String[] args) throws Exception {
// 1. 加载 Mybatis 配置文件,创建 SqlSessionFactory
// 注:在实际的应用中,SqlSessionFactory 应该是单例
InputStream inputStream = Resources.getResourceAsStream("Mybatis/Mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);

// 2. 创建一个 SqlSession 实例,进行数据库操作
SqlSession sqlSession = factory.openSession();

// 3. Mapper 映射并执行
Long params = 1L;
List<User> list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params);
for (User user : list) {
System.out.println("user name: " + user.getName());
}
// 输出:user name: 张三
}

}

说明:

SqlSession 接口是 Mybatis API 核心中的核心,它代表了 Mybatis 和数据库的一次完整会话。

  • Mybatis 会解析配置,并根据配置创建 SqlSession
  • 然后,Mybatis 将 Mapper 映射为 SqlSession,然后传递参数,执行 SQL 语句并获取结果。

Mybatis 生命周期

img

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 的职责

SqlSessionFactoryBuilder 负责创建 SqlSessionFactory 实例SqlSessionFactoryBuilder 可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。

Configuration 类包含了对一个 SqlSessionFactory 实例你可能关心的所有内容。

img

SqlSessionFactoryBuilder 应用了建造者设计模式,它有五个 build 方法,允许你通过不同的资源创建 SqlSessionFactory 实例。

1
2
3
4
5
SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

SqlSessionFactoryBuilder 的生命周期

SqlSessionFactoryBuilder 可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 职责

SqlSessionFactory 负责创建 SqlSession 实例。

img

SqlSessionFactory 应用了工厂设计模式,它提供了一组方法,用于创建 SqlSession 实例。

1
2
3
4
5
6
7
8
9
SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();

方法说明:

  • 默认的 openSession() 方法没有参数,它会创建具备如下特性的 SqlSession
    • 事务作用域将会开启(也就是不自动提交)。
    • 将由当前环境配置的 DataSource 实例中获取 Connection 对象。
    • 事务隔离级别将会使用驱动或数据源的默认设置。
    • 预处理语句不会被复用,也不会批量处理更新。
  • TransactionIsolationLevel 表示事务隔离级别,它对应着 JDBC 的五个事务隔离级别。
  • ExecutorType 枚举类型定义了三个值:
    • ExecutorType.SIMPLE:该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
    • ExecutorType.REUSE:该类型的执行器会复用预处理语句。
    • ExecutorType.BATCH:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。

SqlSessionFactory 生命周期

SqlSessionFactory 应该以单例形式在应用的运行期间一直存在。

SqlSession

SqlSession 职责

Mybatis 的主要 Java 接口就是 SqlSession。它包含了所有执行语句,获取映射器和管理事务等方法。

详细内容可以参考:“ Mybatis 官方文档之 SqlSessions ” 。

SqlSession 类的方法可以按照下图进行大致分类:

img

SqlSession 生命周期

SqlSessions 是由 SqlSessionFactory 实例创建的;而 SqlSessionFactory 是由 SqlSessionFactoryBuilder 创建的。

🔔 注意:当 Mybatis 与一些依赖注入框架(如 Spring 或者 Guice)同时使用时,SqlSessions 将被依赖注入框架所创建,所以你不需要使用 SqlSessionFactoryBuilder 或者 SqlSessionFactory

每个线程都应该有它自己的 SqlSession 实例。

SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 正确在 Web 中使用 SqlSession 的场景是:每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。

编程模式:

1
2
3
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}

映射器

映射器职责

映射器是一些由用户创建的、绑定 SQL 语句的接口。

SqlSession 中的 insertupdatedeleteselect 方法都很强大,但也有些繁琐。更通用的方式是使用映射器类来执行映射语句。一个映射器类就是一个仅需声明与 SqlSession 方法相匹配的方法的接口类

Mybatis 将配置文件中的每一个 <mapper> 节点抽象为一个 Mapper 接口,而这个接口中声明的方法和跟 <mapper> 节点中的 <select|update|delete|insert> 节点相对应,即 <select|update|delete|insert> 节点的 id 值为 Mapper 接口中的方法名称,parameterType 值表示 Mapper 对应方法的入参类型,而 resultMap 值则对应了 Mapper 接口表示的返回值类型或者返回结果集的元素类型。

Mybatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例;Mybatis 会根据这个方法的方法名和参数类型,确定 Statement Id,然后和 SqlSession 进行映射,底层还是通过 SqlSession 完成和数据库的交互。

下面的示例展示了一些方法签名以及它们是如何映射到 SqlSession 上的。

img

注意

  • 映射器接口不需要去实现任何接口或继承自任何类。只要方法可以被唯一标识对应的映射语句就可以了。
  • 映射器接口可以继承自其他接口。当使用 XML 来构建映射器接口时要保证语句被包含在合适的命名空间中。而且,唯一的限制就是你不能在两个继承关系的接口中拥有相同的方法签名(潜在的危险做法不可取)。

映射器生命周期

映射器接口的实例是从 SqlSession 中获得的。因此从技术层面讲,任何映射器实例的最大作用域是和请求它们的 SqlSession 相同的。尽管如此,映射器实例的最佳作用域是方法作用域。 也就是说,映射器实例应该在调用它们的方法中被请求,用过之后即可丢弃。

编程模式:

1
2
3
4
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 你的应用逻辑代码
}
  • 映射器注解

Mybatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,而且映射语句也是定义在 XML 中的。Mybatis 3 以后,支持注解配置。注解配置基于配置 API;而配置 API 基于 XML 配置。

Mybatis 支持诸如 @Insert@Update@Delete@Select@Result 等注解。

详细内容请参考:Mybatis 官方文档之 sqlSessions,其中列举了 Mybatis 支持的注解清单,以及基本用法。

Mybatis 的架构

从 Mybatis 代码实现的角度来看,Mybatis 的主要组件有以下几个:

  • SqlSession - 作为 Mybatis 工作的主要顶层 API,表示和数据库交互的会话,完成必要数据库增删改查功能。
  • Executor - Mybatis 执行器,是 Mybatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护。
  • StatementHandler - 封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 结果集转换成 List 集合。
  • ParameterHandler - 负责对用户传递的参数转换成 JDBC Statement 所需要的参数。
  • ResultSetHandler - 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合。
  • TypeHandler - 负责 java 数据类型和 jdbc 数据类型之间的映射和转换。
  • MappedStatement - MappedStatement 维护了一条 <select|update|delete|insert> 节点的封装。
  • SqlSource - 负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回。
  • BoundSql - 表示动态生成的 SQL 语句以及相应的参数信息。
  • Configuration - Mybatis 所有的配置信息都维持在 Configuration 对象之中。

这些组件的架构层次如下:

img

配置层

配置层决定了 Mybatis 的工作方式。

Mybatis 提供了两种配置方式:

  • 基于 XML 配置文件的方式
  • 基于 Java API 的方式

SqlSessionFactoryBuilder 会根据配置创建 SqlSessionFactory

SqlSessionFactory 负责创建 SqlSessions

接口层

接口层负责和数据库交互的方式。

Mybatis 和数据库的交互有两种方式:

  • 使用 SqlSession:SqlSession 封装了所有执行语句,获取映射器和管理事务的方法。
    • 用户只需要传入 Statement Id 和查询参数给 SqlSession 对象,就可以很方便的和数据库进行交互。
    • 这种方式的缺点是不符合面向对象编程的范式。
  • 使用 Mapper 接口:Mybatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例;Mybatis 会根据这个方法的方法名和参数类型,确定 Statement Id,然后和 SqlSession 进行映射,底层还是通过 SqlSession 完成和数据库的交互。

数据处理层

数据处理层可以说是 Mybatis 的核心,从大的方面上讲,它要完成两个功能:

  • 根据传参 Statement 和参数构建动态 SQL 语句
    • 动态语句生成可以说是 Mybatis 框架非常优雅的一个设计,Mybatis 通过传入的参数值,使用 Ognl 来动态地构造 SQL 语句,使得 Mybatis 有很强的灵活性和扩展性。
    • 参数映射指的是对于 java 数据类型和 jdbc 数据类型之间的转换:这里有包括两个过程:查询阶段,我们要将 java 类型的数据,转换成 jdbc 类型的数据,通过 preparedStatement.setXXX() 来设值;另一个就是对 resultset 查询结果集的 jdbcType 数据转换成 java 数据类型。
  • 执行 SQL 语句以及处理响应结果集 ResultSet
    • 动态 SQL 语句生成之后,Mybatis 将执行 SQL 语句,并将可能返回的结果集转换成 List<E> 列表。
    • Mybatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。

框架支撑层

  • 事务管理机制 - Mybatis 将事务抽象成了 Transaction 接口。Mybatis 的事务管理分为两种形式:

    • 使用 JDBC 的事务管理机制:即利用 java.sql.Connection 对象完成对事务的提交(commit)、回滚(rollback)、关闭(close)等。
    • 使用 MANAGED 的事务管理机制:Mybatis 自身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的管理。
  • 连接池管理

  • SQL 语句的配置 - 支持两种方式:

    • xml 配置
    • 注解配置
  • 缓存机制 - Mybatis 采用两级缓存结构

    • 一级缓存是 Session 会话级别的缓存 - 一级缓存又被称之为本地缓存。一般而言,一个 SqlSession 对象会使用一个 Executor 对象来完成会话操作,Executor 对象会维护一个 Cache 缓存,以提高查询性能。
      • 一级缓存的生命周期是 Session 会话级别的。
    • 二级缓存是 Application 应用级别的缓存 - 用户配置了 "cacheEnabled=true",才会开启二级缓存。
      • 如果开启了二级缓存,SqlSession 会先使用 CachingExecutor 对象来处理查询请求。CachingExecutor 会在二级缓存中查看是否有匹配的数据,如果匹配,则直接返回缓存结果;如果缓存中没有,再交给真正的 Executor 对象来完成查询,之后 CachingExecutor 会将真正 Executor 返回的查询结果放置到缓存中,然后在返回给用户。
      • 二级缓存的生命周期是应用级别的。

img

SqlSession 内部工作机制

从前文,我们已经了解了,Mybatis 封装了对数据库的访问,把对数据库的会话和事务控制放到了 SqlSession 对象中。那么具体是如何工作的呢?接下来,我们通过源码解读来进行分析。

img

SqlSession 对于 insert、update、delete、select 的内部处理机制基本上大同小异。所以,接下来,我会以一次完整的 select 查询流程为例讲解 SqlSession 内部的工作机制。相信读者如果理解了 select 的处理流程,对于其他 CRUD 操作也能做到一通百通。

SqlSession 子组件

前面的内容已经介绍了:SqlSession 是 Mybatis 的顶层接口,它提供了所有执行语句,获取映射器和管理事务等方法。

实际上,SqlSession 是通过聚合多个子组件,让每个子组件负责各自功能的方式,实现了任务的下发。

在了解各个子组件工作机制前,先让我们简单认识一下 SqlSession 的核心子组件。

Executor

Executor 即执行器,它负责生成动态 SQL 以及管理缓存。

img

  • Executor 即执行器接口。
  • BaseExecutorExecutor 的抽象类,它采用了模板方法设计模式,内置了一些共性方法,而将定制化方法留给子类去实现。
  • SimpleExecutor 是最简单的执行器。它只会直接执行 SQL,不会做额外的事。
  • BatchExecutor 是批处理执行器。它的作用是通过批处理来优化性能。值得注意的是,批量更新操作,由于内部有缓存机制,使用完后需要调用 flushStatements 来清除缓存。
  • ReuseExecutor 是可重用的执行器。重用的对象是 Statement,也就是说,该执行器会缓存同一个 SQL 的 Statement,避免重复创建 Statement。其内部的实现是通过一个 HashMap 来维护 Statement 对象的。由于当前 Map 只在该 session 中有效,所以使用完后需要调用 flushStatements 来清除 Map。
  • CachingExecutor 是缓存执行器。它只在启用二级缓存时才会用到。

StatementHandler

StatementHandler 对象负责设置 Statement 对象中的查询参数、处理 JDBC 返回的 resultSet,将 resultSet 加工为 List 集合返回。

StatementHandler 的家族成员:

img

  • StatementHandler 是接口;
  • BaseStatementHandler 是实现 StatementHandler 的抽象类,内置一些共性方法;
  • SimpleStatementHandler 负责处理 Statement
  • PreparedStatementHandler 负责处理 PreparedStatement
  • CallableStatementHandler 负责处理 CallableStatement
  • RoutingStatementHandler 负责代理 StatementHandler 具体子类,根据 Statement 类型,选择实例化 SimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler

ParameterHandler

ParameterHandler 负责将传入的 Java 对象转换 JDBC 类型对象,并为 PreparedStatement 的动态 SQL 填充数值。

ParameterHandler 只有一个具体实现类,即 DefaultParameterHandler

ResultSetHandler

ResultSetHandler 负责两件事:

  • 处理 Statement 执行后产生的结果集,生成结果列表
  • 处理存储过程执行后的输出参数

ResultSetHandler 只有一个具体实现类,即 DefaultResultSetHandler

TypeHandler

TypeHandler 负责将 Java 对象类型和 JDBC 类型进行相互转换。

SqlSession 和 Mapper

先来回忆一下 Mybatis 完整示例章节的 测试程序部分的代码。

MybatisDemo.java 文件中的代码片段:

1
2
3
4
5
6
7
8
9
// 2. 创建一个 SqlSession 实例,进行数据库操作
SqlSession sqlSession = factory.openSession();

// 3. Mapper 映射并执行
Long params = 1L;
List<User> list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params);
for (User user : list) {
System.out.println("user name: " + user.getName());
}

示例代码中,给 sqlSession 对象的传递一个配置的 Sql 语句的 Statement Id 和参数,然后返回结果

io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey 是配置在 UserMapper.xml 的 Statement ID,params 是 SQL 参数。

UserMapper.xml 文件中的代码片段:

1
2
3
4
5
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select id, name, age, address, email
from user
where id = #{id,jdbcType=BIGINT}
</select>

Mybatis 通过方法的全限定名,将 SqlSession 和 Mapper 相互映射起来。

SqlSession 和 Executor

org.apache.ibatis.session.defaults.DefaultSqlSessionselectList 方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public <E> List<E> selectList(String statement) {
return this.selectList(statement, null);
}

@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 1. 根据 Statement Id,在配置对象 Configuration 中查找和配置文件相对应的 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 2. 将 SQL 语句交由执行器 Executor 处理
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

说明:

Mybatis 所有的配置信息都维持在 Configuration 对象之中。中维护了一个 Map<String, MappedStatement> 对象。其中,key 为 Mapper 方法的全限定名(对于本例而言,key 就是 io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey ),value 为 MappedStatement 对象。所以,传入 Statement Id 就可以从 Map 中找到对应的 MappedStatement

MappedStatement 维护了一个 Mapper 方法的元数据信息,其数据组织可以参考下面的 debug 截图:

img

小结:

通过 “SqlSession 和 Mapper” 以及 “SqlSession 和 Executor” 这两节,我们已经知道:

SqlSession 的职能是:根据 Statement ID, 在 Configuration 中获取到对应的 MappedStatement 对象,然后调用 Executor 来执行具体的操作。

Executor 工作流程

继续上一节的流程,SqlSession 将 SQL 语句交由执行器 Executor 处理。Executor 又做了哪些事儿呢?

(1)执行器查询入口

1
2
3
4
5
6
7
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. 根据传参,动态生成需要执行的 SQL 语句,用 BoundSql 对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 根据传参,创建一个缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

执行器查询入口主要做两件事:

  • 生成动态 SQL:根据传参,动态生成需要执行的 SQL 语句,用 BoundSql 对象表示。
  • 管理缓存:根据传参,创建一个缓存 Key。

(2)执行器查询第二入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 略
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// 3. 缓存中有值,则直接从缓存中取数据;否则,查询数据库
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
// 略
return list;
}

实际查询方法主要的职能是判断缓存 key 是否能命中缓存:

  • 命中,则将缓存中数据返回;
  • 不命中,则查询数据库:

(3)查询数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 4. 执行查询,获取 List 结果,并将查询的结果更新本地缓存中
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

queryFromDatabase 方法的职责是调用 doQuery,向数据库发起查询,并将返回的结果更新到本地缓存。

(4)实际查询方法

SimpleExecutor 类的 doQuery()方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 5. 根据既有的参数,创建StatementHandler对象来执行查询操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 6. 创建java.Sql.Statement对象,传递给StatementHandler对象
stmt = prepareStatement(handler, ms.getStatementLog());
// 7. 调用StatementHandler.query()方法,返回List结果
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}

上述的 Executor.query()方法几经转折,最后会创建一个 StatementHandler 对象,然后将必要的参数传递给 StatementHandler,使用 StatementHandler 来完成对数据库的查询,最终返回 List 结果集。
从上面的代码中我们可以看出,Executor 的功能和作用是:

  1. 根据传递的参数,完成 SQL 语句的动态解析,生成 BoundSql 对象,供 StatementHandler 使用;

  2. 为查询创建缓存,以提高性能

  3. 创建 JDBC 的 Statement 连接对象,传递给 StatementHandler 对象,返回 List 查询结果。

prepareStatement() 方法的实现:

1
2
3
4
5
6
7
8
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
//对创建的Statement对象设置参数,即设置SQL 语句中 ? 设置为指定的参数
handler.parameterize(stmt);
return stmt;
}

对于 JDBC 的 PreparedStatement 类型的对象,创建的过程中,我们使用的是 SQL 语句字符串会包含 若干个? 占位符,我们其后再对占位符进行设值。

StatementHandler 工作流程

StatementHandler 有一个子类 RoutingStatementHandler,它负责代理其他 StatementHandler 子类的工作。

它会根据配置的 Statement 类型,选择实例化相应的 StatementHandler,然后由其代理对象完成工作。

【源码】RoutingStatementHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}

}

【源码】RoutingStatementHandlerparameterize 方法源码

【源码】PreparedStatementHandlerparameterize 方法源码

StatementHandler 使用 ParameterHandler 对象来完成对 Statement 的赋值。

1
2
3
4
5
@Override
public void parameterize(Statement statement) throws SQLException {
// 使用 ParameterHandler 对象来完成对 Statement 的设值
parameterHandler.setParameters((PreparedStatement) statement);
}

【源码】StatementHandlerquery 方法源码

StatementHandler 使用 ResultSetHandler 对象来完成对 ResultSet 的处理。

1
2
3
4
5
6
7
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
// 使用ResultHandler来处理ResultSet
return resultSetHandler.handleResultSets(ps);
}

ParameterHandler 工作流程

【源码】DefaultParameterHandlersetParameters 方法

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
 @Override
public void setParameters(PreparedStatement ps) {
// parameterMappings 是对占位符 #{} 对应参数的封装
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// 不处理存储过程中的参数
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
// 获取对应的实际数值
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
// 获取对象中相应的属性或查找 Map 对象中的值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}

TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 通过 TypeHandler 将 Java 对象参数转为 JDBC 类型的参数
// 然后,将数值动态绑定到 PreparedStaement 中
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}

ResultSetHandler 工作流程

ResultSetHandler 的实现可以概括为:将 Statement 执行后的结果集,按照 Mapper 文件中配置的 ResultTypeResultMap 来转换成对应的 JavaBean 对象,最后将结果返回。

【源码】DefaultResultSetHandlerhandleResultSets 方法

handleResultSets 方法是 DefaultResultSetHandler 的最关键方法。其实现如下:

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
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

final List<Object> multipleResults = new ArrayList<>();

int resultSetCount = 0;
// 第一个结果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
// 判断结果集的数量
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
// 遍历处理结果集
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}

String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}

return collapseSingleResultList(multipleResults);
}

参考资料

Shiro 快速入门

Shiro 是一个安全框架,具有认证、授权、加密、会话管理功能。

一、Shiro 简介

Shiro 特性

核心功能:

  • Authentication - 认证。验证用户是不是拥有相应的身份。
  • Authorization - 授权。验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
  • Session Manager - 会话管理。即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中。会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的。
  • Cryptography - 加密。保护数据的安全性,如密码加密存储到数据库,而不是明文存储。

辅助功能:

  • Web Support - Web 支持。可以非常容易的集成到 Web 环境;
  • Caching - 缓存。比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
  • Concurrency - 并发。Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing - 测试。提供测试支持;
  • Run As - 运行方式。允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me - 记住我。即一次登录后,下次再访问免登录。

:bell: 注意:Shiro 不会去维护用户、维护权限;这些需要我们自己去提供;然后通过相应的接口注入给 Shiro 即可。

Shiro 架构概述

  • Subject - 主题。它代表当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它——当前和软件交互的任何事件。Subject 是 Shiro 的入口。

    • PrincipalsSubject 的“识别属性”。Principals 可以是任何可以识别 Subject 的东西,例如名字(姓氏),姓氏(姓氏或姓氏),用户名,社会保险号等。当然,Principals 在应用程序中最好是惟一的。
    • Credentials 通常是仅由 Subject 知道的秘密值,用作他们实际上“拥有”所主张身份的佐证 凭据的一些常见示例是密码,生物特征数据(例如指纹和视网膜扫描)以及 X.509 证书。
  • SecurityManager - 安全管理。它是 Shiro 的核心,所有与安全有关的操作(认证、授权、及会话、缓存的管理)都与 SecurityManager 交互,且它管理着所有 Subject

  • Realm - 。用于访问安全相关数据,可以视为应用自身的数据源,需要开发者自己实现。Shiro 会通过 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

SecurityManager

SecurityManager 是 Shiro 框架核心中的核心,它相当于 Shiro 的总指挥,负责调度所有行为,包括:认证、授权、获取安全数据(调用 Realm)、会话管理等。

img

SecurityManager 聚合了以下组件:

  • Authenticator - 认证器,负责认证。如果用户需要定制认证策略,可以实现此接口。
  • Authorizer - 授权器,负责权限控制。用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • SessionManager - 会话管理器。Shiro 抽象了一个自己的 Session 来管理主体与应用之间交互的数据。
  • SessionDAO - 会话 DAO 用于存储会话,需要用户自己实现。
  • CacheManager - 缓存控制器。用于管理如用户、角色、权限等信息的缓存。
  • Cryptography - 密码器。用于对数据加密、解密。

二、Shiro 认证

认证 Subject

验证 Subject 的过程可以有效地分为三个不同的步骤:

(1)收集 Subject 提交的 PrincipalsCredentials

1
2
3
4
5
//Example using most common scenario of username/password pair:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

//"Remember Me" built-in:
token.setRememberMe(true);

(2)提交 PrincipalsCredentials 以进行身份验证。

1
2
3
Subject currentUser = SecurityUtils.getSubject();

currentUser.login(token);

(3)如果提交成功,则允许访问,否则重试身份验证或阻止访问。

1
2
3
4
5
6
7
8
9
10
try {
currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
//unexpected error?
}

Remembered 和 Authenticated

  • Remembered - 记住我。被记住的 Subject 不是匿名的,并且具有已知的身份(即 subject.getPrincipals() 是非空的)。 但是,在先前的会话期间,通过先前的身份验证会记住此身份。 如果 subject.isRemembered() 返回 true,则认为该主题已被记住。
  • Authenticated - 已认证。已认证的 Subject 是在当前会话期间已成功认证的 Subject。 如果 subject.isAuthenticated() 返回 true,则认为该 Subject 已通过身份验证。

登出

当 Subject 与应用程序完成交互后,可以调用 subject.logout() 登出,即放弃所有标识信息。

1
currentUser.logout();

认证流程

img

  1. 应用程序代码调用 Subject.login 方法,传入构造的 AuthenticationToken 实例,该实例代表最终用户的 PrincipalsCredentials

  2. Subject 实例(通常是 DelegatingSubject(或子类))通过调用 securityManager.login(token)委托应用程序的 SecurityManager,在此处开始实际的身份验证工作。

  3. SecurityManager 接收令牌,并通过调用 authenticator.authenticate(token)来简单地委派给其内部 Authenticator 实例。这几乎总是一个 ModularRealmAuthenticator 实例,它支持在身份验证期间协调一个或多个 Realm 实例。

  4. 如果为该应用程序配置了多个 Realm,则 ModularRealmAuthenticator 实例将利用其配置的 AuthenticationStrategy 发起多域验证尝试。在调用领域进行身份验证之前,期间和之后,将调用 AuthenticationStrategy 以使其对每个领域的结果做出反应。

  5. 请咨询每个已配置的 Realm,以查看其是否支持提交的 AuthenticationToken。 如果是这样,将使用提交的令牌调用支持 RealmgetAuthenticationInfo 方法。 getAuthenticationInfo 方法有效地表示对该特定 Realm 的单个身份验证尝试。

认证策略

当为一个应用程序配置两个或多个领域时,ModularRealmAuthenticator 依赖于内部 AuthenticationStrategy 组件来确定认证尝试成功或失败的条件。

例如,如果只有一个 Realm 成功地对 AuthenticationToken 进行身份验证,而所有其他 Realm 都失败了,那么该身份验证尝试是否被视为成功?还是必须所有领域都成功进行身份验证才能将整体尝试视为成功?或者,如果某个领域成功通过身份验证,是否有必要进一步咨询其他领域? AuthenticationStrategy 根据应用程序的需求做出适当的决定。

AuthenticationStrategy 是无状态组件,在尝试进行身份验证时会被查询 4 次(这 4 种交互所需的任何必要状态都将作为方法参数给出):

  • 在任何领域被调用之前
  • 在调用单个 RealmgetAuthenticationInfo 方法之前
  • 在调用单个 RealmgetAuthenticationInfo 方法之后
  • 在所有领域都被调用之后

AuthenticationStrategy 还负责汇总每个成功 Realm 的结果,并将它们“捆绑”成单个 AuthenticationInfo 表示形式。最终的聚合 AuthenticationInfo 实例是 Authenticator 实例返回的结果,也是 Shiro 用来表示主体的最终身份(也称为委托人)的东西。

AuthenticationStrategy 描述
AtLeastOneSuccessfulStrategy 只要有一个 Realm 成功认证,则整个尝试都被视为成功。
FirstSuccessfulStrategy 仅使用从第一个成功通过身份验证的 Realm 返回的信息,所有其他 Realm 将被忽略。
AllSuccessfulStrategy 只有所有 Realm 成功认证,则整个尝试才被视为成功。

:link: 更多认证细节可以参考:Apache Shiro Authentication

三、Shiro 授权

授权,也称为访问控制,是管理对资源的访问的过程。 换句话说,控制谁有权访问应用程序中的内容。

授权元素

授权有三个核心要素:权限、角色和用户。

权限

权限示例:

  • 打开一个文件
  • 查看 /user/list web 页面
  • 查询记录
  • 删除一条记录

大多数资源都支持一般的 CRUD 操作。除此以外,对于一些特定的资源,任何有意义的行为都是可以的。基本的设计思路是:权限控制,至少是基于资源和行为。

角色

角色是一个命名实体,通常代表一组行为或职责。这些行为会转化为:谁可以在应用程序中执行哪些行为?谁不可以在程序中执行哪些行为?

角色通常是分配给用户帐户的,因此通过关联,用户可以获得自身角色所赋予的权限。

用户

用户本质上是应用程序的“用户”。

用户(即 Shiro 的 Subject)通过与角色或直接权限的关联在应用程序中执行某些行为。

基于角色的授权

如果授权是基于角色赋予权限的数据模型,编程模式如下:

【示例一】

1
2
3
4
5
6
7
Subject currentUser = SecurityUtils.getSubject();

if (currentUser.hasRole("administrator")) {
//show the admin button
} else {
//don't show the button? Grey it out?
}

【示例二】

1
2
3
4
5
6
Subject currentUser = SecurityUtils.getSubject();

// 检查当前 Subject 是否有某种权限
// 如果有,直接跳过;如果没有,Shiro 会抛出 AuthorizationException
currentUser.checkRole("bankTeller");
openBankAccount();

提示:方式二相比方式一,代码更简洁

基于权限的授权

更好的授权策略通常是基于权限的授权。基于权限的授权,由于它和应用程序的原始功能(针对具体资源上的行为)紧密相关,所以基于权限的授权源代码会在功能更改时同步更改(而不是在安全策略发生更改时)。 这意味着与类似的基于角色的授权代码相比,修改代码的影响面要小得多。

【示例】基于对象的权限检查

1
2
3
4
5
6
7
8
9
Permission printPermission = new PrinterPermission("laserjet4400n", "print");

Subject currentUser = SecurityUtils.getSubject();

if (currentUser.isPermitted(printPermission)) {
//show the Print button
} else {
//don't show the button? Grey it out?
}

在对象中存储权限控制信息,但这种方式较为繁重

【示例】字符串定义权限控制信息

1
2
3
4
5
6
7
Subject currentUser = SecurityUtils.getSubject();

if (currentUser.isPermitted("printer:print:laserjet4400n")) {
//show the Print button
} else {
//don't show the button? Grey it out?
}

使用 : 分隔,表示资源类型、行为、资源 ID,Shiro 提供了默认实现: org.apache.shiro.authz.permission.WildcardPermission

这种权限控制方式的好处在于:轻量、灵活。

基于注解的授权

Shiro 提供了一些用于授权的注解,来进一步简化授权代码。

@RequiresAuthentication

@RequiresAuthentication 注解要求当前 Subject 必须是已认证用户才可以访问被修饰的方法。

【示例】

1
2
3
4
5
6
@RequiresAuthentication
public void updateAccount(Account userAccount) {
//this method will only be invoked by a
//Subject that is guaranteed authenticated
...
}

@RequiresGuest

@RequiresGuest 注解要求当前 Subject 的角色是 guest 才可以访问被修饰的方法。

授权流程

img

  1. 应用程序或框架代码调用任何 SubjecthasRole*checkRole*isPermitted*checkPermission* 方法,并传入所需的权限或角色。

  2. Subject 实例,通常是 DelegatingSubject(或子类),通过调用 securityManager 几乎相同的各自 hasRole*checkRole*isPermitted*checkPermission* 方法来委托 SecurityManager (实现了 org.apache.shiro.authz.Authorizer 接口)处理授权。

  3. SecurityManager 通过调用授权者各自的 hasRole*checkRole*isPermitted*checkPermission* 方法来中继/委托其内部的 org.apache.shiro.authz.Authorizer 实例。默认情况下,authorizer 实例是 ModularRealmAuthorizer 实例,该实例支持在任何授权操作期间协调一个或多个 Realm 实例。

  4. 检查每个已配置的 Realm,以查看其是否实现相同的 Authorizer 接口。如果是这样,则将调用 Realm 各自的 hasRole*checkRole*isPermitted*checkPermission* 方法。

:link: 更多授权细节可以参考:Apache Shiro Authorization

四、Shiro 会话管理

Shiro 提供了一套独特的会话管理方案:其 Session 可以使用 Java SE 程序,也可以使用于 Java Web 程序。

在 Shiro 中,SessionManager 负责管理应用所有 Subject 的会话,如:创建、删除、失效、验证等。

【示例】会话使用示例

1
2
3
4
Subject currentUser = SecurityUtils.getSubject();

Session session = currentUser.getSession();
session.setAttribute( "someKey", someValue);

会话超时

默认情况下,Shiro 中的会话有效期为 30 分钟,超时后,该会话将被 Shiro 视为无效。

可以通过 globalSessionTimeout 方法设置 Shiro 会话超时时间。

会话监听

Shiro 提供了 SessionListener 接口(或 SessionListenerAdapter 接口),用于监听重要的会话事件,并允许使用者在事件触发时做定制化处理。

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ShiroSessionListener implements SessionListener {

private final Logger log = LoggerFactory.getLogger(this.getClass());

private final AtomicInteger sessionCount = new AtomicInteger(0);

@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
}

@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
}

@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
}
}

会话存储

大多数情况下,应用需要保存会话信息,以便在稍后可以使用它。

Shiro 提供了 SessionManager 接口,负责将针对会话的 CRUD 操作委派给内部组件 SessionDAO,该组件反映了数据访问对象(DAO)设计模式。

:bell: 注意:由于会话通常具有时效性,所以一般会话天然适合存储于缓存中。存储于 Redis 中是一个不错的选择。

五、Realm

Realm 是 Shiro 访问程序安全相关数据(如:用户、角色、权限)的接口。

Realm 是有开发者自己实现的,开发者可以通过实现 Realm 接口,接入应用的数据源,如:JDBC、文件、Nosql 等等。

认证令牌

Shiro 支持身份验证令牌。在咨询 Realm 进行认证尝试之前,将调用其支持方法。 如果返回值为 true,则仅会调用其 getAuthenticationInfo(token) 方法。通常,Realm 会检查所提交令牌的类型(接口或类),以查看其是否可以处理它。

令牌认证处理流程如下:

  1. 检查用于标识 principal 的令牌(帐户标识信息)。
  2. 根据 principal,在数据源中查找相应的帐户数据。
  3. 确保令牌提供的凭证与数据存储中存储的凭证匹配。
  4. 如果 credentials 匹配,则返回 AuthenticationInfo 实例。
  5. 如果 credentials 不匹配,则抛出 AuthenticationException 异常。

加密

通过前文,可以了解:Shiro 需要通过一对 principal 和 credentials 来确认身份是否匹配(即认证)。

一般来说,成熟软件是不允许存储账户、密码这些敏感数据时,使用明文存储。所以,通常要将密码加密后存储。

Shiro 提供了一些加密器,其思想就是用 MD5、SHA 这种数字签名算法,加 Salt,然后转为 Base64 字符串。为了避免被暴力破解,Shiro 使用多次加密的方式获得最终的 credentials 字符串。

【示例】Shiro 加密密码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
...

//We'll use a Random Number Generator to generate salts. This
//is much more secure than using a username as a salt or not
//having a salt at all. Shiro makes this easy.
//
//Note that a normal app would reference an attribute rather
//than create a new RNG every time:
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();

//Now hash the plain-text password with the random salt and multiple
//iterations and then Base64-encode the value (requires less space than Hex):
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();

User user = new User(username, hashedPasswordBase64);
//save the salt with the new account. The HashedCredentialsMatcher
//will need it later when handling login attempts:
user.setPasswordSalt(salt);
userDAO.create(user);

六、配置

过滤链

运行 Web 应用程序时,Shiro 将创建一些有用的默认 Filter 实例。

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

RememberMe

1
2
3
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
SecurityUtils.getSubject().login(token);

参考资料

Spring Security 快速入门

快速开始

参考:Securing a Web Application

核心 API

设计原理

Spring Security 对于 Servlet 的支持基于过滤链(FilterChain)实现。

Spring 提供了一个名为 DelegatingFilterProxyFilter 实现,该实现允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间进行桥接。 Servlet 容器允许使用其自己的标准注册 Filters,但它不了解 Spring 定义的 Bean。 DelegatingFilterProxy 可以通过标准的 Servlet 容器机制进行注册,但是可以将所有工作委托给实现 Filter 的 Spring Bean。

1
2
3
4
5
6
7
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}

FilterChainProxy 使用 SecurityFilterChain 确定应对此请求调用哪些 Spring Security 过滤器。

SecurityFilterChain 中的安全过滤器通常是 Bean,但它们是使用 FilterChainProxy 而不是 DelegatingFilterProxy 注册的。

实际上,FilterChainProxy 可用于确定应使用哪个 SecurityFilterChain。如果您的应用程序可以为不同的模块提供完全独立的配置。

multi securityfilterchain

ExceptionTranslationFilter 可以将 AccessDeniedException 和 AuthenticationException 转换为 HTTP 响应。

exceptiontranslationfilter

核心源码:

1
2
3
4
5
6
7
8
9
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException e) {
if (!authenticated || e instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}

认证

数据模型

Spring Security 框架中的认证数据模型如下:

img

  • Authentication - 认证信息实体。
    • principal - 用户标识。如:用户名、账户名等。通常是 UserDetails 的实例(后面详细讲解)。
    • credentials - 认证凭证。如:密码等。
    • authorities - 授权信息。如:用户的角色、权限等信息。
  • SecurityContext - 安全上下文。包含一个 Authentication 对象。
  • SecurityContextHolder - 安全上下文持有者。用于存储认证信息。

【示例】注册认证信息

1
2
3
4
5
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

【示例】访问认证信息

认证基本流程

AbstractAuthenticationProcessingFilter 用作验证用户凭据的基本过滤器。 在对凭证进行身份验证之前,Spring Security 通常使用 AuthenticationEntryPoint 请求凭证。

abstractauthenticationprocessingfilter

  • (1)当用户提交其凭据时,AbstractAuthenticationProcessingFilter 从要验证的 HttpServletRequest 创建一个 Authentication。创建的身份验证类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter 根据在 HttpServletRequest 中提交的用户名和密码来创建 UsernamePasswordAuthenticationToken
  • (2)接下来,将身份验证传递到 AuthenticationManager 进行身份验证。
  • (3)如果身份验证失败,则认证失败
    • 清除 SecurityContextHolder
    • 调用 RememberMeServices.loginFail。如果没有配置 remember me,则为空。
    • 调用 AuthenticationFailureHandler
  • (4)如果身份验证成功,则认证成功。
    • 如果是新的登录,则通知 SessionAuthenticationStrategy
    • 身份验证是在 SecurityContextHolder 上设置的。之后,SecurityContextPersistenceFilterSecurityContext 保存到 HttpSession 中。
    • 调用 RememberMeServices.loginSuccess。如果没有配置 remember me,则为空。
    • ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent

用户名/密码认证

读取用户名和密码的方式:

  • 表单
  • 基本认证
  • 数字认证

存储机制

表单认证

spring security 支持通过从 html 表单获取登录时提交的用户名、密码。

loginurlauthenticationentrypoint

一旦,登录信息被提交,UsernamePasswordAuthenticationFilter 就会验证用户名和密码。

usernamepasswordauthenticationfilter

基本认证

1
2
3
4
5
protected void configure(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
}

内存认证

InMemoryUserDetailsManager 实现了 UserDetailsService ,提供了基本的用户名、密码认证,其认证数据存储在内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public UserDetailsService users() {
// The builder will ensure the passwords are encoded before saving in memory
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails user = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}

JDBC 认证

JdbcUserDetailsManager 实现了 UserDetailsService ,提供了基本的用户名、密码认证,其认证数据存储在关系型数据库中,通过 JDBC 方式访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser()
}

基本的 scheam:

1
2
3
4
5
6
7
8
9
10
11
12
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null
);

create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

UserDetailsService

UserDetailsUserDetailsService 返回。 DaoAuthenticationProvider 验证 UserDetails,然后返回身份验证,该身份验证的主体是已配置的 UserDetailsService 返回的 UserDetails

DaoAuthenticationProvider 使用 UserDetailsService 检索用户名,密码和其他用于使用用户名和密码进行身份验证的属性。 Spring Security 提供 UserDetailsService 的内存中和 JDBC 实现。

您可以通过将自定义 UserDetailsService 公开为 bean 来定义自定义身份验证。

PasswordEncoder

Spring Security 的 servlet 支持通过与 PasswordEncoder 集成来安全地存储密码。 可以通过公开一个 PasswordEncoder Bean 来定制 Spring Security 使用的 PasswordEncoder 实现。

daoauthenticationprovider

Remember-Me

Spring Boot 集成

@EnableWebSecurity@Configuration 注解一起使用, 注解 WebSecurityConfigurer 类型的类。

或者利用@EnableWebSecurity注解继承 WebSecurityConfigurerAdapter 的类,这样就构成了 Spring Security 的配置。

  • configure(WebSecurity):通过重载该方法,可配置 Spring Security 的 Filter 链。
  • configure(HttpSecurity):通过重载该方法,可配置如何通过拦截器保护请求。

参考资料

Netty 快速入门

Netty 简介

Netty 是一款基于 NIO(Nonblocking I/O,非阻塞 IO)开发的网络通信框架

Netty 的特性

  • 高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞 IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞 IO),他的并发性能得到了很大提高。
  • 传输快:Netty 的传输依赖于内存零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。
  • 封装好:Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。

核心组件

  • Channel:Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等。
  • EventLoop:主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。
  • ChannelFuture:Netty 框架中所有的 I/O 操作都为异步的,因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。
  • ChannelHandler:充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。
  • ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。

Netty 有两种发送消息的方式:

  • 直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动;
  • 写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。

高性能

Netty 高性能表现在哪些方面:

  • NIO 线程模型:同步非阻塞,用最少的资源做更多的事。
  • 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
  • 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
  • 串形化处理读写:避免使用锁带来的性能开销。
  • 高性能序列化协议:支持 protobuf 等高性能序列化协议。

零拷贝

传统意义的拷贝

是在发送数据的时候,传统的实现方式是:

File.read(bytes)

Socket.send(bytes)

这种方式需要四次数据拷贝和四次上下文切换:

  1. 数据从磁盘读取到内核的 read buffer

  2. 数据从内核缓冲区拷贝到用户缓冲区

  3. 数据从用户缓冲区拷贝到内核的 socket buffer

  4. 数据从内核的 socket buffer 拷贝到网卡接口(硬件)的缓冲区

零拷贝的概念

明显上面的第二步和第三步是非必要的,通过 java 的 FileChannel.transferTo 方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)

  • 调用 transferTo,数据从文件由 DMA 引擎拷贝到内核 read buffer
  • 接着 DMA 从内核 read buffer 将数据拷贝到网卡接口 buffer

上面的两次操作都不需要 CPU 参与,所以就达到了零拷贝。

Netty 中的零拷贝

主要体现在三个方面:

bytebuffer

Netty 发送和接收消息主要使用 bytebuffer,bytebuffer 使用对外内存(DirectMemory)直接进行 Socket 读写。

原因:如果使用传统的堆内存进行 Socket 读写,JVM 会将堆内存 buffer 拷贝一份到直接内存中然后再写入 socket,多了一次缓冲区的内存拷贝。DirectMemory 中可以直接通过 DMA 发送到网卡接口

Composite Buffers

传统的 ByteBuffer,如果需要将两个 ByteBuffer 中的数据组合到一起,我们需要首先创建一个 size=size1+size2 大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用 Netty 提供的组合 ByteBuf,就可以避免这样的操作,因为 CompositeByteBuf 并没有真正将多个 Buffer 组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。

对于 FileChannel.transferTo 的使用

Netty 中使用了 FileChannel 的 transferTo 方法,该方法依赖于操作系统实现零拷贝。

Netty 流程

应用

Netty 是一个广泛使用的 Java 网络编程框架。很多著名软件都使用了它,如:Dubbo、Cassandra、Elasticsearch、Vert.x 等。

有了 Netty,你可以实现自己的 HTTP 服务器,FTP 服务器,UDP 服务器,RPC 服务器,WebSocket 服务器,Redis 的 Proxy 服务器,MySQL 的 Proxy 服务器等等。

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
public class NettyOioServer {

public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
EventLoopGroup group = new OioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); //1

b.group(group) //2
.channel(OioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {//3
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { //4
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
}
});
}
});
ChannelFuture f = b.bind().sync(); //6
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync(); //7
}
}
}

参考资料

Java 缓存中间件

关键词:Spring Cache、J2Cache、JetCache

一 、JSR 107

JSR107 中制订了 Java 缓存的规范。

因此,在很多缓存框架、缓存库中,其 API 都参考了 JSR 107 规范。

img

Java Caching 定义了 5 个核心接口

  • CachingProvider - 定义了创建、配置、获取、管理和控制多个 CacheManager。一个应用可以在运行期访问多个 CachingProvider
  • CacheManager - 定义了创建、配置、获取、管理和控制多个唯一命名的 Cache,这些 Cache 存在于 CacheManager 的上下文中。一个 CacheManager 仅被一个 CachingProvider 所拥有。
  • Cache - 是一个类似 Map 的数据结构并临时存储以 Key 为索引的值。一个 Cache 仅被一个 CacheManager 所拥有。
  • Entry - 是一个存储在 Cache 中的 key-value 对。
  • Expiry - 每一个存储在 Cache 中的条目有一个定义的有效期,即 Expiry Duration。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过 ExpiryPolicy 设置。

二、Spring Cache

详见:Spring Cache 官方文档

Spring 作为 Java 开发最著名的框架,也提供了缓存功能的框架—— Spring Cache。

Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如:EHCache 或 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring Cache 的特点:

  • 通过缓存注解即可支持缓存功能
  • 支持 Spring EL 表达式
  • 支持 AspectJ
  • 支持自定义 key 和缓存管理

开启缓存注解

Spring 为缓存功能提供了注解功能,但是你必须启动注解。

有两种方式:

(一)使用标记注解 @EnableCaching

这种方式对于 Spring 或 Spring Boot 项目都适用。

1
2
3
4
@Configuration
@EnableCaching
public class AppConfig {
}

(二)在 xml 中声明

1
<cache:annotation-driven cache-manager="cacheManager"/>

spring 缓存注解 API

Spring 对缓存的支持类似于对事务的支持。

首先使用注解标记方法,相当于定义了切点,然后使用 Aop 技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。

@Cacheable

@Cacheable 用于触发缓存

表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。

这个注解可以用condition属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。

可以使用key属性来指定 key 的生成规则。

@CachePut

@CachePut 用于更新缓存

@Cacheable不同,@CachePut不仅会缓存方法的结果,还会执行方法的代码段。

它支持的属性和用法都与@Cacheable一致。

@CacheEvict

@CacheEvict 用于清除缓存

@Cacheable功能相反,@CacheEvict表明所修饰的方法是用来删除失效或无用的缓存数据。

下面是@Cacheable@CacheEvict@CachePut基本使用方法的一个集中展示:

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
@Service
public class UserService {
// @Cacheable可以设置多个缓存,形式如:@Cacheable({"books", "isbns"})
@Cacheable(value={"users"}, key="#user.id")
public User findUser(User user) {
return findUserInDB(user.getId());
}

@Cacheable(value = "users", condition = "#user.getId() <= 2")
public User findUserInLimit(User user) {
return findUserInDB(user.getId());
}

@CachePut(value = "users", key = "#user.getId()")
public void updateUser(User user) {
updateUserInDB(user);
}

@CacheEvict(value = "users")
public void removeUser(User user) {
removeUserInDB(user.getId());
}

@CacheEvict(value = "users", allEntries = true)
public void clear() {
removeAllInDB();
}
}

@Caching

@Caching 用于组合定义多种缓存功能

如果需要使用同一个缓存注解(@Cacheable@CacheEvict@CachePut)多次修饰一个方法,就需要用到@Caching

1
2
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

@CacheConfig

@CacheConfig 用于定义公共缓存配置

与前面的缓存注解不同,这是一个类级别的注解。

如果类的所有操作都是缓存操作,你可以使用@CacheConfig来指定类,省去一些配置。

1
2
3
4
5
@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}

三、Spring Boot Cache

详见:Spring Boot Cache 特性官方文档

Spring Boot Cache 是在 Spring Cache 的基础上做了封装,使得使用更为便捷。

Spring Boot Cache 快速入门

(1)引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 按序引入需要的缓存库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

(2)缓存配置

例如,选用缓存为 redis,则需要配置 redis 相关的配置项(如:数据源、连接池等配置信息)

1
2
3
4
5
6
7
8
9
10
# 缓存类型,支持类型:GENERIC、JCACHE、EHCACHE、HAZELCAST、INFINISPAN、COUCHBASE、REDIS、CAFFEINE、SIMPLE
spring.cache.type = redis
# 全局缓存时间
spring.cache.redis.time-to-live = 60s

# Redis 配置
spring.redis.database = 0
spring.redis.host = localhost
spring.redis.port = 6379
spring.redis.password =

(3)使用 @EnableCaching 开启缓存

1
2
3
4
5
@EnableCaching
@SpringBootApplication
public class Application {
// ...
}

(4)缓存注解(@Cacheable@CachePut@CacheEvit 等)使用方式与 Spring Cache 完全一样

四、JetCache

JetCache 是一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。 JetCache 提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了Cache接口用于手工缓存操作。 当前有四个实现,RedisCacheTairCache(此部分未在 github 开源)、CaffeineCache(in memory)和一个简易的LinkedHashMapCache(in memory),要添加新的实现也是非常简单的。

详见:jetcache Github

jetcache 快速入门

如果使用 Spring Boot,可以按如下的方式配置(这里使用了 jedis 客户端连接 redis,如果需要集群、读写分离、异步等特性支持请使用lettuce客户端)。

(1)引入 POM

1
2
3
4
5
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.5.14</version>
</dependency>

(2)配置

配置一个 spring boot 风格的 application.yml 文件,把他放到资源目录中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: 127.0.0.1
port: 6379

(3)开启缓存

然后创建一个 App 类放在业务包的根下,EnableMethodCache,EnableCreateCacheAnnotation 这两个注解分别激活 Cached 和 CreateCache 注解,其他和标准的 Spring Boot 程序是一样的。这个类可以直接 main 方法运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.company.mypackage;

import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class MySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApp.class);
}
}

(4)API 基本使用

创建缓存实例

通过 @CreateCache 注解创建一个缓存实例,默认超时时间是 100 秒

1
2
@CreateCache(expire = 100)
private Cache<Long, UserDO> userCache;

用起来就像 map 一样

1
2
3
UserDO user = userCache.get(123L);
userCache.put(123L, user);
userCache.remove(123L);

创建一个两级(内存+远程)的缓存,内存中的元素个数限制在 50 个。

1
2
@CreateCache(name = "UserService.userCache", expire = 100, cacheType = CacheType.BOTH, localLimit = 50)
private Cache<Long, UserDO> userCache;

name 属性不是必须的,但是起个名字是个好习惯,展示统计数据的使用,会使用这个名字。如果同一个 area 两个 @CreateCache 的 name 配置一样,它们生成的 Cache 将指向同一个实例。

创建方法缓存

使用 @Cached 方法可以为一个方法添加上缓存。JetCache 通过 Spring AOP 生成代理,来支持缓存功能。注解可以加在接口方法上也可以加在类方法上,但需要保证是个 Spring bean。

1
2
3
4
public interface UserService {
@Cached(name="UserService.getUserById", expire = 3600)
User getUserById(long userId);
}

五、j2cache

六、总结

使用缓存框架,使得开发缓存功能非常便捷。

如果你的系统只需要使用一种缓存,那么推荐使用 Spring Boot Cache。Spring Boot Cache 在 Spring Cache 基础上做了封装,使用更简单、方便。

如果你的系统需要使用多级缓存,那么推荐使用 jetcache。

参考资料

Ehcache 快速入门

EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。

img

一、简介

Ehcache 虽然也支持分布式模式,但是分布式方案不是很好好,建议只将其作为单机的进程内缓存使用。

Ehcache 特性

优点

  • 快速、简单
  • 支持多种缓存策略:LRU、LFU、FIFO 淘汰算法
  • 缓存数据有两级:内存和磁盘,因此无需担心容量问题
  • 缓存数据会在虚拟机重启的过程中写入磁盘
  • 可以通过 RMI、可插入 API 等方式进行分布式缓存
  • 具有缓存和缓存管理器的侦听接口
  • 支持多缓存管理器实例,以及一个实例的多个缓存区域
  • 提供 Hibernate 的缓存实现

缺点

  • 使用磁盘 Cache 的时候非常占用磁盘空间
  • 不保证数据的安全
  • 虽然支持分布式缓存,但效率不高(通过组播方式,在不同节点之间同步数据)。

Ehcache 集群

Ehcache 目前支持五种集群方式:

  • RMI
  • JMS
  • JGroup
  • Terracotta
  • Ehcache Server

RMI

使用组播方式通知所有节点同步数据。

如果网络有问题,或某台服务宕机,则存在数据无法同步的可能,导致数据不一致。

Ehcache Image

JMS

JMS 类似 MQ,所有节点订阅消息,当某节点缓存发生变化,就向 JMS 发消息,其他节点感知变化后,同步数据。

Ehcache Image

Cache Server

Ehcache Image

二、快速入门

引入 Ehcache

如果你的项目使用 maven 管理,添加以下依赖到你的 pom.xml 中。

1
2
3
4
5
6
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.2</version>
<type>pom</type>
</dependency>

如果你的项目不使用 maven 管理,请在 Ehcache 官网下载地址 下载 jar 包。

Spring 提供了对于 Ehcache 接口的封装,可以更简便的使用其功能。接入方式如下:

如果你的项目使用 maven 管理,添加以下依赖到你的pom.xml中。

spring-context-support这个 jar 包中含有 Spring 对于缓存功能的抽象封装接口。

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>

添加配置文件

(1)在 classpath 下添加 ehcache.xml
添加一个名为 helloworld 的缓存。

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
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir/ehcache"/>

<!-- 默认缓存 -->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>

<!-- helloworld缓存 -->
<cache name="helloworld"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>

Ehcache 工作示例

Ehcache 会自动加载 classpath 根目录下名为 ehcache.xml 文件。

EhcacheDemo 的工作步骤如下:

  1. 在 EhcacheDemo 中,我们引用 ehcache.xml 声明的名为 helloworld 的缓存来创建Cache对象;
  2. 然后我们用一个键值对来实例化Element对象;
  3. Element对象添加到Cache
  4. 然后用Cache的 get 方法获取Element对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class EhcacheDemo {
public static void main(String[] args) throws Exception {
// Create a cache manager
final CacheManager cacheManager = new CacheManager();

// create the cache called "helloworld"
final Cache cache = cacheManager.getCache("helloworld");

// create a key to map the data to
final String key = "greeting";

// Create a data element
final Element putGreeting = new Element(key, "Hello, World!");

// Put the element into the data store
cache.put(putGreeting);

// Retrieve the data element
final Element getGreeting = cache.get(key);

// Print the value
System.out.println(getGreeting.getObjectValue());
}
}

输出

1
Hello, World!

三、Ehcache API

ElementCacheCacheManager是 Ehcache 最重要的 API。

  • Element - 缓存的元素,它维护着一个键值对。
  • Cache - 它是 Ehcache 的核心类,它有多个Element,并被CacheManager管理。它实现了对缓存的逻辑行为。
  • CacheManager - Cache的容器对象,并管理着Cache的生命周期。CacheManager 支持两种创建模式:单例(Singleton mode)和实例(InstanceMode)。

创建 CacheManager

下面的代码列举了创建 CacheManager 的五种方式。

使用静态方法create()会以默认配置来创建单例的CacheManager实例。

newInstance()方法是一个工厂方法,以默认配置创建一个新的CacheManager实例。

此外,newInstance()还有几个重载函数,分别可以通过传入StringURLInputStream参数来加载配置文件,然后创建CacheManager实例。

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
// 使用Ehcache默认配置获取单例的CacheManager实例
CacheManager.create();
String[] cacheNames = CacheManager.getInstance().getCacheNames();

// 使用Ehcache默认配置新建一个CacheManager实例
CacheManager.newInstance();
String[] cacheNames = manager.getCacheNames();

// 使用不同的配置文件分别创建一个CacheManager实例
CacheManager manager1 = CacheManager.newInstance("src/config/ehcache1.xml");
CacheManager manager2 = CacheManager.newInstance("src/config/ehcache2.xml");
String[] cacheNamesForManager1 = manager1.getCacheNames();
String[] cacheNamesForManager2 = manager2.getCacheNames();

// 基于classpath下的配置文件创建CacheManager实例
URL url = getClass().getResource("/anotherconfigurationname.xml");
CacheManager manager = CacheManager.newInstance(url);

// 基于文件流得到配置文件,并创建CacheManager实例
InputStream fis = new FileInputStream(new File
("src/config/ehcache.xml").getAbsolutePath());
try {
CacheManager manager = CacheManager.newInstance(fis);
} finally {
fis.close();
}

添加缓存

需要强调一点,Cache对象在用addCache方法添加到CacheManager之前,是无效的。

使用 CacheManager 的 addCache 方法可以根据缓存名将 ehcache.xml 中声明的 cache 添加到容器中;它也可以直接将 Cache 对象添加到缓存容器中。

Cache有多个构造函数,提供了不同方式去加载缓存的配置参数。

有时候,你可能需要使用 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
// 除了可以使用xml文件中配置的缓存,你也可以使用API动态增删缓存
// 添加缓存
manager.addCache(cacheName);

// 使用默认配置添加缓存
CacheManager singletonManager = CacheManager.create();
singletonManager.addCache("testCache");
Cache test = singletonManager.getCache("testCache");

// 使用自定义配置添加缓存,注意缓存未添加进CacheManager之前并不可用
CacheManager singletonManager = CacheManager.create();
Cache memoryOnlyCache = new Cache("testCache", 5000, false, false, 5, 2);
singletonManager.addCache(memoryOnlyCache);
Cache test = singletonManager.getCache("testCache");

// 使用特定的配置添加缓存
CacheManager manager = CacheManager.create();
Cache testCache = new Cache(
new CacheConfiguration("testCache", maxEntriesLocalHeap)
.memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
.eternal(false)
.timeToLiveSeconds(60)
.timeToIdleSeconds(30)
.diskExpiryThreadIntervalSeconds(0)
.persistence(new PersistenceConfiguration().strategy(Strategy.LOCALTEMPSWAP)));
manager.addCache(testCache);

删除缓存

删除缓存比较简单,你只需要将指定的缓存名传入removeCache方法即可。

1
2
CacheManager singletonManager = CacheManager.create();
singletonManager.removeCache("sampleCache1");

基本缓存操作

Cache 最重要的两个方法就是 put 和 get,分别用来添加 Element 和获取 Element。

Cache 还提供了一系列的 get、set 方法来设置或获取缓存参数,这里不一一列举,更多 API 操作可参考官方 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
/**
* 测试:使用默认配置或使用指定配置来创建CacheManager
*
* @author Zhang Peng
*/
public class CacheOperationTest {
private final Logger log = LoggerFactory.getLogger(CacheOperationTest.class);

/**
* 使用Ehcache默认配置(classpath下的ehcache.xml)获取单例的CacheManager实例
*/
@Test
public void operation() {
CacheManager manager = CacheManager.newInstance("src/test/resources/ehcache/ehcache.xml");

// 获得Cache的引用
Cache cache = manager.getCache("userCache");

// 将一个Element添加到Cache
cache.put(new Element("key1", "value1"));

// 获取Element,Element类支持序列化,所以下面两种方法都可以用
Element element1 = cache.get("key1");
// 获取非序列化的值
log.debug("key:{}, value:{}", element1.getObjectKey(), element1.getObjectValue());
// 获取序列化的值
log.debug("key:{}, value:{}", element1.getKey(), element1.getValue());

// 更新Cache中的Element
cache.put(new Element("key1", "value2"));
Element element2 = cache.get("key1");
log.debug("key:{}, value:{}", element2.getObjectKey(), element2.getObjectValue());

// 获取Cache的元素数
log.debug("cache size:{}", cache.getSize());

// 获取MemoryStore的元素数
log.debug("MemoryStoreSize:{}", cache.getMemoryStoreSize());

// 获取DiskStore的元素数
log.debug("DiskStoreSize:{}", cache.getDiskStoreSize());

// 移除Element
cache.remove("key1");
log.debug("cache size:{}", cache.getSize());

// 关闭当前CacheManager对象
manager.shutdown();

// 关闭CacheManager单例实例
CacheManager.getInstance().shutdown();
}
}

四、Ehcache 配置

Ehcache 支持通过 xml 文件和 API 两种方式进行配置。

详情参考:Ehcache 官方 XML 配置手册

xml 配置方式

Ehcache 的CacheManager构造函数或工厂方法被调用时,会默认加载 classpath 下名为ehcache.xml的配置文件。如果加载失败,会加载 Ehcache jar 包中的ehcache-failsafe.xml文件,这个文件中含有简单的默认配置。
ehcache.xml 配置参数说明:

  • name:缓存名称。
  • maxElementsInMemory:缓存最大个数。
  • eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。
  • timeToIdleSeconds:置对象在失效前的允许闲置时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。
  • timeToLiveSeconds:缓存数据的生存时间(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是 0 就意味着元素可以停顿无穷长的时间。
  • maxEntriesLocalDisk:当内存中对象数量达到 maxElementsInMemory 时,Ehcache 将会对象写到磁盘中。
  • overflowToDisk:内存不足时,是否启用磁盘缓存。
  • diskSpoolBufferSizeMB:这个参数设置 DiskStore(磁盘缓存)的缓存区大小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区。
  • maxElementsOnDisk:硬盘最大缓存个数。
  • diskPersistent:是否在 VM 重启时存储硬盘的缓存数据。默认值是 false。
  • diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是 120 秒。
  • memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。默认策略是 LRU(最近最少使用)。你可以设置为 FIFO(先进先出)或是 LFU(较少使用)。
  • clearOnFlush:内存数量最大时是否清除。

API 配置方式

xml 配置的参数也可以直接通过编程方式来动态的进行配置(dynamicConfig 没有设为 false)。

1
2
3
4
5
6
Cache cache = manager.getCache("sampleCache");
CacheConfiguration config = cache.getCacheConfiguration();
config.setTimeToIdleSeconds(60);
config.setTimeToLiveSeconds(120);
config.setmaxEntriesLocalHeap(10000);
config.setmaxEntriesLocalDisk(1000000);

也可以通过disableDynamicFeatures()方式关闭动态配置开关。配置以后你将无法再以编程方式配置参数。

1
2
Cache cache = manager.getCache("sampleCache");
cache.disableDynamicFeatures();

五、Spring 集成 Ehcache

Spring3.1 开始添加了对缓存的支持。和事务功能的支持方式类似,缓存抽象允许底层使用不同的缓存解决方案来进行整合。

Spring4.1 开始支持 JSR-107 注解。

注:我本人使用的 Spring 版本为 4.1.4.RELEASE,目前 Spring 版本仅支持 Ehcache2.5 以上版本,但不支持 Ehcache3。

绑定 Ehcache

org.springframework.cache.ehcache.EhCacheManagerFactoryBean这个类的作用是加载 Ehcache 配置文件。
org.springframework.cache.ehcache.EhCacheCacheManager这个类的作用是支持 net.sf.ehcache.CacheManager。

spring-ehcache.xml的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-3.2.xsd">

<description>ehcache缓存配置管理文件</description>

<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache/ehcache.xml"/>
</bean>

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcache"/>
</bean>

<!-- 启用缓存注解开关 -->
<cache:annotation-driven cache-manager="cacheManager"/>
</beans>

使用 Spring 的缓存注解

开启注解

Spring 为缓存功能提供了注解功能,但是你必须启动注解。
你有两个选择:
(1) 在 xml 中声明
像上一节 spring-ehcache.xml 中的做法一样,使用<cache:annotation-driven/>

1
<cache:annotation-driven cache-manager="cacheManager"/>

(2) 使用标记注解
你也可以通过对一个类进行注解修饰的方式在这个类中使用缓存注解。
范例如下:

1
2
3
4
@Configuration
@EnableCaching
public class AppConfig {
}

注解基本使用方法

Spring 对缓存的支持类似于对事务的支持。
首先使用注解标记方法,相当于定义了切点,然后使用 Aop 技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。
下面三个注解都是方法级别:

@Cacheable

表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。
这个注解可以用condition属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。
可以使用key属性来指定 key 的生成规则。

@CachePut

@Cacheable不同,@CachePut不仅会缓存方法的结果,还会执行方法的代码段。
它支持的属性和用法都与@Cacheable一致。

@CacheEvict

@Cacheable功能相反,@CacheEvict表明所修饰的方法是用来删除失效或无用的缓存数据。
下面是@Cacheable@CacheEvict@CachePut基本使用方法的一个集中展示:

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
@Service
public class UserService {
// @Cacheable可以设置多个缓存,形式如:@Cacheable({"books", "isbns"})
@Cacheable(value={"users"}, key="#user.id")
public User findUser(User user) {
return findUserInDB(user.getId());
}

@Cacheable(value = "users", condition = "#user.getId() <= 2")
public User findUserInLimit(User user) {
return findUserInDB(user.getId());
}

@CachePut(value = "users", key = "#user.getId()")
public void updateUser(User user) {
updateUserInDB(user);
}

@CacheEvict(value = "users")
public void removeUser(User user) {
removeUserInDB(user.getId());
}

@CacheEvict(value = "users", allEntries = true)
public void clear() {
removeAllInDB();
}
}

@Caching

如果需要使用同一个缓存注解(@Cacheable@CacheEvict@CachePut)多次修饰一个方法,就需要用到@Caching

1
2
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

@CacheConfig

与前面的缓存注解不同,这是一个类级别的注解。
如果类的所有操作都是缓存操作,你可以使用@CacheConfig来指定类,省去一些配置。

1
2
3
4
5
@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}

参考资料