13.3.2 定义DAO接口

UserDao接口

下面是UserDao接口的源代码。

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
package org.fkit.hrm.dao;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectProvider;
import org.fkit.hrm.dao.provider.UserDynaSqlProvider;
import org.fkit.hrm.domain.User;
// 静态导入数据库表常量
import static org.fkit.hrm.util.common.HrmConstants.USERTABLE;

public interface UserDao
{
// 根据登录名和密码查询员工
@Select("select * from " + USERTABLE
+ " where loginname = #{loginname} and password = #{password}")
User selectByLoginnameAndPassword(@Param("loginname") String loginname,
@Param("password") String password);

// 根据id查询用户
@Select("select * from " + USERTABLE + " where ID = #{id}")
User selectById(Integer id);

// 根据id删除用户
@Delete(" delete from " + USERTABLE + " where id = #{id} ")
void deleteById(Integer id);

// 动态修改用户
@SelectProvider(
type = UserDynaSqlProvider.class,
method = "updateUser")
void update(User user);

// 动态查询
@SelectProvider(
type = UserDynaSqlProvider.class,
method = "selectWhitParam")
List<User> selectByPage(Map<String, Object> params);

// 根据参数查询用户总数
@SelectProvider(
type = UserDynaSqlProvider.class,
method = "count")
Integer count(Map<String, Object> params);

// 动态插入用户
@SelectProvider(
type = UserDynaSqlProvider.class,
method = "insertUser")
void save(User user);
}

Userdao接口中使用了动态SQL提供类:UserDynasqlProvider

UserDynaSqlProvider.java

D:\Desktop\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\13\hrmapp\src\org\fkit\hrm\dao\provider\UserDynaSqlProvider.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package org.fkit.hrm.dao.provider;

import java.util.Map;
import org.apache.ibatis.jdbc.SQL;
import org.fkit.hrm.domain.User;
import static org.fkit.hrm.util.common.HrmConstants.USERTABLE;

public class UserDynaSqlProvider
{
// 分页动态查询
public String selectWhitParam(Map<String, Object> params)
{
String sql = new SQL()
{
{
SELECT("*");
FROM(USERTABLE);
if (params.get("user") != null)
{
User user = (User) params.get("user");
if (user.getUsername() != null
&& !user.getUsername().equals(""))
{
WHERE(" username LIKE CONCAT ('%',#{user.username},'%') ");
}
if (user.getUserstatus() != null
&& !user.getUserstatus().equals(""))
{
WHERE(" userstatus LIKE CONCAT ('%',#{user.userstatus},'%') ");
}
}
}
}.toString();

if (params.get("pageModel") != null)
{
sql += " limit #{pageModel.firstLimitParam} , #{pageModel.pageSize} ";
}
return sql;
}
// 动态查询总数量
public String count(Map<String, Object> params)
{
return new SQL()
{
{
SELECT("count(*)");
FROM(USERTABLE);
if (params.get("user") != null)
{
User user = (User) params.get("user");
if (user.getUsername() != null
&& !user.getUsername().equals(""))
{
WHERE(" username LIKE CONCAT ('%',#{user.username},'%') ");
}
if (user.getUserstatus() != null
&& !user.getUserstatus().equals(""))
{
WHERE(" userstatus LIKE CONCAT ('%',#{user.userstatus},'%') ");
}
}
}
}.toString();
}

// 动态插入
public String insertUser(User user)
{
return new SQL()
{
{
INSERT_INTO(USERTABLE);
if (user.getUsername() != null
&& !user.getUsername().equals(""))
{
VALUES("username", "#{username}");
}
if (user.getUserstatus() != null
&& !user.getUserstatus().equals(""))
{
VALUES("userstatus", "#{userstatus}");
}
if (user.getLoginname() != null
&& !user.getLoginname().equals(""))
{
VALUES("loginname", "#{loginname}");
}
if (user.getPassword() != null
&& !user.getPassword().equals(""))
{
VALUES("password", "#{password}");
}
}
}.toString();
}
// 动态更新
public String updateUser(User user)
{
return new SQL()
{
{
UPDATE(USERTABLE);
if (user.getUsername() != null)
{
SET(" username = #{username} ");
}
if (user.getLoginname() != null)
{
SET(" loginname = #{loginname} ");
}
if (user.getPassword() != null)
{
SET(" password = #{password} ");
}
if (user.getUserstatus() != null)
{
SET(" userstatus = #{userstatus} ");
}
if (user.getCreateDate() != null)
{
SET(" create_date = #{createDate} ");
}
WHERE(" id = #{id} ");
}
}.toString();
}
}

通过上面的DAO接口完成数据库的操作,这种简单的实现较之传统的JDBC持久化访问方便。

DeptDao接口

下面是DeptDao接口的源代码:

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
package org.fkit.hrm.dao;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectProvider;
import org.fkit.hrm.dao.provider.DeptDynaSqlProvider;
import org.fkit.hrm.domain.Dept;
import static org.fkit.hrm.util.common.HrmConstants.DEPTTABLE;

public interface DeptDao
{
// 动态查询
@SelectProvider(
type = DeptDynaSqlProvider.class,
method = "selectWhitParam")
List<Dept> selectByPage(Map<String, Object> params);

@SelectProvider(
type = DeptDynaSqlProvider.class,
method = "count")
Integer count(Map<String, Object> params);

@Select("select * from " + DEPTTABLE + " ")
List<Dept> selectAllDept();

@Select("select * from " + DEPTTABLE + " where ID = #{id}")
Dept selectById(int id);

// 根据id删除部门
@Delete(" delete from " + DEPTTABLE + " where id = #{id} ")
void deleteById(Integer id);

// 动态插入部门
@SelectProvider(
type = DeptDynaSqlProvider.class,
method = "insertDept")
void save(Dept dept);

// 动态修改用户
@SelectProvider(
type = DeptDynaSqlProvider.class,
method = "updateDept")
void update(Dept dept);
}

JobDao接口

下面是JobDao接口的源代码。

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
package org.fkit.hrm.dao;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectProvider;
import org.fkit.hrm.dao.provider.JobDynaSqlProvider;
import org.fkit.hrm.domain.Job;
import static org.fkit.hrm.util.common.HrmConstants.JOBTABLE;

public interface JobDao
{
@Select("select * from " + JOBTABLE + " where ID = #{id}")
Job selectById(int id);

@Select("select * from " + JOBTABLE + " ")
List<Job> selectAllJob();

// 动态查询
@SelectProvider(
type = JobDynaSqlProvider.class,
method = "selectWhitParam")
List<Job> selectByPage(Map<String, Object> params);

@SelectProvider(
type = JobDynaSqlProvider.class,
method = "count")
Integer count(Map<String, Object> params);

// 根据id删除部门
@Delete(" delete from " + JOBTABLE + " where id = #{id} ")
void deleteById(Integer id);

// 动态插入部门
@SelectProvider(
type = JobDynaSqlProvider.class,
method = "insertJob")
void save(Job job);

// 动态修改用户
@SelectProvider(
type = JobDynaSqlProvider.class,
method = "updateJob")
void update(Job job);
}

JobDao接口中使用了动态SQL提供类JobDynaSqlProvider

JobDynaSqlProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package org.fkit.hrm.dao.provider;

import java.util.Map;
import org.apache.ibatis.jdbc.SQL;
import org.fkit.hrm.domain.Job;
import static org.fkit.hrm.util.common.HrmConstants.JOBTABLE;

public class JobDynaSqlProvider
{
// 分页动态查询
public String selectWhitParam(Map<String, Object> params)
{
String sql = new SQL()
{
{
SELECT("*");
FROM(JOBTABLE);
if (params.get("job") != null)
{
Job job = (Job) params.get("job");
if (job.getName() != null && !job.getName().equals(""))
{
WHERE(" name LIKE CONCAT ('%',#{job.name},'%') ");
}
}
}
}.toString();

if (params.get("pageModel") != null)
{
sql += " limit #{pageModel.firstLimitParam} , #{pageModel.pageSize} ";
}

return sql;
}
// 动态查询总数量
public String count(Map<String, Object> params)
{
return new SQL()
{
{
SELECT("count(*)");
FROM(JOBTABLE);
if (params.get("job") != null)
{
Job job = (Job) params.get("job");
if (job.getName() != null && !job.getName().equals(""))
{
WHERE(" name LIKE CONCAT ('%',#{job.name},'%') ");
}
}
}
}.toString();
}
// 动态插入
public String insertJob(Job job)
{

return new SQL()
{
{
INSERT_INTO(JOBTABLE);
if (job.getName() != null && !job.getName().equals(""))
{
VALUES("name", "#{name}");
}
if (job.getRemark() != null && !job.getRemark().equals(""))
{
VALUES("remark", "#{remark}");
}
}
}.toString();
}
// 动态更新
public String updateJob(Job job)
{

return new SQL()
{
{
UPDATE(JOBTABLE);
if (job.getName() != null)
{
SET(" name = #{name} ");
}
if (job.getRemark() != null)
{
SET(" remark = #{remark} ");
}
WHERE(" id = #{id} ");
}
}.toString();
}
}

EmployeeDao.java

下面是EmployeeDao接口的源代码。

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

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.mapping.FetchType;
import org.fkit.hrm.dao.provider.EmployeeDynaSqlProvider;
import org.fkit.hrm.domain.Employee;
import static org.fkit.hrm.util.common.HrmConstants.EMPLOYEETABLE;

