Sky-takeout Day02
【前言】
Day02 的 Task 是实现最基础的 CRUD 功能,流程:先做需求分析,再开始编写代码,然后进行功能测试(接口文档、前后端联调)以及代码完善。
首先,通过员工管理模块的新增、分页查询、启用禁用账号、编辑这四大功能,来了解简单的 CRUD 如何写。
其次,因为菜品分类管理模块的逻辑与员工管理的几乎一样,所以就直接导入了,想巩固的话可以自己手敲一遍,加深印象。
Contents
- 新增员工
- 员工分页查询
- 启用禁用员工账号
- 编辑员工
- 导入菜品分类管理模块代码
1. 新增员工
Q1:用DTO还是Entity实体类?
一般来说
当前端提交的数据和实体类中对应的属性差别比较大时,建议使用 DTO 来封装数据
Entity 管数据库,DTO 管接口。通过分层隔离,Entity 聚焦业务模型和持久化,DTO 聚焦 API 契约和传输效率
维度 Entity(实体类) DTO(数据传输对象) 核心职责 映射数据库表结构,承载业务逻辑 封装接口参数 / 响应,实现跨层数据传输 所属层级 持久层 / 领域层(与数据库直接交互) 应用层 / 表现层(Controller、Service 间传输) 数据库关联 强关联(字段与表结构一一对应) 无关联(结构由 API 需求决定) 设计重点 数据完整性、业务规则(如 ORM 注解、非空约束) API 契约稳定性、传输性能(仅含必要字段) 
代码编写
EmployeeController.java
/**
* 保存员工信息
*
* @param employeeDTO 员工信息DTO
* @return 返回统一的Result封装对象
*/
@PostMapping
@ApiOperation(value = "保存(新增)员工信息")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
log.info("保存(新增)员工信息:{}", employeeDTO);
// 调用service层的方法来处理保存逻辑
employeeService.save(employeeDTO);
return Result.success();
}
EmployeeServiceImpl.java
/**
* 保存员工信息
*
* @param employeeDTO 员工信息
*/
@Override
public void save(EmployeeDTO employeeDTO) {
// 先将 DTO 转换为实体对象, 再进行保存, 用spring中的BeanUtils的copyProperties方法
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);
// 再将实体类的剩余属性设置好
employee.setStatus(StatusConstant.ENABLE); // 设置默认状态为启用
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes())); // 设置初始密码123456, 需要进行MD5加密
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
employee.setCreateUser(10L); // TODO: 这里先写死,后面改为当前登录用户的id
employee.setUpdateUser(10L); // TODO: 这里先写死,后面改为当前登录用户的id
// 最后调用Mapper层的方法进行保存
employeeMapper.insert(employee);
}
EmployeeMapper.java
/**
* 插入员工信息
* @param employee 员工对象
*/
@Insert("insert into employee (username, password, name, phone, sex, id_number, status, create_time, update_time, create_user, update_user)" +
"values" +
"(#{username}, #{password}, #{name}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
void insert(Employee employee);
功能测试
首先先设置一下全局token, 另外过期时间可以设置久一点(在application.yml中jwt部分设置), 避免总是手动设置.
测试成功,返回201
代码功能完善
当前存在问题:
- 用户名有unique约束, 若重复会抛异常, 异常还没处理
- 新增员工时的登录用户id还是定死的
解决用户名重复异常的问题

要将抛出的 SQLIntegrityConstraintViolationException 异常,加入全局异常处理器中, 返回异常消息给前端, 保证友好性.
在GlobalExceptionHandler中添加:
/**
* 捕获SQL异常
* @param ex 捕获到的异常对象
* @return 返回封装的异常信息
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
// Duplicate entry 'hwan1' for key 'employee.idx_username';
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXIST;
return Result.error(msg);
} else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
解决设置当前用户id的问题
JWT 工作流程图
通过Jwt令牌中携带的用户id来获取新增员工时需要设置的当前用户属性

Q2: 如何传递拦截器获取的用户id给service?
解析出登录员工 id 后,如何传递给 Service 的 save 方法?
=>使用ThreadLocal
介绍:
ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量。
ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。常用方法:
public void set(T value) 设置当前线程的线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
public void remove() 移除当前线程的线程局部变量
项目中在BaseContext工具类已经封装了对应方法
解决流程图
在拦截器Interceptor, 将用户id存入ThreadLocal中

在Service里, 从ThreadLocal中获取当前登录用户的id

2. 员工分页查询
接口分析
接口设计
注意事项:
- 请求参数类型为 Query,不是 json 格式提交,在路径后直接拼接。/admin/employee/page?name = zhangsan
- 返回数据中 records 数组中使用 Employee 实体类对属性进行封装。
代码编写
用到了分页插件PageHelper
PageHelper 基本原理
PageHelper 是基于 MyBatis 的分页插件,通过拦截 SQL 执行过程动态添加分页逻辑(如 MySQL 的
LIMIT),并自动统计总记录数。其核心优势是:
- 无需修改 SQL,只需在代码中添加分页参数;
- 支持多数据库(MySQL、Oracle 等);
- 通过
ThreadLocal存储分页参数,保证线程安全。
定义分页查询DTO和分页结果封装
EmployeeController.java
/**
* 员工分页查询
*
* @param employeePageQueryDTO 员工分页查询DTO
* @return 返回统一的Result封装对象
*/
@ApiOperation("员工分页查询")
@GetMapping("/page")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
log.info("员工分页查询:参数{}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
EmployeeServiceImpl.java
/**
* 员工分页查询
*
* @param employeePageQueryDTO 分页查询信息
* @return 分页结果
*/
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
EmployeeMapper.xml
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
</where>
order by update_time desc
</select>
解决分页查询时间格式问题
调试时发现时间格式有问题:
有两种解决方案
方式一:在属性加上注解,可行,但是需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。
方式二:在 WebMvcConfiguration 中扩展 SpringMVC 的消息转换器,统一对日期类型进行格式处理。
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
// 创建消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 设置对象转换器,底层使用Jackson将Java对象转为json
converter.setObjectMapper(new JacksonObjectMapper());
// 将上面的消息转换器对象追加到mvc框架的转换器集合中
// 这里前面的 0 是优先级,设置在最前面,优先使用我们定义的转换器
converters.add(0, converter);
}
JacksonObjectMapper中定义了时间格式:
3. 启用禁用员工账号
接口设计
1). 路径参数携带状态值。
2). 同时,把 id 传递过去,明确对哪个用户进行操作。
3). 返回数据 code 状态是必须,其它是非必须。
代码编写
EmployeeController.java
/**
* 启用禁用员工账号
*
* @param status 状态
* @param id 员工id
* @return 返回统一的Result封装对象
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result<Void> startOrStop(@PathVariable Integer status, Long id) {
log.info("启用禁用员工账号:status={}, id={}", status, id);
employeeService.startOrStop(status, id);
return Result.success();
}
EmployeeServiceImpl.java
/**
* 启用或禁用员工账号
*
* @param status 状态
* @param id 员工id
*/
@Override
public void startOrStop(Integer status, Long id) {
Employee employee = Employee.builder()
.id(id)
.status(status)
.build();
employeeMapper.updateById(employee);
}
EmployeeMapper.xml
根据传入的Employee对象中的非空字段,选择性地更新employee表中的对应记录。它的核心特点是只更新需要修改的字段,避免不必要的数据库操作。
<update id="updateById" parameterType="employee">
update employee
<set>
<if test="name != null">name = #{name},</if>
<if test="username != null">username = #{username},</if>
<if test="password != null">password = #{password},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="idNumber != null">id_Number = #{idNumber},</if>
<if test="updateTime != null">update_Time = #{updateTime},</if>
<if test="updateUser != null">update_User = #{updateUser},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>
这里这个功能的实现虽然只需要修改状态值,但是可以把其他需要根据主键动态修改的属性都加上,方便后续复用
4. 编辑员工
接口设计
编辑员工的页面原型:
在员工管理列表页面点击 “编辑” 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 “保存” 按钮完成编辑操作。
注:点击修改时,数据应该正常回显到修改页面。
根据上述原型图分析,编辑员工功能涉及到两个接口:
- 根据 id 查询员工信息
- 编辑员工信息
设计接口如下:

