4.1.2 input标签

input标签作用 被渲染成HTML input标签

Spring MVCinput标签会被渲染为一个类型为text的普通HTML input标签。

input标签用途 绑定表单数据

使用SpringMVCinput标签的唯一目的就是绑定表单数据,通过path属性来指定要绑定的Model中的值。

input标签属性

Input标签可使用如表4.3所示的属性。表4.3中列出的只是SpringMVCinput标签的常用属性,并没有包含HTML的相关属性。

属性 描述
cssClass 定义要应用到被渲染的Input元素的CSS
cssStyle 定义要应用到被渲染的Input元素的CSS样式
cssErrorClass 定义要应用到被渲染Input元素的CSS类,如果bound属性中包含错误,则覆盖cssClass属性值
htmlEscape boolean值,表示被渲染的值是否应该进行HTML转义
path 要绑定的属性路径

示例: form和input标签的使用

项目结构

展开/折叠
G:\Desktop\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\04\FormTest
├─src\
│ └─org\
│   └─fkit\
│     ├─controller\
│     │ └─UserController.java
│     └─domain\
│       └─User.java
└─WebContent\
  ├─index.jsp
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─content\
    │ ├─registerForm.jsp
    │ └─registerForm2.jsp
    ├─lib\
    │ ├─commons-logging-1.2.jar
    │ ├─spring-aop-5.0.1.RELEASE.jar
    │ ├─spring-aspects-5.0.1.RELEASE.jar
    │ ├─spring-beans-5.0.1.RELEASE.jar
    │ ├─spring-context-5.0.1.RELEASE.jar
    │ ├─spring-context-indexer-5.0.1.RELEASE.jar
    │ ├─spring-context-support-5.0.1.RELEASE.jar
    │ ├─spring-core-5.0.1.RELEASE.jar
    │ ├─spring-expression-5.0.1.RELEASE.jar
    │ ├─spring-instrument-5.0.1.RELEASE.jar
    │ ├─spring-jcl-5.0.1.RELEASE.jar
    │ ├─spring-jdbc-5.0.1.RELEASE.jar
    │ ├─spring-jms-5.0.1.RELEASE.jar
    │ ├─spring-messaging-5.0.1.RELEASE.jar
    │ ├─spring-orm-5.0.1.RELEASE.jar
    │ ├─spring-oxm-5.0.1.RELEASE.jar
    │ ├─spring-test-5.0.1.RELEASE.jar
    │ ├─spring-tx-5.0.1.RELEASE.jar
    │ ├─spring-web-5.0.1.RELEASE.jar
    │ ├─spring-webflux-5.0.1.RELEASE.jar
    │ ├─spring-webmvc-5.0.1.RELEASE.jar
    │ └─spring-websocket-5.0.1.RELEASE.jar
    ├─springmvc-config.xml
    └─web.xml

registerForm.jsp

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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!-- 要使用SpringMVC的表单标签库,必须在JSP页面的开头处声明taglib指令: -->
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>测试form标签</title>
</head>
<body>
<h3>注册页面</h3>
<!-- 如果Model中存在一个属性名称为command的JavaBean, -->
<!-- 而且该JavaBean拥有属性username、sex和age, -->
<!-- 则在渲染上面的代码时就会取command的对应属性值赋给对应标签的属性。 -->
<!-- form绑定一个User对象,form的子标签input绑定User对象的成员变量 -->
<form:form method="post" action="register">
<table>
<tr>
<td>姓名:</td>
<!-- 使用SpringMVC的input标签的唯一目的就是绑定表单数据, -->
<!-- 通过path属性来指定form绑定的command属性表示的 -->
<!-- User对象的username属性的值 -->
<td><form:input path="username" /></td>
</tr>
<tr>
<td>性别:</td>
<td><form:input path="sex" /></td>
</tr>
<tr>
<td>年龄:</td>
<td><form:input path="age" /></td>
</tr>
</table>
</form:form>
</body>
</html>