public interface EmployeeDao
{
// 根据参数查询员工总数
@SelectProvider(
type = EmployeeDynaSqlProvider.class,
method = "count")
Integer count(Map<String, Object> params);

// 根据参数动态查询员工
@SelectProvider(
type = EmployeeDynaSqlProvider.class,
method = "selectWhitParam")
@Results(
{@Result(
id = true,
column = "id",
property = "id"),
@Result(
column = "CARD_ID",
property = "cardId"),
@Result(
column = "POST_CODE",
property = "postCode"),
@Result(
column = "QQ_NUM",
property = "qqNum"),
@Result(
column = "BIRTHDAY",
property = "birthday",
javaType = java.util.Date.class),
@Result(
column = "CREATE_DATE",
property = "createDate",
javaType = java.util.Date.class),
@Result(
column = "DEPT_ID",
property = "dept",
one = @One(
select = "org.fkit.hrm.dao.DeptDao.selectById",
fetchType = FetchType.EAGER)),
@Result(
column = "JOB_ID",
property = "job",
one = @One(
select = "org.fkit.hrm.dao.JobDao.selectById",
fetchType = FetchType.EAGER))})
List<Employee> selectByPage(Map<String, Object> params);

// 动态插入员工
@SelectProvider(
type = EmployeeDynaSqlProvider.class,
method = "insertEmployee")
void save(Employee employee);

// 根据id删除员工
@Delete(" delete from " + EMPLOYEETABLE + " where id = #{id} ")
void deleteById(Integer id);

// 根据id查询员工
@Select("select * from " + EMPLOYEETABLE + " where ID = #{id}")
@Results(
{@Result(
id = true,
column = "id",
property = "id"),
@Result(
column = "CARD_ID",
property = "cardId"),
@Result(
column = "POST_CODE",
property = "postCode"),
@Result(
column = "QQ_NUM",
property = "qqNum"),
@Result(
column = "BIRTHDAY",
property = "birthday",
javaType = java.util.Date.class),
@Result(
column = "CREATE_DATE",
property = "createDate",
javaType = java.util.Date.class),
@Result(
column = "DEPT_ID",
property = "dept",
one = @One(
select = "org.fkit.hrm.dao.DeptDao.selectById",
fetchType = FetchType.EAGER)),
@Result(
column = "JOB_ID",
property = "job",
one = @One(
select = "org.fkit.hrm.dao.JobDao.selectById",
fetchType = FetchType.EAGER))})
Employee selectById(Integer id);

// 动态修改员工
@SelectProvider(
type = EmployeeDynaSqlProvider.class,
method = "updateEmployee")
void update(Employee employee);
}

EmployeeDao接口中使用了动态SQL提供类EmployeeDynaSqlProvider

EmployeeDynaSqlProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
package org.fkit.hrm.dao.provider;

import java.util.Map;
import org.apache.ibatis.jdbc.SQL;
import org.fkit.hrm.domain.Employee;
import static org.fkit.hrm.util.common.HrmConstants.EMPLOYEETABLE;

public class EmployeeDynaSqlProvider
{
// 分页动态查询
public String selectWhitParam(Map<String, Object> params)
{
String sql = new SQL()
{
{
SELECT("*");
FROM(EMPLOYEETABLE);
if (params.get("employee") != null)
{
Employee employee = (Employee) params.get("employee");
if (employee.getDept() != null
&& employee.getDept().getId() != null
&& employee.getDept().getId() != 0)
{
WHERE(" DEPT_ID = #{employee.dept.id} ");
}
if (employee.getJob() != null
&& employee.getJob().getId() != null
&& employee.getJob().getId() != 0)
{
WHERE(" JOB_ID = #{employee.job.id} ");
}
if (employee.getName() != null
&& !employee.getName().equals(""))
{
WHERE(" NAME LIKE CONCAT ('%',#{employee.name},'%') ");
}
if (employee.getPhone() != null
&& !employee.getPhone().equals(""))
{
WHERE(" phone LIKE CONCAT ('%',#{employee.phone},'%') ");
}
if (employee.getCardId() != null
&& !employee.getCardId().equals(""))
{
WHERE(" card_id LIKE CONCAT ('%',#{employee.cardId},'%') ");
}
if (employee.getSex() != null && employee.getSex() != 0)
{
WHERE("sex = #{employee.sex}");
}
}
}
}.toString();

if (params.get("pageModel") != null)
{
sql += " limit #{pageModel.firstLimitParam} , #{pageModel.pageSize} ";
}

return sql;
}
// 动态查询总数量
public String count(Map<String, Object> params)
{
return new SQL()
{
{
SELECT("count(*)");
FROM(EMPLOYEETABLE);
if (params.get("employee") != null)
{
Employee employee = (Employee) params.get("employee");
if (employee.getDept() != null
&& employee.getDept().getId() != null
&& employee.getDept().getId() != 0)
{
WHERE(" DEPT_ID = #{employee.dept.id} ");
}
if (employee.getJob() != null
&& employee.getJob().getId() != null
&& employee.getJob().getId() != 0)
{
WHERE(" JOB_ID = #{employee.job.id} ");
}
if (employee.getName() != null
&& !employee.getName().equals(""))
{
WHERE(" NAME LIKE CONCAT ('%',#{employee.name},'%') ");
}
if (employee.getPhone() != null
&& !employee.getPhone().equals(""))
{
WHERE(" phone LIKE CONCAT ('%',#{employee.phone},'%') ");
}
if (employee.getCardId() != null
&& !employee.getCardId().equals(""))
{
WHERE(" card_id LIKE CONCAT ('%',#{employee.cardId},'%') ");
}
if (employee.getSex() != null && employee.getSex() != 0)
{
WHERE("sex = #{employee.sex}");
}
}
}
}.toString();
}

// 动态插入
public String insertEmployee(Employee employee)
{
return new SQL()
{
{
INSERT_INTO(EMPLOYEETABLE);
if (employee.getName() != null)
{
VALUES("name", "#{name}");
}
if (employee.getCardId() != null)
{
VALUES("card_id", "#{cardId}");
}
if (employee.getAddress() != null)
{
VALUES("address", "#{address}");
}
if (employee.getPostCode() != null)
{
VALUES("post_code", "#{postCode}");
}
if (employee.getTel() != null)
{
VALUES("tel", "#{tel}");
}
if (employee.getPhone() != null)
{
VALUES("phone", "#{phone}");
}
if (employee.getQqNum() != null)
{
VALUES("qq_num", "#{qqNum}");
}
if (employee.getEmail() != null)
{
VALUES("email", "#{email}");
}
if (employee.getSex() != null)
{
VALUES("sex", "#{sex}");
}
if (employee.getParty() != null)
{
VALUES("party", "#{party}");
}
if (employee.getBirthday() != null)
{
VALUES("birthday", "#{birthday}");
}
if (employee.getRace() != null)
{
VALUES("race", "#{race}");
}
if (employee.getEducation() != null)
{
VALUES("education", "#{education}");
}
if (employee.getSpeciality() != null)
{
VALUES("speciality", "#{speciality}");
}
if (employee.getHobby() != null)
{
VALUES("hobby", "#{hobby}");
}
if (employee.getRemark() != null)
{
VALUES("remark", "#{remark}");
}
if (employee.getCreateDate() != null)
{
VALUES("create_Date", "#{createDate}");
}
if (employee.getDept() != null)
{
VALUES("dept_id", "#{dept.id}");
}
if (employee.getJob() != null)
{
VALUES("job_id", "#{job.id}");
}
}
}.toString();
}
// 动态更新
public String updateEmployee(Employee employee)
{
return new SQL()
{
{
UPDATE(EMPLOYEETABLE);
if (employee.getName() != null)
{
SET(" name = #{name} ");
}
if (employee.getCardId() != null)
{
SET(" card_id = #{cardId} ");
}
if (employee.getAddress() != null)
{
SET(" address = #{address} ");
}
if (employee.getPostCode() != null)
{
SET(" post_code = #{postCode} ");
}
if (employee.getTel() != null)
{
SET(" tel = #{tel} ");
}
if (employee.getPhone() != null)
{
SET(" phone = #{phone} ");
}
if (employee.getQqNum() != null)
{
SET(" qq_num = #{qqNum} ");
}
if (employee.getEmail() != null)
{
SET(" email = #{email} ");
}
if (employee.getSex() != null)
{
SET(" sex = #{sex} ");
}
if (employee.getParty() != null)
{
SET(" party = #{party} ");
}
if (employee.getBirthday() != null)
{
SET(" birthday = #{birthday} ");
}
if (employee.getRace() != null)
{
SET(" race = #{race} ");
}
if (employee.getEducation() != null)
{
SET(" education = #{education} ");
}
if (employee.getSpeciality() != null)
{
SET(" speciality = #{speciality} ");
}
if (employee.getHobby() != null)
{
SET(" hobby = #{hobby} ");
}
if (employee.getRemark() != null)
{
SET(" remark = #{remark} ");
}
if (employee.getCreateDate() != null)
{
SET(" create_Date = #{createDate} ");
}
if (employee.getDept() != null)
{
SET(" dept_id = #{dept.id} ");
}
if (employee.getJob() != null)
{
SET(" job_id = #{job.id} ");
}
WHERE(" id = #{id} ");
}
}.toString();
}
}

NoticeDao接口

下面是NoticeDao接口的源代码。

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
package org.fkit.hrm.dao;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.FetchType;
import org.fkit.hrm.dao.provider.NoticeDynaSqlProvider;
import org.fkit.hrm.domain.Notice;
import static org.fkit.hrm.util.common.HrmConstants.NOTICETABLE;

public interface NoticeDao
{
// 动态查询
@SelectProvider(
type = NoticeDynaSqlProvider.class,
method = "selectWhitParam"
)
@Results({
@Result(
id = true,
column = "id",
property = "id"),
@Result(
column = "CREATE_DATE",
property = "createDate",
javaType = java.util.Date.class),
@Result(
column = "USER_ID",
property = "user",
one = @One(
select = "org.fkit.hrm.dao.UserDao.selectById",
fetchType = FetchType.EAGER)
)
})
List<Notice> selectByPage(Map<String, Object> params);

@SelectProvider(
type = NoticeDynaSqlProvider.class,
method = "count")
Integer count(Map<String, Object> params);

@Select("select * from " + NOTICETABLE + " where ID = #{id}")
Notice selectById(int id);

// 根据id删除公告
@Delete(" delete from " + NOTICETABLE + " where id = #{id} ")
void deleteById(Integer id);

// 动态插入公告
@SelectProvider(
type = NoticeDynaSqlProvider.class,
method = "insertNotice")
void save(Notice notice);

// 动态修改公告
@SelectProvider(
type = NoticeDynaSqlProvider.class,
method = "updateNotice")
void update(Notice notice);
}

Noticedao接口中使用了动态SQL提供类NoticeDynaSqlProvider

NoticeDynaSqlProvider.java

下面是DocumentDao接口的源代码。

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

import java.util.Map;
import org.apache.ibatis.jdbc.SQL;
import org.fkit.hrm.domain.Notice;
import static org.fkit.hrm.util.common.HrmConstants.NOTICETABLE;

