整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

深入分析 Java Web 中的中文编码问题

深入分析 Java Web 中的中文编码问题

始,背景:

编码问题一直困扰着程序开发人员,尤其是在 Java 中更加明显,因为 Java 是跨平台的语言,在不同平台的编码之间的切换较多。接下来将介绍 Java 编码问题出现的根本原因;在 Java 中经常遇到的几种编码格式的区别;在 Java 中经常需要编码的场景;出现中文问题的原因分析;在开发 Java Web 中可能存在编码的几个地方;一个 HTTP 请求怎么控制编码格式;如何避免出现中文编码问题等。

深入分析 Java Web 中的中文编码问题

1几种常见的编码格式

1 为什么要编码

2 如何翻译

2在 Java 中需要编码的场景

1 在 IO 操作中存在的编码

2 在内存操作中的编码

3在 Java 中如何编解码

几种编码格式的比较

4在 Java Web 中涉及的编解码

1 URL 的编解码

2 HTTP Header 的编解码

3 POST 表单的编解码

4 HTTP BODY 的编解码

5 其它需要编码的地方

5常见问题分析

1 中文变成了看不懂的字符

2 一个汉字变成一个问号

3 一个汉字变成两个问号

4 一种不正常的正确编码

6总结


1、几种常见的编码格式

1.1 为什么要编码

在计算机中存储信息的最小单元是 1 个字节,即 8 个 bit, 所以能表示的字符范围是 0 ~ 255 个。

要表示的符号太多,无法用 1 个字节来完全表示。

1.2 如何翻译

计算机中提供多种翻译方式,常见的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16等。这些都规定了转化的规则,按照这个规则就可以让计算机正确的表示我们的字符。下面介绍这几种编码格式:

ASCII 码

总共有 128 个,用 1 个字节的低 7 位表示, 0 ~ 31 是控制字符如换行、回车、删除等,32 ~ 126 是打印字符,可以通过键盘输入并且能够显示出来。

ISO-8859-1

128 个字符显然是不够用的,所以 ISO 组织在 ASCII 的基础上扩展,他们是 ISO-8859-1 至 ISO-8859-15,前者涵盖大多数字符,应用最广。ISO-8859-1 仍是单字节编码,它总归能表示 256 个字符。

GB2312

它是双字节编码,总的编码范围是 A1 ~ F7,其中 A1 ~ A9 是符号区,总共包含 682 个符号;B0 ~ F7 是汉字区,包含 6763 个汉字。

GBk

GBK 为《汉字内码扩展规范》,为 GB2312 的扩展,它的编码范围是 8140 ~ FEFE(去掉XX7F),总共有 23940 个码位,能表示 21003 个汉字,和 GB2312的编码兼容,不会有乱码。

UTF-16

它具体定义了 Unicode 字符在计算机中的存取方法。UTF-16 用两个字节来表示 Unicode 的转化格式,它采用定长的表示方法,即不论什么字符用两个字节表示。两个字节是 16 个 bit,所以叫 UTF-16。它表示字符非常方便,没两个字节表示一个字符,这就大大简化了字符串操作。

UTF-8

虽说 UTF-16 统一采用两个字节表示一个字符很简单方便,但是很大一部分字符用一个字节就可以表示,如果用两个字节表示,存储空间放大了一倍,在网络带宽有限的情况下会增加网络传输的流量。UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度不同类型的字符可以由 1 ~ 6 个字节组成。

UTF-8 有以下编码规则:

如果是 1 个字节,最高位(第 8 位)为 0,则表示这是一个 ASCII 字符(00 ~ 7F)

如果是 1 个字节,以 11 开头,则连续的 1 的个数暗示这个字符的字节数

如果是 1 个字节,以 10 开头,表示它不是首字节,则需要向前查找才能得到当前字符的首字节

2、在 Java 中需要编码的场景

2.1 在 I/O 操作中存在的编码

如上图:Reader 类是在 Java 的 I/O 中读取符的父类,而 InputStream 类是读字节的父类, InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而对具体字节到字符的解码实现,它又委托 StreamDecoder 去做,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。值得注意的是,如果你没有指定 Charset,则将使用本地环境中默认的字符集,如在中文环境中将使用 GBK 编码。

如下面一段代码,实现了文件的读写功能:

String file="c:/stream.txt";

String charset="UTF-8";

// 写字符换转成字节流

FileOutputStream outputStream=new FileOutputStream(file);

OutputStreamWriter writer=new OutputStreamWriter(

outputStream, charset);

try {

writer.write("这是要保存的中文字符");

} finally {

writer.close();

}

// 读取字节转换成字符

FileInputStream inputStream=new FileInputStream(file);