如果Model中存在一个属性名称为commandJavaBean,而且该JavaBean拥有属性usernamesexage,则在渲染上面的代码时就会取command这个JavaBean的属性值赋给forminput标签同名属性。(也就是说:Model中名为commandJavaBean的成员变量username赋值给path="username“这个input标签,以此类推)

UserController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.fkit.controller;

import org.fkit.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {

@GetMapping(value = "/registerForm")
public String registerForm(Model model)
{
User user = new User("jack", "男", 28);
// model中添加属性command,值是user对象
// 这里很重要
// 表单标签当将把input标签输入的数据绑定到给对象的属性上
model.addAttribute("command", user);
// 返回输入表单的视图
return "registerForm";
}
@GetMapping(value = "/registerForm2")
public String registerForm2(Model model)
{
User user = new User("小明", "男", 18);
// model中添加属性user,值是User对象
model.addAttribute("user", user);
return "registerForm2";
}
}

分析

注意registerForm方法中的代码:

1
model.addAttribute("command", user);

该代码将user设置到Model当中,属性名为“command”。

web.xml文件和springmvc-config.xml文件和之前描述的一致,此处不再赘述。

测试

部署FormTest这个Web应用,在浏览器中输入如下URL来测试应用:

http://localhost:8080/FormTest/registerForm

会看到如图4.1所示的界面。

渲染效果

在上面的代码中,假设Model中存在一个属性名称为commandJavaBean,且它的usernamesexage属性值分别为“jack”、“男”和“28”,则在浏览器页面单击→右键→查看页面源代码,可以看到Spring MVCfrom标签和input标签渲染时生成的html代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<form id="command" action="register" method="post">
<table>
<tr>
<td>姓名:</td>
<!-- 使用SpringMVC的input标签的唯一目的就是绑定表单数据, -->
<!-- 通过path属性来指定form绑定的command属性表示的 -->
<!-- User对象的username属性的值 -->
<td><input id="username" name="username" type="text" value="jack"/></td>
</tr>
<tr>
<td>性别:</td>
<td><input id="sex" name="sex" type="text" value="男"/></td>
</tr>
<tr>
<td>年龄:</td>
<td><input id="age" name="age" type="text" value="28"/></td>
</tr>
</table>
</form>

从上面生成的代码,我们可以看出,当没有指定form标签的id属性时它会自动获取该form标签绑定的Model中对应属性名称command作为id;而对于input标签,在没有指定id的情况下它会自动获取path指定的属性值作为它的idname

使用form标签的commandName属性指定绑定的JavaBean

Spring MVC指定form标签默认自动绑定的是Modelcommand属性,那么当form对象对应的属性名称不是command时,应该怎么办呢?对于这种情况,Spring提供了一个commandName属性,可以通过commandName属性来指定将使用Model中的哪个属性作为form标签需要绑定的command对象。除了commandName属性外,指定modelAttribute属性也可以达到相同的效果。这里假设上面代码存放在Model中的是user对象而不是默认的command对象,那么我们的代码就可以如下定义了:

registerForm2.jsp

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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!-- 想要使用标签标签,必须引入该标签库 -->
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>测试form标签</title>
</head>
<body>
<h3>注册页面</h3>
<!-- 绑定的数据是Model中的user属性 -->
<!-- 也就是绑定一个User对象到当前表单上 -->
<form:form modelAttribute="user" method="post" action="register">
<table>
<tr>
<td>姓名:</td>
<!-- 绑定User对象的username成员变量 -->
<!-- username成员变量的值将会放到input标签的value属性之中 -->
<td><form:input path="username" /></td>
</tr>
<tr>
<td>性别:</td>
<td><form:input path="sex" /></td>
</tr>
<tr>
<td>年龄:</td>
<td><form:input path="age" /></td>
</tr>
</table>
</form:form>
</body>
</html>

注意,这里使用了form表单标签的modelAttribute属性,设置属性值为“user”:

<form:form modelAttribute="user" method="post" action="register">

对应的请求处理方法

1
2
3
4
5
6
7
8
@GetMapping(value = "/registerForm2")
public String registerForm2(Model model)
{
User user = new User("小明", "男", 18);
// model中添加属性user,值是User对象
model.addAttribute("user", user);
return "registerForm2";
}

注意代码:

model.addAttribute("user", user);

它将user设置到Model当中,属性名不是“command”,而是“user”。

测试

在浏览器中输入如下URL来测试应用:

http://localhost:8080/FormTest/registerForm2

渲染效果

然后查看源码,渲染结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<form id="user" action="register" method="post">
<table>
<tr>
<td>姓名:</td>
<!-- 绑定User对象的username成员变量 -->
<!-- username成员变量的值将会放到input标签的value属性之中 -->
<td><input id="username" name="username" type="text" value="小明"/></td>
</tr>
<tr>
<td>性别:</td>
<td><input id="sex" name="sex" type="text" value="男"/></td>
</tr>
<tr>
<td>年龄:</td>
<td><input id="age" name="age" type="text" value="18"/></td>
</tr>
</table>
</form>

4.1.1 form标签

form标签作用

Spring MVCform标签主要有两个作用:

  1. 自动绑定Model中的一个属性值到当前form对应的实体对象上,默认为command属性,这样我们就可以在form表单体里面方便地使用该对象的属性了。
  2. 支持我们在提交表单时使用除GETPOST之外的其他方法进行提交,包括DELETEPUT等。

form标签属性

form标签可使用如表4.2所示的属性。表4.2中列出的只是Spring MVCform标签的常用属性,并没有包含HTML中如methodaction等属性。

属性 描述
modelAttribute form绑定的模型属性名称,默认为command,可自定义修改
commandName form绑定的模型属性名称,默认为command,可自定义修改
acceptCharset 定义服务器接受的字符编码
cssClass 定义要应用到被渲染的form元素的CSS
cssStyle 定义要应用到被渲染的form元素的CSS样式
htmlEscape boolean值,表示被渲染的值是否应该进行HTML转义

cammandName属性

commandName属性是其中最重要的属性,它定义了模型属性的名称,其中包含了一个绑定的JavaBean对象,该对象的属性将用于填充所生成的表单。如果commandName属性存在,则必须在返回包含该表单的视图的请求处理方法中添加响应的模型属性。

通常我们都会指定commandNamemodelAttribute属性,指定绑定到的JavaBean的名称,这两个属性功能基本一致。

4.1 表单标签库

表单标签库的实现类在spring-webmvc.jar文件当中,标签库描述文件是spring-form.tld

使用表单标签库的前提

要使用SpringMVC的表单标签库,必须在JSP页面的开头处声明taglib指令:

1
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

如何使用表单标签库

表4.1显示了表单标签库中的所有标签。

标签 描述
form 渲染表单元素
nput 渲染<input type=text>元素
password 渲染<input type=password>元素
hidden 渲染<input type=hidden>元素
textarea 渲染textarea元素
checkbox 渲染一个<input type=checkbox>元素
checkboxes 渲染多个<input type=checkbox>元素
radiobutton 渲染一个<input type=ridio>元素
radiobuttons 渲染多个<input type=ridio>元素
select 渲染一个选择元素
option 渲染一个可选元素
options 渲染一个可选元素列表
crrors span元素中渲染字段错误

4.1.1 form标签

本章要点

  • form标签
  • input标签
  • password标签
  • hidden标签
  • textarea标签
  • checkbox标签和checkboxes标签
  • radiobutton标签和radiobuttons标签
  • select标签
  • option标签和options标签
  • errors标签Spring从2.0版开始,

Spring从2.0版开始,提供了一组功能强大的标签用来在JSPSpring Web MVC中处理表单元素。相比其他的标签库,Spring的标签库集成在Spring Web MVC中,因此,这里的标签可以访问控制器处理命令对象和绑定数据,这样一来JSP更容易开发、阅读和维护。

8.8.5 Java9新增的不可变集合

Java9终于增加这个功能了,以前假如要创建一个包含6个元素的set集合,程序需要先创建Set集合,然后调用6次add()方法向Set集合中添加元素。

使用of方法创建不可变集合

Java9对此进行了简化,程序直接调用SetListMapof()方法即可创建包含N个元素的不可变集合,这样一行代码就可创建包含N个元素的集合。
注意of方法创建的是不可变集合,不可变意味着程序不能向集合中添加元素,也不能从集合中删除元素。

Set接口的of方法

方法 描述
static <E> Set<E> of() Returns an unmodifiable set containing zero elements.
static <E> Set<E> of(E e1) Returns an unmodifiable set containing one element.
static <E> Set<E> of(E... elements) Returns an unmodifiable set containing an arbitrary number of elements.
static <E> Set<E> of(E e1, E e2) Returns an unmodifiable set containing two elements.
static <E> Set<E> of(E e1, E e2, E e3) Returns an unmodifiable set containing three elements.
static <E> Set<E> of(E e1, E e2, E e3, E e4) Returns an unmodifiable set containing four elements.
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5) Returns an unmodifiable set containing five elements.
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6) Returns an unmodifiable set containing six elements.
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) Returns an unmodifiable set containing seven elements.
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) Returns an unmodifiable set containing eight elements.
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9) Returns an unmodifiable set containing nine elements.
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) Returns an unmodifiable set containing ten elements.