public class NoticeDynaSqlProvider
{
// 分页动态查询
public String selectWhitParam(Map<String, Object> params)
{
String sql = new SQL()
{
{
SELECT("*");
FROM(NOTICETABLE);
if (params.get("notice") != null)
{
Notice notice = (Notice) params.get("notice");
if (notice.getTitle() != null
&& !notice.getTitle().equals(""))
{
WHERE(" title LIKE CONCAT ('%',#{notice.title},'%') ");
}
if (notice.getContent() != null
&& !notice.getContent().equals(""))
{
WHERE(" content LIKE CONCAT ('%',#{notice.content},'%') ");
}
}
}
}.toString();

if (params.get("pageModel") != null)
{
sql += " limit #{pageModel.firstLimitParam} , #{pageModel.pageSize} ";
}

return sql;
}
// 动态查询总数量
public String count(Map<String, Object> params)
{
return new SQL()
{
{
SELECT("count(*)");
FROM(NOTICETABLE);
if (params.get("notice") != null)
{
Notice notice = (Notice) params.get("notice");
if (notice.getTitle() != null
&& !notice.getTitle().equals(""))
{
WHERE(" title LIKE CONCAT ('%',#{notice.title},'%') ");
}
if (notice.getContent() != null
&& !notice.getContent().equals(""))
{
WHERE(" content LIKE CONCAT ('%',#{notice.content},'%') ");
}
}
}
}.toString();
}
// 动态插入
public String insertNotice(Notice notice)
{

return new SQL()
{
{
INSERT_INTO(NOTICETABLE);
if (notice.getTitle() != null && !notice.getTitle().equals(""))
{
VALUES("title", "#{title}");
}
if (notice.getContent() != null
&& !notice.getContent().equals(""))
{
VALUES("content", "#{content}");
}
if (notice.getUser() != null
&& notice.getUser().getId() != null)
{
VALUES("user_id", "#{user.id}");
}
}
}.toString();
}
// 动态更新
public String updateNotice(Notice notice)
{

return new SQL()
{
{
UPDATE(NOTICETABLE);
if (notice.getTitle() != null && !notice.getTitle().equals(""))
{
SET(" title = #{title} ");
}
if (notice.getContent() != null
&& !notice.getContent().equals(""))
{
SET(" content = #{content} ");
}
if (notice.getUser() != null
&& notice.getUser().getId() != null)
{
SET(" user_id = #{user.id} ");
}
WHERE(" id = #{id} ");
}
}.toString();
}
}

DocumentDao接口

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
package org.fkit.hrm.dao;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.FetchType;
import org.fkit.hrm.dao.provider.DocumentDynaSqlProvider;
import org.fkit.hrm.domain.Document;
import static org.fkit.hrm.util.common.HrmConstants.DOCUMENTTABLE;

public interface DocumentDao{

// 动态查询
@SelectProvider(
type = DocumentDynaSqlProvider.class,
method = "selectWhitParam"
)
@Results(
{@Result(
id = true,
column = "id",
property = "id"
), @Result(
column = "CREATE_DATE",
property = "createDate",
javaType = java.util.Date.class
), @Result(
column = "USER_ID",
property = "user",
one = @One(
select = "org.fkit.hrm.dao.UserDao.selectById",
fetchType = FetchType.EAGER
)
)}
)
List<Document> selectByPage(Map<String, Object> params);

@SelectProvider(
type = DocumentDynaSqlProvider.class,
method = "count"
)
Integer count(Map<String, Object> params);

// 动态插入文档
@SelectProvider(
type = DocumentDynaSqlProvider.class,
method = "insertDocument"
)
void save(Document document);

@Select("select * from " + DOCUMENTTABLE + " where ID = #{id}")
Document selectById(int id);

// 根据id删除文档
@Delete(" delete from " + DOCUMENTTABLE + " where id = #{id} ")
void deleteById(Integer id);

// 动态修改文档
@SelectProvider(
type = DocumentDynaSqlProvider.class,
method = "updateDocument"
)
void update(Document document);
}

DocumentDao接口中使用了动态SQL提供类DocumentDynaSqlProvider

DocumentDynaSqlProvider.java

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

import java.util.Map;
import org.apache.ibatis.jdbc.SQL;
import org.fkit.hrm.domain.Document;
import static org.fkit.hrm.util.common.HrmConstants.DOCUMENTTABLE;

public class DocumentDynaSqlProvider{
// 分页动态查询
public String selectWhitParam(Map<String, Object> params)
{
String sql = new SQL(){
{
SELECT("*");
FROM(DOCUMENTTABLE);
if(params.get("document") != null)
{
Document document = (Document) params.get("document");
if(document.getTitle() != null && !document.getTitle().equals(""))
{
WHERE(" title LIKE CONCAT ('%',#{document.title},'%') ");
}
}
}
}.toString();

if(params.get("pageModel") != null)
{
sql += " limit #{pageModel.firstLimitParam} , #{pageModel.pageSize} ";
}

return sql;
}
// 动态查询总数量
public String count(Map<String, Object> params)
{
return new SQL(){
{
SELECT("count(*)");
FROM(DOCUMENTTABLE);
if(params.get("document") != null)
{
Document document = (Document) params.get("document");
if(document.getTitle() != null && !document.getTitle().equals(""))
{
WHERE(" title LIKE CONCAT ('%',#{document.title},'%') ");
}
}
}
}.toString();
}
// 动态插入
public String insertDocument(Document document)
{

return new SQL(){
{
INSERT_INTO(DOCUMENTTABLE);
if(document.getTitle() != null && !document.getTitle().equals(""))
{
VALUES("title", "#{title}");
}
if(document.getFileName() != null && !document.getFileName().equals(""))
{
VALUES("filename", "#{fileName}");
}
if(document.getRemark() != null && !document.getRemark().equals(""))
{
VALUES("remark", "#{remark}");
}
if(document.getUser() != null && document.getUser().getId() != null)
{
VALUES("user_id", "#{user.id}");
}
}
}.toString();
}

// 动态更新
public String updateDocument(Document document)
{

return new SQL(){
{
UPDATE(DOCUMENTTABLE);
if(document.getTitle() != null && !document.getTitle().equals(""))
{
SET(" title = #{title} ");
}
if(document.getFileName() != null && !document.getFileName().equals(""))
{
SET(" filename = #{fileName} ");
}
if(document.getRemark() != null && !document.getRemark().equals(""))
{
SET("remark = #{remark}");
}
if(document.getUser() != null && document.getUser().getId() != null)
{
SET("user_id = #{user.id}");
}
WHERE(" id = #{id} ");
}
}.toString();
}
}

2.15 本章小结

  • 本章系统介绍了JavaWeb编程的相关知识:JSPServletListenerFilter等。
  • 本章覆盖了JSP所有知识点,包括JSP的3个编译指令、7个动作指令、9个内置对象。
  • 详细介绍了JavaWeb编程所涉及的ServletListenerFilter的使用。
  • 还详细介绍了JSP2自定义标签库开发步骤及标签库的用法,包括简单标签、带属性标签和迭代器标签等。
  • 本章也全面讲解了JSP2所攴持的配置JSP属性、表达式语言和TagFile标签支持等内容。
  • 除此之外,还重点介绍了Servlet3新规范带来的巨大改变:ServletListenerFilter不需要通过web.xml进行配置,只需通过注解修饰即可。
  • Servlet3带来的web模块支持、改进的ServletApi都给web开发带来很大方便,值得掌握。
  • 本书最后还介绍了Servlet3.1的非阻塞IO支持和Tomcat8.5提供的WebSocket,尤其是WebSocket,必将在未来的Web开发中大放异彩
  • 本章内容是轻量级Java EE和经典Java EE都需要的表现层技术,因此非常重要。

2.14 Tomcat8.5的WebSocket支持

严格来说,WebSocket并不属于JavaWeb相关规范,WebSocket属于HTML5规范的一部分,WebSocket允许通过JavaScript建立与远程服务器的连接,从而允许远程服务器将数据推送给浏览器。
通过使用WebSocket,可以构建出实时性要求比较高的应用,比如在线游戏在线证券设备监控新闻在线播报等,只要服务器端有了新数据,服务端就可以直接将数据推送给浏览器,让浏览器显示最新的状态
WebSocket规范已经相当成熟,而且各种主流浏览器(如FirefoxChromeSafariOpera等)都已经支持WebSocket技术,Java EE规范则提供了WebSocket服务端规范,而Tomcat8.5则对该规范提供了优秀的实现。

开发WebSocket服务端的方式

使用Tomcat8.5开发WebSocket服务端非常简单,大致有如下两种方式:

  • 使用注解方式开发,被@ServerEndpoint修饰的Java类即可作为WebSocket服务端。
  • 继承Endpoint基类实现WebSocket服务端

由于使用注解方式开发不仅开发简单,而且是目前的主流方式,因此本书将介绍使用注解方式进行开发。
与开发Servlet一样,Servlet并不需要处理底层并发、网络通信等通用的底层细节——因为Servlet处于Web服务器中运行,Web服务器为Servlet处理了底层的并发、网络通信等;开发WebSocket服务端同样需要位于web服务器中运行,因此此处开发的WebSocket同样无须处理并发、网络通信等细节。
开发被@ServerEndpoint修饰的Java类之后,该类中还可以定义如下方法:

  • @OnOpen修饰的方法:当客户端与该WebSocket服务端建立连接时激发该方法。
  • @OnClose修饰的方法:当客户端与该WebSocket服务端断开连接时激发该方法
  • @OnMessage修饰的方法:当WebSocket服务端收到客户端消息时激发该方法。
  • @OnError修饰的方法:当客户端与该WebSocket服务端连接出现错误时激发该方法。

多人实时聊天程序

思路

下面将基于WebSocket开发一个多人实时聊天的程序,该程序的思路很简单:

  • 在这个程序中,每个客户所用的浏览器都与服务器建立一个WebSocket,从而保持实时连接,这样客户端的浏览器可以随时把数据发送到服务器端;
  • 当服务器收到任何一个浏览器发送来的消息之后,将该消息依次向每个客户端浏览器发送一遍。

图2.54显示了基于WebSocket的多人实时聊天示意图

步骤

