SpringBoot RESTful实战

共 13768字,需浏览 28分钟

 ·

2020-01-04 23:21


本文公众号来源:美码师作者:美码师本文已收录至我的GitHub

一、目标

  1. 了解 Restful 是什么,基本概念及风格;

  2. 能使用SpringBoot 实现一套基础的 Restful 风格接口;

  3. 利用Swagger 生成清晰的接口文档。

二、Restful 入门

什么是REST 

摘自百科的定义:REST即表述性状态转移(英文:Representational State Transfer,简称REST) 是Roy Fielding博士(HTTP规范主要贡献者)在2000年的论文中提出来的一种软件架构风格。 是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。

通俗点说,REST就是一组架构约束准则;在这些准则中,有不少是利用了现有的WEB标准能力。 而最终的目的则是简化当前业务层的设计及开发工作。

Restful API 则是指符合REST架构约束的API,关于这个词在早年前其实已经非常流行,但大多数开发者对其仍然 处于观望状态,并不一定会立即采用。这个相信与当时技术社区的成熟度及氛围是密切相关。 无论如何,在微服务架构如此流行的今天,Restful API已经成为了一种必备的的标准设计风格


关键要点

理解 Restful 风格需要理解以下几点:

  • 资源

资源指的就是一个抽象的信息实体,可以是一个用户、一首歌曲、一篇文章,只要是可作为引用的对象就是资源。 每个资源通常会被映射到一个URI,通过访问这个URI可以获取到信息。

  • 资源的表述

资源表述(Representation)指的则是资源的外在表现形式 比如一个帖子,可以通过HTML格式展现,也可以通过XML、JSON等格式输出到客户端。

在前面的文章(SpringBoot-Scope详解)中提到,HTTP协议通过MIME来统一定义数据信息的格式标准。 通常,AcceptContent-Type可以用来指定客户端及服务端可接受的信息格式,而这个就是资源的表述

  • 状态转移

在HTTP访问过程中,资源的状态发生变化。这里会涉及到以下的几个动词:

名称语义
GET获取资源
POST新建资源
PUT更新资源
DELETE删除资源

对于不同的访问方法,服务器会产生对应的行为并促使资源状态产生转换。

关于无状态

Restful 是无状态的设计,这点意味着交互过程中的请求应该能包含所有需要的信息,而不需要依赖于已有的上下文。 然而 JavaEE中存在一些违背的做法,比如Cookie中设置JSESSIONID, 在多次请求间传递该值作为会话唯一标识,这标识着服务端必须保存着这些会话状态数据。

PlayFramework框架实现了无状态的Session,其将会话数据经过加密编码并置入Cookie中, 这样客户端的请求将直接携带上全部的信息,是无状态的请求**,这点非常有利于服务端的可扩展性。


三、SpringBoot 实现 Restful

接下来,我们利用 SpringBoot 来实现一个Restful 风格的样例。

说明基于 PetStore(宠物店) 的案例,实现对某顾客(Customer)名下的宠物(Pet)的增删改查。

1. 实体定义

Customer

  1. publicclassCustomer{

  2.    privateString name;

  3.    publicCustomer(){

  4.        super();

  5.    }

  6.    publicCustomer(String name){

  7.        super();

  8.        this.name = name;

  9.    }

  10.    publicString getName(){

  11.        return name;

  12.    }

  13.    publicvoid setName(String name){

  14.        this.name = name;

  15.    }

  16. }

Customer 只包含一个name属性,我们假定这是唯一的标志。

Pet

  1. publicclassPet{

  2.    privateString petId;

  3.    privateString name;

  4.    privateString type;

  5.    privateString description;

  6.    publicString getPetId(){

  7.        return petId;

  8.    }

  9.    publicvoid setPetId(String petId){

  10.        this.petId = petId;

  11.    }

  12.    publicString getName(){

  13.        return name;

  14.    }

  15.    publicvoid setName(String name){

  16.        this.name = name;

  17.    }

  18.    publicString getType(){

  19.        return type;

  20.    }

  21.    publicvoid setType(String type){

  22.        this.type = type;

  23.    }

  24.    publicString getDescription(){

  25.        return description;

  26.    }

  27.    publicvoid setDescription(String description){

  28.        this.description = description;

  29.    }

  30. }

