前言

从工作至今,见过 N 多个接口。

那接口,叫一个百花齐放,奇形怪状,怎么写都有。

或许 公司没有一套约定俗成的后端编码规范吧。

下面分享一下我 “多年” 编写接口经验。

返回数据格式

在和前端对接接口时,我们一般会返回 三大件数据给到前端,如下代码所示:

@Data
public class Response<T> {

    private int code;

    private String message;

    private T data;
}
  • Code:接口状态码
    • 明确接口请求成功返回时返回,如返回 0,即表示成功,返回 -1 表示失败
    • 配合 全局异常状态码输出,便于前端逻辑判断处理,通过状态码,也方便自己定位接口异常。
  • Message:错误信息
    • 如果请求成功可以返回,“成功”
    • 如果请求失败时,可以返回对应错误信息,以便前端对接
  • Data: 定义为范型,兼容多种返回数据类型,返回接口所需数据。

Controller

有的接口,入参,出参,Map 一把梭。

有的接口,Domain对象,从头传到尾。

有的接口,没有注释,返回参数一个 “R”

横批:什么玩意?

上代码看看:

// 垃圾代码
@PostMapping("/add")
public R add(Map map) {
   return R.success(service.add(map));
}

// 建议
@PostMapping("/add")
public R<UserAddVO> add(@Valid @RequestBody UserAddDTO dto) {
  return R.success(service.add(dto))
} 
  • 移除Map 入参

    • 建议改为 Json 或 Form-data 等类型入参,明确入参。
    • 团队内部定义好入参后缀,如示例代码 DTO(Data Transfer Object),明确入参值,以及类型,便于后续维护。
  • 重命名R(可选),建议命名规范。可以叫 ResponseKResponseResult等等

  • 声明 R 返回类型:如代码所示:UserAddVO,以及 返回参数后缀,如示例代码:VO(View Object)

  • @Valid 参数校验(可选),使用好@Valid 注解可以减少一些参数校验代码。

Java 是强类型语言,别写成弱类型语言,该明确的得明确,定义好,形成规范。不然新人接手项目,看到不得问候你全家。

AppService

在我们刚刚开始学Java 时,大部人网上教程,都会教到,先定义一个 AService 接口,然后在写 AServiceImpl 实现类。如下代码所示:

public interface AService {
  void a();
}

public class AServiceImpl {
  
  @Override
  public void a () {
    // dosomething...
  }
}

可能大家这样写,挺好的。面向接口编程。

但是写了好几年代码,发现这种 AService 接口,根本有点多余,后面业务代码基本没有扩展可能性,没啥用。

后续我改成直接 AService 类得了,舒服多了。如下所示:

// 建议
public class AService {
  public void a () {
    // dosomething...
  }
}

在回过头来看看,我在标题上写的是 AppService。

为啥这样写呢?

一是受限于传统的三层架构,Controller,Service、Repository / Mapper,Service层随着业务不断发展,代码会逐渐膨胀,不易于维护。

二是学习 DDD 架构模式,从中学了一个 新命名 ApplicationService,简写为 AppService。

简单来说,AppService 是门面,就是在 AppService层,不做复杂业务逻辑,只做 资源编排,具体业务细节,在 对应 Service 中处理。

我们直接看看代码吧:

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class BAppService {

    private final BRepository bRepository;
    // 省略.. service

    public Boolean do(Long userId) {
        
	User user = userRepository.findByUserId(userId);
        if (user.isMan()) {
           throws new RuntimeException("男人不能做!");
        }
      
      	List<GirlFriend> girlFriends = girlFriendRepository.findByUserId(userId);
        for (GirlFriend girlFriend : girlFriends) { 
            // 业务处理..
        }
      
      	B b = new B();
      	// 省略一些set
        bRepository.save(b);
        return true;
    }
}

如 BAppService 所示,在 do 方法里,做了很多复杂的业务逻辑。

相信大家也见过几百上千行的 Service 业务逻辑代码,恐怖如斯。

