任何时候我们都不能避免偏见,即便是讨论技术也是如此,并没有一种技术真的能绝对。

英国作家斯威夫特在《格列佛游记》里以戏谑的语调所描述的大头派和小头派(其争论焦点是吃鸡蛋剥皮时究竟应该从大头敲破还是从小头敲破)原本是为了讥讽英国议会里那些因小题大做而分党立派的荒唐议员,然而事实上我们每个人都是大头派或小头派的一员,只是在某些问题上属于大头派,在另一些问题上属于小头派而已。我们必须承认的是,斯威夫特的寓言所揭示的意义远比作者的意图本身更为深刻。

定义Web API规范

Web开发中,API开发是程序员们非常熟悉的,而使用SpringBoot构建Web API有几种选择,要么使用spring-boot-starter-jersey构建RESTful风格的Web API,要么选择spring-boot-starter-hateoas构建更加有关联性和相对“智能”的Web API,但是,引入任何一种框架都会带来学习成本,如果是一个人开发问题不大,而如果是团队协作开发,沟通成本就会很高。

所以,我们可以选择相对宽松的RPC over HTTP的方式来构建Web API。我们先定义一个简单的json响应格式:

{
    "code" : 0,
    "msg" : "XXXXX",
    "data" : { ... }
}

其中,code表示调用结果的状态,0表示成功,非0表示失败,并且可以根据状态传入需要提示的字段,data字段用于规范定义特定于Web API的响应内容。

实战

新建Spring Boot项目

我们就可以根据规范来构建我们的API了,参考boot-start,或者使用http://start.spring.io构建新项目,并依赖spring-boot-starter-web模块。

示例服务

这里我们创建一个简单的查询服务QueryUserService,如下代码:

@Service
public class QueryUserService {
    public List<Man> query(){
        Man man1 = new Man("mickjoust","1","111111");
        Man man2 = new Man("mia","1","6666666");
        Man man3 = new Man("max","1","33333333");
        List<Man> manList = new ArrayList<>();
        manList.add(man1);
        manList.add(man2);
        manList.add(man3);
        return manList;
    }
}

查询的POJO对象:

public class Man {

    private String name;
    private String age;
    private String phone;

    public Man(String name, String age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
}
//同样省略get、set

提供API

使用Spring技术栈,我们能够快速的构建Web API,这里创建一个API Controller:

@RestController
@SpringBootApplication
public class QueryUserController {

    public static void main(String[] args) {
        SpringApplication.run(QueryUserController.class,args);
    }

    @Autowired
    private QueryUserService queryUserService;

    @RequestMapping("/query1")
    public List<Man> getUsers(){
        return queryUserService.query();
    }
}

其中,@RestController表示这个是一个REST风格的API。

加强约束

到上小节,我们已经能够向外提供一个具有查询用户服务能力的Web API了,但等一下,这时返回的响应数据里似乎并没有我们前面定义的规范,于是,我们这样扩展代码:

@RequestMapping("/query2")
public Map<String,Object> getUsersExt(){
    Map<String,Object> resultMap = new HashMap<>();
    if (null != queryUserService.query()
            || !queryUserService.query().isEmpty()){
        resultMap.put("code",0);
        resultMap.put("msg","sucess");
        resultMap.put("data",queryUserService.query());
    } else {
        resultMap.put("code",1);
        resultMap.put("msg","error");
        resultMap.put("data","");
    }
    return resultMap;
}

不过,这是最简单但最错误的做法,因为下一个开发人员来看代码时,就会大骂这样的一次性代码难于维护。

要开发符合我们的Web API规范的Web API,有两种方案可以选择——

  1. 显式的强类型封装方式(explicit type wrapper)
  2. 隐式的自动转换方式(implicit conversion)

针对第一点,spring-boot-starter-web已经提供了MappingJackson2HttpMessageConverter用于对象类型到JSON的类型转换,不过鉴于很多人喜欢用fastjson,参考文章《Spring Boot 实践折腾记(五):自定义配置,扩展Spring MVC配置并使用fastjson》来进行Converter配置,那么,我们只要提供一个响应类即可,比如:

public class ApiResponse<T> {

