2.7.6 使用Servlet作为控制器

2.7.6 使用Servlet作为控制器

正如前面见到的,使用Servlet作为表现层的工作量太大,所有的HTML标签都需要使用页面输出流生成。因此,使用Servlet作为表现层有如下三个劣势

  1. 开发效率低,所有的HTML标签都需使用页面输出流完成。
  2. 不利于团队协作开发,美工人员无法参与Servlet界面的开发
  3. 程序可维护性差,即使修改一个按钮的标题,都必须重新编辑Java代码,并重新编译

在标准的MVC模式中,Servlet仅作为控制器使用。JavaEE应用架构正是遵循MVC模式的,对于遵循MVC模式的Java EE应用而言,JSP仅作为表现层(view)技术,其作用有两点:

  • 负责收集用户请求参数。
  • 将应用的处理结果、状态数据呈现给用户。

Servlet则仅充当控制器(Controller)角色,它的作用类似于调度员:所有用户请求都发送给Servlet,Servlet调用Model来处理用户请求,并调用JSP来呈现处理结果;或者Servlet直接调用JSP将应用的状态数据呈现给用户。
Model通常由JavaBean来充当,所有业务逻辑、数据访问逻辑都在Model中实现。实际上隐藏在Model下的可能还有很多丰富的组件,例如DAO组件、领域对象等。

程序示例 简单登录验证

下面介绍一个使用Servlet作为控制器的MVC应用,该应用演示了一个简单的登录验证。

login.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
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<!DOCTYPE html>
<html>
<head>
<title> 用户登录 </title>
</head>
<body>
<!-- 输出出错提示 -->
<span style="color:red;font-weight:bold">
<%
if (request.getAttribute("err") != null)
{
out.println(request.getAttribute("err") + "<br/>");
}
%>
</span>
请输入用户名和密码:
<!-- 登录表单,该表单提交到一个Servlet -->
<form id="login" method="post" action="login">
用户名:<input type="text" name="username"/><br/>
密&nbsp;&nbsp码:<input type="password" name="pass"/><br/>
<input type="submit" value="登录"/><br/>
</form>
</body>
</html>

以上页面使用JSP脚本输出错误提示,该页面其实是一个简单的表单页面,用于收集用户名及密码,并将请求提交到指定Servlet,该Servlet充当控制器角色。
根据严格的MVC规范,上面的login.jsp页面也不应该被客户端直接访问,客户的请求应该先发送到指定Servlet,然后由Servlet将请求forward到该JSP页面

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

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.PrintWriter;
import java.io.IOException;
import java.sql.*;

@WebServlet(
name = "login",
urlPatterns =
{"/login"})
public class LoginServlet extends HttpServlet
{
// 响应客户端请求的方法
public void service(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, java.io.IOException
{
String errMsg = "";
// Servlet本身并不输出响应到客户端,因此必须将请求转发到视图页面
RequestDispatcher rd;
// 获取请求参数
String username = request.getParameter("username");
String pass = request.getParameter("pass");
try
{
// Servlet本身并不执行任何的业务逻辑处理,它调用JavaBean处理用户请求
// MySQL5.0
// DbDao dd = new DbDao("com.mysql.jdbc.Driver",
// "jdbc:mysql://localhost:3306/liuyan", "root", "32147");
// Mysql8.0
DbDao dd = new DbDao("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/liuyan?serverTimezone=GMT",
"root", "root");

// 查询结果集
ResultSet rs = dd.query(
"select pass from user_inf" + " where name = ?", username);
if (rs.next())
{
// 用户名和密码匹配
if (rs.getString("pass").equals(pass))
{
// 获取session对象
HttpSession session = request.getSession(true);
// 设置session属性,跟踪用户会话状态
session.setAttribute("name", username);
// 获取转发对象
rd = request.getRequestDispatcher("/welcome.jsp");
// 转发请求
rd.forward(request, response);
} else
{
// 用户名和密码不匹配时
errMsg += "您的用户名密码不符合,请重新输入";
}
} else
{
// 用户名不存在时
errMsg += "您的用户名不存在,请先注册";
}
} catch (Exception e)
{
e.printStackTrace();
}
// 如果出错,转发到重新登录
if (errMsg != null && !errMsg.equals(""))
{
rd = request.getRequestDispatcher("/login.jsp");
request.setAttribute("err", errMsg);
rd.forward(request, response);
}
}
}

控制器负责接收客户端的请求,它既不直接对客户端输出响应,也不处理用户请求,只调用JavaBean来处理用户请求,如程序中粗体字代码所示;JavaBean处理结束后,Servlet根据处理结果,调用不同的JSP页面向浏览器呈现处理结果。
上面Servlet使用@WebServlet注解为该Servlet配置了URL/login,因此向/login发送的请求将会交给该Servlet处理。

DbDao.java

下面是本应用中DaDao的源代码。

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

import java.sql.*;

public class DbDao {
private Connection conn;
private String driver;
private String url;
private String username;
private String pass;

public DbDao() {
}

public DbDao(String driver, String url, String username, String pass) {
this.driver = driver;
this.url = url;
this.username = username;
this.pass = pass;
}

// 下面是各个成员属性的setter和getter方法
public void setDriver(String driver) {
this.driver = driver;
}

public void setUrl(String url) {
this.url = url;
}

public void setUsername(String username) {
this.username = username;
}

public void setPass(String pass) {
this.pass = pass;
}

public String getDriver() {
return (this.driver);
}

public String getUrl() {
return (this.url);
}

public String getUsername() {
return (this.username);
}

public String getPass() {
return (this.pass);
}

// 获取数据库连接
public Connection getConnection() throws Exception {
if (conn == null) {
Class.forName(this.driver);
conn = DriverManager.getConnection(url, username, this.pass);
}
return conn;
}

// 插入记录
public boolean insert(String sql, Object... args) throws Exception {
PreparedStatement pstmt = getConnection().prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
if (pstmt.executeUpdate() != 1) {
return false;
}
pstmt.close();
return true;
}

// 执行查询
public ResultSet query(String sql, Object... args) throws Exception {
PreparedStatement pstmt = getConnection().prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
return pstmt.executeQuery();
}

// 执行修改
public void modify(String sql, Object... args) throws Exception {
PreparedStatement pstmt = getConnection().prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
pstmt.executeUpdate();
pstmt.close();
}

// 关闭数据库连接的方法
public void closeConn() throws Exception {
if (conn != null && !conn.isClosed()) {
conn.close();
}
}
}

上面DaDao负责完成查询、插入、修改等操作。从上面这个应用的结构来看,整个应用的流程非常清晰,下面是MVC中各个角色的对应组件:

  • M:Model,即模型,对应JavaBean
  • V:View,即视图,对应JSP页面。
  • C:Controller,即控制器,对应Servlet

data.sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DROP DATABASE IF EXISTS liuyan;
CREATE DATABASE liuyan;

USE liuyan;

CREATE TABLE user_inf
(
id INT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(255),
pass VARCHAR(255)
);

INSERT INTO user_inf
VALUES(NULL,'crazyit','123');
INSERT INTO user_inf
VALUES(NULL,'tiger','123');
;