appService 关注太多业务细节,违背单一职责原则。同时写单测时也不好测试。

我们来优化一下 Service代码。

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class BAppService {

    private final BRepository bRepository;
    // 省略.. service

    public Boolean do(Long userId) {
        
	/*User user = userRepository.findByUserId(userId);
        if (user.isMan()) {
           throws new RuntimeException("男人不能做!");
        }*/
      
      	// 替换为如下代码:
      	womenUseService.check(user);
      
      	/*List<GirlFriend> girlFriends = girlFriendRepository.findByUserId(userId);
        for (GirlFriend girlFriend : girlFriends) { 
            // 业务处理..
        }*/
      	
      	// 替换为如下代码:
      	girlFriendDoService.do(userId);
      
      	B b = new B();
      	// 省略一些set
        bRepository.save(b);
        return true;
    }
}

在 BAppService类,我们抽取 womenUseService,girlFriendDoService 类,把复杂业务逻辑抽离出去,在 do 方法里,只做 womenUseService和 girlFriendDoService 的调用,即编排业务

ApplicationService更多详情也可以参考 滕云大佬文章:后端开发实践系列——领域驱动设计(DDD)编码实践

Repository / Mapper

Repository / Mapper 后续简称为:RM,我们通常写代码时,会通过 RM 查询出接口,然后进行非空校验,如下代码所示:

User user = userRepository.findByUserId(userId);
if (user == null) {
   throws new RuntimeException("用户不存在")
}

每次需要根据 userId 查询用户时,都会校验用户是否存在的操作,啊,CService 需要,又写一遍…

其实我们增加在 RM 基础上,增加多一层 Dao,隔离持久化框架,如抽取一个 UserDao 类,后续直接可以复用。

@Repository
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserDao {
  
   private final UserRepository userRepository;
  
   public User findByUserId() {
      User user = userRepository.findByUserId(userId);
      if (user == null) {
         throws new RuntimeException("用户不存在")
      }
      return user;
   }
}

编写接口文档

和前端对接,前端主要看的就是接口文档。

这里我以 Swagger 为例:

@Api(tags = "用户相关接口")	 // 增加 swagger 接口文档注解
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserController { 

  	@PostMapping("/add")
    @ApiOperation("添加用户")      // 增加 swagger 接口文档注解
    public R<UserAddVO> add(@Valid @RequestBody UserAddDTO dto) {
      return R.success(service.add(dto))
    } 
}

这里在对应接口 Controller 类上添加 @Api注解,标明这是用户相关接口集合。

然后在每个 Controller 方法,即接口,@ApiOperation注解,标明这个接口干啥的,如上代码所示是添加用户接口。

@Data
public class UserAddVO {
  
 	@ApiModelProperty("姓名") // 增加 swagger注解,标明字段意思
  	private String name;
  
	@ApiModelProperty(value = "年龄", example = "20") // 增加 swagger注解,标明字段意思
  	private int age;
		
    //...省略其他
}

写完接口后,需要还需要在返回参数,增加swagger注解,标明这些字段是啥意思。

swagger 文档,默认会根据类型生成对应默认值,如上代码所示 age 字段,会生成默认为 0,如果有需要自定义值,可以通过 example 属性标明。这样可以方便接口调试,不用每次手动构造数据。

这样前端才好对接,不然靠猜吗?

自测

写完接口后,我们还需要自己测试一下,不要急急忙忙提测,对接前端

自己都没测过,你敢保证自己写的代码没有bug吗?

一般通过 Postman 自测,当然也可以通过自己写集成测试保证接口质量

至于如何集成测试可以看我另一篇文章Java 集成测试

最后

如果是老项目代码,还是建议先别动吧 ,出事不负责 ~

但新写代码,按照如上所示编写,那么恭喜你,你已经超过很多很多程序猿了~

以上就是全部内容,希望能帮助到你~

如有不妥,欢迎指出,大家一起交流学习。

感谢阅读,下次再见。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