持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
在实际的开发中,为了更好地解耦合、使开发人员之间的分工明确、提高代码的可重用性等,通常会采用“三层架构”的模式来组织代码。
所谓“三层”,是指表示层(User Show Layer,USL)、业务逻辑层(Business Logic Layer,BLL)、数据访问层(Data Access Layer,DAL),各层关系如图所示。三层中使用的数据,是通过实体类(封装数据的 JavaBean)来传递的。实体类一般放在 entity 包下。
1. 数据访问层(DAL)
数据访问层也称为持久层,位于三层中的最底层,用于对数据进行处理。该层中的方法一般都是“原子性”的,即每个方法都只完成一个逻辑功能,不可再分。
在程序中,DAL 一般写在 dao 包中,包里面的类名也推荐以 Dao 结尾,如 StudentDao.java、DepartmentDao.java、NewsDao.java 等;换每个 “类名 Dao.java” 类就包含着对该“类名”的所有对象的数据访问操作。
2. 业务逻辑层(BLL)
BLL 位于三层中的中间层(即处于 DAL 与 USL 的中间层),起到了数据交换中承上启下的作用,用于对业务逻辑的封装。BLL 的设计对于一个支持可扩展的架构尤为关键,因为它扮演了两种角色。对于 DAL 而言,它是调用者;对于 USL 而言,它是被调用者。可见,三层中的依赖与被依赖的关系主要就纠结在 BLL 上。
使用上,BLL 实际就是对 DAL 中的方法进行“组装”。比如 BLL 中的“删”不再像 DAL 中那样仅仅实现“删”这一功能,而是在“删”之前要进行业务逻辑的判断:先查找某个对象是否存在(即先执行 DAL 层的“查”),如果存在才会真正地“删”(再执行 DAL 层的“删”),如果该对象不存在则应该提示错误信息。即 BLL 中的“删”,应该是“带逻辑的删”(先“查”后“删”),也就是对 DAL 中的“查”和“删”两个方法进行了“组装”。
在程序中,BLL 一般写在 service 包中,包里面的类名也是以“Service”结尾,如 StudentService.java、DepartmentService.java、NewsService 等。每个“类名 Service.java”类,就包含着对该“类名”的对象的业务操作,如 StudentService.java 中包含对 Student 对象的“带逻辑的删”、“带逻辑的增”等业务逻辑操作,DepartmentService.java 中包含对所有 Department 对象的“带逻辑的删”、“带逻辑的增”等业务逻辑操作。
3. 表示层(USL)
USL 位于三层中的最上层,用于显示数据和接收用户输入的数据,为用户提供一种交互式操作的界面。USL 又进一步细分为“USL 前台代码”和“USL 后台代码”,其中“USL 前台代码”是指用户能直接访问到的界面,一般是程序的外观(如 html/css、JSP 等文件的展示效果),类似于 MVC 模式中的“视图”;“USL 后台代码”是指用来调用业务逻辑层的 Java 代码(如 Servlet),类似于 MVC 模式中的“控制器”。表示层前台代码一般放在 WebContent 目录下,而表示层后台代码目前可以放在 servlet 包下。
不难发现,三层架构与 MVC 设计模式的关系如图所示:
MVC 模式和三层架构,是分别从两个不同的角度去设计的,但目的都是“解耦,分层,代码复用等”。
下面我们来实现简单的学生管理系统
第一步:建立一个javaweb项目,参考前面一个文章
第二步:在 Java Web 开发过程中,需要运行 Web 服务进行测试,这个时候就需要 Jetty 或者 Tomcat。并且需要导入 servlet 解析、MySQL 数据库连接相关 jar 包的依赖。所以,需要先修改刚刚新建的项目配置,添加 Tomcat Maven 插件。
打开项目文件夹下方的 pom.xml 配置文件修改,参考前一个文章
关于Servlet 与 MVC 设计模式 - 掘金 (juejin.cn)
上方的配置中,我们新增了 tomcat7-maven-plugin 插件支持以及相关 jar 包。接下来,打开tomcat
在浏览器输入地址,看看能不能正常访问首页
测试数据库连接
切换到mysql,修改 webapp 下的 index.jsp 文件,进行数据库连接测试。如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" import="java.sql.*"%>
<html>
<head>
<title>数据库连接测试</title>
</head>
<body>
<h2>Hello World!</h2>
<%
try {
Class.forName("com.mysql.jdbc.Driver");
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?characterEncoding=UTF-8", "root", "");
out.print("数据库连接成功:" + con);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}catch (SQLException e) {
e.printStackTrace();
}
%>
</body>
</html>
这样属于连接成功
创建数据库
create database studentManager; #创建数据库
use studentManager; #切换数据库
show tables; #查看库表
#建表:
create table if not exists student(
stuNo integer unsigned primary key,
stuName varchar(20) not null,
stuAge integer unsigned not null,
graName varchar(20)
)engine=InnoDB default charset=utf8; desc student;
然后根据数据库字段开始编写实体类的代码
编写实体类 entity.Student.java,并重写 equals()、hashcode()、toString() 方法,如下:
package entity;
import java.util.Objects;
public class Student{
private int studentNo; // 学号
private String studentName; // 姓名
private int studentAge; // 年龄
private String gradeName; // 年级
。。。这里省略的是get,set方法,可以借助eclipes,idea自动生成
。。。关于equals,hashcode,tostring,可以借助eclipes,idea自动生成
}
然后在dao文件里面dao.StudentDao.java 中对学生信息进行基本的“增删改查”操作,如下:
import entity.Student;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class StudentDao {
private static final String DRIVER_NAME = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/studentManager?characterEncoding=UTF-8";
private static final String USERNAME = "root";
private static final String PWD = "";
// 增加学生
public boolean addStudent(Student stu) {
Connection conn = null;
PreparedStatement pstmt = null;
// flag用来标记是否增加成功,若增加成功则返回true,若增加失败则返回false
boolean flag = true;
try {
Class.forName(DRIVER_NAME);
conn = DriverManager.getConnection(URL, USERNAME, PWD);
String sql = "insert into student(stuNo,stuName,stuAge,graName) values(?,?,?,?)";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, stu.getStudentNo());
pstmt.setString(2, stu.getStudentName());
pstmt.setInt(3, stu.getStudentAge());
pstmt.setString(4, stu.getGradeName());
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
flag = false;
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
flag = false;
}
}
return flag;
}
// 根据学号删除学生
public boolean deleteStudentByNo(int stuNo) {
Connection conn = null;
PreparedStatement pstmt = null;
// flag用来标记是否删除成功,若删除成功则返回true,若删除失败则返回false
boolean flag = true;
try {
Class.forName(DRIVER_NAME);
conn = DriverManager.getConnection(URL, USERNAME, PWD);
String sql = "delete from student where stuNo = ? ";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, stuNo);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
flag = false;
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
flag = false;
}
}
return flag;
}
// 修改学生信息:将原来学号为stuNo的学生信息,修改为实体类stu中的包含信息
public boolean updateStudent(Student stu, int stuNo) {
Connection conn = null;
PreparedStatement pstmt = null;
// flag用来标记是否修改成功,若修改成功则返回true,若修改失败则返回false
boolean flag = true;
try {
Class.forName(DRIVER_NAME);
conn = DriverManager.getConnection(URL, USERNAME, PWD);
String sql = "update student set stuNo = ?,stuName = ?,stuAge = ? ,graName=? where stuNo = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, stu.getStudentNo());
pstmt.setString(2, stu.getStudentName());
pstmt.setInt(3, stu.getStudentAge());
pstmt.setString(4, stu.getGradeName());
pstmt.setInt(5, stuNo);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
flag = false;
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
flag = false;
}
}
return flag;
}
// 根据学号,查询某一个学生
public Student queryStudentByNo(int stuNo) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
Student stu = null;
try {
Class.forName(DRIVER_NAME);
conn = DriverManager.getConnection(URL, USERNAME, PWD);
String sql = "select stuNo,stuName,stuAge,graName from student where stuNo = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, stuNo);
rs = pstmt.executeQuery();
if (rs.next()) {
int sNo = rs.getInt("stuNo");
String sName = rs.getString("stuName");
int sAge = rs.getInt("stuAge");
String gName = rs.getString("graName");
// 将查询到的学生信息封装到stu对象中
stu = new Student(sNo, sName, sAge, gName);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return stu;
}
// 查询全部学生
public List<Student> queryAllStudents() {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
List<Student> students = new ArrayList<Student>();
try {
Class.forName(DRIVER_NAME);
conn = DriverManager.getConnection(URL, USERNAME, PWD);
String sql = "select stuNo,stuName,stuAge,graName from student ";
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
int sNo = rs.getInt("stuNo");
String sName = rs.getString("stuName");
int sAge = rs.getInt("stuAge");
String gName = rs.getString("graName");
// 将查询到的学生信息封装到stu对象中
Student stu = new Student(sNo, sName, sAge, gName);
// 将封装好的stu对象存放到List集合中
students.add(stu);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return students;
}
// 根据学号,判断某一个学生是否已经存在
public boolean isExistByNo(int stuNo) {
boolean isExist = false;
Student stu = this.queryStudentByNo(stuNo);
//如果stu为null,说明查无此人,即此人不存在;否则说明已经存在此人
isExist = (stu == null) ? false : true;
return isExist;
}
}
在这里,你是不是发现,每一个方法里面都要重新连接数据库,这是不是导致代码重复太多了,我们可以把连接数据库抽离处理,放到一个类里面,或者把他从方法里面抽离出来,放到主函数里面,这个本次不细讲。
文件 org.lanqiao.service.StudentService.java 实现带逻辑的“增删改查”操作,本质是对数据访问层的多个方法进行“组装”,如下:
import java.util.List;
import dao.StudentDao;
import entity.Student;
public class StudentService {
// 业务逻辑层依赖于数据访问层
StudentDao stuDao = new StudentDao();
// 增加学生
public boolean addStudent(Student stu) {
// 增加之前先进行逻辑判断,如果此人已经存在,则不能再次增加
if (stuDao.isExistByNo(stu.getStudentNo())) {
System.out.println("此人已经存在,不能重复增加!");
return false;
}
// 调用数据访问层的方法,实现增加操作
return stuDao.addStudent(stu);
}
// 根据学号删除学生
public boolean deleteStudentByNo(int stuNo) {
// 删除之前先进行逻辑判断,如果此人不存在,则给出错误提示
if (!stuDao.isExistByNo(stuNo)) {
System.out.println("查无此人,无法删除!");
return false;
}
// 调用数据访问层的方法,实现删除操作
return stuDao.deleteStudentByNo(stuNo);
}
// 修改学生信息:将原来学号为stuNo的学生信息,修改为实体类stu中的包含信息
public boolean updateStudent(Student stu, int stuNo) {
// 修改之前先进行逻辑判断,如果需要修改的人不存在,则给出错误提示
if (!stuDao.isExistByNo(stuNo)) {
System.out.println("查无此人,无法修改!");
return false;
}
// 调用数据访问层的方法,实现删除操作
return stuDao.updateStudent(stu, stuNo);
}
// 根据学号,查询某一个学生
public Student queryStudentByNo(int stuNo) {
// 查询操作一般不用判断,直接调用数据访问层的方法即可
return stuDao.queryStudentByNo(stuNo);
}
// 查询全部学生
public List<Student> queryAllStudents() {
// 查询操作一般不用判断,直接调用数据访问层的方法即可
return stuDao.queryAllStudents();
}
// 根据学号,判断某一个学生是否已经存在
public boolean isExistByNo(int stuNo) {
// 直接调用数据访问层的方法进行判断
return stuDao.isExistByNo(stuNo);
}
}
通过代码可以发现,业务逻辑层依赖于数据访问层(使用到了数据访问层的对象 stuDao);并且业务逻辑层的实现,就是编写一些逻辑判断代码,然后调用相应数据访问层的方法。
真正的增加学生 AddStudentServlet,如下:
import entity.Student;
import service.StudentService;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import javax.servlet.annotation.WebServlet;
@WebServlet("/AddStudentServlet")
public class AddStudentServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 若是 get 方式的请求,则仍然使用 post 方式进行处理
this.doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
// 接收表单提交的数据
int studentNo = Integer.parseInt(request.getParameter("sno"));
String studentName = request.getParameter("sname");
int studentAge = Integer.parseInt(request.getParameter("sage"));
String gradeName = request.getParameter("gname");
// 将数据封装到实体类中
Student stu = new Student(studentNo, studentName, studentAge, gradeName);
// 调用业务逻辑层代码
StudentService stuService = new StudentService();
boolean result = stuService.addStudent(stu);
if (!result) {
// 如果增加失败,则在 request 中放入一个标识符,标识一下错误
request.setAttribute("addError", "error");
// 返回增加页面。因为需要传递 request 作用域中的数据,所以使用请求转发
request.getRequestDispatcher("addStudent.jsp").forward(request, response);
} else {
response.sendRedirect("QueryAllStudentsServlet");
}
}
}
删除修改查看类似,就不再写了
增加学生 addStudent.jsp,如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>添加学生信息</title>
</head>
<body>
<%-- 如果增加失败,则返回本页时需有错误提示,具体可查看 AddStudentServlet.java 中的代码 --%>
<%
// 如果存放了错误标识符
if(request.getAttribute("addError") != null ){
out.print("<strong>增加失败!</strong>");
}
%>
<form action="AddStudentServlet" method="post">
学号:<input type="text" name="sno" /><br/>
姓名:<input type="text" name="sname" /><br/>
年龄:<input type="text" name="sage" /><br/>
年级:<input type="text" name="gname" /><br/>
<input type="submit" value="增加" /><br/>
</form>
</body>
</html>
查询全部学生信息 index.jsp,如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" import="org.lanqiao.entity.Student,java.util.List"%>
<html>
<head>
<title>学生信息显示列表</title>
</head>
<body>
<%--
如果删除失败,则返回本页时需有错误提示。具体可查看DeleteStudentServlet.java中的代码
--%>
<%
// 如果存放了错误标识符
if(request.getAttribute("delError") != null ){
out.print("<strong>删除失败!</strong>");
}
if(request.getAttribute("addError") != null ){
out.print("<strong>增加失败!</strong>");
}
%>
<table border="1" >
<tr>
<th>学号</th>
<th>姓名</th>
<th>年龄</th>
<th>操作</th>
<%--
年级信息不在列表页显示,只能在具体学生的详情页 showStudentInfo.jsp 显示
--%>
</tr>
<%
List<Student> students = (List<Student>)request.getAttribute("students");
if(students != null){
for(Student stu : students){
%>
<tr>
<%-- 单击“学号”链接,可以进入修改页面 --%>
<td>
<%-- 调用查询某一个学生的 Servlet;并通过地址重写的方式将需要修改学生的学号传递过去 --%>
<a href="QueryStudentByNoServlet?stuNo=<%=stu.getStudentNo() %>">
<%=stu.getStudentNo() %>
</a>
</td>
<td><%=stu.getStudentName() %></td>
<td><%=stu.getStudentAge() %></td>
<%-- 调用删除的 Servlet,并通过地址重写的方式将学号传递过去 --%>
<td>
<a href="DeleteStudentServlet?stuNo=<%=stu.getStudentNo() %>">
删除
</a>
</td>
</tr>
<%
}
}
%>
</table>
<a href="addStudent.jsp">增加</a>
</body>
</html>
需要注意的是,项目运行后的第一个执行程序应该是 QueryAllStudentsServlet,而不是 index.jsp。因为项目的首页 index.jsp 要展示的学生列表信息,必须先通过 QueryAllStudentsServlet 查询后才能得到。这个执行顺序在可以通过 WEB-INF 文件夹下的 web.xml 来配置,如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<welcome-file-list>
<welcome-file>QueryAllStudentsServlet</welcome-file>
</welcome-file-list>
</web-app>
通过 QueryAllStudentsServlet 执行请求转发到 index.jsp,运行结果如图所示:
根据学号删除某一个学生的表示层前端代码,是通过 index.jsp 中的超链接实现的,如下:
<a href="DeleteStudentServlet?stuNo=<%=stu.getStudentNo() %>">删除</a>
程序运行结果如图:
根据学号查询某一个学生信息 showStudentInfo.jsp,如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" import="org.lanqiao.entity.Student"%>
<html>
<head>
<title>学生信息详情页</title>
</head>
<body>
<%
// 如果修改失败,返回本页时需有错误提示。具体可查看 UpdateStudentServlet.java 中的代码
// 如果存放了错误标识符
if(request.getAttribute("updateError") != null ){
out.print("<strong>修改失败!</strong>");
}
// 接收查询到的学生信息
Student stu = (Student)request.getAttribute("stu");
if(stu !=null){
%>
<form action="UpdateStudentServlet" method="post" >
<!--假设学号不能修改-->
学号:<input type="text" name="sno" readonly="readonly"
value="<%=stu.getStudentNo() %>" /><br/>
姓名:<input type="text" name="sname"
value="<%=stu.getStudentName() %> " /><br/>
年龄:<input type="text" name="sage"
value="<%=stu.getStudentAge() %>" /><br/>
年级:<input type="text" name="gname"
value="<%=stu.getGradeName() %>" /><br/>
<input type="submit" value="修改" />
</form>
<%
}
%>
<a href="QueryAllStudentsServlet">返回</a>
</body>
</html>
这里大家是不是发现,sno这些在前面出现过哦。
部署并运行项目,在学生列表页 index.jsp 中单击“学号”链接后,转发到学生信息详情页 ,然后通过学号修改。
<td>
<%--调用查询某一个学生的Servlet;并通过地址重写的方式将需要修改学生的学号传递过去 --%>
<a href="QueryStudentByNoServlet?stuNo=<%=stu.getStudentNo() %>">
<%=stu.getStudentNo() %>
</a>
</td>
总结: 通过本案例可以发现,使用三层搭建的项目,是通过表示层前台代码(index.jsp)和用户交互,然后通过表示层后台代码(QueryAllStudentsServlet.java)调用业务逻辑层(StudentService.java),再通过业务逻辑层调用数据访问层(StudentDao.java),最后通过数据访问层和数据库交互;并且当数据访问层获取到数据以后,再将数据传递给业务逻辑层,而业务逻辑层则将最终的数据传递给表示层。请求自上而下传递,响应自下而上传递,如图所示: