本文纲要
- 环境搭建和项目结构设计
- 查询所有品牌
后台实现
前端实现 - 新增品牌
后台实现
前端实现 Servlet代码优化
问题分析
BaseServlet实现
改造BrandServlet- 批量删除
后台实现
前端实现 - 分页查询
核心概念分析
后台实现
前端实现 - 条件查询
业务分析
后台实现(动态 SQL + 分页)
前端实现 - 前端代码优化(箭头函数)
- 项目结构总览
- 总结
环境搭建和项目结构设计
在正式开始开发之前,需要准备好基础工程和数据库环境。
1 ) 导入初始工程
素材中提供了一个名为 brand-case 的 Maven 工程,该工程已经配置好了 MyBatis、Servlet、Fastjson 等依赖,并提供了基础包结构和前端页面。主要依赖如下:
- MyBatis 3.5.5:持久层框架
- MySQL 驱动:数据库连接
- Fastjson 1.2.62:JSON 序列化工具
- Servlet 3.1.0:Web 层基础
- Log4j:日志输出
2 ) 执行数据库脚本
在资料中有一个 tb_brand 的 SQL 脚本,它会先删除已存在的同名表,再创建新表并插入 48 条测试数据,为后续分页功能提供数据基础。复制脚本到 Navicat 等工具中执行即可。
执行后表中包含以下字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键(自增) |
| brand_name | varchar | 品牌名称 |
| company_name | varchar | 企业名称 |
| ordered | int | 排序字段 |
| description | varchar | 描述信息 |
| status | int | 状态 0禁用 1启用 |
数据库准备好后,环境搭建完成。
3 )项目结构总览
brand-case
├── pom.xml
├── src
│ ├── main
│ │ ├── java/com/wb
│ │ │ ├── mapper
│ │ │ │ └── BrandMapper.java # MyBatis Mapper 接口
│ │ │ ├── pojo
│ │ │ │ ├── Brand.java # 品牌实体类
│ │ │ │ └── PageBean.java # 分页包装类
│ │ │ ├── service
│ │ │ │ ├── BrandService.java # 业务接口
│ │ │ │ └── impl
│ │ │ │ └── BrandServiceImpl.java # 业务实现类
│ │ │ ├── util
│ │ │ │ └── SqlSessionFactoryUtils.java # MyBatis 工具类
│ │ │ └── web/servlet
│ │ │ ├── BaseServlet.java # 自定义分发基类
│ │ │ ├── BrandServlet.java # 品牌核心控制器
│ │ │ ├── UserServlet.java # 示例(其他实体)
│ │ │ └── old
│ │ │ ├── AddServlet.java # 优化前的新增 Servlet
│ │ │ └── SelectAllServlet.java # 优化前的查询 Servlet
│ │ ├── resources
│ │ │ ├── mybatis-config.xml # MyBatis 核心配置
│ │ │ ├── log4j.properties
│ │ │ └── com/wb/mapper
│ │ │ └── BrandMapper.xml # 动态 SQL 映射文件
│ │ └── webapp
│ │ ├── brand.html # 品牌管理前端页面
│ │ ├── js/
│ │ └── WEB-INF/web.xml
查询所有品牌
查询所有功能的后台流程:BrandMapper → BrandService → BrandServlet,前端页面加载完成后发送异步请求,获取 JSON 数据并绑定到表格模型上。
1 ) 后台代码
BrandMapper 接口
public interface BrandMapper {
@Select("select * from tb_brand")
@ResultMap("brandResultMap")
List<Brand> selectAll();
}
在 MyBatis 核心配置文件中已经配置了 resultMap,用于映射下划线字段与驼峰属性:
<resultMap id="brandResultMap" type="brand">
<result property="brandName" column="brand_name" />
<result property="companyName" column="company_name" />
</resultMap>
BrandService 接口与实现
public interface BrandService {
List<Brand> selectAll();
}
public class BrandServiceImpl implements BrandService {
SqlSessionFactory factory = SqlSessionFactoryUtils.getSqlSessionFactory();
@Override
public List<Brand> selectAll() {
SqlSession sqlSession = factory.openSession();
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
List<Brand> brands = mapper.selectAll();
sqlSession.close();
return brands;
}
}
原始 SelectAllServlet(优化前)
@WebServlet("/selectAllServlet")
public class SelectAllServlet extends HttpServlet {
private BrandService brandService = new BrandServiceImpl();
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
List<Brand> brands = brandService.selectAll();
String jsonString = JSON.toJSONString(brands);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonString);
}
}
启动 Tomcat 后,直接访问 /selectAllServlet 若返回 JSON 数组,则后台代码正确。
2 ) 前端代码
前端页面使用 Vue.js 配合 Element UI,在 mounted 生命周期中发送异步请求,并将数据设置到 tableData 模型上。
<script>
new Vue({
el: "#app",
mounted() {
this.selectAll();
},
methods: {
selectAll() {
var _this = this;
axios({
method: "get",
url: "http://localhost:8080/brand-case/selectAllServlet"
}).then(function (resp) {
_this.tableData = resp.data;
});
}
},
data() {
return {
tableData: []
}
}
});
</script>
注意:axios 回调中无法直接使用 this,需要用 _this 保存 Vue 实例引用。
至此,查询所有功能完成,页面可以展示全部 48 条品牌数据。
新增品牌
新增功能的前后端交互流程如下:
1 ) 后台代码
BrandMapper 添加方法
@Insert("insert into tb_brand values(null,#{brandName},#{companyName},#{ordered},#{description},#{status})")
void add(Brand brand);
BrandService 接口与实现
void add(Brand brand);
@Override
public void add(Brand brand) {
SqlSession sqlSession = factory.openSession();
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
mapper.add(brand);
sqlSession.commit(); // 增删改必须提交事务
sqlSession.close();
}
AddServlet(优化前)
@WebServlet("/addServlet")
public class AddServlet extends HttpServlet {
private BrandService brandService = new BrandServiceImpl();
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
BufferedReader br = request.getReader();
String params = br.readLine(); // JSON 字符串
Brand brand = JSON.parseObject(params, Brand.class);
brandService.add(brand);
response.getWriter().write("success");
}
}
2 ) 前端代码
表单数据已通过 v-model 与 brand 模型进行双向绑定,点击“提交”按钮时调用 addOrUpdateBrand 方法(本文只演示新增,修改留给读者练习):
addOrUpdateBrand() {
if (!this.brand.id) { // 新增判断
var _this = this;
axios({
method: "post",
url: "http://localhost:8080/brand-case/brand/add",
data: _this.brand
}).then(function (resp) {
if (resp.data == "success") {
_this.dialogVisible = false; // 关闭弹窗
_this.selectAll(); // 重新查询
_this.$message({
message: '恭喜你,添加成功',
type: 'success'
});
}
});
}
}
关键点:提交成功后必须关闭对话框、刷新列表、给出成功提示。
Servlet 代码优化
1 ) 问题分析
原始写法中,每个业务功能(查询所有、新增、删除等)都需要单独编写一个 Servlet,例如 SelectAllServlet、AddServlet…… 这样会导致 Web 层 Servlet 数量爆炸,不易维护。
参考 MyBatis 中将所有操作方法集中在一个 Mapper 接口的做法,我们也希望将同一实体(如 Brand)的所有操作集中在一个 Servlet 中,通过请求路径中的最后一段来区分具体方法。例如:
/brand/selectAll→ 执行selectAll()方法/brand/add→ 执行add()方法
为此,我们需要自定义一个 BaseServlet,替换 HttpServlet 中根据请求方式(GET/POST)分发的方法,改为根据请求路径最后一段进行方法分发。
2 ) BaseServlet 实现
BaseServlet 继承 HttpServlet,重写 service 方法,核心逻辑:
- 获取请求 URI,如
/brand-case/brand/selectAll - 截取最后一个
/之后的部分作为方法名(selectAll) - 通过反射获取当前子类(如
BrandServlet)的Method对象 - 调用方法,传入
request和response
public class BaseServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取请求路径
String uri = req.getRequestURI(); // /brand-case/brand/selectAll
// 2. 获取方法名
int index = uri.lastIndexOf('/');
String methodName = uri.substring(index + 1); // selectAll
// 3. 获取子类字节码并调用方法
Class<? extends BaseServlet> cls = this.getClass();
try {
Method method = cls.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
method.invoke(this, req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:this 在 BaseServlet 中代表实际调用的子类实例(如 BrandServlet),因为 service 方法是被子类调用的。
3 ) 改造 BrandServlet
让 BrandServlet 继承 BaseServlet,并配置 @WebServlet("/brand/*"),这样所有以 /brand/ 开头的请求都会映射到该 Servlet。
将之前 SelectAllServlet 和 AddServlet 中的方法迁移过来:
@WebServlet("/brand/*")
public class BrandServlet extends BaseServlet {
private BrandService brandService = new BrandServiceImpl();
public void selectAll(HttpServletRequest request, HttpServletResponse response) throws IOException {
List<Brand> brands = brandService.selectAll();
String jsonString = JSON.toJSONString(brands);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonString);
}
public void add(HttpServletRequest request, HttpServletResponse response) throws IOException {
BufferedReader br = request.getReader();
Brand brand = JSON.parseObject(br.readLine(), Brand.class);
brandService.add(brand);
response.getWriter().write("success");
}
}
前端对应的请求路径也需修改:
- 查询所有:/brand/selectAll
- 新增:/brand/add
这样我们就完成了 Servlet 层的优化,同一实体的操作集中管理,后续添加新方法只需在 BrandServlet 中增加对应方法即可。
批量删除
1 ) 业务分析
用户勾选多条记录后点击“批量删除”,需要将选中项的 ID 数组发送到后端,后端根据 ID 列表执行批量删除。SQL 使用 IN 配合动态 foreach 生成占位符。
2 ) 后台代码
BrandMapper 接口与 XML
void deleteByIds(@Param("ids") int[] ids);
<delete id="deleteByIds">
delete from tb_brand where
<foreach collection="ids" item="id" separator="," open="id in(" close=")">
#{id}
</foreach>
</delete>
BrandService 实现
@Override
public void deleteByIds(int[] ids) {
SqlSession sqlSession = factory.openSession();
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
mapper.deleteByIds(ids);
sqlSession.commit();
sqlSession.close();
}
BrandServlet 方法
public void deleteByIds(HttpServletRequest request, HttpServletResponse response) throws IOException {
BufferedReader br = request.getReader();
String params = br.readLine();
int[] ids = JSON.parseObject(params, int[].class);
if (ids.length != 0) {
brandService.deleteByIds(ids);
}
response.getWriter().write("success");
}
3 ) 前端代码
Element UI 表格的 @selection-change 事件会将选中数据集合存入 multipleSelection。需要从中提取 ID 数组,并在确认后发送请求。
deleteByIds() {
// 从 multipleSelection 中提取 ID 数组
for (let i = 0; i < this.multipleSelection.length; i++) {
this.selectedIds[i] = this.multipleSelection[i].id;
}
if (this.selectedIds.length == 0) {
this.$message.error('最少勾选一个');
return;
}
// 确认提示框
this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
var _this = this;
axios({
method: "post",
url: "http://localhost:8080/brand-case/brand/deleteByIds",
data: _this.selectedIds
}).then(function (resp) {
if (resp.data == "success") {
_this.selectedIds = [];
_this.selectAll();
_this.$message({ message: '恭喜你,删除成功', type: 'success' });
}
});
}).catch(() => {
this.$message({ type: 'info', message: '已取消删除' });
});
}
分页查询
1 )核心概念分析
分页查询需后端返回两部分数据:当前页数据集合 和 总记录数。
前端传递两个参数:当前页码 (currentPage) 和 每页显示条数 (·pageSize·)。
数据库使用 LIMIT 实现物理分页:
SELECT * FROM tb_brand LIMIT 起始索引, 查询条目数;
计算公式:
- 起始索引 = (当前页码 - 1) × 每页条数
- 查询条目数 = 每页显示条数
例如:第 2 页,每页 5 条,起始索引 = (2-1)×5 = 5,查询 5 条,SQL 为 LIMIT 5,5。
为统一封装,我们创建 PageBean<T> 对象:
public class PageBean<T> {
private int totalCount; // 总记录数
private List<T> rows; // 当前页数据
// getter & setter ...
}
前后端交互流程:
2 ) 后台代码
BrandMapper 分页方法
@Select("select * from tb_brand limit #{begin}, #{size}")
@ResultMap("brandResultMap")
List<Brand> selectByPage(@Param("begin") int begin, @Param("size") int size);
@Select("select count(*) from tb_brand")
int selectTotalCount();
BrandService 实现
@Override
public PageBean<Brand> selectByPage(int currentPage, int pageSize) {
SqlSession sqlSession = factory.openSession();
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
int begin = (currentPage - 1) * pageSize;
List<Brand> rows = mapper.selectByPage(begin, pageSize);
int totalCount = mapper.selectTotalCount();
sqlSession.close();
PageBean<Brand> pageBean = new PageBean<>();
pageBean.setRows(rows);
pageBean.setTotalCount(totalCount);
return pageBean;
}
BrandServlet 分页查询方法
public void selectByPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
String _currentPage = request.getParameter("currentPage");
String _pageSize = request.getParameter("pageSize");
int currentPage = Integer.parseInt(_currentPage);
int pageSize = Integer.parseInt(_pageSize);
PageBean<Brand> pageBean = brandService.selectByPage(currentPage, pageSize);
String jsonString = JSON.toJSONString(pageBean);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonString);
}
3 ) 前端代码
Element UI 提供了 <el-pagination> 分页组件,我们将其绑定到 currentPage、pageSize、totalCount 属性,并通过 @current-change 和 @size-change 事件重新调用 selectAll 查询。
methods: {
selectAll() {
var _this = this;
axios({
method: "post",
url: "http://localhost:8080/brand-case/brand/selectByPageAndCondition?currentPage=" + this.currentPage + "&pageSize=" + this.pageSize,
data: this.brandSearch
}).then(function (resp) {
_this.tableData = resp.data.rows; // 当前页数据
_this.totalCount = resp.data.totalCount; // 总记录数
});
},
handleSizeChange(val) {
this.pageSize = val;
this.selectAll();
},
handleCurrentChange(val) {
this.currentPage = val;
this.selectAll();
}
},
data() {
return {
currentPage: 1,
pageSize: 5,
totalCount: 100, // 初始值,实际会被覆盖
tableData: []
}
}
页面首次加载、切换页码或每页条数时均会重新请求数据,实现了完整的分页功能。
条件查询
1 ) 业务分析
条件查询需要根据品牌名称、企业名称、状态三个条件进行模糊匹配,并且这些条件不是必填的(即可按任意组合查询)。查询结果同样需要分页。
因此,后台需使用 MyBatis 动态 SQL 构建查询语句,结合 where 和 if 标签,并在查询后加上 limit 进行分页。
条件查询请求中既有 URL 参数(currentPage、pageSize),又有请求体 JSON 数据(封装为 Brand 对象),因此 BrandServlet 中需要分别获取。
流程示意图:
2 ) 后台代码
BrandMapper 接口
List<Brand> selectByPageAndCondition(@Param("begin") int begin,
@Param("size") int size,
@Param("brand") Brand brand);
int selectTotalCountByCondition(Brand brand);
BrandMapper.xml 动态 SQL
<select id="selectByPageAndCondition" resultMap="brandResultMap">
select * from tb_brand
<where>
<if test="brand.brandName != null and brand.brandName != '' ">
and brand_name like #{brand.brandName}
</if>
<if test="brand.companyName != null and brand.companyName != '' ">
and company_name like #{brand.companyName}
</if>
<if test="brand.status != null">
and status = #{brand.status}
</if>
</where>
limit #{begin}, #{size}
</select>
<select id="selectTotalCountByCondition" resultType="int">
select count(*) from tb_brand
<where>
<if test="brandName != null and brandName != '' ">
and brand_name like #{brandName}
</if>
<if test="companyName != null and companyName != '' ">
and company_name like #{companyName}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
注意:selectByPageAndCondition 中因为使用了 @Param("brand"),所以 OGNL 表达式需写成 brand.brandName;而 selectTotalCountByCondition 直接传入 Brand 对象,未加 @Param,因此直接用属性名即可。
BrandService 实现
@Override
public PageBean<Brand> selectByPageAndCondition(int currentPage, int pageSize, Brand brand) {
SqlSession sqlSession = factory.openSession();
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
int begin = (currentPage - 1) * pageSize;
// 处理模糊查询:前后拼接 %
if (brand.getBrandName() != null && !brand.getBrandName().isEmpty()) {
brand.setBrandName("%" + brand.getBrandName() + "%");
}
if (brand.getCompanyName() != null && !brand.getCompanyName().isEmpty()) {
brand.setCompanyName("%" + brand.getCompanyName() + "%");
}
// 防止 brand 为 null 引发空指针
if (brand == null) {
brand = new Brand();
}
List<Brand> rows = mapper.selectByPageAndCondition(begin, pageSize, brand);
int totalCount = mapper.selectTotalCountByCondition(brand);
sqlSession.close();
PageBean<Brand> pageBean = new PageBean<>();
pageBean.setRows(rows);
pageBean.setTotalCount(totalCount);
return pageBean;
}
BrandServlet 条件分页查询方法
public void selectByPageAndCondition(HttpServletRequest request, HttpServletResponse response) throws IOException {
String _currentPage = request.getParameter("currentPage");
String _pageSize = request.getParameter("pageSize");
int currentPage = Integer.parseInt(_currentPage);
int pageSize = Integer.parseInt(_pageSize);
// 读取请求体中的条件 JSON
BufferedReader br = request.getReader();
Brand brand = JSON.parseObject(br.readLine(), Brand.class);
PageBean<Brand> pageBean = brandService.selectByPageAndCondition(currentPage, pageSize, brand);
String jsonString = JSON.toJSONString(pageBean);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonString);
}
3 ) 前端代码
查询表单通过 v-model="brandSearch" 绑定了一个条件对象,点击查询按钮时调用 onSubmit 方法,该方法会触发 selectAll(此时已改为条件分页查询),并将 brandSearch 作为请求体发送。
data() {
return {
brandSearch: {
status: '',
brandName: '',
companyName: ''
}
}
},
methods: {
onSubmit() {
this.selectAll(); // 会携带 brandSearch 条件
},
selectAll() {
var _this = this;
axios({
method: "post",
url: "http://localhost:8080/brand-case/brand/selectByPageAndCondition?currentPage=" + this.currentPage + "&pageSize=" + this.pageSize,
data: this.brandSearch
}).then(function (resp) {
_this.tableData = resp.data.rows;
_this.totalCount = resp.data.totalCount;
});
}
}
表格中当前状态显示汉字使用了 Brand 实体中的逻辑视图 getStatusStr(),前端直接绑定 statusStr 属性即可。
前端代码优化
在 axios 的回调函数中,如果使用 function 定义回调,内部的 this 会丢失 Vue 实例,需要提前用 _this 保存。ES6 提供了箭头函数,它可以自动绑定上下文的 this,使代码更简洁。
优化前:
axios({ ... }).then(function (resp) {
_this.tableData = resp.data.rows;
});
优化后:
axios({ ... }).then(resp => {
this.tableData = resp.data.rows;
});
项目中所有 axios 调用均可替换为箭头函数,不再需要 _this 中转。
总结
本篇通过一个完整的品牌管理案例,串联了 JavaWeb 阶段的核心技术:
- MyBatis:接口 + XML 实现增删改查,动态 SQL(
where、if、foreach)处理复杂条件与批量操作。 - Servlet:通过自定义
BaseServlet实现了基于路径的方法分发,避免了 Servlet 数量膨胀。 - JSON 交互:使用 Fastjson 在前后端之间传递对象数据。
- 分页查询:利用
LIMIT和PageBean封装,配合前端分页组件,实现物理分页。 - 条件查询:动态 SQL 实现多条件模糊查询,并整合分页功能。
- 前端(Vue + Element UI):数据双向绑定、表格、对话框、分页条的使用,以及箭头函数优化。
- 三层架构:Mapper(持久层) → Service(业务层) → Servlet(表现层),职责清晰,易于扩展。
掌握了这些基础之后,未来切换到 Spring + SpringMVC + MyBatis 等框架时,原理相通,上手会非常容易。
6万+

被折叠的 条评论
为什么被折叠?



