如何写一个接口

4933 字
25 分钟
如何写一个接口

一、什么是接口?#

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层:

@Service
public 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接口:

@Mapper
public interface UserMapper {
// 分页查询用户列表
List<UserVo> selectList(
@Param("userName") String userName,
@Param("phone") String phone
);
}

Mapper XML(SQL):

UserMapper.xml
<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”不传参数时的默认值
@ParamMapper接口中用@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);
}

接收参数的实体类:

@Data
public 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层:

@Service
public 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接口:

@Mapper
public 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:

@Data
public class OrderDto {
private String orderNo; // 订单号
private BigDecimal totalPrice; // 总金额
private List<OrderItemDto> itemList; // 订单明细列表(子表数据)
}
@Data
public 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层(核心逻辑):

@Service
public 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接口:

@Mapper
public interface OrderMapper {
int insert(OrderDto orderDto);
}
@Mapper
public interface OrderItemMapper {
int insert(OrderItemDto itemDto);
}

Mapper XML(SQL):

OrderMapper.xml
<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>

新人注意:主子表新增的关键点:

  1. 主表的insert必须配置 useGeneratedKeys="true" keyProperty="id",插入后主键id会自动回写
  2. 循环子表数据时,用 主表对象.getId() 设置子表的关联字段
  3. 必须加 @Transactional,主表和子表要么全成功要么全回滚,不然会出现主表有数据但子表没数据的情况

3、修改接口#

Controller层:

/**
* 修改用户
* 请求方式:PUT
* 接口地址:/api/user/update
*/
@PutMapping("/update")
public Result update(@RequestBody UserDto userDto) {
userService.update(userDto);
return Result.success("修改成功");
}

Service层:

@Override
public 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层(删除相关方法):

@Override
public void deleteById(Integer id) {
// 建议用逻辑删除,而不是物理删除
userMapper.logicDeleteById(id);
}
@Override
public 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:

@Data
public class OrderDetailVo {
private Integer id; // 订单id
private String orderNo; // 订单号
private BigDecimal totalPrice; // 总金额
private Date createTime; // 下单时间
private List<OrderItemVo> itemList; // 订单明细列表(一对多)
}
@Data
public 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层:

@Service
public 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接口:

@Mapper
public interface OrderMapper {
// 一条SQL查询订单详情(主表+子表),MyBatis自动通过resultMap组装
OrderDetailVo selectDetailById(@Param("id") Integer id);
}

Mapper XML(SQL)—— 重点:

OrderMapper.xml
<!-- 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>

注意:

  1. <resultMap> 中的 column 是数据库字段名(下划线),property 是Java属性名(驼峰),MyBatis会自动按这个规则映射,不需要你手动转换
  2. <collection> 标签用来映射一对多关系,property 对应Vo中的List字段名,ofType 指定List中每个元素的类型
  3. 主表和子表都有id字段时,子表的id必须取别名(如 AS item_id),否则<resultMap>会分不清哪个是主表的、哪个是子表的,导致数据错乱
  4. 使用 LEFT JOIN 而不是 INNER JOIN,这样即使订单没有明细也能返回订单基本信息
  5. 如果项目配置了 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、统一的返回格式#

@Data
public 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只判断是否为null
if (userId != null) {
// 字符串才需要判断空
if (StringUtils.isNotEmpty(userName)) {

3、批量插入性能问题#

// 错误:循环调用单条插入,效率很低
for (User user : userList) {
userMapper.insert(user);
}
// 正确:使用批量插入
userMapper.insertBatch(userList);

八、终极大法#

不会写?看项目里已有的接口是怎么写的,Ctrl+C、Ctrl+V,然后改一改!

步骤:#

  1. 找一个类似的接口(比如要写用户查询,就找其他查询接口)
  2. 复制Controller、Service、Mapper的代码
  3. 改接口地址、改参数、改业务逻辑
  4. 用Postman测试
  5. 没问题就提交

记住:工作中的代码,大部分是”改”出来的,不是”写”出来的。

九、写完接口后要做什么?#

  1. 自己先测试:用Postman或Apifox测试接口是否正常
  2. 测试正常流程:输入正确参数,看返回结果
  3. 测试异常情况:参数为空、参数错误等情况
  4. 看日志:检查是否有异常日志
  5. 更新接口文档:如果有要求,更新Swagger或接口文档

十、注意事项#

  1. 参考项目中已有的接口写法,不要自己创造新风格
  2. 多表操作要加事务 @Transactional
  3. 批量插入不要超过5000条,太多要分批处理
  4. Integer类型判空不要加非空字符串判断
  5. 先自测再联调,不要一写完就扔给前端
  6. 重要操作要打日志,方便排查问题

十一、常见报错及解决方法#

写接口时报错了别慌,大部分都是常见问题,对照下面的表排查就行,也可以直接百度或者交给ai分析。

报错信息原因解决方法
Required request body is missingController方法的参数没加@RequestBodyPOST/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 GrammarSQL语法写错了打开控制台看完整的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格式是否合法(多余逗号、缺少引号等)

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
如何写一个接口
https://study-docs-158.pages.dev/posts/如何写一个接口/
作者
我的学习小铺
发布于
2026-08-01
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
我的学习小铺
学而时习之
分类
标签
最新动态
站点统计
文章
41
分类
7
标签
3
总字数
122,278
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.15.3
文章许可
CC BY-NC-SA 4.0