为了实现图2.54所示的示意图,按如下步骤开发WebSocket服务端程序即可:

  1. 定义@OnOpen修饰的方法,每当客户端连接进来时激发该方法,程序使用集合保存所有连接进来的客户端。
  2. 定义@OnMessage修饰的方法,每当该服务端收到客户端消息时激发该方法,服务端收到消息之后遍历保存客户端的集合,并将消息逐个发给所有客户端。
  3. 定义@OnClose修饰的方法,每当客户端断开与该服务端连接时激发该方法,程序将该客户端从集合中删除。

程序示例

项目结构

E:\workspacne_JDK8Tomcat8.5\WebSocket
├─src\
│ └─lee\
│   └─ChatEntpoint.java
└─WebContent\
  ├─chat.html
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─lib\
    └─web.xml

ChatEntpoint.java

下面程序就是基于WebSocket实现多人实时聊天的服务器程序。

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

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.*;
import javax.websocket.server.*;

@ServerEndpoint(value = "/websocket/chat")
public class ChatEntpoint {
private static final String GUEST_PREFIX = "访客";
private static final AtomicInteger connectionIds = new AtomicInteger(0);
// 定义一个集合,用于保存所有接入的WebSocket客户端
private static final Set<ChatEntpoint> clientSet = new CopyOnWriteArraySet<>();
// 定义一个成员变量,记录WebSocket客户端的聊天昵称
private final String nickname;
// 定义一个成员变量,记录与WebSocket之间的会话
private Session session;

public ChatEntpoint() {
nickname = GUEST_PREFIX + connectionIds.getAndIncrement();
}

// 当客户端连接进来时自动激发该方法
@OnOpen
public void start(Session session) {
this.session = session;
// 将WebSocket客户端会话添加到集合中
clientSet.add(this);
String message = String.format("【%s %s】", nickname, "加入了聊天室!");
// 发送消息
broadcast(message);
}

// 当客户端断开连接时自动激发该方法
@OnClose
public void end() {
clientSet.remove(this);
String message = String.format("【%s %s】", nickname, "离开了聊天室!");
// 发送消息
broadcast(message);
}

// 每当收到客户端消息时自动激发该方法
@OnMessage
public void incoming(String message) {
String filteredMessage = String.format("%s: %s", nickname, filter(message));
// 发送消息
broadcast(filteredMessage);
}

// 当客户端通信出现错误时,激发该方法
@OnError
public void onError(Throwable t) throws Throwable {
System.out.println("WebSocket服务端错误 " + t);
}

// 实现广播消息的工具方法
private static void broadcast(String msg) {
// 遍历服务器关联的所有客户端
for (ChatEntpoint client : clientSet) {
try {
synchronized (client) {
// 发送消息
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
System.out.println("聊天错误,向客户端 " + client + " 发送消息出现错误。");
clientSet.remove(client);
try {
client.session.close();
} catch (IOException e1) {
}
String message = String.format("【%s %s】", client.nickname, "已经被断开了连接。");
broadcast(message);
}
}
}

// 定义一个工具方法,用于对字符串中的HTML字符标签进行转义
private static String filter(String message) {
if (message == null)
return null;
char content[] = new char[message.length()];
message.getChars(0, message.length(), content, 0);
StringBuilder result = new StringBuilder(content.length + 50);
for (int i = 0; i < content.length; i++) {
// 控制对尖括号等特殊字符进行转义
switch (content[i]) {
case '<':
result.append("&lt;");
break;
case '>':
result.append("&gt;");
break;
case '&':
result.append("&amp;");
break;
case '"':
result.append("&quot;");
break;
default:
result.append(content[i]);
}
}
return (result.toString());
}
}

上面的ChatEntpoint主要就是实现了@OnOpen@OnClose@OnMessage@OnError这4个注解修饰的方法。
需要说明的是,该ChatEntpoint类并不是真正的WebSocket服务端,它只实现了WebSocket服务端的核心功能,Tomcat会调用它的方法作为WebSocket服务端。因此,Tomcat会为每个WebSocket客户端创建一个ChatEntpoint对象,也就是说,有一个WebSocket客户端,程序就有一个ChatEntpoint对象。所以上面程序中clientSet集合保存了多个ChatEntpoint对象,其中每个ChatEndpoint对象对应一个WebSocket客户端。
编译ChatEntpoint类,并将生成的class文件放在Web应用的WEB-NF/Classes目录下,该ChatEntpoint即可作为WebSocket服务端使用。
上面ChatEntpoint类用到了WebSocket API规范的相关注解,因此读者需要将Tomcatlib目录下的websocket-api.jar添加到CLASSPATH环境变量下。

chat.html

接下来使用JavaScript开发WebSocket客户端。此处WebSocket客户端使用一个简单的HTML页面即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title> 使用WebSocket通信 </title>
<script type="text/javascript">
// 创建WebSocket对象
var webSocket = new WebSocket("ws://127.0.0.1:8888/WebSocket/websocket/chat");
var sendMsg = function()
{
var inputElement = document.getElementById('msg');
// 发送消息
webSocket.send(inputElement.value);
// 清空单行文本框
inputElement.value = "";
}
var send = function(event)
{
if (event.keyCode == 13)
{
sendMsg();
}
};
webSocket.onopen = function()
{
// 为onmessage事件绑定监听器,接收消息
webSocket.onmessage= function(event)
{
var show = document.getElementById('show')
// 接收、并显示消息
show.innerHTML += event.data + "<br/>";
show.scrollTop = show.scrollHeight;
}
document.getElementById('msg').onkeydown = send;
document.getElementById('sendBn').onclick = sendMsg;
};
webSocket.onclose = function ()
{
document.getElementById('msg').onkeydown = null;
document.getElementById('sendBn').onclick = null;
Console.log('WebSocket已经被关闭。');
};
</script>
</head>
<body>
<div style="width:600px;height:240px;
overflow-y:auto;border:1px solid #333;" id="show"></div>
<input type="text" size="80" id="msg" name="msg" placeholder="输入聊天内容"/>
<input type="button" value="发送" id="sendBn" name="sendBn"/>
</body>
</html>

上面程序中代码:

1
var webSocket = new WebSocket("ws://127.0.0.1:8888/WebSocket/websocket/chat");

创建了一个WebSocket对象(WebSocketHTML5规范新增的类)创建对象时指定WebSocket服务端的地址。一旦程序得到了WebSocket对象,接下来程序即可调用WebSocketsend()方法向服务器发送消息。
除此之外,还可以为WebSocket绑定如下三个事件处理函数:

  • onopen:当WebSocket客户端与服务端建立连接时自动激发该事件处理函数。
  • onclose:当WebSocket客户端与服务端关闭连接时自动激发该事件处理函数。
  • onmessage:当WebSocket客户端收到服务端消息时自动激发该事件处理函数。

WebSocket肯定会成为Web应用开发的主流技术,这种技术颠覆了传统Web应用请求响应架构模型,它可以让服务端与浏览器建立实时通信的Socket,因此具有广泛的用途。为了使用WebSocket,还需要JavaScript编程知识,关于JavaScriptWebSocket相关知识,请参考《疯狂HTML5/CSS3/JavaScript讲义》

测试

首先启动ChatEntpoint所在的web应用,使用多个浏览器登录chat.html页面聊天即可看到如图2.55所示的聊天效果。

1
http://localhost:8080/WebSocket/chat.html

2.13 Servlet3.1新增的非阻塞式IO

最新的Java EE 7已经发布,伴随Java EE 7一起发布了Servlet3.1,Servlet3.1引入了少数新特性。Servlet3.1新特性包括强制更改sessionId(由HttpServletRequestchangeSessionId()方法提供)、非阻塞IO等。尤其是Servlet3.1提供的非阻塞IO进行输入、输出,可以更好地提升性能。
Servlet底层的IO是通过如下两个IO流来支持的。

  • ServletInputStream:Servlet用于读取数据的输入流。
  • ServletOutputStream:Servlet用于输出数据的输出流。

Servlet读取数据为例,传统的读取方式采用阻塞式IO——当Servlet读取浏览器提交的数据时,如果数据暂时不可用,或数据没有读取完成,Servlet当前所在线程将会被阻塞,无法继续向下执行。

ReadListener

Servlet3.1开始,ServletInputStream新增了一个setReadListener(ReadListener readListener)方法,该方法允许以非阻塞IO读取数据,实现ReadListener监听器需要实现如下三个方法

  • onAllDataRead():当所有数据读取完成时激发该方法。
  • onDataAvailable():当有数据可用时激发该方法
  • onError(Throwable t):读取数据出现错误时激发该方法。

类似地,ServletOuputStream也提供了setWriterListenerer(WriteListener writeListener)方法,通过这种方式,可以让ServletOuputStream以非阻塞IO进行输出

Servlet中使用非阻塞IO步骤

Servlet中使用非阻塞IO非常简单,主要按如下步骤进行即可:

  • 调用ServletRequeststartAsync()方法开启异步模式
  • 通过ServletRequest获取ServletInputStream,并为ServletInputStream设置监听器(ReadListener实现类)。
  • 实现ReadListener接口来实现监听器,在该监听器的方法中以非阻塞方式读取数据。

程序示例

项目结构

G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\02\2.13\servlet31
├─async.jsp
├─form.html
└─WEB-INF\
  ├─build.xml
  ├─classes\
  │ └─lee\
  │   ├─AsyncServlet.class
  │   └─MyReadListener.class
  ├─lib\
  ├─src\
  │ └─lee\
  │   ├─AsyncServlet.java
  │   └─MyReadListener.java
  └─web.xml

下面的Servlet负责处理表单页面提交的数据,但该Servlet并未使用传统的、阻塞IO来读取客户端数据,而是采用非阻塞IO进行读取。下面是该Servlet的代码。

AsyncServlet.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
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html;charset=GBK");
PrintWriter out = response.getWriter();
out.println("<title>非阻塞IO示例</title>");
out.println("进入Servlet的时间:" + new java.util.Date() + ".<br/>");
// 创建AsyncContext,开始异步调用
AsyncContext context = request.startAsync();
// 设置异步调用的超时时长
context.setTimeout(60 * 1000);
ServletInputStream input = request.getInputStream();
// 为输入流注册监听器
input.setReadListener(new MyReadListener(input, context));
out.println("结束Servlet的时间:" + new java.util.Date() + ".<br/>");
out.flush();
}
}

