水进的Java开发岗,试用期求生之 如何写一个接口

水进的Java开发岗,试用期求生之 如何写一个接口

手把手教你写接口(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 list = warehouseOutService.getOutboundEquipmentInfo(equipAttribute, equipTypeId, warehouseId, operationType, keywords);

// 将查询结果封装到PageInfo中,PageInfo包含了丰富的分页信息

PageInfo pageInfo = new PageInfo<>(list);

// 返回统一的结果封装对象

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 list = warehouseOutService.getOutboundEquipmentInfo(vo); // Service方法签名也需相应调整

PageInfo pageInfo = new PageInfo<>(list);

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 getOutboundEquipmentInfo(String equipAttribute,

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 getOutboundEquipmentInfo(EquipInfoVo vo) {

// 此处包含较复杂的业务逻辑,根据 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 outList = wmEquipInfoMapper.getEquipmentInformation(equipAttribute, equipTypeId, keywords);

List allEquip = wmRepertoryInfoMapper.getAllEquip(warehouseId);

// 数据组装逻辑... (此处代码较长,为保持文章重点,省略部分细节,读者可参考原文或自行实现)

outList.forEach(info->{

Optional wmRepertoryInfoDto = allEquip.stream().filter(all -> all.getEquipId().equals(info.getId())).findFirst();

if (wmRepertoryInfoDto.isPresent()){

info.setInventoryQuantity(wmRepertoryInfoDto.get().getNum());

info.setDamagesNum(wmRepertoryInfoDto.get().getDamagesNum());

}else {

info.setInventoryQuantity(0);

info.setDamagesNum(0);

}

});

return outList;

} else { // 假设默认为出库操作或其他情况

// 出库操作相关查询和处理

List outboundEquipmentInfo = wmEquipInfoMapper.getOutboundEquipmentInfo(equipAttribute, warehouseId, equipTypeId, keywords);

List relationEquipDtos = wmOutWarehouseMapper.getOutboundApproval(warehouseId);

// 数据组装逻辑... (此处代码较长,为保持文章重点,省略部分细节,读者可参考自行实现)

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 getOutboundEquipmentInfo(

@Param("equipAttribute") String equipAttribute,

@Param("warehouseId") Integer warehouseId,

@Param("equipTypeId") Integer equipTypeId,

@Param("keywords") String keywords);

// 示例中 Service 层入库操作用到的另一个查询方法 (补充)

List getEquipmentInformation(@Param("equipAttribute") String equipAttribute,

@Param("equipTypeId") Integer equipTypeId,

@Param("keywords") String keywords);

}

Mapper XML 实现:

MyBatis XML 文件定义了 SQL 语句的具体实现。

注意点:

PageHelper.startPage(pageNum, pageSize); 必须紧跟在它之后的第一个MyBatis查询方法才会生效。PageInfo 是一个非常方便的类,包含了总记录数、总页数、当前页数据等信息。MyBatis XML 中的 标签用于动态拼接SQL。字符串类型的判断通常是 !=null and != ''。Integer 类型的判断只用 != null,这是容易出错的地方!

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 idList = Arrays.stream(ids.split(","))

.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。@Transactional 注解:用于保证Service方法内的多个数据库操作(或单个复杂的Mapper操作)要么全部成功,要么全部失败回滚。在批量删除或涉及到多个表的操作时尤其重要。

Mapper层接口定义:

public interface WarehouseInfoMapper {

// 批量删除方法,@Param 用于给参数起别名,方便在XML中引用

Integer deleteByIds(@Param("idList") List idList,

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

注意点:

使用 标签,因为是修改数据(设置 removed 字段)。:遍历传入的 idList 集合,每个元素命名为 item,每条SQL语句之间用分号 ; 分隔。where id = #{item}:这里的 item 就是 List 中的一个整数值,直接用 # {item} 引用即可。如果传递的是一个包含ID属性的对象列表,才可能使用 #{item.id}。

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 wmRelationEquipDtos = dto.getRelationEquip();

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 batchList : lists) {

result += relationEquipMapper.insertBatch(batchList); // 批量插入一批数据

}

} else {

// 数据量小,直接批量插入

result = relationEquipMapper.insertBatch(wmRelationEquipDtos);

}

} else {

// 没有关联设备,新增结果取决于主表插入

result = (purchaseId != null) ? 1 : 0;

}

return result >= 1 ? 1 : 0; // 返回操作结果标识

}

// 假设的分批工具方法

private List> splitList(List list, int splitCount) {

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 XML 中实现批量插入。批量插入数量限制: 务必注意数据库和驱动对单条SQL语句长度的限制。如果批量插入的数据量可能很大,一定要在Service层进行分批处理。提供了 splitList 工具方法的示例。

Mapper层接口定义:

public interface WmPurchaseInfoMapper {

// 插入采购基本信息

Integer insertSelective(WmPurchaseInfoAddVo addVo); // 参数名 addVo 用于在XML中引用

// ... 其他方法

}

public interface WmRelationEquipMapper {

// 批量插入关联设备信息

Integer insertBatch(@Param("dtoList") List dtoList); // @Param("dtoList") 指定集合别名

}

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通常能自动识别)。:用于在生成的SQL语句前面加上 (,后面加上 ),并移除最后一个字段或值后面的逗号 ,。这使得动态SQL的拼接更方便。

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()

)

注意点:

:遍历名为 dtoList 的集合(通过 @Param("dtoList") 指定),每个元素命名为 item,每个 (...) 之间用逗号 , 分隔。这种方式生成的是一条很长的 INSERT INTO ... VALUES (...), (...), ... SQL 语句,因此有长度限制。

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。 标签:用于在动态更新时,如果前面的所有 都没有匹配成功,会移除掉逗号。如果匹配成功至少一个 ,它会在最后自动加上逗号(如果需要的话)。:只更新DTO中非空的字段。where id = #{id,jdbcType=INTEGER}:通过主键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 注解。