补习系列(5)-springboot-restful应用实战

简介: 一、目标 了解 Restful 是什么,基本概念及风格; 能使用SpringBoot 实现一套基础的 Restful 风格接口; 利用Swagger 生成清晰的接口文档。 二、Restful 入门 什么是REST 摘自百科的定义:REST即表述性状态转移(英文:Representational State Transfer,简称REST)是Roy Fielding博士(HTTP规范主要贡献者)在2000年的论文中提出来的一种软件架构风格。

一、目标

  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

public class Customer {

    private String name;

    public Customer() {
        super();
    }

    public Customer(String name) {
        super();
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

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

Pet

public class Pet {

    private String petId;
    private String name;
    private String type;
    private String description;

    public String getPetId() {
        return petId;
    }

    public void setPetId(String petId) {
        this.petId = petId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

}

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数据进行增删改查
代码如下:

@Component
public class PetManager {

    private static Map<String, Customer> customers = new ConcurrentHashMap<String, Customer>();
    private static Map<String, Map<String, Pet>> pets = new ConcurrentHashMap<String, Map<String, Pet>>();

    @PostConstruct
    public void init() {
        String[] customerNames = new String[] { "Lilei", "Hanmeimei", "Jim Green" };

        for (String customerName : customerNames) {
            customers.put(customerName, new Customer(customerName));
        }
    }

    /**
     * 获取customer
     * 
     * @param customer
     * @return
     */
    public Customer getCustomer(String customer) {
        if (StringUtils.isEmpty(customer)) {
            return null;
        }
        return customers.get(customer);
    }

    /**
     * 获取customer名下的 pet 列表
     * 
     * @param customer
     * @return
     */
    public List<Pet> getPets(String customer) {
        if (StringUtils.isEmpty(customer)) {
            return Collections.emptyList();
        }

        if (!pets.containsKey(customer)) {
            return Collections.emptyList();
        }

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

    /**
     * 获取某个pet
     * 
     * @param customer
     * @param petId
     * @return
     */
    public Pet getPet(String customer, String petId) {
        if (StringUtils.isEmpty(customer) || StringUtils.isEmpty(petId)) {
            return null;
        }

        if (!pets.containsKey(customer)) {
            return null;
        }
        return pets.get(customer).get(petId);
    }

    /**
     * 删除pet
     * 
     * @param customer
     * @param petId
     * @return
     */
    public boolean removePet(String customer, String petId) {
        if (StringUtils.isEmpty(customer) || StringUtils.isEmpty(petId)) {
            return false;
        }

        if (!pets.containsKey(customer)) {
            return false;
        }
        return pets.get(customer).remove(petId) != null;
    }

    /**
     * 添加pet
     * 
     * @param customer
     * @param pet
     * @return
     */
    public Pet addPet(String customer, Pet pet) {
        if (StringUtils.isEmpty(customer) || pet == null) {
            return null;
        }

        Map<String, Pet> customerPets = null;
        if (!pets.containsKey(customer)) {

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

            // 已经存在
            if (previous != null) {
                customerPets = previous;
            }
        } else {
            customerPets = pets.get(customer);
        }

        if (pet.getPetId() == null) {
            pet.setPetId(UUID.randomUUID().toString());
        }

        customerPets.put(pet.getPetId(), pet);
        return pet;
    }

    /**
     * 更新某个pet
     * 
     * @param customer
     * @param petPojo
     * @return
     */
    public Pet updatePet(String customer, Pet petPojo) {
        if (StringUtils.isEmpty(customer) || petPojo == null) {
            return null;
        }

        if (petPojo.getPetId() == null) {
            return null;
        }

        Pet pet = getPet(customer, petPojo.getPetId());
        pet.setType(petPojo.getType());
        pet.setName(petPojo.getName());
        pet.setDescription(petPojo.getDescription());

        return pet;
    }

}

4. 控制层实现

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

@RestController
@RequestMapping("/rest/pets/{customer}")
public class RestApiController {

    @Autowired
    private PetManager dataManager;

    /**
     * 添加宠物
     * 
     * @param customer
     * @param pet
     * @return
     */
    @PostMapping
    public ResponseEntity<Object> addPet(@PathVariable String customer, @RequestBody Pet pet) {
        validateCustomer(customer);

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

        // 返回 201.created
        if (newPet != null) {
            URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{petId}")
                    .buildAndExpand(newPet.getPetId()).toUri();

            return ResponseEntity.created(location).build();
        }

        // 返回 204.noContent
        return ResponseEntity.noContent().build();
    }

    /**
     * 获取宠物列表
     * 
     * @param customer
     * @return
     */
    @GetMapping
    @ResponseBody
    public List<Pet> listPets(@PathVariable String customer) {
        validateCustomer(customer);

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

    /**
     * 获取某个宠物
     * 
     * @param customer
     * @param petId
     */
    @GetMapping("/{petId}")
    @ResponseBody
    public Pet getPet(@PathVariable String customer, @PathVariable String petId) {
        validateCustomer(customer);
        validatePet(customer, petId);

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

    /**
     * 更新宠物信息
     * 
     * @param customer
     * @param petId
     * @param pet
     */
    @PutMapping("/{petId}")
    public ResponseEntity<Object> updatePet(@PathVariable String customer, @PathVariable String petId, @RequestBody Pet pet) {
        validateCustomer(customer);
        validatePet(customer, petId);

        pet.setPetId(petId);
        Pet petObject = dataManager.updatePet(customer, pet);
        if (petObject != null) {
            return ResponseEntity.ok(petObject);
        }

        return ResponseEntity.noContent().build();

    }

    /**
     * 删除某个宠物
     * 
     * @param customer
     * @param petId
     * @return
     */
    @DeleteMapping("/{petId}")
    public ResponseEntity<Object> removePet(@PathVariable String customer, @PathVariable String petId) {
        validateCustomer(customer);
        validatePet(customer, petId);

        dataManager.removePet(customer, petId);
        return ResponseEntity.ok().build();
    }

上述代码中已经实现了完整的增删改查语义。
在Restful 风格的API 接口定义中,往往会引用 HTTP 状态码用于表示不同的结果,比如一些错误的状态类型。
这里我们Customer、Pet 进行存在性校验,若资源不存在返回404_NotFound。

    /**
     * 校验customer是否存在
     * 
     * @param customer
     */
    private void validateCustomer(String customer) {
        if (dataManager.getCustomer(customer) == null) {
            throw new ObjectNotFoundException(String.format("the customer['%s'] is not found", customer));
        }
    }

    /**
     * 校验pet是否存在
     * 
     * @param customer
     */
    private void validatePet(String customer, String petId) {
        if (dataManager.getPet(customer, petId) == null) {
            throw new ObjectNotFoundException(String.format("the pet['%s/%s'] is not found", customer, petId));
        }
    }

自定义异常拦截

    /**
     * 自定义异常,及拦截逻辑
     * 
     * @author atp
     *
     */
    @SuppressWarnings("serial")
    public static class ObjectNotFoundException extends RuntimeException {

        public ObjectNotFoundException(String msg) {
            super(msg);
        }
    }

    @ResponseBody
    @ExceptionHandler(ObjectNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String objectNotFoundExceptionHandler(ObjectNotFoundException ex) {
        return ex.getMessage();
    }

5. 接口验证

1. 添加宠物

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

{
 "name": "Smart Baby",
 "description": "very small and smart also.",
 "type": "Dog"
}

返回示例

201 created
Content-Length →0
Date →Mon, 09 Jul 2018 05:15:01 GMT
Location →http://localhost:8090/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543

2. 获取宠物列表

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

<Empty>

返回示例

200 OK
Content-Type →application/json;charset=UTF-8
Date →Mon, 09 Jul 2018 05:23:27 GMT
Transfer-Encoding →chunked
[
    {
        "petId": "b5400334-e7b3-42f1-b192-f5e7c3193543",
        "name": "Smart Baby",
        "type": "Dog",
        "description": "very small and smart also."
    },
    {
        "petId": "610780af-94f1-4011-a175-7a0f3895163d",
        "name": "Big Cat",
        "type": "Cat",
        "description": "very old but I like it."
    }
]

3. 查询宠物信息

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

<Empty>

返回示例

200 OK
Content-Type →application/json;charset=UTF-8
Date →Mon, 09 Jul 2018 05:25:24 GMT
Transfer-Encoding →chunked
{
    "petId": "b5400334-e7b3-42f1-b192-f5e7c3193543",
    "name": "Smart Baby",
    "type": "Dog",
    "description": "very small and smart also."
}

4. 更新宠物信息

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

{
 "name": "Big Cat V2",
 "description": "I don't like it any more",
 "type": "Cat"
}

返回示例

200 OK
Content-Type →application/json;charset=UTF-8
Date →Mon, 09 Jul 2018 05:31:28 GMT
Transfer-Encoding →chunked
{
    "petId": "a98e4478-e754-4969-851b-bcaccd67263e",
    "name": "Big Cat V2",
    "type": "Cat",
    "description": "I don't like it any more"
}

5. 删除宠物

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

<empty>

返回示例

200 OK
Content-Length →0
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的生成。

引入依赖


        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>


        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>

定义API配置


@EnableSwagger2
@Configuration
public class SwaggerConfig {


    public static final String VERSION = "1.0.0";

    @Value("${swagger.enable}")
    private boolean enabled;

    ApiInfo apiInfo() {
        return new ApiInfoBuilder().
                title("Pet Api Definition")
                .description("The Petstore CRUD Example")
                .license("Apache 2.0")
                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
                .termsOfServiceUrl("")
                .version(VERSION)
                .contact(new Contact("", "", "zalesfoo@163.com"))
                .build();
    }

    @Bean
    public Docket customImplementation() {
        return new Docket(DocumentationType.SWAGGER_2).select()
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                .build()
                .enable(enabled)
                .apiInfo(apiInfo());
    }
}

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

声明API描述

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

@Api(value = "Pet Restful api")
@RestController
@RequestMapping("/rest/pets/{customer}")
public class RestApiController {

    @ApiOperation("添加宠物")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "path", name = "customer", dataType = "String", required = true, value = "客户名", defaultValue = ""),
            @ApiImplicitParam(paramType = "body", name = "pet", dataType = "Pet", required = true, value = "pet 请求", defaultValue = "") })
    @ApiResponses({
        @ApiResponse(code = 201, message = "添加成功"),
        @ApiResponse(code = 404, message = "资源不存在")
    })
    @PostMapping
    public ResponseEntity<Object> addPet(@PathVariable String customer, @RequestBody Pet pet) {
        ...

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

@ApiModel("宠物信息")
public class Pet {

    @ApiModelProperty(name="petId", value="宠物ID")
    private String petId;
    
    @ApiModelProperty(name="name", value="宠物名称")
    private String name;
    
    @ApiModelProperty(name="type", value="宠物类型")
    private String type;
    
    @ApiModelProperty(name="description", value="宠物描述")
    private String description;

相关的注解:

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

访问文档

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

参考文档

SpringBoot-tutorials-bookmarks
阮一峰-理解Restful架构
SprintBoot-使用Swagger发布API
swagger-2-documentation-for-spring-rest-api

欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容^-^

目录
相关文章
|
15天前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
63 6
|
1月前
|
XML JSON API
深入浅出:RESTful API 设计实践与最佳应用
【9月更文挑战第32天】 在数字化时代的浪潮中,RESTful API已成为现代Web服务通信的黄金标准。本文将带您一探究竟,了解如何高效地设计和维护一个清晰、灵活且易于扩展的RESTful API。我们将从基础概念出发,逐步深入到设计原则和最佳实践,最终通过具体案例来展示如何将理论应用于实际开发中。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的指导和灵感。
|
2月前
|
缓存 NoSQL Java
Springboot实战——黑马点评之秒杀优化
【9月更文挑战第27天】在黑马点评项目中,秒杀功能的优化对提升系统性能和用户体验至关重要。本文提出了多项Spring Boot项目的秒杀优化策略,包括数据库优化(如索引和分库分表)、缓存优化(如Redis缓存和缓存预热)、并发控制(如乐观锁、悲观锁和分布式锁)以及异步处理(如消息队列和异步任务执行)。这些策略能有效提高秒杀功能的性能和稳定性,为用户提供更佳体验。
128 6
|
2月前
|
存储 前端开发 API
告别繁琐,拥抱简洁!Python RESTful API 设计实战,让 API 调用如丝般顺滑!
在 Web 开发的旅程中,设计一个高效、简洁且易于使用的 RESTful API 是至关重要的。今天,我想和大家分享一次我在 Python 中进行 RESTful API 设计的实战经历,希望能给大家带来一些启发。
35 3
|
3月前
|
NoSQL Java Redis
Redis6入门到实战------ 八、Redis与Spring Boot整合
这篇文章详细介绍了如何在Spring Boot项目中整合Redis,包括在`pom.xml`中添加依赖、配置`application.properties`文件、创建配置类以及编写测试类来验证Redis的连接和基本操作。
Redis6入门到实战------ 八、Redis与Spring Boot整合
|
3月前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
3月前
|
Java API UED
【实战秘籍】Spring Boot开发者的福音:掌握网络防抖动,告别无效请求,提升用户体验!
【8月更文挑战第29天】网络防抖动技术能有效处理频繁触发的事件或请求,避免资源浪费,提升系统响应速度与用户体验。本文介绍如何在Spring Boot中实现防抖动,并提供代码示例。通过使用ScheduledExecutorService,可轻松实现延迟执行功能,确保仅在用户停止输入后才触发操作,大幅减少服务器负载。此外,还可利用`@Async`注解简化异步处理逻辑。防抖动是优化应用性能的关键策略,有助于打造高效稳定的软件系统。
69 2
|
3月前
|
前端开发 应用服务中间件 数据库
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
这篇文章通过一个具体的项目案例,详细讲解了如何使用SpringMVC、Thymeleaf、Bootstrap以及RESTful风格接口来实现员工信息的增删改查功能。文章提供了项目结构、配置文件、控制器、数据访问对象、实体类和前端页面的完整源码,并展示了实现效果的截图。项目的目的是锻炼使用RESTful风格的接口开发,虽然数据是假数据并未连接数据库,但提供了一个很好的实践机会。文章最后强调了这一章节主要是为了练习RESTful,其他方面暂不考虑。
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
|
3月前
|
Java 缓存 数据库连接
揭秘!Struts 2性能翻倍的秘诀:不可思议的优化技巧大公开
【8月更文挑战第31天】《Struts 2性能优化技巧》介绍了提升Struts 2 Web应用响应速度的关键策略,包括减少配置开销、优化Action处理、合理使用拦截器、精简标签库使用、改进数据访问方式、利用缓存机制以及浏览器与网络层面的优化。通过实施这些技巧,如懒加载配置、异步请求处理、高效数据库连接管理和启用GZIP压缩等,可显著提高应用性能,为用户提供更快的体验。性能优化需根据实际场景持续调整。
71 0
|
3月前
|
JSON API 数据库
探索FastAPI:不仅仅是一个Python Web框架,更是助力开发者高效构建现代化RESTful API服务的神器——从环境搭建到CRUD应用实战全面解析
【8月更文挑战第31天】FastAPI 是一个基于 Python 3.6+ 类型提示标准的现代 Web 框架,以其高性能、易用性和现代化设计而备受青睐。本文通过示例介绍了 FastAPI 的优势及其在构建高效 Web 应用中的强大功能。首先,通过安装 FastAPI 和 Uvicorn 并创建简单的“Hello, World!”应用入门;接着展示了如何处理路径参数和查询参数,并利用类型提示进行数据验证和转换。
90 0