上面程序调用requeststartAsync()方法开启异步调用之后,程序中粗体字代码为Servlet输入流注册了一个监听器,这样就无须在该Servlet中使用阻塞IO来获取数据了。而是改为由MyReadListener负责读取数据,这样Servlet就可以继续向下执行,不会因为IO阻塞线程。
MyReadListener需要实现Readlistener接口,并重写它的三个方法。

MyReadListener.java

下面是MyReadListener的代码。

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
package lee;

import javax.servlet.*;
import java.io.*;

public class MyReadListener implements ReadListener {
private ServletInputStream input;
private AsyncContext context;

public MyReadListener(ServletInputStream input, AsyncContext context) {
this.input = input;
this.context = context;
}

@Override
public void onDataAvailable() {
System.out.println("数据可用!!");
try {
// 暂停5秒,模拟读取数据是一个耗时操作。
Thread.sleep(5000);
StringBuilder sb = new StringBuilder();
int len = -1;
byte[] buff = new byte[1024];
// 采用原始IO方式读取浏览器向Servlet提交的数据
while (input.isReady() && (len = input.read(buff)) > 0) {
String data = new String(buff, 0, len);
sb.append(data);
}
System.out.println(sb);
// 将数据设置为request范围的属性
context.getRequest().setAttribute("info", sb.toString());
// 转发到视图页面
context.dispatch("/async.jsp");
} catch (Exception ex) {
ex.printStackTrace();
}
}

@Override
public void onAllDataRead() {
System.out.println("数据读取完成");
}

@Override
public void onError(Throwable t) {
t.printStackTrace();
}
}

上面程序中MyReadListeneronDataAvailable()方法先暂停线程5秒,用于模拟耗时操作,接下来程序使用普通IO流读取浏览器提交的数据。
如果程序直接让Servlet读取浏览器提交的数据,那么该Servlet就需要阻塞5秒,不能继续向下执行;改为使用非阻塞IO进行读取,虽然读取数据的IO操作需要5秒,但它不会阻塞Servlet执行,因此可以提升Servlet的性能。
程序使用一个简单的表单向该Servlet提交请求,该表单内包含了请求数据。提交的表单效果如图2.53所示。

2.12.4 改进的Servlet API

Servlet3还有一个改变是改进了部分API,这种改进很好地简化了Java Web开发。其中两个较大的改进是

  • HttpServletRequest增加了对文件上传的支持。
  • ServletContext允许通过编程的方式动态注册ServletFilter

HttpServletRequest文件上传方法

HttpServletRequest提供了如下两个方法来处理文件上传

  • Part getPart(String name):根据名称来获取文件上传域。
  • Collection<Part> getParms():获取所有的文件上传域

通过Part类对象来访问文件信息 将文件写入服务器

上面两个方法的返回值都涉及一个API:Part,每个Part对象对应于一个文件上传域,该对象提供了大量方法来访问上传文件的文件类型、大小、输入流等,并提供了一个write(String file)方法将上传文件写入服务器磁盘。

HTML文件域

为了向服务器上传文件,需要在表单里使用<input type="file">文件域,这个文件域会在HTML页面上产生一个单行文本框和一个“浏览”按钮,浏览者可通过该按钮选择需要上传的文件。

需要设置HTML form元素的enctype属性

除此之外,上传文件一定要为表单域设置enctype属性。

  • application/x-www-form-urlencoded:这是默认的编码方式,它只处理表单域里的value属性值,采用这种编码方式的表单会将表单域的值处理成URL编码方式。
  • multipart/form-data:这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数里。
  • text/plain:这种编码方式当表单的action属性为mailto:URL的形式时比较方便,这种方式主要适用于直接通过表单发送邮件的方式。

如果将enctype设置为application/x-www-form-urlencoded,或不设置enctype属性,提交表单时只会发送文件域的文本框里的字符串,也就是浏览者所选择文件的绝对路径,对服务器获取该文件在客户端上的绝对路径没有任何作用,因为服务器不可能访问客户机的文件系统。

upload.jsp

下面定义了一个文件上传的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<!DOCTYPE html>
<html>
<head>
<title> 文件上传 </title>
</head>
<body>
<form method="post" action="upload" enctype="multipart/form-data">
文件名:<input type="text" id="name" name="name" /><br/>
选择文件:<input type="file" id="file" name="file" /><br/>
<input type="submit" value="上传" /><br/>
</form>
</body>
</html>

上面的页面中的表单需要设置enctype="multipart/form-data",这表明该表单可用于上传文件。
上面的表单中定义了两个表单域:

  • 一个普通的文本框,它将生成普通请求参数;
  • 一个文件上传域,它用于上传文件。

对于传统的文件上传需要借助于common-fileupload等工具,处理起来极为复杂,借助于Servlet3API,处理文件上传将变得十分简单。看下面的Servlet类代码

UploadServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;
import java.util.*;

@WebServlet(name = "upload", urlPatterns = { "/upload" })
@MultipartConfig
public class UploadServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html;charset=GBK");
PrintWriter out = response.getWriter();
request.setCharacterEncoding("GBK");
// 获取普通请求参数
String name = request.getParameter("name");
out.println("普通的name参数为:" + name + "<br/>");
// 获取文件上传域
Part part = request.getPart("file");
// 获取上传文件的文件类型
out.println("上传文件的类型为:" + part.getContentType() + "<br/>");
// 获取上传文件的大小。
out.println("上传文件的大小为:" + part.getSize() + "<br/>");
// 获取该文件上传域的Header Name
Collection<String> headerNames = part.getHeaderNames();
// 遍历文件上传域的Header Name、Value
for (String headerName : headerNames) {
out.println(headerName + "--->" + part.getHeader(headerName) + "<br/>");
}
// 获取包含原始文件名的字符串
String fileNameInfo = part.getHeader("content-disposition");
// 提取上传文件的原始文件名
String fileName = fileNameInfo.substring(fileNameInfo.indexOf("filename=\"") + 10, fileNameInfo.length() - 1);
// 将上传的文件写入服务器
part.write(getServletContext().getRealPath("/uploadFiles") + "/" + fileName); // ①
}
}

上面Servlet使用了@MultipartConfig注解修饰,处理文件上传的Servlet应该使用该注解修饰。接下来该ServletHttpServletrequest就可通过getPart( String name)方法来获取文件上传域——就像获取普通请求参数一样。

web.xml中配置

Servlet所有注解相似的是,Servlet@MultipartConfig提供了相似的配置元素,同样可以通过在<servlet>元素中添加<multipart-config/>子元素来达到相同的效果
获取了上传文件对应的Part之后,可以非常简单地将文件写入服务器磁盘,如上面的①号代码所示。当然也可以通过Part获取所上传文件的文件类型、文件大小等各种详细信息。
例如选择一个*.png图片,然后单击上传将可看到如图2.52所示的页面。

上面的Servlet中将会把上传的文件保存到Web应用的根路径下的uploadFiles目录下,因此读者还应该在Web应用的根路径下创建uploadFiles目录

避免上传的文件同名

上面Servlet上传时保存的文件名直接使用了上传文件的原始文件名,在实际项目中一般不会这么做,因为可能多个用户可能上传同名的文件,这样将导致后面用户上传的文件覆盖前面用户上传的文件。在实际项目中可借助于java.util.UUID工具类生成文件名。

动态注册Servlet Filter

ServletContext则提供了如下方法来动态地注册ServetFilter,并允许动态设置Web应用的初始化参数。

  • 多个重载的addServlet()方法:动态地注册ServletE
  • 多个重载的addFilter()方法:动态地注册Filter
  • 多个重载的addListener()方法:动态地注册Listener
  • setInitParameter(String name, String value)方法:为Web应用设置初始化参数。

2.12.3 Servlet3提供的异步处理

在以前的Servlet规范中,如果Servlet作为控制器调用了一个耗时的业务方法,那么Servlet必须等到业务方法完全返回之后才会生成响应,这将使得Servlet对业务方法的调用变成一种阻塞式的调用,因此效率比较低。
Servlet3规范引入了异步处理来解决这个问题,异步处理允许Servlet重新发起一条新线程去调用耗时的业务方法,这样就可避免等待。

AsyncContext

Servlet3的异步处理是通过AsyncContext类来处理的,Servlet可通过ServletRequest的如下两个方开启异步调用、创建AsyncContext对象。

  • AsyncContext startAsync()
  • AsyncContext startAsync(ServletRequest, ServletResponse)

重复调用上面的方法将得到同一个AsyncContext对象。AsyncContext对象代表异步处理的上下文,它提供了一些工具方法,可完成设置异步调用的超时时长,dispatch用于请求、启动后台线程、获取requestresponse对象等功能

程序示例

项目结构

G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\02\2.12\servlet3
├─async.jsp
├─upload.jsp
├─uploadFiles\
│ └─疯狂iOS讲义(上)—立体图.png
└─WEB-INF\
  ├─build.xml
  ├─classes\
  │ └─lee\
  │   ├─AsyncServlet.class
  │   ├─GetBooksTarget.class
  │   ├─MyAsyncListener.class
  │   └─UploadServlet.class
  ├─lib\
  │ ├─crazyit.jar
  │ ├─jstl.jar
  │ ├─leegang.jar
  │ └─standard.jar
  ├─src\
  │ └─lee\
  │   ├─AsyncServlet.java
  │   ├─GetBooksTarget.java
  │   ├─MyAsyncListener.java
  │   └─UploadServlet.java
  └─web.xml

AsyncServlet.java

下面是一个进行异步处理的Servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;
import java.util.*;

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html;charset=GBK");
PrintWriter out = response.getWriter();
out.println("<title>异步调用示例</title>");
out.println("进入Servlet的时间:" + new java.util.Date() + ".<br/>");
// 创建AsyncContext,开始异步调用
AsyncContext actx = request.startAsync();
// 设置异步调用的超时时长
actx.setTimeout(60 * 1000);
// 启动异步调用的线程,该线程以异步方式执行
actx.start(new GetBooksTarget(actx));
out.println("结束Servlet的时间:" + new java.util.Date() + ".<br/>");
out.flush();
}
}

上面的Servlet类中代码:

1
2
3
4
5
6
// 创建AsyncContext,开始异步调用
AsyncContext actx = request.startAsync();
// 设置异步调用的超时时长
actx.setTimeout(60 * 1000);
// 启动异步调用的线程,该线程以异步方式执行
actx.start(new GetBooksTarget(actx));

