手把手教你写接口(Controller方法)
此接口非彼接口
当你刚接触后端开发时,听到“接口”这个词,可能会想到Java语言中用 interface 关键字定义的抽象类型。但今天我们要聊的“接口”,是后端服务对外提供的API接口,特指MVC设计模式中Controller层接收并处理HTTP请求的方法。
Controller层作为整个应用的入口,负责:
接收来自前端(Web页面、App)或其他服务(第三方系统、微服务)的请求。对请求参数进行初步处理(如校验、绑定)。调用Service层的业务逻辑。将Service层的处理结果进行封装,最终返回给请求方。
写好Controller接口是构建健壮、清晰后端服务的关键一步。接下来,我们就结合实际代码示例,看看如何编写不同功能的API接口。
1. MVC分层概述
在开始编写代码之前,我们先快速回顾一下典型的MVC(或更接近的MVCS/MVCL)分层结构:
Controller (控制层): 负责接收请求、调用业务逻辑、组织响应数据。它不处理具体的业务细节。Service (业务逻辑层): 负责处理核心业务逻辑,协调多个Mapper/DAO操作,进行复杂的计算或判断。Mapper/DAO (数据访问层): 负责与数据库交互,执行SQL语句进行数据的增删改查。
清晰的分层能够让代码职责明确、易于维护和测试。Controller层应该尽可能保持“瘦”,将复杂的业务逻辑下沉到Service层。
2. 编写Controller接口示例
我们将通过几个常见的操作(查询、删除、新增、编辑)来展示Controller接口的写法。
2.1 模糊分页查询接口
查询接口是最常见的类型之一,通常需要处理分页和多种查询条件。
需求: 实现一个根据多种条件模糊查询设备信息并支持分页的接口。
Controller层实现:
首先看使用 @RequestParam 接收参数的方式,通常用于参数较少的情况,或者使用 GET 请求。
@RestController
@RequestMapping("camera") // 定义Controller的根路径
public class CameraAlarmController {
@Autowired
private IWarehouseOutService warehouseOutService; // 注入Service层接口
/**
* 获取设备相关信息 (使用 @RequestParam 接收参数)
* @param operationType 操作类型
* @param warehouseId 仓库ID
* @param equipAttribute 设备属性
* @param equipTypeId 设备类型ID
* @param keywords 关键字 (模糊查询)
* @param pageNum 页码
* @param pageSize 每页数量
* @return 查询结果 (包含分页信息)
*/
@GetMapping("getOutboundEquipmentInfo") // 指定HTTP方法和子路径
public Result getOutboundEquipmentInfo(
@RequestParam(value = "operationType", required = false) Integer operationType, // 非必填参数
@RequestParam(value = "warehouseId", required = false) Integer warehouseId,
@RequestParam(value = "equipAttribute", required = false) String equipAttribute,
@RequestParam(value = "equipTypeId", required = false) Integer equipTypeId,
@RequestParam(value = "keywords", required = false) String keywords,
@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum, // 带默认值的非必填参数
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize // 带默认值的非必填参数
) {
// 使用PageHelper进行分页设置,紧跟在该设置后的第一个Mapper查询语句会被分页
PageHelper.startPage(pageNum, pageSize);
// 调用Service层方法执行查询
List
// 将查询结果封装到PageInfo中,PageInfo包含了丰富的分页信息
PageInfo
// 返回统一的结果封装对象
return new SuccessResult(pageInfo);
}
}
优点: 参数直接清晰地体现在接口方法签名中,易于理解。
缺点: 当参数很多时,方法签名会变得非常冗长。
为了解决参数过多的问题,通常更推荐使用一个**请求参数实体类(VO/DTO)**来接收前端传递的参数,尤其是在参数较多或使用 POST 请求时。
/**
* 获取设备相关信息 (使用 @RequestBody 接收参数实体)
* 推荐在参数较多或使用 POST 请求时使用
* @param vo 请求参数实体
* @return 查询结果 (包含分页信息)
*/
@PostMapping("/getOutboundEquipmentInfo") // 接收请求体中的JSON数据,通常改为 POST 或 PUT
public Result getOutboundEquipmentInfo(@RequestBody EquipInfoVo vo) {
// 从VO中获取分页参数
PageHelper.startPage(vo.getPageNum(), vo.getPageSize());
// 调用Service层方法,直接将VO传递下去
List
PageInfo
return new SuccessResult(pageInfo);
}
请求参数实体类 EquipInfoVo:
这个类用于封装前端通过请求体(通常是JSON)发送过来的参数。
@Data // Lombok 注解,自动生成 getter, setter, toString, equals, hashCode
@Accessors(chain = true) // Lombok 注解,setter 方法返回 this,支持链式调用
public class EquipInfoVo {
// @JsonProperty(value = "operationType") // 如果前端传的字段名与Java字段名不同,可以使用此注解映射
private Integer operationType;
// @JsonProperty(value = "warehouseId")
private Integer warehouseId;
// @JsonProperty(value = "equipAttribute")
private String equipAttribute;
// @JsonProperty(value = "equipTypeId")
private Integer equipTypeId;
// @JsonProperty(value = "keywords")
private String keywords;
// @JsonProperty(value = "pageNum")
private Integer pageNum = 1; // 设置默认值
// @JsonProperty(value = "pageSize")
private Integer pageSize = 10; // 设置默认值
}
优点: Controller方法签名简洁,易于维护;方便参数的扩展;可以结合 @Validated 进行统一参数校验。
缺点: 需要额外定义VO类。
Service层接口定义 (Java Interface):
这是Java中用 interface 关键字定义的接口,用于规范Service层的行为。
//这里不用加service注解
public interface IWarehouseOutService {
List
Integer equipTypeId,
Integer warehouseId,
Integer operationType,
String keywords);
}
Service层实现:
@Service // 标记这是一个Service组件
@Slf4j // Lombok 注解,用于日志记录
public class WarehouseOutServiceImpl implements IWarehouseOutService {
@Autowired
private WmEquipInfoMapper wmEquipInfoMapper; // 注入Mapper
@Autowired
private WmRepertoryInfoMapper wmRepertoryInfoMapper; // 注入其他Mapper (示例中有用到)
@Autowired
private WmOutWarehouseMapper wmOutWarehouseMapper; // 注入其他Mapper (示例中有用到)
@Override
// 对应 @RequestBody VO 的 Service 方法实现
public List
// 此处包含较复杂的业务逻辑,根据 operationType 进行不同的查询和数据处理
// 例如:
// 从VO获取参数
String equipAttribute = vo.getEquipAttribute();
Integer equipTypeId = vo.getEquipTypeId();
Integer warehouseId = vo.getWarehouseId();
Integer operationType = vo.getOperationType();
String keywords = vo.getKeywords();
if (operationType != null && operationType == WmConstant.EQUIPMENT_INFO_INCOME){ // 假设 WmConstant.EQUIPMENT_INFO_INCOME 是表示入库的常量
// 入库操作相关查询和处理
List
List
// 数据组装逻辑... (此处代码较长,为保持文章重点,省略部分细节,读者可参考原文或自行实现)
outList.forEach(info->{
Optional
if (wmRepertoryInfoDto.isPresent()){
info.setInventoryQuantity(wmRepertoryInfoDto.get().getNum());
info.setDamagesNum(wmRepertoryInfoDto.get().getDamagesNum());
}else {
info.setInventoryQuantity(0);
info.setDamagesNum(0);
}
});
return outList;
} else { // 假设默认为出库操作或其他情况
// 出库操作相关查询和处理
List
List
// 数据组装逻辑... (此处代码较长,为保持文章重点,省略部分细节,读者可参考自行实现)
outboundEquipmentInfo.forEach(out-> relationEquipDtos.forEach(re->{
if (Integer.valueOf(re.getEquipId().toString()).equals(out.getId())){
Integer num=out.getInventoryQuantity()-re.getNum();
out.setInventoryQuantity(num<0?0:num);
}
})
);
return outboundEquipmentInfo;
}
}
}
Mapper层接口定义:
Mapper接口负责定义数据库操作的方法。
// 方式一:在启动类上使用 @MapperScan("...")
// 方式二:在此接口上添加 @Mapper 注解
public interface WmEquipInfoMapper {
// 对应查询方法,使用 @Param 注解将方法参数与Mapper XML中的SQL参数关联
List
@Param("equipAttribute") String equipAttribute,
@Param("warehouseId") Integer warehouseId,
@Param("equipTypeId") Integer equipTypeId,
@Param("keywords") String keywords);
// 示例中 Service 层入库操作用到的另一个查询方法 (补充)
List
@Param("equipTypeId") Integer equipTypeId,
@Param("keywords") String keywords);
}
Mapper XML 实现:
MyBatis XML 文件定义了 SQL 语句的具体实现。
SELECT
t2.num inventoryQuantity,
t2.damages_num,
( SELECT `name` FROM t_dic_item WHERE type_code = "equip_attribute" AND CODE = t1.equip_attribute ) `equipAttributeName`,
( SELECT `name` FROM t_dic_item WHERE type_code = "equip_unit" AND CODE = t1.equip_unit ) equipUnitName,
t1.equip_name,
t1.equip_brand,
t1.id,
t1.equip_model,
t3.`name` equipTypeName
FROM
t_common_wm_equip_info t1
LEFT JOIN t_common_wm_repertory_info t2 ON t1.id = t2.equip_id
LEFT JOIN t_common_wm_equip_type t3 ON t1.equip_type_id = t3.id
WHERE
t1.removed = 0
AND t2.removed = 0
AND t3.removed = 0
AND t2.warehouse_id = #{warehouseId}
AND t1.equip_type_id = #{equipTypeId}
AND (t1.equip_name like concat('%', #{keywords}, '%') OR t1.equip_model like concat('%', #{keywords}, '%')
OR t1.equip_brand like concat('%', #{keywords}, '%'))
注意点:
PageHelper.startPage(pageNum, pageSize); 必须紧跟在它之后的第一个MyBatis查询方法才会生效。PageInfo 是一个非常方便的类,包含了总记录数、总页数、当前页数据等信息。MyBatis XML 中的
2.2 批量删除接口
删除操作通常分为物理删除(从数据库中彻底移除)和逻辑删除(标记数据为无效,实际保留)。此处示例为逻辑删除。批量删除是一个常见需求。
需求: 根据提供的多个ID,批量逻辑删除仓库信息。
Controller层实现:
@DeleteMapping(value = "/delete", name = "仓库管理,删除仓库信息") // 使用 @DeleteMapping 表示 DELETE 请求
public Result deleteWareHouseInfo(@RequestParam(value = "ids") String ids, HttpServletRequest request) {
// 从请求中获取当前用户信息,通常用于记录操作日志或权限校验
Integer userId = (Integer) RequestUtils.getCurrentUser(request).get("userId"); // 假设 RequestUtils 是一个获取当前用户的工具类
// 调用Service层方法执行删除
return new SuccessResult(wareHouseInfoService.deleteWareHouseInfo(ids, userId));
}
Service层实现:
@Service
// @Transactional // 单表删除如果失败,默认会回滚。多表关联删除或者有其他业务逻辑时,强烈建议加上此注解保证事务一致性。
public class WarehouseOutServiceImpl implements IWarehouseOutService {
@Autowired
private WarehouseInfoMapper warehouseInfoMapper; // 注入Mapper
// 对应删除方法
@Transactional // 加上事务注解,确保批量删除的原子性
public Object deleteWareHouseInfo(String ids, Integer userId) {
// 将逗号分隔的ID字符串转为 Integer 列表
List
.map(s -> Integer.parseInt(s.trim())) // 转换为 Integer
.collect(Collectors.toList());
// 调用Mapper方法进行批量删除
return warehouseInfoMapper.deleteByIds(idList, userId);
}
}
注意点:
@DeleteMapping 注解。@RequestParam 可以接收单个值,当参数名为 ids 且期望接收多个ID时,通常前端会将ID拼接成 1,2,3 这样的字符串发送。在Service层将字符串分割并转换为 List
Mapper层接口定义:
public interface WarehouseInfoMapper {
// 批量删除方法,@Param 用于给参数起别名,方便在XML中引用
Integer deleteByIds(@Param("idList") List
@Param("userId") Integer userId);
}
Mapper XML 实现:
update t_warehouse_info
set removed = 1, rm_uid = #{userId}, rm_time = NOW(), up_time = now() where id = #{item}
注意点:
使用
2.3 新增接口
新增接口通常接收一个包含待新增数据的实体或DTO/VO对象,然后将其保存到数据库。
需求: 新增采购信息,包括采购单基本信息和关联的设备列表。
Controller层实现:
@PostMapping(value = "/add", name = "仓库管理,新增采购") // 使用 @PostMapping 表示 POST 请求
public Result addPurchaseInfo(@RequestBody WmPurchaseInfoAddVo dto, HttpServletRequest request) {
// 从请求中获取当前用户信息
Integer userId = (Integer) RequestUtils.getCurrentUser(request).get("userId");
// 调用Service层方法执行新增
return new SuccessResult(wmPurchaseInfoService.addPurchaseInfo(dto, userId));
}
Service层实现:
@Override
@Transactional // 新增操作可能涉及多个表的插入,强烈建议加上事务注解
public Integer addPurchaseInfo(WmPurchaseInfoAddVo dto, Integer userId) {
Integer result = 0;
// 生成采购单号 (示例,具体实现根据业务规则)
String orderCode = wmUtils.generateWmCode(WmConstant.PURCHASE_CODE_PREFIX, 1); // 假设 wmUtils 是工具类
// 设置创建人和更新人ID
dto.setOrderCode(orderCode).setCrUid(userId).setUpUid(userId);
// 插入采购基本信息
// insertSelective 方法会只插入对象中非空的字段对应的值
// 需要注意的是:如果你的数据库表主键没有设置自增,则需要在Service或Mapper中手动设置主键值。
// 如果设置了自增,则需要在 Mapper XML 中配置 `useGeneratedKeys="true" keyProperty="id"` 来获取生成的ID
purchaseInfoMapper.insertSelective(dto);
// 获取刚插入的采购单ID,这个ID会赋值到 dto 对象的 id 属性中 (因为 Mapper XML 配置了 keyProperty="id")
Integer purchaseId = dto.getId();
// 获取关联设备列表
List
if (wmRelationEquipDtos != null && !wmRelationEquipDtos.isEmpty()) {
// 设置关联设备的关联ID (即采购单ID) 和关联类型
wmRelationEquipDtos.forEach(e -> {
e.setRelationId(purchaseId) // 设置外键关联到采购单
.setType(WmConstant.PURCHASE_RELATION) // 设置关联类型
.setStockPendingNum(e.getNum()); // 设置待入库数量
});
// 插入关联设备信息 (可能需要批量插入)
// 注意:做批量插入的时候,插入的数量不能太多,否则会因为SQL语句过长而出现无法执行的问题。
// 一般建议每次批量插入的记录数不超过 1000-5000 条,具体上限取决于数据库和驱动配置。
// 如果数据量大,需要分批插入。
if (wmRelationEquipDtos.size() > 5000) {
// 数据量大,分批插入
List> lists = splitList(wmRelationEquipDtos, 5000); // 假设 splitList 是一个分批工具方法
for (List
result += relationEquipMapper.insertBatch(batchList); // 批量插入一批数据
}
} else {
// 数据量小,直接批量插入
result = relationEquipMapper.insertBatch(wmRelationEquipDtos);
}
} else {
// 没有关联设备,新增结果取决于主表插入
result = (purchaseId != null) ? 1 : 0;
}
return result >= 1 ? 1 : 0; // 返回操作结果标识
}
// 假设的分批工具方法
private > splitList(List
if (list == null || list.isEmpty() || splitCount <= 0) {
return Collections.emptyList();
}
int length = list.size();
int num = (length + splitCount - 1) / splitCount; // 计算需要分成的批次数量
List> newList = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
int fromIndex = i * splitCount;
int toIndex = Math.min((i + 1) * splitCount, length); // 确保不会越界
newList.add(new ArrayList<>(list.subList(fromIndex, toIndex))); // subList 是视图,建议拷贝一份新的 ArrayList
}
return newList;
}
}
注意点:
@PostMapping 注解。@RequestBody 接收一个复杂对象(VO/DTO)。@Transactional 非常重要,确保主表和子表的插入操作原子性。获取自增主键: 在 insertSelective 的 Mapper XML 中配置 useGeneratedKeys="true" keyProperty="id" 可以让MyBatis在插入成功后将生成的自增主键值设置到传入的DTO对象的 id 属性上。批量插入: 当需要插入多条记录时,批量插入效率远高于单条循环插入。使用
Mapper层接口定义:
public interface WmPurchaseInfoMapper {
// 插入采购基本信息
Integer insertSelective(WmPurchaseInfoAddVo addVo); // 参数名 addVo 用于在XML中引用
// ... 其他方法
}
public interface WmRelationEquipMapper {
// 批量插入关联设备信息
Integer insertBatch(@Param("dtoList") List
}
Mapper XML 实现 (insertSelective):
parameterType="vip.dtcloud.domain.warehousemanage.vo.WmPurchaseInfoAddVo"> insert into t_common_wm_purchase_info order_code, order_name, purchase_method, up_time, cr_time, up_uid, cr_uid, #{addVo.orderCode,jdbcType=VARCHAR}, #{addVo.orderName,jdbcType=VARCHAR}, #{addVo.purchaseMethod,jdbcType=VARCHAR}, now(), now(), #{addVo.upUid,jdbcType=INTEGER}, #{addVo.crUid,jdbcType=INTEGER},
注意点:
useGeneratedKeys="true": 告诉MyBatis使用数据库的自增主键功能。keyProperty="id": 将生成的自增主键值设置到传入参数对象的 id 属性上。keyColumn="id": 指定数据库表中主键的列名(可选,MyBatis通常能自动识别)。
Mapper XML 实现 (insertBatch):
insert into t_common_wm_relation_equip (relation_id, type,
equip_id, equip_serial, equip_code,
num, stock_pending_num, up_time
)
values
( #{item.relationId,jdbcType=INTEGER}, #{item.type,jdbcType=INTEGER},
#{item.equipId,jdbcType=BIGINT}, #{item.equipSerial,jdbcType=VARCHAR}, #{item.equipCode,jdbcType=VARCHAR},
#{item.num,jdbcType=INTEGER}, #{item.stockPendingNum,jdbcType=INTEGER}, now()
)
注意点:
2.4 编辑接口
编辑接口用于修改现有数据。通常根据记录ID来定位数据,然后更新相应字段。
需求: 根据ID编辑仓库信息。
Controller层实现:
@PutMapping(value = "/edit", name = "仓库管理,修改仓库信息") // 使用 @PutMapping 表示 PUT 请求
public Result editWareHouseInfo(@RequestBody WmWarehouseInfoDto dto, HttpServletRequest request) {
// 从请求中获取当前用户信息
Integer userId = (Integer) RequestUtils.getCurrentUser(request).get("userId");
// 调用Service层方法执行更新
return new SuccessResult(wareHouseInfoService.editWareHouseInfo(dto, userId));
}
Service层实现:
@Service
public class WarehouseOutServiceImpl implements IWarehouseOutService {
@Autowired
private WarehouseInfoMapper warehouseInfoMapper; // 注入Mapper
@Override
// 编辑通常是对单条记录的操作,如果仅更新一个表,不加 @Transactional 也可,
// 但如果涉及多个表或复杂业务逻辑,建议加上。
public Integer editWareHouseInfo(WmWarehouseInfoDto dto, Integer userId) {
// 设置更新人ID
dto.setUpUid(userId);
// 调用Mapper方法按主键有选择地更新非空字段
return warehouseInfoMapper.updateByPrimaryKeySelective(dto);
}
}
Mapper层接口定义:
public interface WarehouseInfoMapper {
// 按主键有选择地更新 (只更新DTO对象中非空的字段)
Integer updateByPrimaryKeySelective(WmWarehouseInfoDto dto);
// ... 其他方法
}
Mapper XML 实现:
update t_common_wm_warehouse_info
warehouse_name = #{warehouseName,jdbcType=VARCHAR},
warehouse_location = #{warehouseLocation,jdbcType=VARCHAR},
up_time = now(),
up_uid = #{upUid,jdbcType=INTEGER},
where id = #{id,jdbcType=INTEGER}
注意点:
@PutMapping 注解。@RequestBody 接收包含更新数据的DTO/VO,其中必须包含记录的ID。
3. 常用注解与技巧回顾
在上面的示例中,我们使用了许多Spring和MyBatis的常用注解和XML标签:
Spring:
@RestController: 组合了 @Controller 和 @ResponseBody,表示这是一个处理RESTful请求的Controller,并且方法的返回值直接作为响应体。@RequestMapping: 用于映射请求路径。可以用于类或方法上。@GetMapping, @PostMapping, @DeleteMapping, @PutMapping: 分别对应HTTP的GET, POST, DELETE, PUT方法,是 @RequestMapping 的快捷方式。@RequestParam: 绑定请求URL中的参数到方法的参数。@RequestBody: 绑定请求体(通常是JSON)到方法的参数对象。@Autowired: 依赖注入,用于自动装配Service或Mapper对象。@Service: 标记这是一个Service组件。@Transactional: 开启事务管理,保证方法的原子性。
MyBatis:
@Param: 用于在Mapper接口方法中为参数指定名称,以便在Mapper XML中通过 # {参数名} 或 #{参数名.属性名} 引用。@MapperScan: 在启动类上扫描Mapper接口,或者在单个Mapper接口上使用 @Mapper 注解。
4. 代码规范与最佳实践
除了上述示例中的写法,为了写出更高质量的API接口,还有一些规范和最佳实践建议:
统一结果封装: 使用统一的 Result 类(如 SuccessResult 和可能的 ErrorResult)来封装所有接口的返回值,包含状态码、消息、数据等,这能提高前端对接的效率和一致性。参数校验: 在Controller层或Service层对接收的参数进行严格校验,防止非法数据。可以使用JSR 303 (Bean Validation) 结合Spring的 @Validated 注解在请求参数实体类上定义校验规则。全局异常处理: 使用 @ControllerAdvice 和 @ExceptionHandler 构建全局异常处理机制,统一处理各类运行时异常,向前端返回友好的错误信息,避免敏感信息泄露。接口文档: 使用Swagger/OpenAPI等工具生成接口文档,方便前后端协作。日志记录: 在关键位置(如Controller入口、Service方法开始/结束、异常发生时)记录日志,方便问题排查。权限控制: 在Controller方法上添加权限校验,确保只有授权用户才能访问。幂等性考虑: 对于PUT、DELETE等修改操作,考虑接口的幂等性,即多次调用产生的结果与一次调用相同。合理使用HTTP方法: 遵循RESTful风格,GET用于查询,POST用于新增,PUT用于修改,DELETE用于删除。Controller层职责“瘦”: 避免在Controller层编写复杂的业务逻辑,将其下沉到Service层。
5. 总结
本文详细介绍了在Spring Boot项目中,如何结合MyBatis编写Controller层的API接口,涵盖了模糊分页查询、批量删除、新增和编辑等常见操作。我们通过代码示例展示了 @RequestParam 和 @RequestBody 的用法、分页处理、事务管理、批量操作以及MyBatis动态SQL的应用。
编写高质量的API接口是后端开发者的基本功。理解MVC分层、掌握常用注解和技巧,并遵循良好的代码规范和最佳实践,将帮助你构建出清晰、健壮、易于维护的后端服务。
终极大法: 如同文章开头引用的那样,学习最好的方法之一确实是阅读和学习优秀的开源项目或同事的代码,在此基础上进行模仿和改进。但前提是你需要理解代码背后的原理和设计思想,而不是简单的Ctrl+C和Ctrl+V。希望本文提供的基础知识和示例能帮助你更好地理解和实践!
互动环节:
你平时写Controller接口有什么特别的习惯或技巧吗?在处理批量操作或参数校验时,你遇到过哪些坑?欢迎在评论区分享!如果觉得本文对你有帮助,别忘了点赞、收藏、评论,你的支持是我持续创作的动力!
一回生,二回熟,三回四回好朋友,这里是小猴头,下期见!