List接口的of方法

方法 描述
static <E> List<E> of() Returns an unmodifiable list containing zero elements.
static <E> List<E> of(E e1) Returns an unmodifiable list containing one element.
static <E> List<E> of(E... elements) Returns an unmodifiable list containing an arbitrary number of elements.
static <E> List<E> of(E e1, E e2) Returns an unmodifiable list containing two elements.
static <E> List<E> of(E e1, E e2, E e3) Returns an unmodifiable list containing three elements.
static <E> List<E> of(E e1, E e2, E e3, E e4) Returns an unmodifiable list containing four elements.
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5) Returns an unmodifiable list containing five elements.
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6) Returns an unmodifiable list containing six elements.
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) Returns an unmodifiable list containing seven elements.
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) Returns an unmodifiable list containing eight elements.
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9) Returns an unmodifiable list containing nine elements.
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) Returns an unmodifiable list containing ten elements.

Map接口的of方法

方法 描述
static <K,​V> Map<K,​V> of() Returns an unmodifiable map containing zero mappings.
static <K,​V> Map<K,​V> of(K k1, V v1) Returns an unmodifiable map containing a single mapping.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2) Returns an unmodifiable map containing two mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3) Returns an unmodifiable map containing three mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) Returns an unmodifiable map containing four mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) Returns an unmodifiable map containing five mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6) Returns an unmodifiable map containing six mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) Returns an unmodifiable map containing seven mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) Returns an unmodifiable map containing eight mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) Returns an unmodifiable map containing nine mappings.
static <K,​V> Map<K,​V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) Returns an unmodifiable map containing ten mappings.

使用Map接口的ofEntries方法创建不可变Map

方法 描述
static <K,​V> Map<K,​V> ofEntries(Map.Entry<? extends K,​? extends V> ... entries) Returns an unmodifiable map containing keys and values extracted from the given entries.

程序 使用of方法创建不可变集合