创建了AsyncContext对象,并通过该对象以异步方式启动了一条后台线程。该线程执行体模拟调用耗时的业务方法.

GetBooksTarget.java

下面是线程执行体的代码

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

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;
import java.util.*;

public class GetBooksTarget implements Runnable {
private AsyncContext actx = null;

public GetBooksTarget(AsyncContext actx) {
this.actx = actx;
}

public void run() {
try {
// 等待5秒钟,以模拟业务方法的执行
Thread.sleep(5 * 1000);
ServletRequest request = actx.getRequest();
List<String> books = new ArrayList<String>();
books.add("疯狂Java讲义");
books.add("轻量级Java EE企业应用实战");
books.add("疯狂前端开发讲义");
request.setAttribute("books", books);
actx.dispatch("/async.jsp");
} catch (Exception e) {
e.printStackTrace();
}
}
}

该线程执行体内让线程暂停5秒来模拟调用耗时的业务方法,最后调用AsyncContextdispatch方法把请求dispatch到指定JSP页面.

async.jsp

被异步请求dispatch的目标页面需要指定session="false"“,表明该页面不会重新创建session。下面是async.jsp页面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page contentType="text/html; charset=GBK" language="java"
session="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<ul>
<c:forEach items="${books}" var="book">
<li>${book}</li>
</c:forEach>
</ul>
<%out.println("业务调用结束的时间:" + new java.util.Date());
if (request.isAsyncStarted()) {
// 完成异步调用
request.getAsyncContext().complete();
}%>

上面的页面只是一个普通JSP页面,只是使用了JSTL标签库来迭代输出books集合,因此读者需要将JSTL的两个JAR包复制到Web应用的WEB-INF\lib路径下。

为Servlet开启异步调用

对于希望启用异步调用的Servlet而言,开发者必须显式指定开启异步调用,为Servlet开启异步调用有两种方式。

  1. @WebServlet指定asyncSupported=true属性
  2. web.xml文件的<servlet>元素中增加<async-supported>子元素

web.xml

例如,希望开启上面Servlet的异步调用可通过如下配置片段:

1
2
3
4
5
6
7
8
9
<servlet> 
<servlet-name>async</servlet-name>
<servlet-class>lee.AsyncServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>async</servlet-name>
<url-pattern>/async</url-pattern>
</servlet-mapping>

对于支持异步调用的Servlet来说,当Servlet以异步方式启用新线程之后,该Servlet的执行不会被阻塞,该Servlet将可以向客户端浏览器生成响应——当新线程执行完成后,新线程生成的响应再次被送往客户端浏览器。
通过浏览器访问上面的Servlet将看到如图2.51所示的页面

异步监听器

Servlet启用异步调用的线程之后,该线程的执行过程对开发者是透明的。但在有些情况下,开发者需要了解该异步线程的执行细节,并针对特定的执行结果进行针对性处理,这可借助于Servlet3提供的异步监听器来实现。
异步监听器需要实现AsyncListener接口,实现该接口的监听器类需要实现如下4个方法。

  • onStartAsync(AsyncEvent even):当异步调用开始时触发该方法。
  • onComplete(AsyncEvent even):当异步调用完成时触发该方法。
  • onError(AsyncEvent even):当异步调用出错时触发该方法。
  • onTimeout(AsyncEvent even):当异步调用超时时触发该方法。

MyAsyncListener.java

接下来为上面的异步调用定义如下监听器类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class MyAsyncListener implements AsyncListener {
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("------异步调用完成------" + new Date());
}

public void onError(AsyncEvent event) throws IOException {
}

public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("------异步调用开始------" + new Date());
}

public void onTimeout(AsyncEvent event) throws IOException {
}
}

上面实现的异步监听器类只实现了onStartAsynconComplete两个方法,表明该监听器只能监听异步调用开始、异步调用完成两个事件。

给AsyncContext注册异步监听器

提供了异步监听器之后,还需要通过AsyncContext来注册监听器,调用该对象的addListener()方法即可注册监听器。例如,在上面的Servlet中增加如下代码即可注册监听器:

1
2
3
AsyncContext actx=request.startAsync();
//为该异步调用注册监听器
actx.addListener(new MyAsyncListener());

为异步调用注册了监听器之后,接下来的异步调用过程将会不断地触发该监听器的不同方法。
虽然上面的MyAsyncListener监听器类可以监听异步调用开始、异步调用完成两个事件,但从实际运行的结果来看,它并不能监听到异步调用开始事件,这可能是因为注册该监听器时异步调用已经开始了的缘故。
需要指出的是,虽然上面介绍的例子都是基于Servlet的,但由于FilterServlet具有很大的相似性,因此Servlet3规范完全支持在Filter中使用异步调用。在Filter中进行异步调用与在Servlet中进行异步调用的效果完全相似,故此处不再赘述。

2.12.2 Servlet3的Web模块支持

Servlet3为模块化开发提供了良好的支持,Servlet3规范不再要求所有web组件(如ServletListenerFilter等)都部署在web.xml文件中,而是允许采用“web模块”来部署、管理它们。

Web模块结构

Web模块通常对应于一个JAR包,这个JAR包有如下文件结构:

1
2
3
4
<webModule>.jar——这是Web模块的JAR包,可以改变
├─META-INF
│ └─web-fragment.xml
└─Web模块所用的类文件、资源文件等

web模块部署描述符

从上面的文件结构可以看出,Web模块与普通JAR的最大区别在于需要在META-INF目录下添加个web-fragment.xml文件,这个文件也被称为web模块部署描述符。

web-fragment.xml文件与web.xml文件的作用、文档结构都基本相似,因为它们都用于部署、管理各种web组件。只是web-fragment.xml用于部署、管理web模块而已,但web-fragment.xml文件可以多指定如下两个元素:

  1. <name>:用于指定该Web模块的名称
  2. <ordering>:用于指定加载该Web模块的相对顺序。

上面<ordering>元素用于指定加载当前web模块的相对顺序,该元素的内部结构如图2.49所示。

程序示例

crazyit模块

下面开发第一个Web模块,该web模块内只定义了一个简单的ServletContextListner,该Web模块对应的web-fragment.xml文件如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<web-fragment xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd" version="3.1">
<!-- 指定该Web模块的唯一标识 -->
<name>crazyit</name>
<listener>
<listener-class>lee.CrazyitListener</listener-class>
</listener>
<ordering>
<!-- 用配置该Web模块必须位于哪些模块之前加载 -->
<before>
<!-- 用于指定位于其他所有模块之前加载 -->
<others/>
</before>
</ordering>
</web-fragment>

上面的web模块部署描述文件的根元素是<web-fragment>
代码:

1
<name>crazyit</name>

指定该Web模块的名称是crazyit,接下来代码:

1
2
3
4
5
6
7
<ordering>
<!-- 用配置该Web模块必须位于哪些模块之前加载 -->
<before>
<!-- 用于指定位于其他所有模块之前加载 -->
<others/>
</before>
</ordering>

指定该web模块将在其他所有web模块之前加载.

leegang模块

接下来再开发一个web模块,接下来的Web模块同样只定义了一个ServletContextListener,该Web模块对应的web-Fragment.xm文件如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="GBK"?>
<web-fragment xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd" version="3.1">
<!-- 指定该Web模块的唯一标识 -->
<name>leegang</name>
<!-- 配置Listener -->
<listener>
<listener-class>lee.LeegangListener</listener-class>
</listener>
<ordering>
<!-- 用配置该Web模块必须位于哪些模块之后加载 -->
<after>
<!-- 此处可用多个name元素列出该模块必须位于这些模块之后加载 -->
<name>crazyit</name>
</after>
</ordering>
</web-fragment>

将这两个Web模块打包成JAR包,Web模块JAR包的内部结构如图2.50所示。
将这两个Web模块对应的JAR包复制到任意Web应用的WEBNF/lib目录下,启动Web应用,将可以看到两个Web模块被加载:先加载crazyit模块,再加载leegang模块。

web.xml中指定Web模块加载的绝对顺序

Web应用除了可按web-fragment.xml文件中指定的加载顺序来加载Web模块之外,还可以通过web.xml文件指定各Web模块加载的绝对顺序。在web.xml文件中指定的加载顺序将会覆盖Web模块中web-fragment.xml文件所指定的加载顺序.
假如在Web应用的web.xml文件中增加如下配置片段:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="GBK"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">
<absolute-ordering>
<!-- 指定Web模块按如下顺序加载 -->
<name>leegang</name>
<name>crazyit</name>
</absolute-ordering>
</web-app>

上面的配置片段指定了先加载leegang模块,后加载crazyit模块,如果重新启动该Web应用,将可看到leegang模块被优先加载

Web模块化开发的优点

Servlet3web模块支持为模块化开发、框架使用提供了巨大的方便,例如需要在web应用中使用web框架,这就只要将该框架的JAR包复制到Web应用中即可。因为这个JAR包的META-NF目录下可以通过web-fragment.xml文件来配置该框架所需的ServletListenerFilter等,从而避免修改Web应用的web.xml文件。Web模块支持对于模块化开发也有很大的帮助,开发者可以将不同模块的web组件部署在不同的web-fragment.xml文件中,从而避免所有模块的配置、部署信息都写在web.xml文件中,这对以后的升级、维护将更加方便。

2.12 Servlet3新特性

伴随Java EE6一起发布的Servlet 3规范是Servlet规范历史上最重要的变革之一,它的许多特性都极大地简化了JavaWeb应用的开发,例如前面介绍开发ServletListenerFilter时所使用的注解。这些变革必将带给广大Java开发人员巨大的便利,大大加快JavaWeb应用的开发效率。

2.12.1 Servlet3的注解

Servlet3的一个显著改变是“顺应”了潮流,抛弃了采用web.xml配置ServletFilterListener的烦琐步骤,允许开发人员使用注解修饰它们,从而进行部署。

Servlet 3注解