InputStreamReader reader=new InputStreamReader(

inputStream, charset);

StringBuffer buffer=new StringBuffer();

char[] buf=new char[64];

int count=0;

try {

while ((count=reader.read(buf)) !=-1) {

buffer.append(buffer, 0, count);

}

} finally {

reader.close();

}

在我们的应用程序中涉及 I/O 操作时,只要注意指定统一的编解码 Charset 字符集,一般不会出现乱码问题。

2.2 在内存操作中的编码

在内存中进行从字符到字节的数据类型转换。

1、String 类提供字符串转换到字节的方法,也支持将字节转换成字符串的构造函数。

String s="字符串";

byte[] b=s.getBytes("UTF-8");

String n=new String(b, "UTF-8");

2、Charset 提供 encode 与 decode,分别对应 char[] 到 byte[] 的编码 和 byte[] 到 char[] 的解码。

Charset charset=Charset.forName("UTF-8");

ByteBuffer byteBuffer=charset.encode(string);

CharBuffer charBuffer=charset.decode(byteBuffer);

3、在 Java 中如何编解码

Java 编码类图

首先根据指定的 charsetName 通过 Charset.forName(charsetName) 设置 Charset 类,然后根据 Charset 创建 CharsetEncoder 对象,再调用 CharsetEncoder.encode 对字符串进行编码,不同的编码类型都会对应到一个类中,实际的编码过程是在这些类中完成的。下面是 String. getBytes(charsetName) 编码过程的时序图

Java 编码时序图

从上图可以看出根据 charsetName 找到 Charset 类,然后根据这个字符集编码生成 CharsetEncoder,这个类是所有字符编码的父类,针对不同的字符编码集在其子类中定义了如何实现编码,有了 CharsetEncoder 对象后就可以调用 encode 方法去实现编码了。这个是 String.getBytes 编码方法,其它的如 StreamEncoder 中也是类似的方式。

经常会出现中文变成“?”很可能就是错误的使用了 ISO-8859-1 这个编码导致的。中文字符经过 ISO-8859-1 编码会丢失信息,通常我们称之为“黑洞”,它会把不认识的字符吸收掉。由于现在大部分基础的 Java 框架或系统默认的字符集编码都是 ISO-8859-1,所以很容易出现乱码问题,后面将会分析不同的乱码形式是怎么出现的。

几种编码格式的比较

对中文字符后面四种编码格式都能处理,GB2312 与 GBK 编码规则类似,但是 GBK 范围更大,它能处理所有汉字字符,所以 GB2312 与 GBK 比较应该选择 GBK。UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,相对来说 UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复,想比较而言 UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,所以 UTF-8 在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。

4、在 Java Web 中涉及的编解码

对于使用中文来说,有 I/O 的地方就会涉及到编码,前面已经提到了 I/O 操作会引起编码,而大部分 I/O 引起的乱码都是网络 I/O,因为现在几乎所有的应用程序都涉及到网络操作,而数据经过网络传输都是以字节为单位的,所以所有的数据都必须能够被序列化为字节。在 Java 中数据被序列化必须继承 Serializable 接口。

一段文本它的实际大小应该怎么计算,我曾经碰到过一个问题:就是要想办法压缩 Cookie 大小,减少网络传输量,当时有选择不同的压缩算法,发现压缩后字符数是减少了,但是并没有减少字节数。所谓的压缩只是将多个单字节字符通过编码转变成一个多字节字符。减少的是 String.length(),而并没有减少最终的字节数。例如将“ab”两个字符通过某种编码转变成一个奇怪的字符,虽然字符数从两个变成一个,但是如果采用 UTF-8 编码这个奇怪的字符最后经过编码可能又会变成三个或更多的字节。同样的道理比如整型数字 1234567 如果当成字符来存储,采用 UTF-8 来编码占用 7 个 byte,采用 UTF-16 编码将会占用 14 个 byte,但是把它当成 int 型数字来存储只需要 4 个 byte 来存储。所以看一段文本的大小,看字符本身的长度是没有意义的,即使是一样的字符采用不同的编码最终存储的大小也会不同,所以从字符到字节一定要看编码类型。

我们能够看到的汉字都是以字符形式出现的,例如在 Java 中“淘宝”两个字符,它在计算机中的数值 10 进制是 28120 和 23453,16 进制是 6bd8 和 5d9d,也就是这两个字符是由这两个数字唯一表示的。Java 中一个 char 是 16 个 bit 相当于两个字节,所以两个汉字用 char 表示在内存中占用相当于四个字节的空间。

这两个问题搞清楚后,我们看一下 Java Web 中那些地方可能会存在编码转换?

