JavaWeb快速入门:基于MyBatis综合案例环境搭建与查询所有功能实现

本文纲要

  1. 环境搭建和项目结构设计
  2. 查询所有品牌
    后台实现
    前端实现
  3. 新增品牌
    后台实现
    前端实现
  4. Servlet 代码优化
    问题分析
    BaseServlet 实现
    改造 BrandServlet
  5. 批量删除
    后台实现
    前端实现
  6. 分页查询
    核心概念分析
    后台实现
    前端实现
  7. 条件查询
    业务分析
    后台实现(动态 SQL + 分页)
    前端实现
  8. 前端代码优化(箭头函数)
  9. 项目结构总览
  10. 总结

环境搭建和项目结构设计

在正式开始开发之前,需要准备好基础工程和数据库环境。

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 等工具中执行即可。

执行后表中包含以下字段:

字段名类型说明
idint主键(自增)
brand_namevarchar品牌名称
company_namevarchar企业名称
orderedint排序字段
descriptionvarchar描述信息
statusint状态 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 

查询所有品牌

查询所有功能的后台流程:BrandMapperBrandServiceBrandServlet,前端页面加载完成后发送异步请求,获取 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 条品牌数据。

新增品牌

新增功能的前后端交互流程如下:

数据库BrandMapperBrandServiceAddServlet/BrandServlet浏览器(brand.html)数据库BrandMapperBrandServiceAddServlet/BrandServlet浏览器(brand.html)POST 提交品牌 JSON 数据读取请求体,JSON → Brand 对象add(brand)add(brand)INSERT INTO tb_brand ...成功voidvoidresponse "success"关闭对话框、重新查询数据、提示成功

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-modelbrand 模型进行双向绑定,点击“提交”按钮时调用 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,例如 SelectAllServletAddServlet…… 这样会导致 Web 层 Servlet 数量爆炸,不易维护。

参考 MyBatis 中将所有操作方法集中在一个 Mapper 接口的做法,我们也希望将同一实体(如 Brand)的所有操作集中在一个 Servlet 中,通过请求路径中的最后一段来区分具体方法。例如:

  • /brand/selectAll → 执行 selectAll() 方法
  • /brand/add → 执行 add() 方法

为此,我们需要自定义一个 BaseServlet,替换 HttpServlet 中根据请求方式(GET/POST)分发的方法,改为根据请求路径最后一段进行方法分发。

2 ) BaseServlet 实现

BaseServlet 继承 HttpServlet,重写 service 方法,核心逻辑:

  1. 获取请求 URI,如 /brand-case/brand/selectAll
  2. 截取最后一个 / 之后的部分作为方法名(selectAll)
  3. 通过反射获取当前子类(如 BrandServlet)的 Method 对象
  4. 调用方法,传入 requestresponse
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();
        }
    }
}

注意:thisBaseServlet 中代表实际调用的子类实例(如 BrandServlet),因为 service 方法是被子类调用的。

3 ) 改造 BrandServlet

BrandServlet 继承 BaseServlet,并配置 @WebServlet("/brand/*"),这样所有以 /brand/ 开头的请求都会映射到该 Servlet。

将之前 SelectAllServletAddServlet 中的方法迁移过来:

@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 ...
}

前后端交互流程:

数据库BrandMapperBrandServiceBrandServlet前端数据库BrandMapperBrandServiceBrandServlet前端GET/POST currentPage&pageSizeselectByPage(currentPage, pageSize)计算起始索引 begin = (page-1)*sizeselectByPage(begin, size)SELECT * FROM tb_brand LIMIT begin,size结果集selectTotalCount()SELECT count(*) FROM tb_brand总数PageBean(rows, totalCount)JSON 数据更新表格、分页条

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> 分页组件,我们将其绑定到 currentPagepageSizetotalCount 属性,并通过 @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 参数(currentPagepageSize),又有请求体 JSON 数据(封装为 Brand 对象),因此 BrandServlet 中需要分别获取。

流程示意图:

DBMapperServiceServletBrowserDBMapperServiceServletBrowserPOST /brand/selectByPageAndCondition?currentPage=1&pageSize=5Body:{"brandName":"华为","status":1}获取 currentPage/pageSize (GET参数)读取请求体转 Brand 对象selectByPageAndCondition(currentPage,pageSize,brand)处理模糊查询(添加 %)selectByPageAndCondition(begin,size,brand)动态SQL + LIMITselectTotalCountByCondition(brand)带条件的 count(*)PageBeanJSON

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(whereifforeach)处理复杂条件与批量操作。
  • Servlet:通过自定义 BaseServlet 实现了基于路径的方法分发,避免了 Servlet 数量膨胀。
  • JSON 交互:使用 Fastjson 在前后端之间传递对象数据。
  • 分页查询:利用 LIMITPageBean 封装,配合前端分页组件,实现物理分页。
  • 条件查询:动态 SQL 实现多条件模糊查询,并整合分页功能。
  • 前端(Vue + Element UI):数据双向绑定、表格、对话框、分页条的使用,以及箭头函数优化。
  • 三层架构:Mapper(持久层) → Service(业务层) → Servlet(表现层),职责清晰,易于扩展。

掌握了这些基础之后,未来切换到 Spring + SpringMVC + MyBatis 等框架时,原理相通,上手会非常容易。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值