Servlet3规范在javax.servlet.annotation包下提供了如下注解。

  • @WebServlet:用于修饰一个Servlet类,用于部署Servlet类。
  • @WebInitParam:用于与@WebServlet@WebFilter一起使用,为ServletFilter配置参数
  • @WebListener:用于修饰Listener类,用于部署Listener类。
  • @WebFilter:用于修饰Filter类,用于部署Filter类。
  • @MultipartConfig:用于修饰Servlet,指定该Servlet将会负责处理multipart/form-data类型的求(主要用于文件上传)。
  • @ServletSecurity:这是一个与JAAS有关的注解,修饰Servlet指定该Servlet的安全与授权控制
  • @HttpConstraint:用于与@ServletSecurity一起使用,用于指定该Servlet的安全与授权控制。
  • @HttpMethodConstraint:用于与@ServletSecurity一起使用,用于指定该Servlet的安全与授权控

上面这些注解有一些已经在前面有了详细的介绍,此处不再赘述。@MultipartConfig的用法将会在2124节有更详细的说明。至于上面三个与JAAS相关的注解,由于本书并没有涉及JAAS方面的内容,因此请参考本书姊妹篇《经典Java EE企业应用实战》的相关章节。

2.11.3 Tag File支持

Tag File是自定义标签的简化用法,使用Tag File可以无须定义标签处理类和标签库文件,但仍然可以在JP页面中使用自定义标签

Tag File建立一个迭代器标签

下面以Tag File建立一个迭代器标签,其步骤如下。

建立Tag文件

JSP所支持Tag File规范下,Tag File代理了标签处理类,它的格式类似于JSP文件。可以这样理解:如同JSP可以代替Servlet作为表现层一样,Tag File则可以代替标签处理类。
Tag File具有以下5个编译指令。

  • taglib:作用与JSP文件中的taglib指令效果相同,用于导入其他标签库。
  • include:作用与JsP文件中的include指令效果相同,用于导入其他JSP或静态页面
  • tag:作用类似于JsP文件中的page指令,有pageEncodingbody-content等属性,用于设置页面编码等属性。
  • attribute:用于设置自定义标签的属性,类似于自定义标签处理类中的标签属性
  • variable:用于设置自定义标签的变量,这些变量将传给JSP页面使用。

iterator.tag

下面是迭代器标签的Tag File,这个Tag File的语法与JSP语法非常相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%@ tag pageEncoding="UTF-8" import="java.util.List"%>
<!-- 定义了四个标签属性 -->
<%@ attribute name="bgColor" %>
<%@ attribute name="cellColor" %>
<%@ attribute name="title" %>
<%@ attribute name="bean" %>
<table border="1" bgcolor="${bgColor}">
<tr>
<td><b>${title}</b></td>
</tr>
<%List<String> list = (List<String>)
request.getAttribute("a");
// 遍历输出list集合的元素
for (Object ele : list){%>
<tr>
<td bgcolor="${cellColor}">
<%=ele%>
</td>
</tr>
<%}%>
</table>

Tag File只生成HTML片段

上面的页面代码中的粗体字代码就是Tag File的核心代码,可能细心的读者会发现上面的Tag File并不会输出完整的HTML页面,它只包含一个table元素,即只有一个表格,这是正确的。
回忆自定义标签的作用:通过简单的标签在页面上生成一个内容片段。同理,这个Tag File也只负责生成一个页面片段,所以它并不需要输出完整的HTML页面。
Tag File的命名必须遵守如下规则:tagName.tag。即Tag File的主文件名就是标签名,文件名后缀必须是.tag
将该文件存在web应用的某个路径下,这个路径相当于标签库的URI名。这里将其放在/WEB-INF/tags下,即此处标签库路径为/WEB-INF/tags

先导入标签库 再使用标签

在页面中使用自定义标签时,需要先导入标签库,再使用标签。使用Tag File标签与普通自定义标签的用法完全相同,只是在导入标签库时存在一些差异。由于此时的标签库没有URI,只有标签库路径。因此导入标签时,使用如下语法格式

1
<%@ taglib prefix="tagPrefix" tagdir="path" %>
  • prefix与之前的taglib指令的prefix属性完全相同,用于确定标签前缀;
  • tagdir标签库路径下存放很多TagFile,每个TagFile对应一个标签。

useTagFile.jsp

下面是使用Tag File标签的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
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<%@ page import="java.util.*" %>
<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html>
<head>
<title>迭代器tag file</title>
</head>
<body>
<h2>迭代器tag file</h2>
<%
// 创建集合对象,用于测试Tag File所定义的标签
List<String> a = new ArrayList<String>();
a.add("疯狂Java讲义");
a.add("轻量级Java EE企业应用实战");
a.add("疯狂前端开发讲义");
// 将集合对象放入页面范围
request.setAttribute("a" , a);
%>
<h3>使用自定义标签</h3>
<tags:iterator bgColor="#99dd99" cellColor="#9999cc"
title="迭代器标签" bean="a" />
</body>
</html>

从上面的代码可以看出,在JSP页面中使用Tag File标签也很简单。在该JSP页面中,使用了如下代码导入标签:

1
<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %>

即以tags开头的标签,使用/WEB-INF/tags路径下的标签文件处理。
JSP页面中则使用如下代码来使用标签:

1
2
<tags:iterator bgColor="#99dd99" cellColor="#9999cc"
title="迭代器标签" bean="a" />
  • tags表明该标签使用/WEB-INf/tags路径下的TagFile来处理标签;
  • iterator是标签名,即使用/WEB-INf/tags路径下的iterator.tag文件负责处理该标签。

useTagFile.jsp页面最终的执行效果如图2.48所示。

Tag File是自定义标签的简化。事实上,就如同JSP文件会编译成Servlet一样,TagFile也会编译成标签处理类,自定义标签的后台依然由标签处理类完成,而这个过程由容器完成。
打开Tomcatwork\Catalina\localhost\jsp2\org\apache\jsp\tag\web路径,即可看到iterator_tag.javaiterator_tag.class两个文件,这两个文件就是Tag File所对应的标签处理类。
通过查看iterator_tag.java文件的内容不难发现,Tag File中只有如下几个内置对象。

  • request:与JSP脚本中的request对象对应。
  • response:与JSP脚本中的response对象对应。
  • session:与JSP脚本中的session对象对应
  • application:与JSP脚本中的application对象对应。
  • config:与JSP脚本中的config对象对应
  • out:与JSP脚本中的out对象对应

2.11.2 表达式语言

表达式语言(Expression Language)是一种简化的数据访问方式。使用表达式语言可以方便地访问JSP的隐含对象和JavaBeans组件,在JSP2规范中,建议尽量使用表达式语言使JSP文件的格式一致,避免使用JSP小脚本。
表达式语言可用于简化JSP页面的开发,允许美工设计人员使用表达式语言的语法获取业务逻辑组件传过来的变量值。
表达式语言是JSP2的一个重要特性,它并不是一种通用的程序语言,而仅仅是一种数据访问语言,可以方便地访问应用程序数据,建议避免使用JSP小脚本。实际上,Servlet3.1规范再次强化了表达式语言的功能.

表达式语法格式

表达式语言的语法格式是:

1
${expression}

1. 表达式语言支持的算术运算符和逻辑运算符

表达式语言支持的算术运算符和逻辑运算符非常多,所有在Java语言里支持的算术运算符,表达式语言都可以使用;甚至Java语言不支持的一些算术运算符和逻辑运算符,表达式语言也支持。

JSP 表达式 中使用算术运算符

arithmeticOperator.jsp

下面的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
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
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<!DOCTYPE html>
<html>
<head>
<title> 表达式语言 - 算术运算符 </title>
</head>
<body>
<h2>表达式语言 - 算术运算符</h2><hr/>
<table border="1" bgcolor="#aaaadd">
<tr>
<td><b>表达式语言</b></td>
<td><b>计算结果</b></td>
</tr>
<!-- 直接输出常量 -->
<tr>
<td>\${1}</td>
<td>${1}</td>
</tr>
<!-- 计算加法 -->
<tr>
<td>\${1.2 + 2.3}</td>
<td>${1.2 + 2.3}</td>
</tr>
<!-- 计算加法 -->
<tr>
<td>\${1.2E4 + 1.4}</td>
<td>${1.2E4 + 1.4}</td>
</tr>
<!-- 计算减法 -->
<tr>
<td>\${-4 - 2}</td>
<td>${-4 - 2}</td>
</tr>
<!-- 计算乘法 -->
<tr>
<td>\${21 * 2}</td>
<td>${21 * 2}</td>
</tr>
<!-- 计算除法 -->
<tr>
<td>\${3/4}</td>
<td>${3/4}</td>
</tr>
<!-- 计算除法 -->
<tr>
<td>\${3 div 4}</td>
<td>${3 div 4}</td>
</tr>
<!-- 计算除法 -->
<tr>
<td>\${3/0}</td>
<td>${3/0}</td>
</tr>
<!-- 计算求余 -->
<tr>
<td>\${10%4}</td>
<td>${10%4}</td>
</tr>
<!-- 计算求余 -->
<tr>
<td>\${10 mod 4}</td>
<td>${10 mod 4}</td>
</tr>
<!-- 计算三目运算符 -->
<tr>
<td>\${(1==2) ? 3 : 4}</td>
<td>${(1==2) ? 3 : 4}</td>
</tr>
</table>
</body>
</html>

上面的页面中示范了表达式语言所支持的加、减、乘、除、求余等算术运算符的功能,读者可能也发现了表达式语言还支持divmod等运算符。而且表达式语言把所有数值都当成浮点数处理,所以3/0的实质是3.0/0.0,得到的结果应该是Infinity.

测试

浏览arithmeticOperator.jsp页面,将看到如图2.46所示的效果。

如果需要在支持表达式语言的页面中正常输出“$”符号,则在“$”符号前加转义字符“\”,否则系统以为“$”是表达式语言的特殊标记

JSP 表达式 中使用逻辑运算符

