本教程将一步步构建一个 Java Web 的 CRUD(创建、读取、更新、删除)及分页功能的示例应用,涵盖从基本概念到完整项目架构的各个层次。我们以产品(Product) 信息管理为例,演示如何使用 Servlet、JSP、JDBC/MyBatis 等技术按照 MVC 分层架构开发,并实现分页查询。
内容包括:基础知识介绍、项目结构说明、数据库连接配置、DAO 层实现、Service 层设计、Servlet 控制层编写、JSP 前端展示、分页功能实现、日志配置以及 Tomcat 部署运行。
-
基础知识:Servlet、JSP、JDBC、MVC 架构
在开始实战之前,需要了解一些 Java Web 开发的基础概念:
● Servlet :Servlet 是运行在服务器端的 Java 程序,用于处理客户端(浏览器)发送的 HTTP 请求,并生成响应。Servlet 可以看作控制器(Controller),负责接收请求、调用业务逻辑处理,然后将响应结果发送回客户端。在 MVC 架构中,Servlet 通常扮演控制器角色,负责协调模型和视图的交互。
● JSP :JSP(JavaServer Pages)是一种动态网页技术,是带有特殊标签和脚本的 HTML 页面。JSP 会被转换编译成 Servlet 来运行,用于呈现动态内容。JSP 通常作为视图(View)组件,负责显示数据和页面布局,将模型数据展示给用户。在 JSP 页面中可以通过表达式语言(EL)或者 JSTL 标签来访问由 Servlet 提供的数据,从而将数据填充到 HTML 模板中。
● JDBC :JDBC(Java Database Connectivity)是 Java 提供的一套用于访问数据库的 API。通过 JDBC,我们可以使用 SQL 语句对数据库进行 CRUD 操作。步骤通常包括:加载数据库驱动、获取数据库连接、执行 SQL、处理结果集、关闭连接等。JDBC 是底层直接操作数据库的方式,需要编写较多样板代码。
● MVC 架构:MVC 即模型-视图-控制器(Model-View-Controller)架构,是一种分层设计模式。模型(Model) 包含应用的数据和业务逻辑,比如 JavaBean 实体类和数据访问层;视图(View) 是用户界面,比如 JSP 页面;控制器(Controller) 则是业务流程控制,比如 Servlet。在 MVC 模式下,Servlet 接收请求并调用模型层处理业务,将结果数据交给 JSP 显示,从而将数据处理和页面展示分离,提升代码的可维护性和扩展性。简单来说,Servlet 和 JSP 是 Java Web 的两大组件:Servlet 作为控制器处理请求和响应,JSP 作为视图展示数据,而 JDBC 等用于模型层与数据库交互。
了解以上概念后,我们就可以按照 MVC 分层思想组织我们的 JavaWeb 项目,在不同层次各司其职:数据访问和业务逻辑在模型层,页面展示在视图层,请求控制在控制层。接下来,我们将设计项目的整体结构。
-
项目结构:包目录划分 (cn.wolfcode.product)
一个良好的项目结构有助于分清职责,提高开发效率。我们以包名 cn.wolfcode.product 为例划分项目的主要目录结构,各层次功能如下:
scss
cn.wolfcode.product
├── domain // 领域模型层:存放实体类(JavaBean),如 Product 等
│ └── Product.java // 产品实体类,封装产品的属性和 get/set 方法
├── dao // 数据访问层:存放 DAO 接口和实现,用于与数据库交互
│ ├── ProductDAO.java // 产品DAO接口,定义 CRUD 和分页方法
│ └── ProductMapper.xml // (使用 MyBatis 时)产品 DAO 对应的 SQL 映射文件
├── service // 业务服务层:存放 Service 类,封装业务逻辑
│ └── ProductService.java // 产品 Service,调用 DAO 方法并处理业务规则
├── web // Web 控制层:存放 Servlet 等控制器类,处理 HTTP 请求
│ └── ProductServlet.java // 产品 Servlet,接收请求参数调用 Service,转发视图
├── util (可选) // 工具配置层:存放工具类,例如数据库连接工具、MyBatis 配置加载等
│ └── MyBatisUtil.java // MyBatis 工厂工具类(如需)
└── resources // 资源文件目录:存放配置文件、日志配置、MyBatis 配置等
├── db.properties // 数据库连接配置文件
└── log4j.properties // 日志配置文件
此外,项目的 Web 内容部分包括:
● WebContent/Webapp(具体名称取决于构建工具和 IDE):存放前端页面和 WEB-INF 配置。
○ WEB-INF/web.xml :Web 应用部署描述文件,用于配置 Servlet 映射等(如果不使用注解配置 Servlet)。
○ 前端 JSP 页面文件,例如 showAll.jsp(用于显示产品列表),以及可能的 editProduct.jsp(编辑表单页面)等。JSP 文件通常放在 WebContent 根目录或其子文件夹下(不放在 WEB-INF 下,以便客户端可访问,除非通过控制器转发)。
○ WEB-INF/lib :放置项目依赖的库 JAR,如 MyBatis 的 jar 包、MySQL 驱动 mysql-connector-java.jar、以及 JSTL 标签库等。
○ WEB-INF/classes:编译后的 .class 文件以及资源文件(如 properties)会放在这里。在构建 WAR 包时,classes 下的内容和 lib 下的 jar 共同构成应用的类路径。
上述结构清晰地将模型、视图、控制器和配置分离。例如,Product.java 定义产品数据结构,ProductDAO 专注于数据存取,ProductService 则封装业务操作,而 ProductServlet 负责接收浏览器请求、调用 Service 并将数据传递给 JSP。接下来,我们将逐层实现上述架构,首先从数据库连接配置开始。
- 数据库连接:使用 db.properties 配置并通过 MyBatis/JDBC 访问 MySQL
数据库层是应用的基础。在本教程中,我们使用 MySQL 数据库。为了方便管理数据库连接信息,我们将这些配置放入属性文件 db.properties,从而在代码中读取使用,避免硬编码连接参数。
(1)编写数据库配置文件 db.properties:
在项目的资源目录下(如 src/main/resources 或直接放在类路径下 WEB-INF/classes),创建 db.properties 文件,内容示例如下:
java
# db.properties - 数据库连接配置
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/product_db?useSSL=false&characterEncoding=utf8
username=root
password=123456
如上,我们定义了驱动类名、URL、用户名和密码等。请将其中的 URL、用户名和密码修改为你自己的数据库名称和账号。将该文件放在类路径下后,应用启动时可以方便地读取这些配置。
(2)加载数据库配置并建立连接:
有两种方式来利用上述配置:直接使用 JDBC API 或集成 MyBatis。
● 直接使用 JDBC:通过 Properties 对象加载 db.properties,获取各配置项,然后使用 DriverManager 获取数据库连接。例如,我们可以在一个工具类中编写如下代码:
java
// 从类路径加载 db.properties 文件
InputStream in = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("db.properties");
Properties props = new Properties();
props.load(in);
String driver = props.getProperty("driver");
String url = props.getProperty("url");
String user = props.getProperty("username");
String pwd = props.getProperty("password");
Class.forName(driver); // 加载数据库驱动
Connection conn = DriverManager.getConnection(url, user, pwd);
// ... 使用 conn 进行 SQL 操作
上述代码演示了如何读取属性文件并建立 JDBC 连接。在实际应用中,我们会将其封装到 DAO 或工具类中以重用。需要注意管理连接的关闭,防止资源泄漏。此外,可以使用数据库连接池来提高性能,但这里不展开。
● 使用 MyBatis 框架:MyBatis 能简化数据访问代码。首先需要整合 MyBatis 的配置。在资源目录下新建 MyBatis 全局配置文件(一般命名为 mybatis-config.xml)。在该配置中指定环境,包括数据源和事务管理方式。例如:
java
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="cn/wolfcode/product/mapper/ProductMapper.xml"/>
</mappers>
</configuration>
上述 XML 将数据库连接信息通过占位符引用,我们可以在同级目录下再创建一个 db.properties 并定义与占位符对应的键值(driver/url/username/password),或者直接在启动时通过代码读取 db.properties 后设置到环境中。MyBatis 提供了 Resources 工具类方便读取配置并构建 SqlSessionFactory,例如:
java
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(); // 获取数据库会话
上述代码使用 MyBatis 官方推荐的方式从 XML 配置构建 SqlSessionFactory。SqlSessionFactory 相当于数据库连接池,创建后应用中应重复使用。通常我们会将其封装在一个单例的工具类中(例如 MyBatisUtil),提供一个静态方法来获取 SqlSession。
(3)配置 MySQL 驱动依赖:
使用 JDBC 或 MyBatis,都需要 MySQL 的 JDBC 驱动。确保在项目的类路径(WEB-INF/lib 或 Maven 依赖)中包含 MySQL Connector/J 的 jar 包。此外,如果使用 MyBatis,还需加入 MyBatis 的相关 jar。
完成数据库连接配置后,我们可以进行数据库表的准备(例如创建一个 product 表,包含 id, name, price, quantity 等字段)。接下来将进入数据访问层,实现对数据库的 CRUD 操作。
- DAO 层:编写 ProductDAO 并使用 MyBatis 进行数据库操作
DAO(Data Access Object)层负责与数据库直接交互。我们将在 DAO 层编写接口和实现,用于定义对产品数据的基本操作,包括:添加产品、根据 ID 删除产品、更新产品信息、根据 ID 查询产品、查询所有产品,以及分页查询等方法。
首先,我们定义产品的领域模型类 Product,以便映射数据库中的记录:
java
package cn.wolfcode.product.domain;
public class Product {
private Long id;
private String name;
private Double price;
private Integer quantity;
// 构造器、Getter 和 Setter 略
}
编写 DAO 接口 ProductDAO:
接下来,在 cn.wolfcode.product.dao 包中定义 DAO 接口。若使用 MyBatis,我们只需定义接口方法,由 MyBatis 框架根据映射文件自动提供实现;如果使用纯 JDBC,则需要手工编写实现类。这里我们以 MyBatis 为例:
java
package cn.wolfcode.product.dao;
import cn.wolfcode.product.domain.Product;
import java.util.List;
public interface ProductDAO {
void insert(Product product); // 插入新产品
void update(Product product); // 更新产品
void deleteById(Long id); // 按 ID 删除产品
Product selectById(Long id); // 按 ID 查询单个产品
List<Product> selectAll(); // 查询全部产品
// 分页查询:查询从 offset 开始的 pageSize 条产品记录
List<Product> selectByPage(int offset, int pageSize);
// 统计总记录数(用于分页计算)
int selectCount();
}
以上接口定义了 CRUD 以及分页所需的方法。接着我们为 MyBatis 编写对应的映射文件 ProductMapper.xml(通常与接口同名,不同扩展名,放在资源目录,对应 mapper 配置的路径)。该 XML 中为接口的方法编写具体 SQL:
java
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.wolfcode.product.dao.ProductDAO">
<!-- 插入产品 -->
<insert id="insert" parameterType="cn.wolfcode.product.domain.Product">
INSERT INTO product(name, price, quantity)
VALUES(#{name}, #{price}, #{quantity});
</insert>
<!-- 更新产品 -->
<update id="update" parameterType="cn.wolfcode.product.domain.Product">
UPDATE product
SET name=#{name}, price=#{price}, quantity=#{quantity}
WHERE id=#{id};
</update>
<!-- 删除产品 -->
<delete id="deleteById" parameterType="long">
DELETE FROM product WHERE id=#{id};
</delete>
<!-- 查询单个产品 -->
<select id="selectById" parameterType="long" resultType="cn.wolfcode.product.domain.Product">
SELECT * FROM product WHERE id=#{id};
</select>
<!-- 查询所有产品 -->
<select id="selectAll" resultType="cn.wolfcode.product.domain.Product">
SELECT * FROM product;
</select>
<!-- 分页查询产品 -->
<select id="selectByPage" resultType="cn.wolfcode.product.domain.Product">
SELECT * FROM product LIMIT #{offset}, #{pageSize};
</select>
<!-- 查询总记录数 -->
<select id="selectCount" resultType="int">
SELECT COUNT(*) FROM product;
</select>
</mapper>
上述 MyBatis 映射文件中,SQL 语句使用 #{} 占位获取参数,resultType 指定将结果映射为 Product 对象(要求表字段名和 Product 属性名一致或通过别名配置)。这样,MyBatis 会根据查询结果自动封装 Product 对象列表,极大减少我们手动处理 ResultSet 的代码量。
使用 MyBatis DAO:
配置好接口和 XML 后,我们可以通过 MyBatis 获取 DAO 接口的实现实例。在需要使用 DAO 的地方(例如 Service 层或测试代码中),通过 SqlSession 来获取:
java
SqlSession session = sqlSessionFactory.openSession();
ProductDAO productDAO = session.getMapper(ProductDAO.class);
// 然后就可以调用 productDAO.insert(product) 等方法,MyBatis 会自动执行映射的 SQL
MyBatis 会帮我们把 SQL 发送到数据库并返回结果,我们直接得到 Java 对象。这种基于接口的 Mapper 机制,使 DAO 层的实现无需手写(MyBatis 自动实现接口),专注于定义数据库操作规范。如果不使用 MyBatis,则需要我们手动编写实现类,在其中通过 JDBC 代码执行 SQL 并封装结果,在此不做展开.
小结:DAO 层的作用是数据存取,为上层提供简洁的方法。通过 MyBatis,我们仅需编写接口和 SQL 映射,不必关心底层 JDBC 操作细节。接下来看看 Service 层如何使用 DAO 层来处理业务逻辑.
- Service 层:ProductService 设计原则及其与 DAO 层的交互
Service 层建立在 DAO 层之上,主要负责业务逻辑处理和事务控制。Service 往往针对应用的某一领域提供操作接口,例如 ProductService 提供产品相关的增删改查业务。这样控制层(Servlet)调用 Service 方法即可完成相应功能,而无需关心细节。同时,Service 可以在调用 DAO 基础上增加逻辑,例如参数校验、业务规则、异常处理、事务管理等。
我们设计一个 ProductService 类(或接口+实现)包含产品相关操作。例如:
java
package cn.wolfcode.product.service;
import cn.wolfcode.product.dao.ProductDAO;
import cn.wolfcode.product.domain.Product;
import java.util.List;
public class ProductService {
// 通常通过依赖注入获取 DAO,这里直接示例创建
private ProductDAO productDAO;
public ProductService() {
// 获取 DAO 实例,假设通过 MyBatisUtil 获取 SqlSession 再拿 Mapper
this.productDAO = MyBatisUtil.getSession().getMapper(ProductDAO.class);
}
public void addProduct(Product product) {
// 业务规则校验(例如检查字段非空等)可以在这里做
productDAO.insert(product);
// 提交事务(如 MyBatis 需要手动提交则在这里 commit)
}
public void updateProduct(Product product) {
// 可以先检查 product 是否存在,再更新
productDAO.update(product);
// 提交事务
}
public void deleteProduct(Long id) {
productDAO.deleteById(id);
// 提交事务
}
public Product getProduct(Long id) {
return productDAO.selectById(id);
}
public List<Product> getAllProducts() {
return productDAO.selectAll();
}
// 分页查询业务方法
public List<Product> getProductsByPage(int currentPage, int pageSize) {
int offset = (currentPage - 1) * pageSize;
return productDAO.selectByPage(offset, pageSize);
}
public int getTotalCount() {
return productDAO.selectCount();
}
}
以上 ProductService 简要实现了各 CRUD 方法。实际应用中可能将 ProductService 定义为接口,由实现类去引用 DAO。但在入门阶段,上述方式足以说明问题。Service 的方法中可以增加更多业务处理,例如在删除产品前检查是否有关联数据,或在新增/更新时进行合法性验证等。
事务处理:如果一次业务操作需要多个 DAO 方法配合完成,则 Service 层应保证这些操作要么全部成功要么全部失败(回滚)。在不借助 Spring 事务的情况下,可以手动控制 MyBatis SqlSession 的提交和回滚。例如在 addProduct 等方法中,在 DAO 操作后调用 session.commit(),如果异常则 session.rollback()。为了简化,本示例暂未展示手动事务代码,但要意识到 Service 是处理事务的适当层次。
通过 Service 层,我们将控制器(Servlet)与底层数据访问解耦,控制器只需关注调用 Service 以及根据结果跳转视图。下面进入控制层的实现.
- Servlet 层:编写 ProductServlet 处理 CRUD 请求
Servlet 控制层负责接收 HTTP 请求、调用相应的 Service 方法处理业务,并将数据传递给视图(JSP)呈现或直接重定向。我们以 ProductServlet 为例,设计一个 Servlet 来统一处理与产品相关的各种操作请求(列表查询、新增、删除、编辑等)。
Servlet映射 :
首先,需要将 Servlet 注册到容器。我们有两种方式:在 web.xml 中配置,或使用注解。如采用 web.xml,可如下配置:
xml
<servlet>
<servlet-name>ProductServlet</servlet-name>
<servlet-class>cn.wolfcode.product.web.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ProductServlet</servlet-name>
<url-pattern>/product</url-pattern> <!-- 所有以 /product 的请求交给此 Servlet -->
</servlet-mapping>
这样,访问路径如 /product 就由 ProductServlet 处理。如果使用注解,则可以在 ProductServlet 类上添加例如 @WebServlet("/product") 达到同样效果。
编写 ProductServlet 类 :
该 Servlet 继承自 HttpServlet,我们主要实现其 doGet 和 doPost 方法来分别处理 GET 请求和 POST 请求。通常浏览器的表单提交(新增/修改)是 POST,请求数据(查询列表、删除动作触发的重定向)可以用 GET。我们可以在 doGet/doPost 内通过参数判别执行何种操作。举例如下:
java
package cn.wolfcode.product.web;
import cn.wolfcode.product.domain.Product;
import cn.wolfcode.product.service.ProductService;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.List;
public class ProductServlet extends HttpServlet {
private ProductService productService = new ProductService();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取请求参数,判断要执行的动作
String action = request.getParameter("action");
if (action == null) action = "list"; // 默认动作为列表
switch(action) {
case "list":
// 查询所有产品并跳转列表页面
List<Product> products = productService.getAllProducts();
request.setAttribute("products", products);
// 转发到 JSP 显示列表
request.getRequestDispatcher("/showAll.jsp").forward(request, response);
break;
case "edit":
// 编辑:按 ID 查询产品并跳转编辑页面
Long editId = Long.parseLong(request.getParameter("id"));
Product prod = productService.getProduct(editId);
request.setAttribute("product", prod);
request.getRequestDispatcher("/editProduct.jsp").forward(request, response);
break;
case "delete":
// 删除产品,操作完毕重定向回列表
Long delId = Long.parseLong(request.getParameter("id"));
productService.deleteProduct(delId);
response.sendRedirect(request.getContextPath() + "/product?action=list");
break;
default:
// 默认也当作 list 处理
response.sendRedirect(request.getContextPath() + "/product?action=list");
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 处理表单提交(新增或更新)
// 设置请求字符编码以处理中文参数
request.setCharacterEncoding("UTF-8");
String idStr = request.getParameter("id");
String name = request.getParameter("name");
String priceStr = request.getParameter("price");
String quantityStr = request.getParameter("quantity");
// 将参数转换为合适的数据类型
double price = Double.parseDouble(priceStr);
int quantity = Integer.parseInt(quantityStr);
Product product = new Product();
product.setName(name);
product.setPrice(price);
product.setQuantity(quantity);
if (idStr == null || idStr.isEmpty()) {
// id 为空,执行新增操作
productService.addProduct(product);
} else {
// id 不为空,执行更新操作
product.setId(Long.parseLong(idStr));
productService.updateProduct(product);
}
// 操作完成后重定向到列表页,避免表单重复提交
response.sendRedirect(request.getContextPath() + "/product?action=list");
}
}
代码说明:
● 在 doGet 中,我们通过查询参数 action 来确定请求的具体动作类型。如果没有提供该参数,默认视为请求产品列表。
○ action=list 时,通过 Service 查询所有产品列表,放入 request 属性,然后使用 RequestDispatcher 转发到 showAll.jsp 页面。这里用的是服务器端转发(forward),路径 /showAll.jsp 前面的斜杠表示相对于当前 Web 应用的根路径。转发可以携带 request 中的数据给 JSP。
○ action=edit 时,表示请求编辑某个产品。代码获取请求参数中的 id,通过 Service 查询对应 Product 对象,放入 request,再转发到 editProduct.jsp(该 JSP 会显示一个表单,表单字段填入 product 的当前值,供用户修改提交)。
○ action=delete 时,获取 id 执行删除操作。删除后使用 response.sendRedirect 重定向到列表页面。重定向会让浏览器发起新请求,这里通过 request.getContextPath() 获取应用上下文前缀,确保正确跳转到 /product?action=list 列表请求。重定向用于避免浏览器重复提交删除操作以及刷新页面时再次触发非幂等操作。
● 在 doPost 中,我们处理表单提交(可能是来自新增或编辑表单)。通过 request.getParameter 获取表单字段,例如产品名称、价格、数量等,将它们转换成适当的数据类型并封装到 Product 对象中。然后判断是否有 id:如果没有,则调用 addProduct 新增;如果有 id 则调用 updateProduct 更新。完成后,同样使用重定向跳转回列表页面。注意:在处理 POST 请求时,我们设置了 request.setCharacterEncoding("UTF-8"),以正确处理表单的中文输入避免乱码。
通过 ProductServlet,我们实现了对产品数据的增删改查控制流程:列表(list)和编辑页面跳转(edit)用 GET+forward 显示 JSP,新增/更新提交用 POST,然后 redirect 回列表,删除用 GET(或 POST 也可)然后 redirect。这样整个 CRUD 流程的前后端交互就完成了。接下来看看 JSP 页面如何获取 Servlet 传递的数据并显示给用户。
- 前端 JSP 页面:showAll.jsp 接收 Servlet 数据并显示
前端 JSP 作为视图层,负责将模型数据渲染为用户可见的网页。以展示产品列表的 showAll.jsp 为例,它需要从 ProductServlet 转发来的 request 中获取产品列表数据,并生成 HTML 表格予以显示。
假设在 Servlet 中,我们使用如下代码转发数据:
java
List<Product> products = productService.getAllProducts();
request.setAttribute("products", products);
request.getRequestDispatcher("/showAll.jsp").forward(request, response);
在 JSP 中,就可以通过表达式语言 EL 或 JSP 脚本来访问 request 范围内的 products 属性。我们将使用 JSTL 标签库来方便地遍历集合。在 JSP 文件顶部先引入 JSTL 核心库:
java
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
接下来编写页面主体,例如:
html
<html>
<head>
<meta charset="UTF-8">
<title>产品列表</title>
</head>
<body>
<h2>产品列表</h2>
<table border="1" cellpadding="5" cellspacing="0">
<tr>
<th>ID</th><th>名称</th><th>价格</th><th>数量</th><th>操作</th>
</tr>
<c:forEach var="p" items="${products}">
<tr>
<td>${p.id}</td>
<td>${p.name}</td>
<td>${p.price}</td>
<td>${p.quantity}</td>
<td>
<!-- 操作链接:编辑和删除 -->
<a href="${pageContext.request.contextPath}/product?action=edit&id=${p.id}">编辑</a>
<a href="${pageContext.request.contextPath}/product?action=delete&id=${p.id}"
onclick="return confirm('确定删除?');">删除</a>
</td>
</tr>
</c:forEach>
</table>
<!-- 链接到新增产品的表单页面 -->
<p><a href="${pageContext.request.contextPath}/addProduct.jsp">添加新产品</a></p>
</body>
</html>
代码说明:
● <c:forEach var="p" items="${products}">
用于遍历 products 列表,其中每个元素作为变量 p。items="${products}" 通过 EL 表达式获得 request 属性中的产品列表。我们依次输出每个产品的各字段。
● 在操作列,我们提供了"编辑"和"删除"两个操作链接。编辑链接指向 /product?action=edit&id=...,这样 ProductServlet 会接收到 action=edit 并据 ID 查询数据后跳转到编辑表单页面。删除链接指向 /product?action=delete&id=...,点击时会让 ProductServlet 执行删除操作。我们加上 onclick="return confirm('确定删除?');"
来在前端确认,以防误删。
● ${pageContext.request.contextPath}
用于获取当前应用的根路径,这样即使部署路径改变,链接仍然有效。比如本地测试通常 context path 是项目名。
● 最后提供一个链接跳转到添加新产品的页面(例如我们创建一个静态的 addProduct.jsp,里面包含一个 HTML 表单,表单的 action 提交到 /product 对应 Servlet,让其 doPost 执行新增)。
在 JSP 中,我们避免直接编写 Java 脚本,而采用 JSTL/EL,这是更佳的实践,可读性和可维护性更好。此页面渲染后,就会看到一个 HTML 表格列出所有产品及其信息,并在最后一列提供编辑、删除操作入口。
现在,我们已经可以显示所有产品并进行基本的增删改操作。接下来我们来实现分页功能,解决当产品很多时列表过长的问题.
- 分页实现:DAO、Service、Servlet、JSP 协同工作
当数据量较大时,分页显示能够提高用户体验和性能。分页的思想是在服务器端每次只查询一页的数据,并提供翻页导航,让用户按需查看。我们将在各层次加入分页支持:
(DAO 层分页查询) :
先前的 ProductDAO 我们已经定义了 selectByPage(int offset, int pageSize) 和 selectCount() 方法,对应的 SQL 也在 ProductMapper.xml 中编写了 LIMIT #{offset}, #{pageSize} 以及 COUNT(*)。这两个方法分别用于获取某一页的数据列表和获取总记录数。
(Service 层分页处理) :
在 ProductService 中,我们添加两个方法:一个用于按页获取数据列表(可直接调用 productDAO.selectByPage(offset, pageSize)),另一个用于获取总记录数(调用 selectCount())。然后 Service 层可以根据总数计算分页信息,如总页数。比如:
java
/**
* 分页查询产品列表的方法
*
* @param currentPage 当前请求的页码(页码从 1 开始)
* @param pageSize 每页显示的记录数量
* @return 返回一个封装了分页数据的 PageResult 对象,
* 包含当前页的产品列表、总记录数、每页显示记录数以及当前页码
*/
public PageResult listByPage(int currentPage, int pageSize) {
// 调用 DAO 层的方法获取所有产品的总记录数
int totalCount = productDAO.selectCount();
// 如果数据库中没有任何产品记录,则返回一个空的分页结果对象
if (totalCount == 0) {
return new PageResult(Collections.emptyList(), 0, pageSize, currentPage);
}
// 计算总页数:将总记录数除以每页显示的记录数并向上取整,确保所有记录都能显示
int totalPage = (int) Math.ceil(totalCount / (double) pageSize);
// 如果请求的当前页码大于总页数,则将当前页码调整为最后一页,防止超出范围
if (currentPage > totalPage) {
currentPage = totalPage;
}
// 如果请求的当前页码小于 1,则将当前页码调整为 1,保证页码的下限为 1
if (currentPage < 1) {
currentPage = 1;
}
// 计算当前页数据在数据库中的起始索引
int offset = (currentPage - 1) * pageSize;
// 根据起始索引和每页记录数调用 DAO 层分页查询方法,获取当前页产品数据列表
List<Product> data = productDAO.selectByPage(offset, pageSize);
// 构造并返回分页结果对象,其中包含:
// - 当前页的产品数据列表(data)
// - 数据库中的总记录数(totalCount)
// - 每页显示的记录数(pageSize)
// - 当前页码(currentPage)
return new PageResult(data, totalCount, pageSize, currentPage);
}
在上述代码中,我们创建了一个 PageResult 类用于封装分页结果(包含当前页数据列表、总记录数、每页大小、当前页码、总页数等信息)。Math.ceil(totalCount/(double)pageSize) 用于计算总页数。如果没有数据,总页数记为 0。然后根据当前页请求调整边界(不超过总页数且不小于 1)。最后根据当前页计算查询的起始索引 offset 并获取当前页的数据列表。PageResult 可以是如下的简单结构:
java
public class PageResult {
private List<Product> data; // 当前页数据列表
private int totalCount; // 总记录数
private int pageSize; // 每页显示条数
private int currentPage; // 当前页码
private int totalPage; // 总页数
// 构造方法、Getter 省略
}
这样 Service 返回一个 PageResult 对象给 Servlet 使用。当然,你也可以不封装对象,直接将需要的信息都算好,由 Servlet 来组织,但封装成对象让结构更清晰.
(Servlet 层处理分页参数) :
Servlet 需要能够接收页面请求中的页码参数,并调用 Service 获取对应页的数据。常见做法是在请求 URL 上加查询参数,如 /product?action=list&page=2
表示请求第 2 页。我们修改 ProductServlet 对应列表的部分:
java
case "list":
// 获取分页参数
String pageStr = request.getParameter("page");
int currentPage = (pageStr != null) ? Integer.parseInt(pageStr) : 1;
int pageSize = 10; // 每页大小可以固定或通过参数获取
PageResult result = productService.listByPage(currentPage, pageSize);
request.setAttribute("pageResult", result);
request.getRequestDispatcher("/showAll.jsp").forward(request, response);
break;
现在 Servlet 将查询到的 PageResult 放入 request,转发给 JSP.
(JSP 显示分页导航) :
我们需要在 showAll.jsp 中增加分页导航栏。前面我们已经输出了表格数据(改动后应从 pageResult 中取得列表,如 ${pageResult.data}
迭代,这里为了简单理解也可以直接把 pageResult.getData() 返回的 List 放到 request,或之前就把 products 属性改为当前页数据)。无论如何,我们假设 JSP 已拿到当前页的数据列表并显示,接下来获取分页信息以显示导航:
在 JSP 中,我们可以通过 ${pageResult.currentPage}
、${pageResult.totalPage}
来获取当前页码和总页数,通过这些信息生成导航链接。例如在表格下方加入:
java
<c:if test="${pageResult.totalPage > 1}">
当前第 ${pageResult.currentPage} 页,共 ${pageResult.totalPage} 页
<!-- 首页和上一页 -->
<c:choose>
<c:when test="${pageResult.currentPage > 1}">
<a href="${pageContext.request.contextPath}/product?action=list&page=1">首页</a>
<a href="${pageContext.request.contextPath}/product?action=list&page=${pageResult.currentPage - 1}">上一页</a>
</c:when>
<c:otherwise>
首页
上一页
</c:otherwise>
</c:choose>
<!-- 中间页码链接,可根据需要生成具体页码范围 -->
<c:forEach begin="1" end="${pageResult.totalPage}" var="p">
<c:choose>
<c:when test="${p == pageResult.currentPage}">
[${p}]
</c:when>
<c:otherwise>
<a href="${pageContext.request.contextPath}/product?action=list&page=${p}">[${p}]</a>
</c:otherwise>
</c:choose>
</c:forEach>
<!-- 下一页和末页 -->
<c:choose>
<c:when test="${pageResult.currentPage < pageResult.totalPage}">
<a href="${pageContext.request.contextPath}/product?action=list&page=${pageResult.currentPage + 1}">下一页</a>
<a href="${pageContext.request.contextPath}/product?action=list&page=${pageResult.totalPage}">末页</a>
</c:when>
<c:otherwise>
下一页
末页
</c:otherwise>
</c:choose>
</c:if>
导航栏说明:
● 当总页数大于 1 时,显示分页导航。
● "首页"和"上一页":如果当前页是第一页,就不提供可点链接,否则提供链接指向第一页和前一页。
● 中间页码:简单起见,这里直接从 1 遍历到总页数,每个页码如果是当前页则用非链接的方括号显示,其他则作为可点击链接。实际应用中如果总页数很多,可以只显示附近页码或采用跳转输入。
● "下一页"和"末页":道理类似,如果当前页已是最后一页,则不提供链接,否则可以跳转到下一页或末页。
这样,用户在 showAll.jsp 页面底部就能看到类似"当前第 2 页,共 5 页 [1] [2] [3] [4] [5] 下一页 末页"的导航,当点击不同页码或上下页时,通过请求参数改变 page 来获取不同的数据列表,整个分页过程各层配合如下:
● 浏览器请求 /product?action=list&page=2
● ProductServlet 中 action=list 分支获取 page=2,调用 Service 查询该页数据及分页信息
● Servlet 将结果存入 request,转发 JSP
● JSP 遍历显示当前页列表,并根据分页信息显示导航链接
● 用户点击"下一页"等链接再次发请求,周而复始
通过分页,服务器每次只查询当前页的数据,从而提高效率和可扩展性.
- 日志 & 配置:使用 log4j.properties 进行日志管理
良好的日志可以帮助我们调试和监控应用。JavaWeb 项目通常使用 Log4j 或其他日志框架。这里介绍使用 Log4j 1.x 通过属性文件进行配置。
(1)添加 Log4j 配置文件:
在类路径下(WEB-INF/classes)创建 log4j.properties,内容例如:
xml
# 全局日志配置
log4j.rootLogger = INFO, stdout, file
# 控制台输出
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} %-5p [%c] - %m%n
# 文件输出(可选)
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File = logs/app.log
log4j.appender.file.MaxFileSize = 1MB
log4j.appender.file.MaxBackupIndex = 5
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} %-5p [%c] - %m%n
上述配置将日志级别设为 INFO,并指定输出到控制台和文件(文件日志滚动保存,最大 1MB,备份 5 个)。你可以根据需要调整日志级别(DEBUG/INFO/WARN/ERROR)和输出方式。
将 log4j.properties 放在类路径下后,Log4j 会默认自动读取该配置。如果没有自动初始化,也可以在应用启动时手工指定配置文件路径进行初始化,例如使用 PropertyConfigurator.configure("WEB-INF/classes/log4j.properties")
。
(2)在代码中使用日志:
在需要的类中引入 Log4j 日志记录器,例如在 ProductServlet 中:
java
import org.apache.log4j.Logger;
public class ProductServlet extends HttpServlet {
private static final Logger logger = Logger.getLogger(ProductServlet.class);
...
protected void doGet(...) {
logger.info("Received request: action=" + request.getParameter("action"));
...
try {
// 业务处理
} catch(Exception e) {
logger.error("Error processing request", e);
throw e;
}
}
}
通过 Logger.getLogger(Class)
获取日志记录器,一般定义为静态。然后可以使用 logger.info, logger.debug, logger.error 等方法打印不同级别的日志信息。日志会按照我们在配置文件中设定的格式输出到控制台和文件。例如,info 级别日志输出格式类似:
2025-03-06 01:10:17 INFO [cn.wolfcode.product.web.ProductServlet] - Received request: action=list
通过日志,我们可以跟踪应用的运行流程(如每次进入 Servlet、SQL 执行情况、异常栈信息等)。在实际开发中,可以在 DAO 层打印 SQL 或参数,在 Service 层打印关键业务流程,在异常处理处打印错误信息等,以便快速定位问题.
(3)其他配置:
除了日志,项目中可能还有其他配置文件,例如上面的数据库配置 db.properties,MyBatis 的 mybatis-config.xml 等。建议都放在类路径下,便于管理和读取。通过良好的配置文件管理,可以做到修改配置无需改动代码.
现在,日志配置就绪,项目的开发基本完成。最后,我们来看如何在 Tomcat 中部署运行这个应用.
- Tomcat 部署:配置 Tomcat 并运行 JavaWeb 项目
完成编码后,需要将应用部署到 Servlet 容器才能运行。Apache Tomcat 是常用的轻量级 Servlet 容器。以下是部署步骤和注意事项:
(1)准备 WAR 包或直接部署:
如果使用 IDE(如 Eclipse),你可以将项目配置使用本地 Tomcat 运行;如果使用 Maven 构建,也可以运行 mvn package 打包成 WAR 文件。在没有 IDE 的情况下,可以手动将项目打包:
● 确保项目目录下有正确的 WEB-INF/web.xml 配置了 Servlet。我们的 web.xml 至少包含 Servlet 映射(如果用了注解也最好保留 web.xml 基本结构):
xml
<web-app>
...
<welcome-file-list>
<welcome-file>index.jsp</welcome-file> <!-- 欢迎页配置,如有 -->
</welcome-file-list>
</web-app>
(如果未使用任何欢迎页,可省略上述配置。)
● 将编译输出的类文件、所有 JSP、WEB-INF、以及 lib 下依赖 JAR,一并打包成 WAR。WAR 文件实际上就是一个 zip 压缩包,包含上述文件结构.
(2)部署到 Tomcat:
有以下几种方式:
● 将 WAR 文件复制到 Tomcat 安装目录下的 webapps/ 目录。Tomcat 启动时会自动解压部署该 WAR。同样地,如果你的项目文件夹(包含 WEB-INF 等)直接放在 webapps 下,也会被当作应用部署。
● 使用 Tomcat 管理界面上传部署 WAR(需要开启 Tomcat 管理应用并配置用户)。
● 在 IDE 中配置 Tomcat Server,将项目加入并直接点运行,IDE 会帮你部署.
(3)配置数据库:
确保 MySQL 数据库已启动,并且 db.properties 中的连接信息正确无误,数据库中已经创建了需要的表(如 product 表)和必要的初始数据。Tomcat 本身不需要特别配置数据库,只要应用能够连接即可。如果数据库驱动 jar 没有放在 WEB-INF/lib 下,则需要放入 Tomcat 的 lib 中,但一般我们会把 mysql-connector-java.jar 放在应用自身的 lib 中以避免不同应用的冲突.
(4)启动 Tomcat:
运行 Tomcat(如果在 IDE 中则启动 Server,如果独立 Tomcat 则执行 startup.sh / startup.bat)。观察 Tomcat 控制台输出,确认我们的应用没有报错地部署成功(可留意有无类加载或 SQL 连接错误日志)。
(5)访问应用:
假设应用上下文路径 (context path) 为 product(如果将 WAR 命名为 product.war 则默认 context path 是 product;在 IDE 中可配置),Servlet 映射为 /product,则可以通过浏览器访问:
java
http://localhost:8080/product/product?action=list
这将触发 ProductServlet 列出产品列表,并由 showAll.jsp 显示出来。如果一切配置正确,你应该看到产品列表页面。如需测试新增、编辑等功能,点击"添加新产品"链接,提交表单后应重定向回列表并显示更新后的数据;删除操作会要求确认,确认后该条目应从列表消失.
(6)调试技巧:
如果页面没有正常显示或出现错误码(404 找不到页面,500 服务器错误等),可以查看 Tomcat 日志(比如控制台或 logs/catalina.out)了解错误原因。常见问题如:JSP 编译错误、Servlet 类未找到(包路径是否正确,web.xml 映射名称是否一致)、数据库连接失败(检查 db.properties 和驱动 Jar)等。在开发阶段可以打开日志的 DEBUG 级别获取更多信息.
部署到 Tomcat 后,我们的 JavaWeb CRUD 和分页系统就真正运行起来了。通过本地浏览器即可对产品信息进行添加、浏览、修改、删除,并测试分页功能.
通过以上步骤,我们完成了一个分层清晰的 JavaWeb 项目。从基础概念到具体实现,每一层都有各自职责:前端 JSP 负责展示,Servlet 控制页面跳转和请求分发,Service 封装业务逻辑,DAO 专注数据持久化,MyBatis/JDBC 负责与数据库通讯。日志和配置则贯穿其中提供支持。MVC 架构的应用使得代码结构清晰、松耦合易维护。希望这个详细教程能帮助你理解 JavaWeb 开发的整体流程,并为进一步学习更复杂的框架(如 Spring MVC、Spring Boot 等)打下基础。