下程序示范了如何创建不可变集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class Java9Collection {
public static void main(String[] args) {
// 创建包含4个元素的Set集合
Set set = Set.of("Java", "Kotlin", "Go", "Swift");
System.out.println(set);
// 不可变集合,下面代码导致运行时错误
// set.add("Ruby");
// 创建包含4个元素的List集合
List list = List.of(34, -25, 67, 231);
System.out.println(list);
// 不可变集合,下面代码导致运行时错误
// list.remove(1);
// 创建包含3组key-value对的Map集合
Map map = Map.of("语文", 89, "数学", 82, "英语", 92);
System.out.println(map);
// 不可变集合,下面代码导致运行时错误
// map.remove("语文");
// 使用Map.entry()方法显式构建key-value对
Map map2 = Map.ofEntries(Map.entry("语文", 89), Map.entry("数学", 82), Map.entry("英语", 92));
System.out.println(map2);
}
}
1
2
3
4
[Go, Kotlin, Java, Swift]
[34, -25, 67, 231]
{数学=82, 英语=92, 语文=89}
{数学=82, 英语=92, 语文=89}

小结

创建不可变Set、不可变List比较简单,程序只要为它们的of方法传入N个集合元素即可创建SetList集合。

创建不可变的Map集合有两个方法:

  • 使用of方法时只要依次传入多个key-Value对即可;
  • 还可使用ofEntries()方法,该方法可接受多个Enty对象,因此程序显式使用Map.entry方法来创建MapEntry对象。使用这种方法比较美观,建议使用ofEntries方法。

Map接口的of方法不支持变参

还有就是Map接口的of方法不支持变参,所以Map接口的of方法只能创建最多包含10个key-valuemap
如果超过10个的话需要使用ofEntries方法

14.3.3 使用注解的示例

下面分别介绍两个使用注解的例子,第一个注解@Testable没有任何成员变量,仅是一个标记注解,它的作用是标记哪些方法是可测试的。

程序 定义注解

1
2
3
4
5
6
7
8
9
import java.lang.annotation.*;

// 使@Retention指定注解的保留到运行时
@Retention(RetentionPolicy.RUNTIME)
// 使用@Target指定被修饰的注解可用于修饰方法
@Target(ElementType.METHOD)
// 定义一个标记注解,不包含任何成员变量,即不可传入元数据
public @interface Testable {
}

上面程序定义了一个@Testable注解,定义该注解时使用了@Retention@Target两个JDK的元注解,其中@Retention注解指定@Testable注解可以保留到运行时(JVM可以提取到该注解的信息),而@Target注解指定@Testable只能修饰方法。

上面的@Testable用于标记哪些方法是可测试的,该注解可以作为JUnit测试框架的补充,在JUnit框架中它要求测试用例的测试方法必须以test开头。如果使用@Testable注解,则可把任何方法标记为可测试的。

如下MyTest测试用例中定义了8个方法,这8个方法没有太大的区别,其中4个方法使用@Testable注解来标记这些方法是可测试的。

程序 使用注解

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
public class MyTest {
// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m1() {
}

public static void m2() {
}

// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m3() {
throw new IllegalArgumentException("参数出错了!");
}

public static void m4() {
}

// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m5() {
}

public static void m6() {
}

// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m7() {
throw new RuntimeException("程序业务出现异常!");
}

public static void m8() {
}
}

正如前面提到的,仅仅使用注解来标记程序元素对程序是不会有仼何影响的,这也是Java注解的条重要原则。为了让程序中的这些注解起作用,接下来必须为这些注解提供一个注解处理工具。

程序 反射运行注解修饰的方法

下面的注解处理工具会分析目标类,如果目标类中的方法使用了@Testable注解修饰,则通过反射来运行该测试方法。

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
import java.lang.reflect.*;

public class ProcessorTest {
public static void process(String clazz) throws ClassNotFoundException {
int passed = 0;
int failed = 0;
// 遍历clazz对应的类里的所有方法
for (Method m : Class.forName(clazz).getMethods()) {
// 如果该方法使用了@Testable修饰
if (m.isAnnotationPresent(Testable.class)) {
try {
// 调用m方法
m.invoke(null);
// 测试成功,passed计数器加1
passed++;
} catch (Exception ex) {
System.out.println("方法" + m + "运行失败,异常:" + ex.getCause());
// 测试出现异常,failed计数器加1
failed++;
}
}
}
// 统计测试结果
System.out
.println("共运行了:" + (passed + failed) + "个方法,其中:\n" + "失败了:" + failed + "个,\n" + "成功了:" + passed + "个!");
}
}

ProcessorTest类里只包含一个process(String clazz)方法,该方法可接收一个字符串参数,该方法将会分析clazz参数所代表的类,并运行该类里使用@Testable修饰的方法

该程序的主类非常简单,提供主方法,使用ProcessorTest来分析目标类即可。

程序 测试类

1
2
3
4
5
6
public class RunTests {
public static void main(String[] args) throws Exception {
// 处理MyTest类
ProcessorTest.process("MyTest");
}
}

运行上面程序,会看到如下运行结果:

