分享

SpringBoot RESTful实战

 oldzhoua 2020-01-04
本文公众号来源:美码师
作者:美码师
本文已收录至我的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. public class Customer {


  2.    private String name;


  3.    public Customer() {

  4.        super();

  5.    }


  6.    public Customer(String name) {

  7.        super();

  8.        this.name = name;

  9.    }


  10.    public String getName() {

  11.        return name;

  12.    }


  13.    public void setName(String name) {

  14.        this.name = name;

  15.    }


  16. }

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

Pet

  1. public class Pet {


  2.    private String petId;

  3.    private String name;

  4.    private String type;

  5.    private String description;


  6.    public String getPetId() {

  7.        return petId;

  8.    }


  9.    public void setPetId(String petId) {

  10.        this.petId = petId;

  11.    }


  12.    public String getName() {

  13.        return name;

  14.    }


  15.    public void setName(String name) {

  16.        this.name = name;

  17.    }


  18.    public String getType() {

  19.        return type;

  20.    }


  21.    public void setType(String type) {

  22.        this.type = type;

  23.    }


  24.    public String getDescription() {

  25.        return description;

  26.    }


  27.    public void 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. public class PetManager {


  3.    private static Map<String, Customer> customers = new ConcurrentHashMap<String, Customer>();

  4.    private static Map<String, Map<String, Pet>> pets = new ConcurrentHashMap<String, Map<String, Pet>>();


  5.    @PostConstruct

  6.    public void init() {

  7.        String[] customerNames = new String[] { 'Lilei', 'Hanmeimei', 'Jim Green' };


  8.        for (String customerName : customerNames) {

  9.            customers.put(customerName, new Customer(customerName));

  10.        }

  11.    }


  12.    /**

  13.     * 获取customer

  14.     *

  15.     * @param customer

  16.     * @return

  17.     */

  18.    public Customer getCustomer(String customer) {

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

  20.            return null;

  21.        }

  22.        return customers.get(customer);

  23.    }


  24.    /**

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

  26.     *

  27.     * @param customer

  28.     * @return

  29.     */

  30.    public List<Pet> getPets(String customer) {

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

  32.            return Collections.emptyList();

  33.        }


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

  35.            return Collections.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.    public Pet getPet(String customer, String petId) {

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

  48.            return null;

  49.        }


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

  51.            return null;

  52.        }

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

  54.    }


  55.    /**

  56.     * 删除pet

  57.     *

  58.     * @param customer

  59.     * @param petId

  60.     * @return

  61.     */

  62.    public boolean removePet(String customer, String petId) {

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

  64.            return false;

  65.        }


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

  67.            return false;

  68.        }

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

  70.    }


  71.    /**

  72.     * 添加pet

  73.     *

  74.     * @param customer

  75.     * @param pet

  76.     * @return

  77.     */

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

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

  80.            return null;

  81.        }


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

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


  84.            customerPets = new LinkedHashMap<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.    public Pet updatePet(String customer, Pet petPojo) {

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

  108.            return null;

  109.        }


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

  111.            return null;

  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. public class RestApiController {


  4.    @Autowired

  5.    private PetManager dataManager;


  6.    /**

  7.     * 添加宠物

  8.     *

  9.     * @param customer

  10.     * @param pet

  11.     * @return

  12.     */

  13.    @PostMapping

  14.    public ResponseEntity<Object> addPet(@PathVariable String customer, @RequestBody Pet 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.            return ResponseEntity.created(location).build();

  22.        }


  23.        // 返回 204.noContent

  24.        return ResponseEntity.noContent().build();

  25.    }


  26.    /**

  27.     * 获取宠物列表

  28.     *

  29.     * @param customer

  30.     * @return

  31.     */

  32.    @GetMapping

  33.    @ResponseBody

  34.    public List<Pet> listPets(@PathVariable String 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.    public Pet getPet(@PathVariable String customer, @PathVariable String 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.    public ResponseEntity<Object> updatePet(@PathVariable String customer, @PathVariable String petId, @RequestBody Pet pet) {

  62.        validateCustomer(customer);

  63.        validatePet(customer, petId);


  64.        pet.setPetId(petId);

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

  66.        if (petObject != null) {

  67.            return ResponseEntity.ok(petObject);

  68.        }


  69.        return ResponseEntity.noContent().build();


  70.    }


  71.    /**

  72.     * 删除某个宠物

  73.     *

  74.     * @param customer

  75.     * @param petId

  76.     * @return

  77.     */

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

  79.    public ResponseEntity<Object> removePet(@PathVariable String customer, @PathVariable String petId) {

  80.        validateCustomer(customer);

  81.        validatePet(customer, petId);


  82.        dataManager.removePet(customer, petId);

  83.        return ResponseEntity.ok().build();

  84.    }

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

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

  1.    /**

  2.     * 校验customer是否存在

  3.     *

  4.     * @param customer

  5.     */

  6.    private void validateCustomer(String customer) {

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

  8.            throw new ObjectNotFoundException(String.format('the customer['%s'] is not found', customer));

  9.        }

  10.    }


  11.    /**

  12.     * 校验pet是否存在

  13.     *

  14.     * @param customer

  15.     */

  16.    private void validatePet(String customer, String petId) {

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

  18.            throw new ObjectNotFoundException(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.    public static class ObjectNotFoundException extends RuntimeException {


  9.        public ObjectNotFoundException(String msg) {

  10.            super(msg);

  11.        }

  12.    }


  13.    @ResponseBody

  14.    @ExceptionHandler(ObjectNotFoundException.class)

  15.    @ResponseStatus(HttpStatus.NOT_FOUND)

  16.    public String 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-Length →0

  3. Date →Mon, 09 Jul 2018 05:15:01 GMT

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

2. 获取宠物列表

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

  1. <Empty>

返回示例

  1. 200 OK

  2. Content-Type →application/json;charset=UTF-8

  3. Date →Mon, 09 Jul 2018 05:23:27 GMT

  4. Transfer-Encoding →chunked

  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. <Empty>

返回示例

  1. 200 OK

  2. Content-Type →application/json;charset=UTF-8

  3. Date →Mon, 09 Jul 2018 05:25:24 GMT

  4. Transfer-Encoding →chunked

  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-Type →application/json;charset=UTF-8

  3. Date →Mon, 09 Jul 2018 05:31:28 GMT

  4. Transfer-Encoding →chunked

  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. <empty>

返回示例

  1. 200 OK

  2. Content-Length →0

  3. Date →Mon, 09 Jul 2018 05: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.        <dependency>

  2.            <groupId>io.springfox</groupId>

  3.            <artifactId>springfox-swagger2</artifactId>

  4.            <version>2.7.0</version>

  5.        </dependency>



  6.        <dependency>

  7.            <groupId>io.springfox</groupId>

  8.            <artifactId>springfox-swagger-ui</artifactId>

  9.            <version>2.7.0</version>

  10.        </dependency>

定义API配置

  1. @EnableSwagger2

  2. @Configuration

  3. public class SwaggerConfig {



  4.    public static final String VERSION = '1.0.0';


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

  6.    private boolean enabled;


  7.    ApiInfo apiInfo() {

  8.        return new ApiInfoBuilder().

  9.                title('Pet Api Definition')

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

  11.                .license('Apache 2.0')

  12.                .licenseUrl('http://www./licenses/LICENSE-2.0.html')

  13.                .termsOfServiceUrl('')

  14.                .version(VERSION)

  15.                .contact(new Contact('', '', 'zalesfoo@163.com'))

  16.                .build();

  17.    }


  18.    @Bean

  19.    public Docket customImplementation() {

  20.        return new Docket(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. public class RestApiController {


  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.    public ResponseEntity<Object> addPet(@PathVariable String customer, @RequestBody Pet pet) {

  15.        ...

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

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

  2. public class Pet {


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

  4.    private String petId;


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

  6.    private String name;


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

  8.    private String type;


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

  10.    private String description;

相关的注解:

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

访问文档

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多