代码编写
以下代码包含了:根据id查询员工信息回显给前端、修改员工信息
- (在实现 启用禁用员工账号 功能时,已实现 employeeMapper.update(employee),在此不需写 Mapper 层代码。)
EmployeeController.java
/**
* 根据id查询员工信息
*
* @param id 员工id
* @return 返回统一的Result封装对象
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id) {
log.info("根据id查询员工信息:id={}", id);
Employee employee = employeeService.getById(id);
return Result.success(employee);
}
/**
* 修改员工信息
*
* @param employeeDTO 员工信息DTO
* @return 返回统一的Result封装对象
*/
@PutMapping
@ApiOperation("修改员工信息")
public Result<Void> update(@RequestBody EmployeeDTO employeeDTO) {
log.info("修改员工信息:{}", employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}
EmployeeServiceImpl.java
/**
* 根据id查询员工信息
*
* @param id 员工id
* @return 员工信息
*/
@Override
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
if (employee == null) { // 如果没有查询到员工信息, 则抛出异常
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
return employee; // 返回员工信息
}
@Override
public void update(EmployeeDTO employeeDTO) {
// 传来的是DTO对象, 要将 DTO 转换为实体对象
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);
// 设置修改相关属性,修改时间和修改人
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.updateById(employee); // 更新员工信息 (前面在写启用禁用员工账号时,在Mapper中已经配置了根据id动态更新)
}
EmployeeMapper.java
/**
* 根据id查询员工信息
* @param id 员工id
* @return 员工对象
*/
@Select("select * from employee where id = #{id}")
Employee getById(Long id);
功能测试
1)根据id查询员工并回显信息:返回员工信息json数据
2)修改信息