1
2
3
4
5
6
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\14\14.3\01>java RunTests
方法public static void MyTest.m3()运行失败,异常:java.lang.IllegalArgumentException: 参数出错了!
方法public static void MyTest.m7()运行失败,异常:java.lang.RuntimeException: 程序业务出现异常!
共运行了:4个方法,其中:
失败了:2个,
成功了:2个!

通过这个运行结果可以看出,程序中的@Testable起作用了,MyTest类里以@Testable注解修饰的方法都被测试了。

通过上面例子读者不难看出,其实注解十分简单,它是对源代码増加的一些特殊标记,这些特殊标记可通过反射获取,当程序获取这些特殊标记后,程序可以做出相应的处理(当然也可以完全忽略这些注解)。

使用注解来简化事件编程

前面介绍的只是一个标记注解,程序通过判断该注解存在与否来决定是否运行指定方法。下面程序通过使用注解来简化事件编程,在传统的事件编程中总是需要通过addActionListener()方法来为事件源绑定事件监听器,本示例程序中则通过@ActionListenerFor来为程序中的按钮绑定事件监听器。

程序 定义注解

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.*;
import java.awt.event.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
// 定义一个成员变量,用于设置元数据
// 该listener成员变量用于保存监听器实现类
Class<? extends ActionListener> listener();
}

定义了这个@ActionListenerFor之后,使用该注解时需要指定一个listener成员变量,该成员变量用于指定监听器的实现类。

下面程序使用@ActionListenerFor注解来为两个按钮绑定事件监听器。

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
import java.awt.event.*;
import javax.swing.*;

public class AnnotationTest {
private JFrame mainWin = new JFrame("使用注解绑定事件监听器");
// 使用@ActionListenerFor注解为ok按钮绑定事件监听器
@ActionListenerFor(listener = OkListener.class)
private JButton ok = new JButton("确定");
// 使用@ActionListenerFor注解为cancel按钮绑定事件监听器
@ActionListenerFor(listener = CancelListener.class)
private JButton cancel = new JButton("取消");

public void init() {
// 初始化界面的方法
JPanel jp = new JPanel();
jp.add(ok);
jp.add(cancel);
mainWin.add(jp);
ActionListenerInstaller.processAnnotations(this); // ①
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}

public static void main(String[] args) {
new AnnotationTest().init();
}
}

// 定义ok按钮的事件监听器实现类
class OkListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
JOptionPane.showMessageDialog(null, "单击了确认按钮");
}
}

// 定义cancel按钮的事件监听器实现类
class CancelListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
JOptionPane.showMessageDialog(null, "单击了取消按钮");
}
}

上面程序中定义了两个JButton按钮,并使用@ActionListenerFor注解为这两个按钮绑定了事件监听器,使用@ActionListenerFor注解时传入了listener元数据,该数据用于设定每个按钮的监听器实现类
正如前面提到的,如果仅在程序中使用注解是不会起任何作用的,必须使用注解处理工具来处理程序中的注解。程序中①处代码使用了ActionListenerInstaller类来处理本程序中的注解,该处理器分析目标对象中的所有成员变量,如果该成员变量前使用了@ActionListenerFor修饰,则取出该注解中的listener元数据,并根据该数据来绑定事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.lang.reflect.*;
import java.awt.event.*;
import javax.swing.*;

