关于三层架构

256 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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>

这样属于连接成功

image.png

创建数据库

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),最后通过数据访问层和数据库交互;并且当数据访问层获取到数据以后,再将数据传递给业务逻辑层,而业务逻辑层则将最终的数据传递给表示层。请求自上而下传递,响应自下而上传递,如图所示:

图片描述