如何写一个接口
一、什么是接口?
1、先理解MVC分层架构
很多新人刚进公司,听到”写个接口”这个词会一脸懵。别慌,我们先搞清楚项目是怎么分层的。
用户(浏览器/APP) ↓ Controller层(接口层) ← 你写的接口就在这里! ↓ Service层(业务逻辑层) ↓ Mapper/DAO层(数据访问层) ↓ 数据库每一层的职责:
| 层 | 职责 | 你要做什么 |
|---|---|---|
| Controller层 | 接收请求、返回响应 | 定义接口地址、接收参数、调用Service、返回结果 |
| Service层 | 处理业务逻辑 | 写业务代码、事务控制 |
| Mapper/DAO层 | 操作数据库 | 写SQL、增删改查 |
2、“接口”到底是什么?
此接口非彼接口(Java里的interface)。在工作中,我们说的”接口”通常指的是:
Controller层里的一个方法,能够接收前端的请求,处理后将结果返回。
一个接口就是一个URL地址 + 对应的处理方法。
比如:
GET /api/user/list- 查询用户列表POST /api/user/add- 新增用户PUT /api/user/update- 修改用户DELETE /api/user/delete- 删除用户
3、新人常见的误区
| 误区 | 正确理解 |
|---|---|
| ”接口就是Java的interface” | 工作中说的接口通常指Controller里的方法 |
| ”接口一定要很复杂” | 大部分接口就是简单的增删改查 |
| ”我要从零开始写” | 先看项目里已有的接口,照葫芦画瓢 |
二、写接口的正确姿势
1、写接口前先想清楚这几件事
| 问题 | 说明 |
|---|---|
| 接口地址是什么? | 如 /api/user/list |
| 请求方式是什么? | GET查询、POST新增、PUT修改、DELETE删除 |
| 接收哪些参数? | 查询条件、分页参数等 |
| 返回什么数据? | 列表数据、详情数据、操作结果 |
| 要不要登录? | 是否需要Token验证 |
2、写接口的基本步骤
第一步:看项目中已有的接口是怎么写的(最重要!)第二步:在Controller里写方法,定义接口地址和参数第三步:在Service里写业务逻辑第四步:在Mapper里写SQL第五步:用Postman测试新人必看:不要一上来就自己写,先找项目中类似的接口,复制粘贴然后改!
重要习惯:边写边测试! 不要一口气把5个接口全写完再一起测。写完查询接口就测查询,写完新增就测新增。写一个、测一个、通一个,有问题立即发现立即修。不把问题带到后续接口的开发中!
三、常见接口类型示例
提醒:下面的示例只是一般写法,不同项目的规范可能不同。实际开发时一定要参考项目中已有接口的写法来写,以项目现有规范为准!
1、分页查询接口(最常用)
Controller层:
@RestController@RequestMapping("/api/user")public class UserController {
@Autowired private UserService userService;
/** * 查询用户列表(分页) * 请求方式:GET * 接口地址:/api/user/list */ @GetMapping("/list") public Result list( @RequestParam(value = "userName", required = false) String userName, // 用户名(可选) @RequestParam(value = "phone", required = false) String phone, // 手机号(可选) @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum, // 页码,默认第1页 @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize // 每页条数,默认10条 ) { // Controller只负责接收参数、调用Service、返回结果,不写业务逻辑 PageInfo<UserVo> pageInfo = userService.list(userName, phone, pageNum, pageSize); return Result.success(pageInfo); }}Service层:
@Servicepublic class UserServiceImpl implements UserService {
@Autowired private UserMapper userMapper;
@Override public PageInfo<UserVo> list(String userName, String phone, Integer pageNum, Integer pageSize) { // 开启分页 PageHelper.startPage(pageNum, pageSize); // 查询数据 List<UserVo> list = userMapper.selectList(userName, phone); // 封装分页结果 return new PageInfo<>(list); }}Mapper接口:
@Mapperpublic interface UserMapper {
// 分页查询用户列表 List<UserVo> selectList( @Param("userName") String userName, @Param("phone") String phone );}Mapper XML(SQL):
<select id="selectList" resultType="com.example.vo.UserVo"> SELECT id, user_name, phone, email, create_time FROM sys_user WHERE deleted = 0 <if test="userName != null and userName != ''"> AND user_name LIKE CONCAT('%', #{userName}, '%') </if> <if test="phone != null and phone != ''"> AND phone = #{phone} </if> ORDER BY create_time DESC</select>新人注意:
<if>标签用于动态SQL,参数为空时不会拼接该条件,这样查询就是”查全部”。
参数说明:
| 参数 | 说明 |
|---|---|
| @RequestParam | 接收URL后面的参数,如 /list?userName=张三 |
| required = false | 表示这个参数可以不传 |
| defaultValue = “1” | 不传参数时的默认值 |
| @Param | Mapper接口中用@Param给参数起别名,XML里才能用#{userName}引用 |
| resultType | 指定返回结果的类型,指向你的Vo类的全限定名 |
参数太多怎么办?用实体类接收:
@PostMapping("/list")public Result list(@RequestBody UserQueryVo query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<UserVo> list = userService.list(query); PageInfo<UserVo> pageInfo = new PageInfo<>(list); return Result.success(pageInfo);}接收参数的实体类:
@Datapublic class UserQueryVo { private String userName; // 用户名 private String phone; // 手机号 private Integer pageNum = 1; // 页码,默认1 private Integer pageSize = 10; // 每页条数,默认10}2、新增接口
2.1 单表新增(简单场景)
Controller层:
/** * 新增用户 * 请求方式:POST * 接口地址:/api/user/add */@PostMapping("/add")public Result add(@RequestBody UserDto userDto) { userService.add(userDto); return Result.success("新增成功");}Service层:
@Servicepublic class UserServiceImpl implements UserService {
@Autowired private UserMapper userMapper;
@Override @Transactional // 加事务,保证数据一致性 public void add(UserDto userDto) { // 1. 参数校验 if (StringUtils.isEmpty(userDto.getUserName())) { throw new BusinessException("用户名不能为空"); }
// 2. 检查用户名是否已存在 User existUser = userMapper.selectByUserName(userDto.getUserName()); if (existUser != null) { throw new BusinessException("用户名已存在"); }
// 3. 设置创建时间等默认值 userDto.setCreateTime(new Date()); userDto.setDeleted(0);
// 4. 保存到数据库 userMapper.insert(userDto); }}Mapper接口:
@Mapperpublic interface UserMapper {
// 根据用户名查询(用于检查用户名是否已存在) User selectByUserName(@Param("userName") String userName);
// 新增用户 int insert(UserDto userDto);}Mapper XML(SQL):
<select id="selectByUserName" resultType="com.example.entity.User"> SELECT id, user_name, phone, email FROM sys_user WHERE user_name = #{userName} AND deleted = 0</select>
<insert id="insert" parameterType="com.example.dto.UserDto" useGeneratedKeys="true" keyProperty="id"> INSERT INTO sys_user (user_name, phone, email, create_time, deleted) VALUES (#{userName}, #{phone}, #{email}, #{createTime}, #{deleted})</insert>新人注意:
useGeneratedKeys="true" keyProperty="id"表示插入后MyBatis会自动把数据库生成的主键id回写到userDto的id属性中,后续可以直接用userDto.getId()获取。需要注意的是,如果表没有设置主键自增,则需要手动设置主键值,此时不需要配置useGeneratedKeys。
2.2 主子表新增(进阶场景)
实际项目中经常会遇到主子表的情况,比如:订单(主表)+ 订单明细(子表)。新增时需要先插主表拿到主键id,再把主键id设置到子表的关联字段上,然后插入子表。
场景:新增订单,同时新增订单下的多个商品明细
订单表(主表)order_info 订单明细表(子表)order_item┌────┬──────────┐ ┌────┬──────────┬──────────────┐│ id │ order_no │ │ id │ order_id │ product_name │├────┼──────────┤ ├────┼──────────┼──────────────┤│ 1 │ OD001 │ ←────关联──── │ 1 │ 1 │ 手机 ││ │ │ │ 2 │ 1 │ 耳机 │└────┴──────────┘ └────┴──────────┴──────────────┘ order_id关联主表的id接收参数的DTO:
@Datapublic class OrderDto { private String orderNo; // 订单号 private BigDecimal totalPrice; // 总金额 private List<OrderItemDto> itemList; // 订单明细列表(子表数据)}
@Datapublic class OrderItemDto { private String productName; // 商品名称 private Integer quantity; // 数量 private BigDecimal price; // 单价}前端传入的JSON示例:
{ "orderNo": "OD001", "totalPrice": 5999.00, "itemList": [ {"productName": "手机", "quantity": 1, "price": 4999.00}, {"productName": "耳机", "quantity": 2, "price": 500.00} ]}Service层(核心逻辑):
@Servicepublic class OrderServiceImpl implements OrderService {
@Autowired private OrderMapper orderMapper;
@Autowired private OrderItemMapper orderItemMapper;
@Override @Transactional public void add(OrderDto orderDto) { // 1. 参数校验 if (StringUtils.isEmpty(orderDto.getOrderNo())) { throw new BusinessException("订单号不能为空"); } if (CollectionUtils.isEmpty(orderDto.getItemList())) { throw new BusinessException("订单明细不能为空"); }
// 2. 设置默认值 orderDto.setCreateTime(new Date());
// 3. 先插入主表(订单) orderMapper.insert(orderDto); // 插入后 orderDto.getId() 就有值了(因为配置了 useGeneratedKeys)
// 4. 遍历子表数据,设置主表id后插入子表 for (OrderItemDto item : orderDto.getItemList()) { item.setOrderId(orderDto.getId()); // 关键:把主表id设到子表 item.setCreateTime(new Date()); orderItemMapper.insert(item); } }}Mapper接口:
@Mapperpublic interface OrderMapper { int insert(OrderDto orderDto);}
@Mapperpublic interface OrderItemMapper { int insert(OrderItemDto itemDto);}Mapper XML(SQL):
<insert id="insert" parameterType="com.example.dto.OrderDto" useGeneratedKeys="true" keyProperty="id"> INSERT INTO order_info (order_no, total_price, create_time) VALUES (#{orderNo}, #{totalPrice}, #{createTime})</insert>
<!-- OrderItemMapper.xml --><insert id="insert" parameterType="com.example.dto.OrderItemDto" useGeneratedKeys="true" keyProperty="id"> INSERT INTO order_item (order_id, product_name, quantity, price, create_time) VALUES (#{orderId}, #{productName}, #{quantity}, #{price}, #{createTime})</insert>新人注意:主子表新增的关键点:
- 主表的insert必须配置
useGeneratedKeys="true" keyProperty="id",插入后主键id会自动回写- 循环子表数据时,用
主表对象.getId()设置子表的关联字段- 必须加
@Transactional,主表和子表要么全成功要么全回滚,不然会出现主表有数据但子表没数据的情况
3、修改接口
Controller层:
/** * 修改用户 * 请求方式:PUT * 接口地址:/api/user/update */@PutMapping("/update")public Result update(@RequestBody UserDto userDto) { userService.update(userDto); return Result.success("修改成功");}Service层:
@Overridepublic void update(UserDto userDto) { // 1. 检查用户是否存在 User user = userMapper.selectById(userDto.getId()); if (user == null) { throw new BusinessException("用户不存在"); }
// 2. 更新数据 userDto.setUpdateTime(new Date()); userMapper.updateById(userDto);}Mapper接口:
// 在UserMapper中添加User selectById(@Param("id") Integer id);int updateById(UserDto userDto);Mapper XML(SQL):
<select id="selectById" resultType="com.example.entity.User"> SELECT id, user_name, phone, email FROM sys_user WHERE id = #{id} AND deleted = 0</select>
<update id="updateById" parameterType="com.example.dto.UserDto"> UPDATE sys_user <set> <if test="userName != null and userName != ''"> user_name = #{userName}, </if> <if test="phone != null and phone != ''"> phone = #{phone}, </if> <if test="email != null and email != ''"> email = #{email}, </if> update_time = #{updateTime}, </set> WHERE id = #{id} AND deleted = 0</update>新人注意:
<set>标签会自动去掉最后一个多余的逗号,比手动拼SET语句更安全,且只更新非空的字段。
4、删除接口
单个删除:
/** * 删除用户 * 请求方式:DELETE * 接口地址:/api/user/delete/{id} */@DeleteMapping("/delete/{id}")public Result delete(@PathVariable("id") Integer id) { userService.deleteById(id); return Result.success("删除成功");}批量删除(更常用):
/** * 批量删除用户 * 请求方式:DELETE * 接口地址:/api/user/delete?ids=1,2,3 */@DeleteMapping("/delete")public Result delete(@RequestParam("ids") String ids) { // ids = "1,2,3" 需要转成列表 List<Integer> idList = Arrays.stream(ids.split(",")) .map(String::trim) .map(Integer::parseInt) .collect(Collectors.toList()); userService.deleteByIds(idList); return Result.success("删除成功");}Service层(删除相关方法):
@Overridepublic void deleteById(Integer id) { // 建议用逻辑删除,而不是物理删除 userMapper.logicDeleteById(id);}
@Overridepublic void deleteByIds(List<Integer> idList) { userMapper.logicDeleteByIds(idList);}Mapper接口:
// 在UserMapper中添加int logicDeleteById(@Param("id") Integer id);int logicDeleteByIds(@Param("idList") List<Integer> idList);Mapper XML(SQL):
<update id="logicDeleteById"> UPDATE sys_user SET deleted = 1 WHERE id = #{id}</update>
<update id="logicDeleteByIds"> UPDATE sys_user SET deleted = 1 WHERE id IN <foreach collection="idList" item="id" open="(" separator="," close=")"> #{id} </foreach></update>注意:
1)正式项目中推荐使用逻辑删除**(把
deleted字段设为1),而不是DELETE FROM物理删除。逻辑删除的数据可以恢复,也不会影响关联数据。<foreach>标签用于遍历集合,自动拼接IN (1, 2, 3)。
2)
DELETE请求在某些服务器或Nginx配置下可能会被禁用(出于安全考虑)。如果遇到DELETE请求返回405或被拦截,可以改成用POST请求来代替删除操作,具体以项目现有规范为准。
5、详情查询接口(一对多关联查询)
实际项目中,详情查询经常需要关联查询子表数据。比如:查订单详情,需要同时返回订单基本信息和订单下的商品明细列表。下面用MyBatis的
<resultMap>+<collection>实现一对多映射,一条SQL搞定。
场景:根据订单id查询订单详情,包含订单基本信息 + 商品明细列表
数据库字段(下划线命名) Java实体类字段(驼峰命名)───────────────────── ─────────────────────order_info表: OrderDetailVo: order_no ──映射──→ orderNo total_price ──映射──→ totalPrice create_time ──映射──→ createTime
order_item表: OrderItemVo: order_id ──映射──→ orderId product_name ──映射──→ productName数据库字段用下划线命名(如
order_no),Java实体类用驼峰命名(如orderNo),需要在<resultMap>中用<result>标签做映射,或者在application.yml中配置map-underscore-to-camel-case: true开启全局自动驼峰转换。下面演示手动映射的方式,让你清楚了解映射关系。
返回的Vo:
@Datapublic class OrderDetailVo { private Integer id; // 订单id private String orderNo; // 订单号 private BigDecimal totalPrice; // 总金额 private Date createTime; // 下单时间 private List<OrderItemVo> itemList; // 订单明细列表(一对多)}
@Datapublic class OrderItemVo { private Integer id; // 明细id private Integer orderId; // 关联的订单id private String productName; // 商品名称 private Integer quantity; // 数量 private BigDecimal price; // 单价}返回数据示例:
{ "code": 200, "message": "操作成功", "data": { "id": 1, "orderNo": "OD001", "totalPrice": 5999.00, "createTime": "2026-05-07 10:30:00", "itemList": [ {"id": 1, "orderId": 1, "productName": "手机", "quantity": 1, "price": 4999.00}, {"id": 2, "orderId": 1, "productName": "耳机", "quantity": 2, "price": 500.00} ] }}Controller层:
/** * 查询订单详情 * 请求方式:GET * 接口地址:/api/order/detail/{id} */@GetMapping("/detail/{id}")public Result detail(@PathVariable("id") Integer id) { OrderDetailVo order = orderService.getDetailById(id); return Result.success(order);}Service层:
@Servicepublic class OrderServiceImpl implements OrderService {
@Autowired private OrderMapper orderMapper;
@Override public OrderDetailVo getDetailById(Integer id) { OrderDetailVo order = orderMapper.selectDetailById(id); if (order == null) { throw new BusinessException("订单不存在"); } return order; }}Mapper接口:
@Mapperpublic interface OrderMapper { // 一条SQL查询订单详情(主表+子表),MyBatis自动通过resultMap组装 OrderDetailVo selectDetailById(@Param("id") Integer id);}Mapper XML(SQL)—— 重点:
<!-- 1. 先定义主表的resultMap --><resultMap id="OrderDetailResultMap" type="com.example.vo.OrderDetailVo">
<!-- 主表字段映射:column=数据库字段名,property=Vo属性名 --> <id column="id" property="id"/> <result column="order_no" property="orderNo"/> <result column="total_price" property="totalPrice"/> <result column="create_time" property="createTime"/>
<!-- 一对多映射:用collection标签关联子表数据 --> <collection property="itemList" ofType="com.example.vo.OrderItemVo"> <!-- 子表字段映射 --> <id column="item_id" property="id"/> <result column="order_id" property="orderId"/> <result column="product_name" property="productName"/> <result column="quantity" property="quantity"/> <result column="price" property="price"/> </collection>
</resultMap>
<!-- 2. 查询语句:主表LEFT JOIN子表,一条SQL查出所有数据 --><select id="selectDetailById" resultMap="OrderDetailResultMap"> SELECT o.id, o.order_no, o.total_price, o.create_time, i.id AS item_id, -- 子表id取别名,避免和主表id冲突 i.order_id, i.product_name, i.quantity, i.price FROM order_info o LEFT JOIN order_item i ON o.id = i.order_id WHERE o.id = #{id} ORDER BY i.id ASC</select>注意:
<resultMap>中的column是数据库字段名(下划线),property是Java属性名(驼峰),MyBatis会自动按这个规则映射,不需要你手动转换<collection>标签用来映射一对多关系,property对应Vo中的List字段名,ofType指定List中每个元素的类型- 主表和子表都有
id字段时,子表的id必须取别名(如AS item_id),否则<resultMap>会分不清哪个是主表的、哪个是子表的,导致数据错乱- 使用
LEFT JOIN而不是INNER JOIN,这样即使订单没有明细也能返回订单基本信息- 如果项目配置了
map-underscore-to-camel-case: true,可以不用写<result>标签,但主子表id冲突的问题仍然需要取别名处理
四、Mapper层核心知识
1、Mapper接口和XML的关系
Mapper接口(Java) Mapper XML(SQL)┌──────────────────┐ ┌──────────────────────┐│ selectById(id) │ ──→ │ <select id="selectById"> ││ insert(user) │ ──→ │ <insert id="insert"> ││ updateById(user) │ ──→ │ <update id="updateById"> ││ deleteById(id) │ ──→ │ <delete id="deleteById"> │└──────────────────┘ └──────────────────────┘核心规则:
- Mapper接口的方法名必须和XML中的id一致
- XML文件放在
resources/mapper/目录下 - XML的
namespace必须指向Mapper接口的全限定名
<!-- UserMapper.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.example.mapper.UserMapper">
<!-- 你的SQL写在这里 -->
</mapper>2、常用MyBatis动态SQL标签
| 标签 | 作用 | 示例场景 |
|---|---|---|
| 条件判断,满足才拼接 | 参数不为空时加WHERE条件 | |
| 更新字段,自动去尾逗号 | 只更新非空字段 | |
| 自动处理WHERE和AND | 动态拼接查询条件 | |
| 遍历集合,拼IN语句 | 批量删除 IN (1,2,3) | |
| 多条件分支(类似if-else) | 多种排序方式 |
3、#{} 和 ${} 的区别(面试常考)
<!-- #{} 推荐!预编译处理,防SQL注入 -->WHERE user_name = #{userName}-- 实际执行:WHERE user_name = ?
<!-- ${} 字符串拼接,有SQL注入风险,尽量少用 -->WHERE user_name = '${userName}'-- 实际执行:WHERE user_name = '张三'
<!-- ${} 的使用场景:动态表名、动态列名(不能用#{}的情况) -->ORDER BY ${orderColumn} ${orderDir}新人记住:99%的情况用
#{}就对了,只有动态表名/列名才用${}。
五、参数接收方式详解
1、@RequestParam - 接收URL参数
// 请求:GET /api/user/list?name=张三&pageNum=1@GetMapping("/list")public Result list( @RequestParam("name") String name, @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum) { // ...}2、@PathVariable - 接收路径参数
// 请求:GET /api/user/detail/123@GetMapping("/detail/{id}")public Result detail(@PathVariable("id") Integer id) { // id = 123}3、@RequestBody - 接收JSON格式参数
// 请求:POST /api/user/add// Body: {"userName":"张三","phone":"13800138000"}@PostMapping("/add")public Result add(@RequestBody UserDto userDto) { // userDto.getUserName() = "张三"}六、返回结果封装
1、统一的返回格式
@Datapublic class Result<T> { private Integer code; // 状态码 private String message; // 提示信息 private T data; // 返回数据
// 成功 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; }
// 失败 public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; }}2、返回示例
成功:
{ "code": 200, "message": "操作成功", "data": { "userId": 1, "userName": "张三" }}失败:
{ "code": 500, "message": "用户名已存在", "data": null}七、新人避坑指南
1、常见错误
| 错误 | 正确做法 |
|---|---|
| 接口地址写错 | 按项目规范来,参考已有接口 |
| 请求方式用错 | 查询用GET、新增用POST、修改用PUT、删除用DELETE |
| 参数接收不到 | 检查注解是否正确:@RequestParam、@RequestBody、@PathVariable |
| 没加事务 | 多表操作一定要加 @Transactional |
| 没做参数校验 | 必填参数要判空 |
2、Integer类型判空的坑
// 错误:Integer类型不能用 != "" 判断if (userId != null && userId != "") { // 编译报错!
// 正确:Integer只判断是否为nullif (userId != null) {
// 字符串才需要判断空if (StringUtils.isNotEmpty(userName)) {3、批量插入性能问题
// 错误:循环调用单条插入,效率很低for (User user : userList) { userMapper.insert(user);}
// 正确:使用批量插入userMapper.insertBatch(userList);八、终极大法
不会写?看项目里已有的接口是怎么写的,Ctrl+C、Ctrl+V,然后改一改!
步骤:
- 找一个类似的接口(比如要写用户查询,就找其他查询接口)
- 复制Controller、Service、Mapper的代码
- 改接口地址、改参数、改业务逻辑
- 用Postman测试
- 没问题就提交
记住:工作中的代码,大部分是”改”出来的,不是”写”出来的。
九、写完接口后要做什么?
- 自己先测试:用Postman或Apifox测试接口是否正常
- 测试正常流程:输入正确参数,看返回结果
- 测试异常情况:参数为空、参数错误等情况
- 看日志:检查是否有异常日志
- 更新接口文档:如果有要求,更新Swagger或接口文档
十、注意事项
- 参考项目中已有的接口写法,不要自己创造新风格
- 多表操作要加事务
@Transactional - 批量插入不要超过5000条,太多要分批处理
- Integer类型判空不要加非空字符串判断
- 先自测再联调,不要一写完就扔给前端
- 重要操作要打日志,方便排查问题
十一、常见报错及解决方法
写接口时报错了别慌,大部分都是常见问题,对照下面的表排查就行,也可以直接百度或者交给ai分析。
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
| Required request body is missing | Controller方法的参数没加@RequestBody | POST/PUT请求用JSON传参时,参数前必须加@RequestBody |
| Parameter ‘id’ not found | @PathVariable的名字和路径里的占位符不一致 | 路径是{id},注解就写@PathVariable(“id”),两边名字要一样 |
| Invalid bound statement (not found) | Mapper XML没被扫描到 | 1. 检查XML的namespace是否和Mapper接口全限定名一致 2. 检查XML的id是否和接口方法名一致 3. 检查XML文件是否放在正确的目录下(如resources/mapper/) 4. 检查application.yml中mybatis.mapper-locations配置路径是否正确 |
| Unknown column ‘xxx’ | SQL里写的字段名在数据库表中不存在 | 检查数据库表结构,确认字段名是否正确,注意下划线命名和驼峰命名的对应 |
| Bad SQL Grammar | SQL语法写错了 | 打开控制台看完整的SQL语句,复制到Navicat里执行排查 |
| No serializer found for class | 返回的对象没有getter方法 | 实体类加上@Data注解(Lombok)或者手动写getter/setter |
| Request method ‘POST’ not supported | 请求方式和Controller定义的不匹配 | Controller用的是@GetMapping,前端却用了POST请求,改成一致的就行 |
| Ambiguous mapping | 接口地址重复了 | 两个方法用了相同的URL和请求方式,改掉其中一个的地址 |
| NullPointerException | 空指针,某个对象是null却调用了它的方法 | 重点检查:①Service有没有加@Service ②@Autowired有没有加 ③Mapper查询结果是否为null但没有判空 |
| HttpMessageNotReadableException | 前端传的JSON格式不对 | 检查JSON的字段名是否和DTO的属性名一致,JSON格式是否合法(多余逗号、缺少引号等) |
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!