    public static final int SUCCESS_CODE = 0;
    public static final int ERROR_CODE = 1;
    private int code; //0-成功,1错误
    private String msg;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(SUCCESS_CODE);
        response.setData(data);
        response.setMsg("sucess");
        return response;
    }

    public static <T> ApiResponse<T> error(String error,T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(ERROR_CODE);
        response.setData(data);//注意这里的该怎么处理?对于移动端来说,如果抛null会直接导致app退出
        response.setMsg((null==error||"".equals(error)?"error":error));
        return response;
    }
//我就是不写get、set

然后,所有的Web API的处理方法统一定义为返回ApiResponse作为结果类型:

@RequestMapping("/query3")
public ApiResponse<List<Man>> getUsers3(){
    return ApiResponse.success(queryUserService.query());
}

不过,这种模式过于强调规范的管控,对开发者来说不是太友好(不过架构师都很喜欢,因为好管控),但从API的使用者角度来看,这种设计并非最优,最好的方式其实应该是隐式的自动转换方式。

这里要注意一点,不要直接继承AbstractHttpMessage-Converter<Object>,在定制的同时打破了Boot对MVC的良好支持,而有时想要支持视图就会出现问题,毕竟每个人的选择都是有所偏见的。

在隐式的自动转换方式下,用户的Web API处理方法定义保持不变,直接返回最原始的值类型(比如:List),具体实现可以参考boot-properties里的代码实现,但原来的代码是基于1.0写的,在boot2.0里WebMvcConfigurerAdapter已被废弃,原因是因为适配的接口WebMvcConfigurer在java8里,接口可以提供默认实现方法,不需要单独包装一层。

API的一个难题

写原生API虽然方便,特别是现在有Spring Boot后更是方便,但同样有最核心的问题没有解决,就是API文档,API的一个作用除了提供对应功能,还需要使用告诉使用者该怎么使用(对于很多开发同学来说,面对大量繁琐的说明文档的,曾经受伤过的人,举手!)

而一个难题是,不同团队间都在使用API时,就设计到接口修改和文档修改后的同步,这是很让人头疼的一件事。

为了缓解这个难题,现在出现了很多API Doc和API Mock类库,可以方便我们使用,比如,swagger,有兴趣的可以查看这篇文章来构建swagger《Spring Boot 实践折腾记(七):使用Swagger 2构建RESTful API文档》。

小结

本文主要介绍了根据一定规范来构建Web API的方法,这种方法不一定是最优的,但提供了一种构建API的“笨办法”,同时实战了一个简单的boot应用,以及API的短板和不足之处和缓解办法。如何在约束性和便捷性间平衡,是一个值得思考的问题。

最后 关于boot中的MVC配置,,spring-boot-starter-web默认将为我们一些Spring MVC必要组件进行了自动配置,如下:

-1、必要的ViewResolver,比如ContentNegotiatingViewResolverBean-NameViewResolver
-2、将必要的ConverterGenericConverterFormatter等bean注册到IoC容器。
-3、添加一系列的HttpMessageConverter以便支持对Web请求和相应的类型转换。
-4、自动配置和注册MessageCodesResolver

请记住,任何时候构建项目,我们都是可以选择的,比如对默认提供的SpringMVC组件设定不满意,都可以在IoC容器中注册新的同类型的bean定义来替换,或者直接提供一个基于WebMvcConfigurerAdapter类型的bean定义来定制,也可以提供一个标注了@EnableWebMvc@Configuration配置类来完全接管所有SpringMVC的相关配置,自己完全重新配置。

参考资源

1、Spring Boot官网
2、《Spring Boot 揭秘》


持续践行,我们一路同行。