用户从浏览器端发起一个 HTTP 请求,需要存在编码的地方是 URL、Cookie、Parameter。服务器端接受到 HTTP 请求后要解析 HTTP 协议,其中 URI、Cookie 和 POST 表单参数需要解码,服务器端可能还需要读取数据库中的数据,本地或网络中其它地方的文本文件,这些数据都可能存在编码问题,当 Servlet 处理完所有请求的数据后,需要将这些数据再编码通过 Socket 发送到用户请求的浏览器里,再经过浏览器解码成为文本。这些过程如下图所示:

一次 HTTP 请求的编码示例

4.1 URL 的编解码

用户提交一个 URL,这个 URL 中可能存在中文,因此需要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?如下图一个 URL:

-上图中以 Tomcat 作为 Servlet Engine 为例,它们分别对应到下面这些配置文件中:

Port 对应在 Tomcat 的 中配置,而 Context Path 在 中配置,Servlet Path 在 Web 应用的 web.xml 中的

<servlet-mapping>

<servlet-name>junshanExample</servlet-name>

<url-pattern>/servlets/servlet/*</url-pattern>

</servlet-mapping>

中配置,PathInfo 是我们请求的具体的 Servlet,QueryString 是要传递的参数,注意这里是在浏览器里直接输入 URL 所以是通过 Get 方法请求的,如果是 POST 方法请求的话,QueryString 将通过表单方式提交到服务器端。

上图中 PathInfo 和 QueryString 出现了中文,当我们在浏览器中直接输入这个 URL 时,在浏览器端和服务端会如何编码和解析这个 URL 呢?为了验证浏览器是怎么编码 URL 的我选择的是360极速浏览器并通过 Postman 插件观察我们请求的 URL 的实际的内容,以下是 URL:

君山的编码结果是:e5 90 9b e5 b1 b1,和《深入分析 Java Web 技术内幕》中的结果不一样,这是因为我使用的浏览器和插件和原作者是有区别的,那么这些浏览器之间的默认编码是不一样的,原文中的结果是:

君山的编码结果分别是:e5 90 9b e5 b1 b1,be fd c9 bd,查阅上一届的编码可知,PathInfo 是 UTF-8 编码而 QueryString 是经过 GBK 编码,至于为什么会有“%”?查阅 URL 的编码规范 RFC3986 可知浏览器编码 URL 是将非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”,所以最终的 URL 就成了上图的格式了。

从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不一样的,不同浏览器对 PathInfo 也可能不一样,这就对服务器的解码造成很大的困难,下面我们以 Tomcat 为例看一下,Tomcat 接受到这个 URL 是如何解码的。

解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:

protected void convertURI(MessageBytes uri, Request request)

throws Exception {

ByteChunk bc=uri.getByteChunk();

int length=bc.getLength();

CharChunk cc=uri.getCharChunk();

cc.allocate(length, -1);

String enc=connector.getURIEncoding();

if (enc !=null) {

B2CConverter conv=request.getURIConverter();

try {

if (conv==null) {

conv=new B2CConverter(enc);

request.setURIConverter(conv);

}

} catch (IOException e) {...}

if (conv !=null) {

try {

conv.convert(bc, cc, cc.getBuffer().length -

cc.getEnd());

uri.setChars(cc.getBuffer(), cc.getStart(),

cc.getLength());

return;

} catch (IOException e) {...}

}

}

// Default encoding: fast conversion

byte[] bbuf=bc.getBuffer();

char[] cbuf=cc.getBuffer();

int start=bc.getStart();

for (int i=0; i < length; i++) {

cbuf[i]=(char) (bbuf[i + start] & 0xff);

}

uri.setChars(cbuf, 0, length);

}

从上面的代码中可以知道对 URL 的 URI 部分进行解码的字符集是在 connector 的 中定义的,如果没有定义,那么将以默认编码 ISO-8859-1 解析。所以如果有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。

QueryString 又如何解析? GET 方式 HTTP 请求的 QueryString 与 POST 方式 HTTP 请求的表单参数都是作为 Parameters 保存,都是通过 request.getParameter 获取参数值。对它们的解码是在 request.getParameter 方法第一次被调用时进行的。request.getParameter 方法被调用时将会调用 org.apache.catalina.connector.Request 的 parseParameters 方法。这个方法将会对 GET 和 POST 方式传递的参数进行解码,但是它们的解码字符集有可能不一样。POST 表单的解码将在后面介绍,QueryString 的解码字符集是在哪定义的呢?它本身是通过 HTTP 的 Header 传到服务端的,并且也在 URL 中,是否和 URI 的解码字符集一样呢?从前面浏览器对 PathInfo 和 QueryString 的编码采取不同的编码格式不同可以猜测到解码字符集肯定也不会是一致的。的确是这样 QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的 中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并不是对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码,这一点还要特别注意。

从上面的 URL 编码和解码过程来看,比较复杂,而且编码和解码并不是我们在应用程序中能完全控制的,所以在我们的应用程序中应该尽量避免在 URL 中使用非 ASCII 字符,不然很可能会碰到乱码问题,当然在我们的服务器端最好设置 中的 URIEncoding 和 useBodyEncodingForURI 两个参数。

4.2 HTTP Header 的编解码

当客户端发起一个 HTTP 请求除了上面的 URL 外还可能会在 Header 中传递其它参数如 Cookie、redirectPath 等,这些用户设置的值很可能也会存在编码问题,Tomcat 对它们又是怎么解码的呢?

对 Header 中的项进行解码也是在调用 request.getHeader 是进行的,如果请求的 Header 项没有解码则调用 MessageBytes 的 toString 方法,这个方法将从 byte 到 char 的转化使用的默认编码也是 ISO-8859-1,而我们也不能设置 Header 的其它解码格式,所以如果你设置 Header 中有非 ASCII 字符解码肯定会有乱码。

我们在添加 Header 时也是同样的道理,不要在 Header 中传递非 ASCII 字符,如果一定要传递的话,我们可以先将这些字符用 org.apache.catalina.util.URLEncoder 编码然后再添加到 Header 中,这样在浏览器到服务器的传递过程中就不会丢失信息了,如果我们要访问这些项时再按照相应的字符集解码就好了。

4.3 POST 表单的编解码

在前面提到了 POST 表单提交的参数的解码是在第一次调用 request.getParameter 发生的,POST 表单参数传递方式与 QueryString 不同,它是通过 HTTP 的 BODY 传递到服务端的。当我们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码然后提交到服务器端,在服务器端同样也是用 ContentType 中字符集进行解码。所以通过 POST 表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过 request.setCharacterEncoding(charset) 来设置。

另外针对 multipart/form-data 类型的参数,也就是上传的文件编码同样也是使用 ContentType 定义的字符集编码,值得注意的地方是上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及到字符编码,而真正编码是在将文件内容添加到 parameters 中,如果用这个编码不能编码时将会用默认编码 ISO-8859-1 来编码。

4.4 HTTP BODY 的编解码

当用户请求的资源已经成功获取后,这些内容将通过 Response 返回给客户端浏览器,这个过程先要经过编码再到浏览器进行解码。这个过程的编解码字符集可以通过 response.setCharacterEncoding 来设置,它将会覆盖 request.getCharacterEncoding 的值,并且通过 Header 的 Content-Type 返回客户端,浏览器接受到返回的 socket 流时将通过 Content-Type 的 charset 来解码,如果返回的 HTTP Header 中 Content-Type 没有设置 charset,那么浏览器将根据 Html 的 中的 charset 来解码。如果也没有定义的话,那么浏览器将使用默认的编码来解码。

4.5 其它需要编码的地方

除了 URL 和参数编码问题外,在服务端还有很多地方可能存在编码,如可能需要读取 xml、velocity 模版引擎、JSP 或者从数据库读取数据等。

xml 文件可以通过设置头来制定编码格式

<?xml version="1.0" encoding="UTF-8"?>

Velocity 模版设置编码格式:

services.VelocityService.input.encoding=UTF-8

JSP 设置编码格式:

<%@page contentType="text/html; charset=UTF-8"%>

访问数据库都是通过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,可以通过设置 JDBC URL 来制定如 MySQL:url=”jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK”。

5、常见问题分析

下面看一下,当我们碰到一些乱码时,应该怎么处理这些问题?出现乱码问题唯一的原因都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于往往一次操作涉及到多次编解码,所以出现乱码时很难查找到底是哪个环节出现了问题,下面就几种常见的现象进行分析。

5.1 中文变成了看不懂的字符

例如,字符串“淘!我喜欢!”变成了“ì ? £ ?? ò ?2?? £ ?”编码过程如下图所示:

字符串在解码时所用的字符集与编码字符集不一致导致汉字变成了看不懂的乱码,而且是一个汉字字符变成两个乱码字符

5.2 一个汉字变成一个问号

例如,字符串“淘!我喜欢!”变成了“??????”编码过程如下图所示:

将中文和中文符号经过不支持中文的 ISO-8859-1 编码后,所有字符变成了“?”,这是因为用 ISO-8859-1 进行编解码时遇到不在码值范围内的字符时统一用 3f 表示,这也就是通常所说的“黑洞”,所有 ISO-8859-1 不认识的字符都变成了“?”。

5.3 一个汉字变成两个问号

例如,字符串“淘!我喜欢!”变成了“????????????”编码过程如下图所示:

这种情况比较复杂,中文经过多次编码,但是其中有一次编码或者解码不对仍然会出现中文字符变成“?”现象,出现这种情况要仔细查看中间的编码环节,找出出现编码错误的地方。

5.4 一种不正常的正确编码

还有一种情况是在我们通过 request.getParameter 获取参数值时,当我们直接调用

String value=request.getParameter(name); 会出现乱码,但是如果用下面的方式

String value=String(request.getParameter(name).getBytes(" ISO-8859-1"), "GBK");

解析时取得的 value 会是正确的汉字字符,这种情况是怎么造成的呢?

看下如所示:

这种情况是这样的,ISO-8859-1 字符集的编码范围是 0000-00FF,正好和一个字节的编码范围相对应。这种特性保证了使用 ISO-8859-1 进行编码和解码可以保持编码数值“不变”。虽然中文字符在经过网络传输时,被错误地“拆”成了两个欧洲字符,但由于输出时也是用 ISO-8859-1,结果被“拆”开的中文字的两半又被合并在一起,从而又刚好组成了一个正确的汉字。虽然最终能取得正确的汉字,但是还是不建议用这种不正常的方式取得参数值,因为这中间增加了一次额外的编码与解码,这种情况出现乱码时因为 Tomcat 的配置文件中 useBodyEncodingForURI 配置项没有设置为”true”,从而造成第一次解析式用 ISO-8859-1 来解析才造成乱码的。

6、总结

本文首先总结了几种常见编码格式的区别,然后介绍了支持中文的几种编码格式,并比较了它们的使用场景。接着介绍了 Java 那些地方会涉及到编码问题,已经 Java 中如何对编码的支持。并以网络 I/O 为例重点介绍了 HTTP 请求中的存在编码的地方,以及 Tomcat 对 HTTP 协议的解析,最后分析了我们平常遇到的乱码问题出现的原因。

综上所述,要解决中文问题,首先要搞清楚哪些地方会引起字符到字节的编码以及字节到字符的解码,最常见的地方就是读取会存储数据到磁盘,或者数据要经过网络传输。然后针对这些地方搞清楚操作这些数据的框架的或系统是如何控制编码的,正确设置编码格式,避免使用软件默认的或者是操作系统平台默认的编码格式。

、FreeMaker介绍

FreeMarker是一款免费的Java模板引擎,是一种基于模板和数据生成文本(HMLT、电子邮件、配置文件、源代码等)的工具,它不是面向最终用户的,而是一款程序员使用的组件。

FreeMarker最初设计是用来在MVC模式的Web开发中生成HTML页面的,所以没有绑定Servlet或任意Web相关的东西上,所以它可以运行在非Web应用环境中。

发展史

FreeMarker第一版在1999年未就发布了,2002年初使用JavaCC(Java Compiler Compiler是一个用Java开发的语法分析生成器)重写了FreeMarker的核心代码,2015年FreeMarker代码迁移到了Apache下。

GitHub地址:https://github.com/apache/freemarker

工作原理

FreeMarker模板存储在服务器上,当有用户访问的时候,FreeMarker会查询出相应的数据,替换模板中的标签,生成最终的HTML返回给用户,如下图:

二、FreeMarker基础使用

基础使用分为3部分,这3部分组成了FreeMarker:

  • 指令
  • 表达式

指令是FreeMarker用来识别转换的特殊标签,表达式是标签里具体的语法实现,其他部分是一些不好分类的模板。

2.1 指令

使用FTL(freemarker template language)标签来调用指令。

指令速览:

  • assign
  • attempt, recover
  • compress
  • escape, noescape
  • flush
  • ftl
  • function, return
  • global
  • if, else, elseif
  • import
  • include
  • list, else, items, sep, break
  • local
  • macro, nested, return
  • noparse
  • nt
  • setting
  • stop
  • switch, case, default, break
  • t, lt, rt
  • visit, recurse, fallback
  • 用户自定义标签

下来我们分别来看每个指令对应具体使用。

2.1.1 assign 代码声明

assign 分为变量和代码片段声明两种。

2.1.1.1 变量声明

可以是单变量声明,或多变量声明,下面是多变量声明的示例:

<#assign name="adam" age=18 "sex"="man">

${name} - ${age} - ${"sex"}

单个变量的话,只写一个就可以了。

2.1.1.2 代码片段声明

<#assign code>

<#list ["java","golang"] as c>

${c}

</#list>

</#assign>

${code}

其中 ${code} 是用来执行方法的,如果不调用话,代码片段不会执行。

2.1.2 attempt, recover 异常指令

attempt(尝试), recover(恢复)指令类似于程序的try catch,示例如下:

<#attempt>

i am ${name}

<#recover>

error name

</#attempt>

如果有变量“name”就会正常显示,显示“i am xxx”,如果没有变量就会显示“error name”。

2.1.3 compress 压缩代码移除空白行

<#compress>

1 2 3 4 5

?

test only

?

I said, test only

</#compress>

?

1 2 3 4 5

?

?

test only

?

I said, test only

效果如下:

对空白不敏感的格式,移除空白行还是挺有用的功能。

2.1.4 escape, noescape 转义,不转义

2.1.4.1 escape使用

<#escape x as x?html>

${firstName}

${lastName}

</#escape>

上面的代码,类似于:

${firstName?html}

${lastName?html}

Java代码:

@RequestMapping("/")

public ModelAndView index() {

ModelAndView modelAndView=new ModelAndView("/index");

modelAndView.addObject("firstName", "<span style='color:red'>firstName</span>");

modelAndView.addObject("lastName", "lastName");

return modelAndView;

}

最终的效果是:

2.1.4.2 “?html”语法解析

单问号后面跟的是操作函数,类似于Java中的方法名,html属于内建函数的一个,表示字符串会按照HTML标记输出,字符替换规则如下:

  • < 替换为 <
  • > 替换为 >
  • & 替换为 &
  • " 替换为 "

2.1.4.3 noescape使用

HTML代码:

<#escape x as x?html>

<#noescape>

${firstName}

</#noescape>

${lastName}

</#escape>

Java代码:

@RequestMapping("/")

public ModelAndView index() {

ModelAndView modelAndView=new ModelAndView("/index");

modelAndView.addObject("firstName", "<span style='color:red'>firstName</span>");

modelAndView.addObject("lastName", "lastName");

return modelAndView;

}

最终效果:

2.1.5 function, return 方法声明

代码格式:

<#function name param1 param2 ... paramN>

...

<#return returnValue>

...

</#function>

  • name 为方法名称
  • param1, param2,paramN 方法传递过来的参数,可以有无限个参数,或者没有任何参数
  • return 方法返回的值,可以出现在function的任何位置和出现任意次数

示例代码如下:

<#function sum x y z>

<#return x+y+z>

</#function>

?

${sum(5,5,5)}

注意:function如果没有return是没有意义的,相当于返回null,而function之中信息是不会打印到页面的,示例如下:

<#function wantToPrint>

这里的信息是显示不了的

</#function>

?

<#if wantToPrint()??>

Message:${wantToPrint()}

</#if>

“??”用于判断值是否是null,如果为null是不执行的。如果不判null直接使用${}打印,会报模板错误,效果如下:

2.1.6 global 全局代码声明

语法如下:

<#global name=value>

<#global name1=value1 name2=value2 ... nameN=valueN>

<#global name>

capture this

</#global>

global使用和assign用法类似,只不过global声明是全局的,所有的命名空间都是可见的。

2.1.7 if elseif else 条件判断

语法如下:

<#if condition>

...

<#elseif condition2>

...

<#elseif condition3>

...

...

<#else>

...

</#if>

示例如下:

<#assign x=1 >

<#if x==1>

x is 1

<#elseif x==2>

x is 2

<#else>

x is not 1

</#if>

2.1.8 import 引入模板

语法: <#import path as hash>

示例如下

footer.ftl 代码如下:

<html>

<head>

<title>王磊的博客</title>

</head>

<body>

this is footer.ftl

<#assign copy="来自 王磊的博客">

</body>

</html>

index.ftl 代码如下:

<html>

<head>

<title>王磊的博客</title>

</head>

<body>

<#import "footer.ftl" as footer>

${footer.copy}

</body>

</html>

最终输出内容:

来自 王磊的博客

2.1.9 include 嵌入模板

语法: <#include path>

示例如下

footer.ftl 代码如下:

<html>

<head>

<title>王磊的博客</title>

</head>

<body>

this is footer.ftl

<#assign copy="来自 王磊的博客">

</body>

</html>

index.ftl 代码如下:

<html>

<head>

<title>王磊的博客</title>

</head>

<body>

<#include "footer.ftl">

</body>

</html>

最终内容如下:

this is footer.ftl

2.1.10 list, else, items, sep, break 循环

2.1.10.1 正常循环

输出1-3的数字,如果等于2跳出循环,代码如下:

<#list 1..3 as n>

${n}

<#if n==2>

<#break>

</#if>

</#list>

注意:“1..3”等于[1,2,3]。

结果: 1 2

2.1.10.2 使用items输出

示例如下:

<#list 1..3>

<ul>

<#items as n>

<li>${n}</li>

</#items>

</ul>

</#list>

2.1.10.3 sep 使用

跳过最后一项

<#list 1..3 as n>

${n}

<#sep>,</#sep>

</#list>

最终结果:1 , 2 , 3

2.1.10.4 数组最后一项

代码如下:

<#list 1..3 as n>

${n}

<#if !n_has_next>

最后一项

</#if>

</#list>

使用“变量_has_next”判断是否还有下一个选项,来找到最后一项,最终的结果:1 2 3 最后一项

2.1.11 macro 宏

宏:是一个变量名的代码片段,例如:

<#macro sayhi name>

Hello, ${name}

</#macro>

?

<@sayhi "Adam" />

相当于声明了一个名称为“sayhi”有一个参数“name”的宏,使用自定义标签“@”调用宏。

输出的结果: Hello, Adam

2.1.12 switch, case, defalut, break 多条件判断

示例代码如下:

<#assign animal="dog" >

<#switch animal>

<#case "pig">

This is pig

<#break>

<#case "dog">

This is dog

<#break>

<#default>

This is Aaimal

</#switch>

2.1.13 扩展知识

指令自动忽略空格特性

FreeMarker会忽略FTL标签中的空白标记,所以可以直接写:

<#list ["老王","老李","老张"]

as

p>

${p}

</#list>

即使是这个格式也是没有任何问题的,FreeMarker会正常解析。

2.2 表达式

2.2.1 字符串拼接

字符拼接代码:

<#assign name="ABCDEFG">

${"Hello, ${name}"}

结果:Hello, ABCDEFG

2.2.2 算术运算

2.2.2.1 算术符

算术符有五种:

  • +
  • -
  • *
  • /
  • % 求余(求模)

示例代码:

${100 - 10 * 20}

输出:

-100

2.2.2.2 数值转换

${1.999?int}

输出:

1

注意:数值转换不会进行四舍五入,会舍弃小数点之后的。

2.2.3 内建函数(重点)

内建函数:相当于我们Java类里面的内置方法,非常常用,常用的内建函数有:时间内建函数、字符内建函数、数字内建函数等。

2.2.3.1 单个问号和两个问号的使用和区别

单问号:在FreeMarker中用单个问号,来调用内建函数,比如: ${"admin"?length} 查看字符串“admin”的字符长度,其中length就是字符串的内建函数。

双引号:表示用于判断值是否为null,比如:

<#if admin??>

Admin is not null

</#if>

2.2.3.2 字符串内建函数

2.2.3.2.1 是否包含判断

使用contains判断,代码示例:

<#if "admin"?contains("min")>

min

<#else >

not min

</#if>

输出:

min

2.2.3.2.2 大小写转换

示例代码:

<#assign name="Adam">

${name?uncap_first}

${name?upper_case}

${name?cap_first}

${name?lower_case}

输出:

adam ADAM Adam adam

更多的字符串内建函数:https://freemarker.apache.org/docs/ref_builtins_string.html

2.2.3.3 数字内建函数

示例代码:

${1.23569?string.percent}

${1.23569?string["0.##"]}

${1.23569?string["0.###"]}

输出:

124% 1.24 1.236

注意:

  • 使用string.percent计算百分比,会自动四舍五入。
  • 使用“?string["0.##"]”可以自定义取小数点后几位,会自动四舍五入。

2.2.3.4 时间内建函数

2.2.3.4.1 时间戳转换为任何时间格式

代码:

<#assign timestamp=1534414202000>

${timestamp?number_to_datetime?string["yyyy/MM/dd HH:mm"]}

输出:

2018/08/16 18:10

2.2.3.4.2 时间格式化

示例代码:

<#assign nowTime=.now>

${nowTime} <br />

${nowTime?string["yyyy/MM/dd HH:mm"]} <br />

输出:

2018-8-16 18:33:50

2018/08/16 18:33

更多内建方法:https://freemarker.apache.org/docs/ref_builtins.html

三、Spring Boot 集成

3.1 集成环境

  • Spring Boot 2.0.4
  • FreeMaker 2.3.28
  • JDK 8
  • Windows 10
  • IDEA 2018.2.1

3.2 集成步骤

3.2.1 pom.xml 添加FreeMaker依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-freemarker</artifactId>

</dependency>

3.2.2 application.properties 配置模板

主要配置,如下:

## Freemarker 配置

spring.freemarker.template-loader-path=classpath:/templates/

spring.freemarker.cache=false

spring.freemarker.charset=UTF-8

spring.freemarker.check-template-location=true

spring.freemarker.content-type=text/html

spring.freemarker.expose-request-attributes=false

spring.freemarker.expose-session-attributes=false

spring.freemarker.request-context-attribute=request

spring.freemarker.suffix=.ftl

配置项类型默认值建议值说明spring.freemarker.template-loader-pathStringclasspath:/templates/默认模版存放路径spring.freemarker.cachebooltrue默认是否开启缓存,生成环境建议开启spring.freemarker.charsetString-UTF-8编码spring.freemarker.content-typeStringtext/htmltext/htmlcontent-type类型spring.freemarker.suffixString.ftl.ftl模板后缀spring.freemarker.expose-request-attributesboolfalsefalse设定所有request的属性在merge到模板的时候,是否要都添加到model中spring.freemarker.expose-session-attributesboolfalsefalse设定所有HttpSession的属性在merge到模板的时候,是否要都添加到model中.spring.freemarker.request-context-attributeString-requestRequestContext属性的名称

更多配置:

# FREEMARKER (FreeMarkerProperties)

spring.freemarker.allow-request-override=false # Whether HttpServletRequest attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.freemarker.allow-session-override=false # Whether HttpSession attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.freemarker.cache=false # Whether to enable template caching.

spring.freemarker.charset=UTF-8 # Template encoding.

spring.freemarker.check-template-location=true # Whether to check that the templates location exists.

spring.freemarker.content-type=text/html # Content-Type value.

spring.freemarker.enabled=true # Whether to enable MVC view resolution for this technology.

spring.freemarker.expose-request-attributes=false # Whether all request attributes should be added to the model prior to merging with the template.

spring.freemarker.expose-session-attributes=false # Whether all HttpSession attributes should be added to the model prior to merging with the template.

spring.freemarker.expose-spring-macro-helpers=true # Whether to expose a RequestContext for use by Spring's macro library, under the name "springMacroRequestContext".

spring.freemarker.prefer-file-system-access=true # Whether to prefer file system access for template loading. File system access enables hot detection of template changes.

spring.freemarker.prefix=# Prefix that gets prepended to view names when building a URL.

spring.freemarker.request-context-attribute=# Name of the RequestContext attribute for all views.

spring.freemarker.settings.*=# Well-known FreeMarker keys which are passed to FreeMarker's Configuration.

spring.freemarker.suffix=.ftl # Suffix that gets appended to view names when building a URL.

spring.freemarker.template-loader-path=classpath:/templates/ # Comma-separated list of template paths.

spring.freemarker.view-names=# White list of view names that can be resolved.

3.2.3 编写HTML代码

<html>

<head>

<title>王磊的博客</title>

</head>

<body>

<div>

Hello,${name}

</div>

</body>

</html>

3.2.4 编写Java代码

新建index.java文件,Application.java(入口文件)代码不便,index.java代码如下:

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;

?

@Controller

@RequestMapping("/")

public class Index {

@RequestMapping("/")

public ModelAndView index() {

ModelAndView modelAndView=new ModelAndView("/index");

modelAndView.addObject("name", "老王");

return modelAndView;

}

}

?

关键代码解读:

  1. @Controller注解:标识自己为控制器,只需要配置@RequestMapping之后,就可以把用户URL映射到控制器;
  2. 使用ModelAndView对象,指定视图名&添加视图对象。

3.2.5 运行

执行上面4个步骤之后,就可以运行这个Java项目了,如果是IDEA使用默认快捷键“Shift + F10”启动调试,在页面访问:http://localhost:8080/ 就可看到如下效果:

四、参考资料

FreeMarker官方文档:https://freemarker.apache.org/

FreeMarker翻译的中文网站:http://freemarker.foofun.cn/toc.html




近看了一些关于JavaScript的测试脚本,觉得JS 中问号的用法还是蛮有意思的,于是做了一下总结,在这里分享给大家!JS中的问号大概有三种用法,分别是:空值合并操作符、可选链操作符和三目运算。

问号问号(??)

空值合并操作符??是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

例如

console.log(null ?? "xx")
输出 xx
console.log(1 ?? "xx")
输出 1

问号点 (?.)

可选链操作符(?.)可选链操作符允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。 使用它的好处是引用为null 或者 undefined的情况下不会引起错误。

语法:obj?.prop obj?.[expr] arr?.[index] func?.(args)

例如

var obj={a:{b:1}}
console.log(obj?.a?.b)
输出1
console.log(obj?.a?.c)
输出 undefined

问号冒号(?: )

这是三目运算,具体表达式是(condition ? exprIfTrue : exprIfFalse)

该表达式的含义是 条件condition是真,则执行exprIfTrue ,否则执行exprIfFalse

举个例子大家就懂了

var n=10;
console.log((n >=11) ? "a" : "b");
输出b
当 var n=12;
输出a

如果您还知道哪些JS 中关于问号的特殊用法欢迎留言讨论。如果文章帮到了您,劳烦点赞转发!