也可以在表达式语言中使用逻辑运算符,如下面的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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<!DOCTYPE html>
<html>
<head>
<title> 表达式语言 - 逻辑运算符 </title>
</head>
<body>
<h2>表达式语言 - 逻辑运算符</h2><hr/>
数字之间的比较:
<table border="1" bgcolor="#aaaadd">
<tr>
<td><b>表达式语言</b></td>
<td><b>计算结果</b></td>
</tr>
<!-- 直接比较两个数字 -->
<tr>
<td>\${1 &lt; 2}</td>
<td>${1 < 2}</td>
</tr>
<!-- 使用lt比较运算符 -->
<tr>
<td>\${1 lt 2}</td>
<td>${1 lt 2}</td>
</tr>
<!-- 使用>比较运算符 -->
<tr>
<td>\${1 &gt; (4/2)}</td>
<td>${1 > (4/2)}</td>
</tr>
<!-- 使用gt比较运算符 -->
<tr>
<td>\${1 gt (4/2)}</td>
<td>${1 gt (4/2)}</td>
</tr>
<!-- 使用>=比较运算符 -->
<tr>
<td>\${4.0 &gt= 3}</td>
<td>${4.0 >= 3}</td>
</tr>
<!-- 使用ge比较运算符 -->
<tr>
<td>\${4.0 ge 3}</td>
<td>${4.0 ge 3}</td>
</tr>
<!-- 使用<=比较运算符 -->
<tr>
<td>\${4 &lt;= 3}</td>
<td>${4 <= 3}</td>
</tr>
<!-- 使用le比较运算符 -->
<tr>
<td>\${4 le 3}</td>
<td>${4 le 3}</td>
</tr>
<!-- 使用==比较运算符 -->
<tr>
<td>\${100.0 == 100}</td>
<td>${100.0 == 100}</td>
</tr>
<!-- 使用eq比较运算符 -->
<tr>
<td>\${100.0 eq 100}</td>
<td>${100.0 eq 100}</td>
</tr>
<!-- 使用!=比较运算符 -->
<tr>
<td>\${(10*10) != 100}</td>
<td>${(10*10) != 100}</td>
</tr>
<!-- 先执行运算,再进行比较运算,使用ne比较运算符-->
<tr>
<td>\${(10*10) ne 100}</td>
<td>${(10*10) ne 100}</td>
</tr>
</table>
字符之间的比较:
<table border="1" bgcolor="#aaaadd">
<tr>
<td><b>表达式语言</b></td>
<td><b>计算结果</b></td>
</tr>
<tr>
<td>\${'a' &lt; 'b'}</td>
<td>${'a' < 'b'}</td>
</tr>
<tr>
<td>\${'hip' &gt; 'hit'}</td>
<td>${'hip' > 'hit'}</td>
</tr>
<tr>
<td>\${'4' &gt; 3}</td>
<td>${'4' > 3}</td>
</tr>
</table>
</body>
</html>

从上面程序的粗体字代码中可以看出:
表达式语言不仅可在数字与数字之间比较,还可在字符与字符之间比较,字符串的比较是根据其对应Unicode值来比较大小的。

2. 表达式语言的内置对象

使用表达式语言可以直接获取请求参数值,可以获取页面中JavaBean的指定属性值,获取请求头及获取pagerequestsessionapplication范围的属性值等,这些都得益于表达式语言的内置对象。

JSP表达式的11个内置对象

表达式语言包含如下11个内置对象

  1. pageContext:代表该页面的pageContext对象,与JSPpageContext内置对象相同。
  2. pageScope:用于获取page范围的属性值。
  3. requestScope:用于获取request范围的属性值。
  4. sessionScope:用于获取session范围的属性值。
  5. applicationScope:用于获取application范围的属性值。
  6. param:用于获取请求的参数值
  7. paramValues:用于获取请求的参数值,与param的区别在于,该对象用于获取属性值为数组的属性值。
  8. header:用于获取请求头的属性值。
  9. headerValues:用于获取请求头的属性值,与header的区别在于,该对象用于获取属性值为数组的属性值。
  10. initParam:用于获取请求Web应用的初始化参数
  11. cookie:用于获取指定的Cookie值。

implicit-objects.jsp

下面的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
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
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<!DOCTYPE html>
<html>
<head>
<title> 表达式语言 - 内置对象 </title>
</head>
<body>
<h2>表达式语言 - 内置对象</h2>
请输入你的名字:
<!-- 通过表单提交请求参数 -->
<form action="implicit-objects.jsp" method="post">
<!-- 通过${param['name']} 获取请求参数 -->
你的名字 = <input type="text" name="name" value="${param['name']}"/>
<input type="submit" value='提交'/>
</form><br/>
<% session.setAttribute("user" , "abc");
// 下面三行代码添加Cookie
Cookie c = new Cookie("name" , "yeeku");
c.setMaxAge(24 * 3600);
response.addCookie(c);
%>
<table border="1" width="660" bgcolor="#aaaadd">
<tr>
<td width="170"><b>功能</b></td>
<td width="200"><b>表达式语言</b></td>
<td width="300"><b>计算结果</b></td>
<tr>
<!-- 使用两种方式获取请求参数值 -->
<td>取得请求参数值</td>
<td>\${param.name}</td>
<td>${param.name}&nbsp;</td>
</tr>
<tr>
<td>取得请求参数值</td>
<td>\${param["name"]}</td>
<td>${param["name"]}&nbsp;</td>
</tr>
<tr>
<!-- 使用两种方式获取指定请求头信息 -->
<td>取得请求头的值</td>
<td>\${header.host}</td>
<td>${header.host}</td>
</tr>
<tr>
<td>取得请求头的值</td>
<td>\${header["accept"]}</td>
<td>${header["accept"]}</td>
</tr>
<!-- 获取Web应用的初始化参数值 -->
<tr>
<td>取得初始化参数值</td>
<td>\${initParam["author"]}</td>
<td>${initParam["author"]}</td>
</tr>
<!-- 获取session返回的属性值 -->
<tr>
<td>取得session的属性值</td>
<td>\${sessionScope["user"]}</td>
<td>${sessionScope["user"]}</td>
</tr>
<!-- 获取指定Cookie的值 -->
<tr>
<td>取得指定Cookie的值</td>
<td>\${cookie["name"].value}</td>
<td>${cookie["name"].value}</td>
</tr>
</table>
</body>
</html>

浏览上面页面,并通过页面中表单来提交请求,将看到如图2.47所示的效果。

3. 表达式语言的自定义函数

表达式语言除了可以使用基本的运算符外,还可以使用自定义函数。通过自定义函数,能够大大加强表达式语言的功能。自定义函数的开发步骤非常类似于标签的开发步骤,定义方式也几乎一样。区别在于自定义标签直接在页面上生成输出,而自定义函数则需要在表达式语言中使用。
函数功能大大扩充了EL的功能,EL本身只是一种数据访问语言,因此它不支持调用方法。如果需要在EL中进行更复杂的处理,就可以通过函数来完成。**函数的本质是:提供一种语法允许在EL中调用某个类的静态方法**。

表达式语言中自定义函数的步骤

下面介绍表达式语言中自定义函数的开发步骤。

开发函数处理类

函数处理类就是普通类,这个普通类中包含若干个静态方法,每个静态方法都可定义成一个函数。实际上这个步骤也是可省略的,完全可以直接使用JDK或其他项目提供的类,只要这个类包含静态方法即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
package lee;

public class Functions {
// 对字符串进行反转
public static String reverse(String text) {
return new StringBuffer(text).reverse().toString();
}

// 统计字符串的个数
public static int countChar(String text) {
return text.length();
}
}

完全可以直接使用JDK或其他项目提供的类作为函数处理类,只要这个类包含静态方法即可。

使用标签库定义函数

定义函数的方法与定义标签的方法大致相似。

  • <taglib>元素下增加<tag>元素用于定义自定义标签;
  • <taglib>元素下增加<function>元素则用于定义自定义函数。每个<function>素只要三个子元素即可。
    • name:指定自定义函数的函数名
    • function-class:指定自定义函数的处理类。
    • function-signature:指定自定义函数对应的方法。

下面的标签库定义(TLD)文件将上面的Functions.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
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
web-jsptaglibrary_2_0.xsd" version="2.0">
<tlib-version>1.0</tlib-version>
<short-name>crazyit</short-name>
<!-- 定义该标签库的URI -->
<uri>http://www.crazyit.org/tags</uri>
<!-- 定义第一个函数 -->
<function>
<!-- 定义函数名:reverse -->
<name>reverse</name>
<!-- 定义函数的处理类 -->
<function-class>lee.Functions</function-class>
<!-- 定义函数的实现方法-->
<function-signature>
java.lang.String reverse(java.lang.String)</function-signature>
</function>
<!-- 定义第二个函数: countChar -->
<function>
<!-- 定义函数名:countChar -->
<name>countChar</name>
<!-- 定义函数的处理类 -->
<function-class>lee.Functions</function-class>
<!-- 定义函数的实现方法-->
<function-signature>int countChar(java.lang.String)
</function-signature>
</function>
</taglib>

上面的粗体字代码定义了两个函数,不难发现其实定义函数比定义自定义标签更简单,因为自定义函数只需配置三个子元素即可,变化更少

在JSP页面的EL中使用函数

一样需要先导入标签库,然后再使用函数。下面是使用函数的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
38
39
40
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<%@ taglib prefix="crazyit" uri="http://www.crazyit.org/tags"%>
<!DOCTYPE html>
<html>
<head>
<title> new document </title>
</head>
<body>
<h2>表达式语言 - 自定义函数</h2><hr/>
请输入一个字符串:
<form action="useFunctions.jsp" method="post">
字符串 = <input type="text" name="name" value="${param['name']}">
<input type="submit" value="提交">
</form>
<table border="1" bgcolor="aaaadd">
<tr>
<td><b>表达式语言</b></td>
<td><b>计算结果</b></td>
<tr>
<tr>
<td>\${param["name"]}</td>
<td>${param["name"]}&nbsp;</td>
</tr>
<!-- 使用reverse函数-->
<tr>
<td>\${crazyit:reverse(param["name"])}</td>
<td>${crazyit:reverse(param["name"])}&nbsp;</td>
</tr>
<tr>
<td>\${crazyit:reverse(crazyit:reverse(param["name"]))}</td>
<td>${crazyit:reverse(crazyit:reverse(param["name"]))}&nbsp;</td>
</tr>
<!-- 使用countChar函数 -->
<tr>
<td>\${crazyit:countChar(param["name"])}</td>
<td>${crazyit:countChar(param["name"])}&nbsp;</td>
</tr>
</table>
</body>
</html>

如上面程序中粗体字代码所示,导入标签库定义文件后(实质上也是函数库定义文件),就可以在表达式语言中使用函数定义库文件里定义的各函数了。
通过上面的介绍不难发现自定义函数的实质:就是将指定Java类的静态方法暴露成可以在EL中使用的函数,所以可以定义成函数的方法必须用public static修饰。