5. 导入菜品分类管理模块
产品原型分析
后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类 和 套餐分类 。
下面以分析 菜品分类 相关功能来举例, 套餐的类似。
新增菜品分类: 当我们在后台系统中添加菜品时需要选择一个菜品分类,在移动端也会按照菜品分类来展示对应的菜品。
菜品分类分页查询: 系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
根据 id 删除菜品分类: 在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是***当分类关联了菜品或者套餐时,此分类不允许删除。***
修改菜品分类: 在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。
启用禁用菜品分类: 在分类管理列表页面,可以对某个分类进行启用或者禁用操作。
分类类型查询: 当点击分类类型下拉框时,从数据库中查询所有的菜品分类数据进行展示。
注意事项
- 导入时如果想避免看到一堆红的报错,就按照Mapper =》Servie =》 Controller的顺序来导入;
- 因为在删除分类等分类相关业务时,会设计到 Setmeal 和 Dish ,要判断分类地下是否还挂有菜品和套餐,所以这里也需要导入SetmealMapper和DishMapper
- 其次,因为这些代码是我们直接拷贝进来的,可能不会自动编译,保险起见最好在 maven 那里 compile 手动编译一下,防止报错
- 业务规则:
- 分类名称必须是唯一的
- 分类按照类型可以分为菜品分类和套餐分类
- 新添加的分类状态默认为“禁用”,
- 为什么?=》如果默认是启用的话,新增分类时,该分类下是空的,在用户端显示是空的,没用意义,为了避免这种情况,直接把默认状态设为禁用即可)
代码导入
CategoryController.java
package com.sky.controller.admin;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 分类管理
*/
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 新增分类
* @param categoryDTO 分类信息
* @return 操作结果
*/
@PostMapping
@ApiOperation("新增分类")
public Result<String> save(@RequestBody CategoryDTO categoryDTO){
log.info("新增分类:{}", categoryDTO);
categoryService.save(categoryDTO);
return Result.success();
}
/**
* 分类分页查询
* @param categoryPageQueryDTO 分页查询参数
* @return 分页查询结果
*/
@GetMapping("/page")
@ApiOperation("分类分页查询")
public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
log.info("分页查询:{}", categoryPageQueryDTO);
PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
return Result.success(pageResult);
}
/**
* 删除分类
* @param id 分类id
* @return 操作结果
*/
@DeleteMapping
@ApiOperation("删除分类")
public Result<String> deleteById(Long id){
log.info("删除分类:{}", id);
categoryService.deleteById(id);
return Result.success();
}
/**
* 修改分类
* @param categoryDTO 分类信息
* @return 操作结果
*/
@PutMapping
@ApiOperation("修改分类")
public Result<String> update(@RequestBody CategoryDTO categoryDTO){
categoryService.update(categoryDTO);
return Result.success();
}
/**
* 启用、禁用分类
* @param status 0-禁用,1-启用
* @param id 分类id
* @return 操作结果
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用分类")
public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
categoryService.startOrStop(status,id);
return Result.success();
}
/**
* 根据类型查询分类
* @param type 分类类型
* @return 分类列表
*/
@GetMapping("/list")
@ApiOperation("根据类型查询分类")
public Result<List<Category>> list(Integer type){
List<Category> list = categoryService.list(type);
return Result.success(list);
}
}
CategoryServiceImpl.java
package com.sky.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
/**
* 分类业务层
*/
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
/**
* 新增分类
* @param categoryDTO 分类信息
*/
public void save(CategoryDTO categoryDTO) {
Category category = new Category();
//属性拷贝
BeanUtils.copyProperties(categoryDTO, category);
//分类状态默认为禁用状态0
category.setStatus(StatusConstant.DISABLE);
//设置创建时间、修改时间、创建人、修改人
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(BaseContext.getCurrentId());
category.setUpdateUser(BaseContext.getCurrentId());
categoryMapper.insert(category);
}
/**
* 分页查询
* @param categoryPageQueryDTO 分页查询参数
* @return 分页结果
*/
public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
//下一条sql进行分页,自动加入limit关键字分页
Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
/**
* 根据id删除分类
* @param id 分类id
*/
public void deleteById(Long id) {
//查询当前分类是否关联了菜品,如果关联了就抛出业务异常
Integer count = dishMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
}
//查询当前分类是否关联了套餐,如果关联了就抛出业务异常
count = setmealMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
}
//删除分类数据
categoryMapper.deleteById(id);
}
/**
* 修改分类
* @param categoryDTO 分类信息
*/
public void update(CategoryDTO categoryDTO) {
Category category = new Category();
BeanUtils.copyProperties(categoryDTO,category);
//设置修改时间、修改人
category.setUpdateTime(LocalDateTime.now());
category.setUpdateUser(BaseContext.getCurrentId());
categoryMapper.update(category);
}
/**
* 启用、禁用分类
* @param status 分类状态
* @param id 分类id
*/
public void startOrStop(Integer status, Long id) {
Category category = Category.builder()
.id(id)
.status(status)
.updateTime(LocalDateTime.now())
.updateUser(BaseContext.getCurrentId())
.build();
categoryMapper.update(category);
}
/**
* 根据类型查询分类
* @param type 分类类型
* @return 分类列表
*/
public List<Category> list(Integer type) {
return categoryMapper.list(type);
}
}
CategoryMapper.java
package com.sky.mapper;
import com.github.pagehelper.Page;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CategoryMapper {
/**
* 插入数据
* @param category 分类
*/
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" VALUES" +
" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
void insert(Category category);
/**
* 分页查询
* @param categoryPageQueryDTO 分页查询参数
* @return 分页结果
*/
Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);
/**
* 根据id删除分类
* @param id 分类id
*/
@Delete("delete from category where id = #{id}")
void deleteById(Long id);
/**
* 根据id修改分类
* @param category 分类
*/
void update(Category category);
/**
* 根据类型查询分类
* @param type 分类类型
* @return 分类列表
*/
List<Category> list(Integer type);
}
DishMapper.java
package com.sky.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId 分类Id
* @return 数量
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
}
SetmealMapper.java
package com.sky.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface SetmealMapper {
/**
* 根据分类id查询套餐的数量
* @param id 分类Id
* @return 数量
*/
@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(Long id);
}
CategoryMapper.xml
<?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="com.sky.mapper.CategoryMapper">
<select id="pageQuery" resultType="com.sky.entity.Category">
select * from category
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
<if test="type != null">
and type = #{type}
</if>
</where>
order by sort asc , create_time desc
</select>
<update id="update" parameterType="Category">
update category
<set>
<if test="type != null">
type = #{type},
</if>
<if test="name != null">
name = #{name},
</if>
<if test="sort != null">
sort = #{sort},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>
<select id="list" resultType="Category">
select * from category
where status = 1
<if test="type != null">
and type = #{type}
</if>
order by sort ,create_time desc
</select>
</mapper>
附:其他盲区知识补充
在 Spring MVC 中,@RequestBody注解的核心作用是将 HTTP 请求体中的数据绑定到控制器方法的参数上。是否需要添加该注解,主要取决于参数的数据来源和请求的内容格式。
一、需要添加@RequestBody的场景
当参数的数据来自HTTP 请求体(Request Body),且请求体格式为application/json、application/xml等非表单格式时,必须使用@RequestBody。
典型场景:
前端发送 JSON 格式数据当前端通过
POST、PUT等请求,以JSON格式提交数据(Content-Type: application/json)时,后端需要用@RequestBody接收并将 JSON 转换为 Java 对象。示例:前端请求(JSON 格式):
// 请求体内容 { "username": "zhangsan", "password": "123456" }后端 Controller 接收:
@PostMapping("/login") public Result login(@RequestBody User user) { // 需要@RequestBody将JSON转换为User对象 // 处理登录逻辑 }接收复杂对象或数组当参数是复杂 Java 对象(包含多个字段)、集合(如
List、Map)或数组时,即使请求体是其他格式(如 XML),也需要@RequestBody绑定数据。示例(接收数组):
@PostMapping("/batch-delete") public Result batchDelete(@RequestBody List<Long> ids) { // 接收JSON数组[1,2,3] // 批量删除逻辑 }
二、不需要添加@RequestBody的场景
当参数的数据来自URL 路径(PathVariable)、查询参数(Query String) 或表单提交(Form Data) 时,不需要@RequestBody。
典型场景:
**参数来自 URL 路径(
@PathVariable)**参数嵌在 URL 中(如/user/123中的123),用@PathVariable接收,无需@RequestBody。@GetMapping("/user/{id}") public Result getUser(@PathVariable Long id) { // 从URL路径获取id // 查询用户逻辑 }**参数来自查询参数(
@RequestParam)**参数在 URL 末尾(如/search?name=zhangsan&age=20),用@RequestParam接收(简单类型可省略该注解)。@GetMapping("/search") public Result search(@RequestParam String name, Integer age) { // 从查询参数获取 // 搜索逻辑 }**表单提交(
application/x-www-form-urlencoded)**前端通过表单提交数据(Content-Type: application/x-www-form-urlencoded),此时参数在请求体中,但格式是键值对(非 JSON),无需@RequestBody,Spring 会自动绑定到对象。示例:前端表单:
<form method="post" action="/register"> <input name="username" value="zhangsan"> <input name="password" value="123456"> </form>后端接收:
@PostMapping("/register") public Result register(User user) { // 无需@RequestBody,自动绑定表单字段 // 注册逻辑 }
三、关键判断依据
- 数据位置:参数来自请求体(Body)还是 URL(路径 / 查询参数)?
- 来自请求体 → 可能需要
@RequestBody(除非是表单键值对)。 - 来自 URL → 不需要。
- 来自请求体 → 可能需要
- 请求格式:请求体是 JSON/XML 还是表单键值对?
- JSON/XML → 需要
@RequestBody。 - 表单键值对(
application/x-www-form-urlencoded) → 不需要(复杂对象也能自动绑定)。
- JSON/XML → 需要
- 参数类型:简单类型(String、int 等)还是复杂对象 / 集合?
- 复杂对象 / 集合且来自请求体 → 需要
@RequestBody。 - 简单类型且来自查询参数 / 表单 → 不需要。
- 复杂对象 / 集合且来自请求体 → 需要
总结
@RequestBody的核心是处理请求体中的非表单格式数据(如 JSON),将其转换为 Java 对象。如果参数来自 URL 或表单提交的键值对,则无需添加该注解。
四、比较用 == 还是 Objects.equals() ?
比较数字包装类(如 Integer、Long 等)时,== 存在潜在问题,推荐用更安全的方式(如 Objects.equals)替代。具体原因和建议如下:
1. == 的问题:比较 “引用地址” 而非 “值相等”
包装类(如 Integer)是对象,== 比较的是对象的内存地址是否相同,而非 “值是否相等”。
示例 1(缓存范围内,看似正常):
Integer a = 127; Integer b = 127; System.out.println(a == b); // true(因为 Integer 缓存了 -128~127 的对象)示例 2(超出缓存范围,逻辑错误):
Integer c = 128; Integer d = 128; System.out.println(c == d); // false(超出缓存,是两个不同对象)此时
c和d的值都是 128,但==结果为false(因为是不同对象实例)。
2. == 无法安全处理 null
如果比较的对象包含 null,== 会直接抛出 NullPointerException:
Integer a = null;
Integer b = 10;
System.out.println(a == b); // 直接抛 NPE
推荐用法:Objects.equals(...)
java.util.Objects.equals(Object a, Object b) 是最安全的方式,原因:
- 内部先判断
a或b是否为null,避免空指针异常; - 再调用
equals方法比较值是否相等(而非引用地址)。
示例:
Integer a = 128;
Integer b = 128;
System.out.println(Objects.equals(a, b)); // true(值相等)
Integer c = null;
Integer d = 10;
System.out.println(Objects.equals(c, d)); // false(安全,无 NPE)
总结:当需要比较 ** 数字包装类的 “值是否相等”** 时,应避免用 ==,推荐使用 Objects.equals(...) 保证逻辑正确且避免空指针。
五、什么时候xml映射文件「可以不指定」parameterType?
MyBatis 3.4.0 及以上版本(苍穹外卖用的是高版本,如 3.5.x),满足以下条件时,parameterType 可省略:
1. 参数是「JavaBean 类型」(如 Employee、EmployeeDTO)
高版本 MyBatis 会通过 反射 自动推断参数类型:
- 若 Mapper 接口方法的参数是
Employee(如void update(Employee employee)),MyBatis 会直接识别参数类型为Employee,无需显式写parameterType="Employee"。
例如:即使去掉 parameterType,以下 SQL 仍能正常运行:
<update id="update"> <!-- 省略 parameterType -->
update employee
<set>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>
2. 参数用 @Param 注解显式标记
若 Mapper 接口方法用 @Param 指定参数名,MyBatis 会通过 @Param 识别参数,无需 parameterType:
Mapper 接口:
void updateStatus(@Param("id") Long id, @Param("status") Integer status);映射文件(无需parameterType)
<update id="updateStatus"> update employee set status = #{status} where id = #{id} </update>
六、什么时候xml映射文件「必须指定」parameterType
只有两种场景下,parameterType 是必须显式声明的:
参数类型是「集合 / 数组 / Map」等非 JavaBean 类型
当参数是 List、Array、Map 等特殊类型时,MyBatis 无法通过方法参数自动推断具体类型,必须显式指定 parameterType:
示例 1:批量删除员工(参数是List
) <delete id="batchDelete" parameterType="java.util.List"> delete from employee where id in <foreach collection="list" item="id" open="(" separator="," close=")"> #{id} </foreach> </delete>示例 2:用 Map 传多参数(参数是Map)
<select id="getByMap" parameterType="java.util.Map" resultType="Employee"> select * from employee where name = #{name} and status = #{status} </select>