Pet 包含了以下几个属性

属性名描述
petId宠物ID编号
name宠物名称
type宠物类型
description宠物的描述

2. URL资源

基于Restful 的原则,我们定义了以下的一组URL:

接口方法URL
添加宠物POST/rest/pets/{customer}
获取宠物列表GET/rest/pets/{customer}
获取宠物信息GET/rest/pets/{customer}/{petId}
更新宠物信息PUT/rest/pets/{customer}/{petId}
删除宠物DELETE/rest/pets/{customer}/{petId}

3. 数据管理

接下来实现一个PetManager 类,用于模拟在内存中对Pet数据进行增删改查 代码如下:

  1. @Component

  2. publicclassPetManager{

  3.    privatestaticMap<String,Customer> customers =newConcurrentHashMap<String,Customer>();

  4.    privatestaticMap<String,Map<String,Pet>> pets =newConcurrentHashMap<String,Map<String,Pet>>();

  5.    @PostConstruct

  6.    publicvoid init(){

  7.        String[] customerNames =newString[]{"Lilei","Hanmeimei","Jim Green"};

  8.        for(String customerName : customerNames){

  9.            customers.put(customerName,newCustomer(customerName));

  10.        }

  11.    }

  12.    /**

  13.     * 获取customer

  14.     *

  15.     * @param customer

  16.     * @return

  17.     */

  18.    publicCustomer getCustomer(String customer){

  19.        if(StringUtils.isEmpty(customer)){

  20.            returnnull;

  21.        }

  22.        return customers.get(customer);

  23.    }

  24.    /**

  25.     * 获取customer名下的 pet 列表

  26.     *

  27.     * @param customer

  28.     * @return

  29.     */

  30.    publicList<Pet> getPets(String customer){

  31.        if(StringUtils.isEmpty(customer)){

  32.            returnCollections.emptyList();

  33.        }

  34.        if(!pets.containsKey(customer)){

  35.            returnCollections.emptyList();

  36.        }

  37.        return pets.get(customer).values().stream().collect(Collectors.toList());

  38.    }

  39.    /**

  40.     * 获取某个pet

  41.     *

  42.     * @param customer

  43.     * @param petId

  44.     * @return

  45.     */

  46.    publicPet getPet(String customer,String petId){

  47.        if(StringUtils.isEmpty(customer)||StringUtils.isEmpty(petId)){

  48.            returnnull;

  49.        }

  50.        if(!pets.containsKey(customer)){

  51.            returnnull;

  52.        }

  53.        return pets.get(customer).get(petId);

  54.    }

  55.    /**

  56.     * 删除pet

  57.     *

  58.     * @param customer

  59.     * @param petId

  60.     * @return

  61.     */

  62.    publicboolean removePet(String customer,String petId){

  63.        if(StringUtils.isEmpty(customer)||StringUtils.isEmpty(petId)){

  64.            returnfalse;

  65.        }

  66.        if(!pets.containsKey(customer)){

  67.            returnfalse;

  68.        }

  69.        return pets.get(customer).remove(petId)!=null;

  70.    }

  71.    /**

  72.     * 添加pet

  73.     *

  74.     * @param customer

  75.     * @param pet

  76.     * @return

  77.     */

  78.    publicPet addPet(String customer,Pet pet){

  79.        if(StringUtils.isEmpty(customer)|| pet ==null){

  80.            returnnull;

  81.        }

  82.        Map<String,Pet> customerPets =null;

  83.        if(!pets.containsKey(customer)){

  84.            customerPets =newLinkedHashMap<String,Pet>();

  85.            Map<String,Pet> previous = pets.putIfAbsent(customer, customerPets);

  86.            // 已经存在

  87.            if(previous !=null){

  88.                customerPets = previous;

  89.            }

  90.        }else{

  91.            customerPets = pets.get(customer);

  92.        }

  93.        if(pet.getPetId()==null){

  94.            pet.setPetId(UUID.randomUUID().toString());

  95.        }

  96.        customerPets.put(pet.getPetId(), pet);

  97.        return pet;

  98.    }

  99.    /**

  100.     * 更新某个pet

  101.     *

  102.     * @param customer

  103.     * @param petPojo

  104.     * @return

  105.     */

  106.    publicPet updatePet(String customer,Pet petPojo){

  107.        if(StringUtils.isEmpty(customer)|| petPojo ==null){

  108.            returnnull;

  109.        }

  110.        if(petPojo.getPetId()==null){

  111.            returnnull;

  112.        }

  113.        Pet pet = getPet(customer, petPojo.getPetId());

  114.        pet.setType(petPojo.getType());

  115.        pet.setName(petPojo.getName());

  116.        pet.setDescription(petPojo.getDescription());

  117.        return pet;

  118.    }

  119. }