public class ActionListenerInstaller {
// 处理注解的方法,其中obj是包含注解的对象
public static void processAnnotations(Object obj) {
try {
// 获取obj对象的类
Class cl = obj.getClass();
// 获取指定obj对象的所有成员变量,并遍历每个成员变量
for (Field f : cl.getDeclaredFields()) {
// 将该成员变量设置成可自由访问。
f.setAccessible(true);
// 获取该成员变量上ActionListenerFor类型的注解
ActionListenerFor a = f.getAnnotation(ActionListenerFor.class);
// 获取成员变量f的值
Object fObj = f.get(obj);
// 如果f是AbstractButton的实例,且a不为null
if (a != null && fObj != null && fObj instanceof AbstractButton) {
// 获取a注解里的listner元数据(它是一个监听器类)
Class<? extends ActionListener> listenerClazz = a.listener();
// 使用反射来创建listner类的对象
ActionListener al = listenerClazz.newInstance();
AbstractButton ab = (AbstractButton) fObj;
// 为ab按钮添加事件监听器
ab.addActionListener(al);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.lang.reflect.*;
import java.awt.event.*;
import javax.swing.*;

public class ActionListenerInstaller {
// 处理注解的方法,其中obj是包含注解的对象
public static void processAnnotations(Object obj) {
try {
// 获取obj对象的类
Class cl = obj.getClass();
// 获取指定obj对象的所有成员变量,并遍历每个成员变量
for (Field f : cl.getDeclaredFields()) {
// 将该成员变量设置成可自由访问。
f.setAccessible(true);
// 获取该成员变量上ActionListenerFor类型的注解
ActionListenerFor a = f.getAnnotation(ActionListenerFor.class);
// 获取成员变量f的值
Object fObj = f.get(obj);
// 如果f是AbstractButton的实例,且a不为null
if (a != null && fObj != null && fObj instanceof AbstractButton) {
// 获取a注解里的listner元数据(它是一个监听器类)
Class<? extends ActionListener> listenerClazz = a.listener();
// 使用反射来创建listner类的对象
ActionListener al = listenerClazz.newInstance();
AbstractButton ab = (AbstractButton) fObj;
// 为ab按钮添加事件监听器
ab.addActionListener(al);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

上面程序中的两行粗体字代码根据@ActionListenerFor注解的元数据取得了监听器实现类,然后通过反射来创建监听器对象,接下来将监听器对象绑定到指定的按钮(按钮由被@ActionListenerFor修饰的Field表示)。
运行上面的AnnotationTest程序,会看到如图14.3所示的窗口

单击“确定”按钮,将会弹出如图14.4所示的“单击了确认按钮”对话框:

这表明使用该注解成功地为okcancel两个按钮绑定了事件监听器。

14.3.2 提取注解信息

使用注解修饰了类、方法、成员变量等成员之后,这些注解不会自己生效,必须由开发者提供相应的工具来提取并处理注解信息。
Java使用java.lang.annotation.Annotation接口来代表程序元素前面的注解,该接口是所有注解的父接口。Java5java.lang.reflect包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类

AnnotatedElement接口实现类 描述
Class 类定义。
Constructor 构造器定义。
Field 类的成员变量定义
Method 类的方法定义。
Package 类的包定义。

java.lang.reflect包下主要包含一些实现反射功能的工具类,从Java5开始,java.lang.reflect包所提供的反射API增加了读取运行时注解的能力。只有当定义注解时使用了@Retention(RetentionPolicy.RUNTIME)修饰,该注解才会在运行时可见,JVM才会在装载*.class文件时读取保存在class文件中的注解信息

AnnotatedElement接口方法

AnnotatedElement接口是所有程序元素(如ClassMethodConstructor等)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象(如ClassMethodConstructor等)之后,程序就可以调用该对象的如下几个方法来访问注解信息。

方法 描述
<T extends Annotation> T getAnnotation(Class<T> annotationClass) 返回该程序元素上存在的、指定类型的注解,如果该类型的注解不存在,则返回null
Annotation[] getAnnotations() 返回该程序元素上存在的所有注解。
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) 该方法的功能与前面介绍的getAnnotation()方法基本相似。但由于Java8增加了重复注解功能,因此需要使用该方法获取修饰该程序元素、指定类型的多个注解
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) 这是Java8新增的方法,该方法尝试获取直接修饰该程序元素、指定类型的注解。如果该类型的注解不存在,则返回null
Annotation[] getDeclaredAnnotations() 返回直接修饰该程序元素的所有注解
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) 该方法的功能与前面介绍的getDeclaredAnnotations()方法基本相似。但由于Java8增加了重复注解功能,因此需要使用该方法获取直接修饰该程序元素、指定类型的多个注解
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 判断该程序元素上是否存在指定类型的注解,如果存在则返回true,否则返回false

程序 获取注解

下面程序片段用于获取Test类的info()方法里的所有注解,并将这些注解打印出来。

1
2
3
4
5
6
//获取Test类的info方法的所有注解
Annotation[] aArray = Class.forName("Test" ).getMethod("info").getAnnotations();
//遍历所有注解
for (Annotation an : aArray){
System.out.println(an);
}

如果需要获取某个注解里的元数据,则可以将注解强制类型转换成所需的注解类型,然后通过注解对象的抽象方法来访问这些元数据。如下代码片段所示。

未完待续…

14.5 本章小结

本章主要介绍了Java的注解支持,通过使用注解可以为程序提供一些元数据,这些元数据可以在编译、运行时被读取,从而提供更多额外的处理信息。
本章详细介绍了JDK提供的5个基本注解的用法,也详细讲解了JDK提供的4个用于修饰注解的元注解的用法。
除此之外,本章也介绍了如何自定义并使用注解,
最后还介绍了使用APT工具来处理注解

本章练习

  1. 定义一个简单的@Foo注解,该注解只能修饰类、方法,该注解只在源代码阶段有效。
  2. 定义一个@Bar注解,并为该注解提供nameprice两个属性,该注解只能修饰方法、成员变量
  3. 定义@Getter@Setter注解,它们只能修饰成员变量。为这两个注解编写APT工具,APT工具会为它们修饰的成员变量对应地添加gettersetter方法。

14.4 编译时处理注解

APT(Annotation Processing Tool)是一种注解处理工具,它对源代码文件进行检测,并找出源文件所包含的注解信息,然后针对注解信息进行额外的处理。
使用APT工具处理注解时可以根据源文件中的注解生成额外的源文件和其他的文件(文件的具体内容由注解处理器的编写者决定),APT还会将生成的源代码文件和原来的源文件一起编译生成class文件。
使用APT的主要目的是简化开发者的工作量,因为APT可以在编译程序源代码的同时生成一些附属文件(比如源文件、类文件、程序发布描述文件等),这些附属文件的内容也都与源代码相关。换句话说,使用APT可以代替传统的对代码信息和附属文件的维护工作。
了解过Hibernate早期版本的读者都知道:每写一个Java类文件,还必须额外地维护一个Hibernate映射文件(名为*.hbm.xml的文件,也有一些工具可以自动生成)。下面将使用注解来简化这步操作。
不了解Hibernate的读者也无须担心,你只需要明白此处要做什么即可:
通过注解可以在Java源文件中放置一些注解,然后使用APT工具就可以根据该注解生成另一份XML文件,这就是注解的作用

通过javac命令的-processor选项 指定注解管理器

Java提供的javac.exe工具有一个-processor选项,该选项可指定一个注解处理器,如果在编译Java源文件时通过该选项指定了注解处理器,那么这个注解处理器将会在编译时提取并处理Java源文件中的注解。

如何实现注解管理器

每个注解处理器都需要实现Javax.annotation.processing包下的Processor接口。不过实现该接口必须实现它里面所有的方法,因此通常会采用继承AbstractProcessor的方式来实现注解处理器。一个注解处理器可以处理一种或者多种注解类型。

程序

为了示范使用APT根据源文件中的注解来生成额外的文件,下面将定义3种注解类型,分别用于修饰持久化类、标识属性和普通成员属性。

定义@Persistent注解

1
2
3
4
5
6
7
8
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Persistent {
String table();
}

这是一个非常简单的注解,它能修饰类、接口等类型声明,这个注解使用了@Retention元注解指定它仅在Java源文件中保留,运行时不能通过反射来读取该注解信息。

定义 @Id注解

下面是修饰标识属性的@Id注解。

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Id {
String column();

String type();

String generator();
}

这个@Id与前一个@Persistent的结构基本相似,只是多了两个成员变量而已。

定义 @Property注解

下面还有一个用于修饰普通成员属性的注解。

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Property {
String column();

String type();
}

使用上面的注解

定义了这三个注解之后,下面提供一个简单的Java类文件,这个Java类文件使用这三个注解来修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Persistent(table = "person_inf")
public class Person {
@Id(column = "person_id", type = "integer", generator = "identity")
private int id;
@Property(column = "person_name", type = "string")
private String name;
@Property(column = "person_age", type = "integer")
private int age;

// 无参数的构造器
public Person() {
}

// 初始化全部成员变量的构造器
public Person(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// 下面省略所有成员变量的setter和getter方法
}

上面的Person类是一个非常普通的Java类,但这个普通的Java类中使用了@Persistent@Id@Property三个注解进行修饰。

处理注解的APT工具

下面为这三个注解提供一个APT工具,该工具的功能是根据注解来生成一个Hibernate映射文件(不懂Hibernate也没有关系,读者只需要明白可以根据这些注解来生成另一份XML文件即可)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.lang.model.*;

import java.io.*;
import java.util.*;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 指定可处理@Persistent、@Id、@Property三个注解
@SupportedAnnotationTypes({ "Persistent", "Id", "Property" })
public class HibernateAnnotationProcessor extends AbstractProcessor {
// 循环处理每个需要处理的程序对象
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 定义一个文件输出流,用于生成额外的文件
PrintStream ps = null;
try {
// 遍历每个被@Persistent修饰的class文件
for (Element t : roundEnv.getElementsAnnotatedWith(Persistent.class)) {
// 获取正在处理的类名
Name clazzName = t.getSimpleName();
// 获取类定义前的@Persistent注解
Persistent per = t.getAnnotation(Persistent.class);
// 创建文件输出流
ps = new PrintStream(new FileOutputStream(clazzName + ".hbm.xml"));
// 执行输出
ps.println("<?xml version=\"1.0\"?>");
ps.println("<!DOCTYPE hibernate-mapping PUBLIC");
ps.println(" \"-//Hibernate/Hibernate " + "Mapping DTD 3.0//EN\"");
ps.println(" \"http://www.hibernate.org/dtd/" + "hibernate-mapping-3.0.dtd\">");
ps.println("<hibernate-mapping>");
ps.print(" <class name=\"" + t);
// 输出per的table()的值
ps.println("\" table=\"" + per.table() + "\">");
for (Element f : t.getEnclosedElements()) {
// 只处理成员变量上的注解
if (f.getKind() == ElementKind.FIELD) // ①
{
// 获取成员变量定义前的@Id注解
Id id = f.getAnnotation(Id.class); // ②
// 当@Id注解存在时输出<id.../>元素
if (id != null) {
ps.println(" <id name=\"" + f.getSimpleName() + "\" column=\"" + id.column()
+ "\" type=\"" + id.type() + "\">");
ps.println(" <generator class=\"" + id.generator() + "\"/>");
ps.println(" </id>");
}
// 获取成员变量定义前的@Property注解
Property p = f.getAnnotation(Property.class); // ③
// 当@Property注解存在时输出<property.../>元素
if (p != null) {
ps.println(" <property name=\"" + f.getSimpleName() + "\" column=\"" + p.column()
+ "\" type=\"" + p.type() + "\"/>");
}
}
}
ps.println(" </class>");
ps.println("</hibernate-mapping>");
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (ps != null) {
try {
ps.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
return true;
}
}

上面的注解处理器其实非常简单,与前面通过反射来获取注解信息不同的是,这个注解处理器使用RoundEnvironment来获取注解信息。

获取需要处理的程序单元

RoundEnvironment里包含了一个getElementsAnnotatedWith()方法,可根据注解获取需要处理的程序单元,这个程序单元由Element代表。

程序处理单元Element介绍

Element里包含一个getKind()方法,该方法返回Element所代表的程序单元,返回值可以是ElementKind.CLASS(类)、ElementKind.FIELD(成员变量)……
除此之外,Element还包含一个getEnclosedElements()方法,该方法可用于获取该Element里定义的所有程序单元,包括成员变量、方法、构造器、内部类等。

接下来程序只处理成员变量前面的注解,因此程序先判断这个Element必须是ElementKind.FIELD如上程序中①号粗体字代码所示)
再接下来程序调用了Element提供的getAnnotation(Class clazz)方法来获取修饰该Element的注解,如上程序中②③号粗体字部分就是获取成员变量上注解对象的代码。获取到成员变量上的@Id@Property注解之后,接下来就根据它们提供的信息执行输出。

编译时指定APT

提供了上面的注解处理器类之后,接下来就可使用带-processor选项的javac.exe命令来编译Person.java了。
例如如下命令,

1
2
3
rem 使用HibernateAnnotationProcessor作为APT处理Person.java中的Annotation
javac *.java
javac -processor HibernateAnnotationProcessor Person.java

使用HibernateAnnotationProcessor作为APT处理Person.java中的Annotation

通过上面的命令编译Person.java后,将可以看到在相同路径下生成了一个Person.hbm.xml文件该文件就是根据Person.java里的注解生成的。该文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="Person" table="person_inf">
<id name="id" column="person_id" type="integer">
<generator class="identity"/>
</id>
<property name="name" column="person_name" type="string"/>
<property name="age" column="person_age" type="integer"/>
</class>
</hibernate-mapping>

对比上面XML文件中的粗体字部分与Person.Java中的注解部分,它们是完全对应的,这即表明这份XML文件是根据Person.Java中的注解生成的。从生成的这份XML文件可以看出,通过使用APT工具确实可以简化程序开发,程序员只需把一些关键信息通过注解写在程序中,然后使用APT工具就可生成额外的文件

14.3.5 Java8新增的类型注解

Java8ElementType枚举增加了TYPE_PARAMETERTYPE_USE两个枚举值,这样就允许定义枚举时使用@Target(ElementType.TYPE_USE)修饰,这种注解被称为类型注解(Type Annotation),类型注解可用于修饰在任何地方出现的类型。

Java 8为增加的ElementType枚举值 描述
TYPE_PARAMETER Type parameter declaration
TYPE_USE Use of a type

Java8以前,只能在定义各种程序元素(定义类、定义接口、定义方法、定义成员变量……)时使用注解。Java8开始,类型注解可以修饰在任何地方出现的类型。比如,允许在如下位置使用类型注解。

  • 创建对象(用new关键字创建)
  • 类型转换。
  • 使用implements实现接口
  • 使用throws声明抛出异常。

上面这些情形都会用到类型,因此都可以使用类型注解来修饰。

程序

下面程序将会定义一个简单的类型注解,然后就可在任何用到类型的地方使用类型注解了,读者可通过该示例了解类型注解无处不在的神奇魔力。

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
import java.util.*;
import java.io.*;
import javax.swing.*;
import java.lang.annotation.*;

// 定义一个简单的类型注解,不带任何成员变量
@Target(ElementType.TYPE_USE)
@interface NotNull {
}

// 定义类时使用类型注解
@NotNull
public class TypeAnnotationTest implements @NotNull /* implements时使用类型注解 */ Serializable {
// 方法形参中使用类型注解
public static void main(@NotNull String[] args)
// throws时使用类型注解
throws @NotNull FileNotFoundException {
Object obj = "fkjava.org";
// 强制类型转换时使用类型注解
String str = (@NotNull String) obj;
// 创建对象时使用类型注解
Object win = new @NotNull JFrame("疯狂软件");
}

// 泛型中使用类型注解
public void foo(List<@NotNull String> info) {
}
}

上面的程序中都是可正常使用类型注解的例子,从这个示例可以看到,Java程序到处“写满”了类型注解,这种“无处不在”的类型注解可以让编译器执行更严格的代码检查,从而提高程序的健壮性
需要指出的是,上面程序虽然大量使用了@NotNull注解,但这些注解暂时不会起任何作用——因为并没有为这些注解提供处理工具。而且Java8本身并没有提供对类型注解执行检査的框架,因此如果需要让这些类型注解发挥作用,开发者需要自己实现类型注解检査框架

幸运的是,Java8提供了类型注解之后,第三方组织在发布他们的框架时,可能会随着框架一起发布类型注解检査工具,这样普通开发者即可直接使用第三方框架提供的类型注解,从而让编译器执行更严格的检查,保证代码更加健壮