4. 控制层实现

SpringBoot 提供了 @RestController,用于快速定义一个Restful 风格的Controller类@RestController=@ResponseBody + @Controller

  1. @RestController

  2. @RequestMapping("/rest/pets/{customer}")

  3. publicclassRestApiController{

  4.    @Autowired

  5.    privatePetManager dataManager;

  6.    /**

  7.     * 添加宠物

  8.     *

  9.     * @param customer

  10.     * @param pet

  11.     * @return

  12.     */

  13.    @PostMapping

  14.    publicResponseEntity<Object> addPet(@PathVariableString customer,@RequestBodyPet pet){

  15.        validateCustomer(customer);

  16.        Pet newPet = dataManager.addPet(customer, pet);

  17.        // 返回 201.created

  18.        if(newPet !=null){

  19.            URI location =ServletUriComponentsBuilder.fromCurrentRequest().path("/{petId}")

  20.                    .buildAndExpand(newPet.getPetId()).toUri();

  21.            returnResponseEntity.created(location).build();

  22.        }

  23.        // 返回 204.noContent

  24.        returnResponseEntity.noContent().build();

  25.    }

  26.    /**

  27.     * 获取宠物列表

  28.     *

  29.     * @param customer

  30.     * @return

  31.     */

  32.    @GetMapping

  33.    @ResponseBody

  34.    publicList<Pet> listPets(@PathVariableString customer){

  35.        validateCustomer(customer);

  36.        List<Pet> pets = dataManager.getPets(customer);

  37.        return pets;

  38.    }

  39.    /**

  40.     * 获取某个宠物

  41.     *

  42.     * @param customer

  43.     * @param petId

  44.     */

  45.    @GetMapping("/{petId}")

  46.    @ResponseBody

  47.    publicPet getPet(@PathVariableString customer,@PathVariableString petId){

  48.        validateCustomer(customer);

  49.        validatePet(customer, petId);

  50.        Pet pet = dataManager.getPet(customer, petId);

  51.        return pet;

  52.    }

  53.    /**

  54.     * 更新宠物信息

  55.     *

  56.     * @param customer

  57.     * @param petId

  58.     * @param pet

  59.     */

  60.    @PutMapping("/{petId}")

  61.    publicResponseEntity<Object> updatePet(@PathVariableString customer,@PathVariableString petId,@RequestBodyPet pet){

  62.        validateCustomer(customer);

  63.        validatePet(customer, petId);

  64.        pet.setPetId(petId);

  65.        Pet petObject = dataManager.updatePet(customer, pet);

  66.        if(petObject !=null){

  67.            returnResponseEntity.ok(petObject);

  68.        }

  69.        returnResponseEntity.noContent().build();

  70.    }

  71.    /**

  72.     * 删除某个宠物

  73.     *

  74.     * @param customer

  75.     * @param petId

  76.     * @return

  77.     */

  78.    @DeleteMapping("/{petId}")

  79.    publicResponseEntity<Object> removePet(@PathVariableString customer,@PathVariableString petId){

  80.        validateCustomer(customer);

  81.        validatePet(customer, petId);

  82.        dataManager.removePet(customer, petId);

  83.        returnResponseEntity.ok().build();

  84.    }

上述代码中已经实现了完整的增删改查语义。 在Restful 风格的API 接口定义中,往往会引用 HTTP 状态码用于表示不同的结果,比如一些错误的状态类型。

这里我们对Customer、Pet 进行存在性校验,若资源不存在返回404_NotFound。

  1.    /**

  2.     * 校验customer是否存在

  3.     *

  4.     * @param customer

  5.     */

  6.    privatevoid validateCustomer(String customer){

  7.        if(dataManager.getCustomer(customer)==null){

  8.            thrownewObjectNotFoundException(String.format("the customer['%s'] is not found", customer));

  9.        }

  10.    }

  11.    /**

  12.     * 校验pet是否存在

  13.     *

  14.     * @param customer

  15.     */

  16.    privatevoid validatePet(String customer,String petId){

  17.        if(dataManager.getPet(customer, petId)==null){

  18.            thrownewObjectNotFoundException(String.format("the pet['%s/%s'] is not found", customer, petId));

  19.        }

  20.    }

自定义异常拦截

  1.    /**

  2.     * 自定义异常,及拦截逻辑

  3.     *

  4.     * @author atp

  5.     *

  6.     */

  7.    @SuppressWarnings("serial")

  8.    publicstaticclassObjectNotFoundExceptionextendsRuntimeException{

  9.        publicObjectNotFoundException(String msg){

  10.            super(msg);

  11.        }

  12.    }

  13.    @ResponseBody

  14.    @ExceptionHandler(ObjectNotFoundException.class)

  15.    @ResponseStatus(HttpStatus.NOT_FOUND)

  16.    publicString objectNotFoundExceptionHandler(ObjectNotFoundException ex){

  17.        return ex.getMessage();

  18.    }


5. 接口验证

1. 添加宠物

URLPOST http://{{server}}/rest/pets/LiLei请求内容

  1. {

  2. "name":"Smart Baby",

  3. "description":"very small and smart also.",

  4. "type":"Dog"

  5. }

返回示例

  1. 201 created

  2. Content-Length0

  3. DateMon,09Jul201805:15:01 GMT

  4. Locationhttp://localhost:8090/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543

2. 获取宠物列表

URLGET http://{{server}}/rest/pets/LiLei请求内容

返回示例

  1. 200 OK

  2. Content-Typeapplication/json;charset=UTF-8

  3. DateMon,09Jul201805:23:27 GMT

  4. Transfer-Encodingchunked

  5. [

  6.    {

  7.        "petId":"b5400334-e7b3-42f1-b192-f5e7c3193543",

  8.        "name":"Smart Baby",

  9.        "type":"Dog",

  10.        "description":"very small and smart also."

  11.    },

  12.    {

  13.        "petId":"610780af-94f1-4011-a175-7a0f3895163d",

  14.        "name":"Big Cat",

  15.        "type":"Cat",

  16.        "description":"very old but I like it."

  17.    }

  18. ]

3. 查询宠物信息

URLGET http://{{server}}/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543请求内容

返回示例

  1. 200 OK

  2. Content-Typeapplication/json;charset=UTF-8

  3. DateMon,09Jul201805:25:24 GMT

  4. Transfer-Encodingchunked

  5. {

  6.    "petId":"b5400334-e7b3-42f1-b192-f5e7c3193543",

  7.    "name":"Smart Baby",

  8.    "type":"Dog",

  9.    "description":"very small and smart also."

  10. }

4. 更新宠物信息

URLPUT http://{{server}}/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543请求内容

  1. {

  2. "name":"Big Cat V2",

  3. "description":"I don't like it any more",

  4. "type":"Cat"

  5. }

返回示例

  1. 200 OK

  2. Content-Typeapplication/json;charset=UTF-8

  3. DateMon,09Jul201805:31:28 GMT

  4. Transfer-Encodingchunked

  5. {

  6.    "petId":"a98e4478-e754-4969-851b-bcaccd67263e",

  7.    "name":"Big Cat V2",

  8.    "type":"Cat",

  9.    "description":"I don't like it any more"

  10. }

5. 删除宠物

URLDELETE http://{{server}}/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543请求内容

返回示例

  1. 200 OK

  2. Content-Length0

  3. DateMon,09Jul201805:32:51 GMT

相关出错

  • 客户不存在:404 the customer['test'] is not found

  • 宠物不存在:404 the pet['LiLei/b5400334-e7b3-42f1-b192-f5e7c31935431'] is not found

四、Swagger 的使用

关于Swagger

Swagger是目前非常流行的一个API设计开发框架(基于OpenApi), 可用于API的设计、管理、代码生成以及Mock测试等。

目前Swagger的应用非常广,其涵盖的开源模块也比较多,这里将使用swagger-ui实现API在线DOC的生成。

引入依赖

  1.        

  2.            io.springfox

  3.            springfox-swagger2

  4.            2.7.0

  5.        

  6.        

  7.            io.springfox

  8.            springfox-swagger-ui

  9.            2.7.0

  10.        

定义API配置

  1. @EnableSwagger2

  2. @Configuration

  3. publicclassSwaggerConfig{

  4.    publicstaticfinalString VERSION ="1.0.0";

  5.    @Value("${swagger.enable}")

  6.    privateboolean enabled;

  7.    ApiInfo apiInfo(){

  8.        returnnewApiInfoBuilder().

  9.                title("Pet Api Definition")

  10.                .description("The Petstore CRUD Example")

  11.                .license("Apache 2.0")

  12.                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")

  13.                .termsOfServiceUrl("")

  14.                .version(VERSION)

  15.                .contact(newContact("","","zalesfoo@163.com"))

  16.                .build();

  17.    }

  18.    @Bean

  19.    publicDocket customImplementation(){

  20.        returnnewDocket(DocumentationType.SWAGGER_2).select()

  21.                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))

  22.                .build()

  23.                .enable(enabled)

  24.                .apiInfo(apiInfo());

  25.    }

  26. }

@EnableSwagger2声明了Swagger的启用,Docket的Bean定义是API配置的入口, 可以设置API名称、版本号,扫描范围等。

声明API描述

在原有的Controller 方法上添加关于API的声明,如下:

  1. @Api(value ="Pet Restful api")

  2. @RestController

  3. @RequestMapping("/rest/pets/{customer}")

  4. publicclassRestApiController{

  5.    @ApiOperation("添加宠物")

  6.    @ApiImplicitParams({

  7.            @ApiImplicitParam(paramType ="path", name ="customer", dataType ="String", required =true, value ="客户名", defaultValue =""),

  8.            @ApiImplicitParam(paramType ="body", name ="pet", dataType ="Pet", required =true, value ="pet 请求", defaultValue ="")})

  9.    @ApiResponses({

  10.        @ApiResponse(code =201, message ="添加成功"),

  11.        @ApiResponse(code =404, message ="资源不存在")

  12.    })

  13.    @PostMapping

  14.    publicResponseEntity<Object> addPet(@PathVariableString customer,@RequestBodyPet pet){

  15.        ...

为了能描述返回对象的文档说明,为Pet类做API声明:

  1. @ApiModel("宠物信息")

  2. publicclassPet{

  3.    @ApiModelProperty(name="petId", value="宠物ID")

  4.    privateString petId;

  5.    @ApiModelProperty(name="name", value="宠物名称")

  6.    privateString name;

  7.    @ApiModelProperty(name="type", value="宠物类型")

  8.    privateString type;

  9.    @ApiModelProperty(name="description", value="宠物描述")

  10.    privateString description;

相关的注解:

注解描述
@ApiModelProperty用在出入参数对象的字段上
@Api用于controller类
@ApiOperation用于controller方法,描述操作
@ApiResponses用于controller方法,描述响应
@ApiResponse用于@ApiResponses内,描述单个响应结果
@ApiImplicitParams用于controller的方法,描述入参
@ApiImplicitParam用于@ApiImplicitParams内,描述单个入参
@ApiModel用于返回对象类

访问文档

最后,访问 http://localhost:8000/swagger_ui.html,可看到生成的文档界面:

bf991dc04c675e6bc7f2b59a25d408b5.webp


欢迎加入交流群学习,备注加群说实话在这个群,哪怕您不说话,光看聊天记录,都能学到东西




两年呕心沥血的文章「面试题」「基础」「进阶」这里全都有!

300多篇原创技术文章加入交流群学习海量视频资源精美脑图面试题

长按扫码可关注获取 

在看和分享对我非常重要!a2749161df7dffaa68a479835e46a51d.webp

浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报