作者:禅与计算机程序设计艺术
1.背景介绍
概述
近年来,基于云计算、物联网、大数据、人工智能等新兴技术的驱动下,互联网应用逐渐转向了以服务端为中心的架构模式。目前,越来越多的互联网公司开始采用面向服务的架构设计模式,即将复杂且庞大的业务系统通过服务化拆分成独立部署的微服务,并通过API Gateway统一暴露,为第三方客户端提供访问接入点。
在这种架构模式下,开发人员需要编写健壮可靠的服务端应用程序,实现业务逻辑的持久化、数据缓存、异步处理等功能。在Java语言中,Spring Boot框架是构建快速、可靠、生产级的基于Spring的微服务应用程序的不二之选。
本教程通过“SpringBoot编程基础教程:RESTful API开发”系列教程,给学习者以全新的视角,从零开始,轻松掌握SpringBoot RESTful API的开发技巧。以下会详细阐述SpringBoot RESTful API开发的相关技术要点,带领大家亲自动手实践,完成一个完整的RESTful风格的服务端开发项目。
知识要求
基本的Java开发能力,包括Java语法、类/对象、集合类、异常处理、IO流、多线程、反射机制、网络编程、数据库操作、正则表达式、设计模式等;熟悉HTTP协议、Restful规范。
2.核心概念与联系
RESTful API(Representational State Transfer)是一种基于HTTP的应用层协议,定义了客户端-服务器之间交换数据的标准接口,旨在提升互联网软件在各个层次的可伸缩性。它将互联网从静态页面到动态资源的变化,从基于表单的Web应用到分布式的API-first系统,都从RESTful的角度重新审视了Web服务的设计方式。
2.1 URI
URI(Uniform Resource Identifier),即统一资源标识符,它是一种抽象的用来标识互联网资源名称的字符串,它可以使得互联网上各种信息资源的位置唯一且可读。RESTful API一般以名词表示资源类型,比如:
GET /users:获取用户列表 POST /users:创建新用户 DELETE /users/{id}:删除指定ID的用户 PUT /users/{id}:更新指定ID的用户
2.2 HTTP方法
HTTP方法(Hypertext Transfer Protocol Method)是指客户端向服务器发送请求的方法。RESTful API一般以动词或名词表示操作类型,用不同的方法对同一资源执行不同的操作,常用的HTTP方法包括:
GET:获取资源,对应查询操作 POST:新建资源(实体),对应创建操作 PUT:更新资源(实体),对应修改操作 PATCH:更新资源(局部),对应部分修改操作 DELETE:删除资源,对应删除操作
2.3 请求参数
请求参数(Request Parameter)是指客户端向服务器传递的数据,例如查询条件、提交表单数据、上传文件等。请求参数通过URL或者JSON格式发送给服务器。
2.4 返回结果
返回结果(Response Result)是指服务器返回给客户端的数据,例如查询结果、文件下载、JSON格式数据等。返回结果通常是JSON格式。
3.核心算法原理和具体操作步骤以及数学模型公式详细讲解
为了帮助大家理解RESTful API的开发过程,本节将介绍RESTful API开发的一些基本概念和流程。
3.1 启动类
Spring Boot应用一般都是使用主类作为启动入口。其中,@SpringBootApplication注解可以标注启动类,其作用相当于将启动类所在包及子包下的所有类扫描到Spring容器中,方便IoC依赖注入。
package com.example.demo;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//Spring Boot启动运行
}
}
3.2 配置类
配置类(Configuration Class)是Spring Boot提供的用于配置Spring Bean的注解。在配置类上加入@Configuration注解,表明它是一个配置类,然后在该类里声明Bean,即可在其他Bean中使用。
package com.example.demo;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@ComponentScan("com.example.demo")//注册当前包及子包中的组件
@EnableWebMvc//启用MVC支持
public class AppConfig {
}
3.3 控制器(Controller)
控制器(Controller)是 Spring MVC 中的一个组件,负责接收客户端的请求,并按照指定的规则生成相应的响应。可以通过@RestController注解来定义一个控制器类,并在类中声明各个请求的映射方法。
package com.example.demo;
import org.springframework.web.bind.annotation.*;
@RestController
public class HelloWorldController {
@RequestMapping("/")
public String hello() {
return "Hello World!";
}
}
3.4 服务(Service)
服务(Service)是用于业务逻辑处理的类,一般以业务领域的名词命名,比如UserService。在业务层中,我们可以定义各种操作数据库的方法,然后使用@Autowired注解注入Repository(DAO层)或者其他服务。
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserDao userDao;
public List<User> getUsers() {
return userDao.findAll();
}
...
}
3.5 DAO(Data Access Object)
DAO(Data Access Object)是用于访问数据库的类,一般以数据源+表名命名。在DAO层中,我们可以使用Spring Data JPA(Java Persistence API)进行ORM映射。
package com.example.demo.dao;
import org.springframework.data.jpa.repository.JpaRepository;
interface UserDao extends JpaRepository<User, Long> {}
3.6 ORM映射
ORM(Object Relational Mapping,对象关系映射)是一种编程范式,它将关系型数据库的一组表结构映射到对象的形式,以方便开发人员操作数据库。在Spring Boot中,我们可以使用Spring Data JPA(Java Persistence API)来实现ORM映射。
3.7 Jackson序列化
Jackson(Java Automatic Serialization Library)是一个Java开发的高性能的序列化库,其提供了简洁灵活的API,能够将Java对象转换为JSON格式或XML格式,也可以将JSON或XML格式的字符串还原为Java对象。
package com.example.demo.vo;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)//JSON键值驼峰策略
public class UserVo {
private Long id;
private String username;
private Integer age;
private LocalDateTime createTime;
}
4.具体代码实例和详细解释说明
4.1 创建用户
创建一个Controller类,通过@PostMapping("/users")注解定义添加用户的接口。并定义一个User对象,绑定到参数上。最后调用UserService中的addUser方法保存用户到数据库。
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/users")
public ResponseEntity create(@RequestBody @Valid User user){
log.info("create user: {}",user);
try{
userService.save(user);
}catch (Exception e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
return ResponseEntity.ok().build();
}
}
4.2 更新用户
创建一个Controller类,通过@PutMapping("/users/{id}")注解定义更新指定ID的用户的接口。并定义一个User对象,绑定到参数上。最后调用UserService中的updateUser方法根据ID更新用户。
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PutMapping("/users/{id}")
public ResponseEntity update(@PathVariable Long id,@RequestBody @Valid User user){
log.info("update user by id: {}, user:{}",id,user);
Optional<User> optional = userService.findById(id);
if(!optional.isPresent()){
throw new ResourceNotFoundException("user","id",id);
}
try{
userService.save(user);
}catch (Exception e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
return ResponseEntity.noContent().build();
}
}
4.3 删除用户
创建一个Controller类,通过@DeleteMapping("/users/{id}")注解定义删除指定ID的用户的接口。并调用UserService中的deleteUser方法根据ID删除用户。
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@DeleteMapping("/users/{id}")
public ResponseEntity delete(@PathVariable Long id){
log.info("delete user by id: {}",id);
Optional<User> optional = userService.findById(id);
if(!optional.isPresent()){
throw new ResourceNotFoundException("user","id",id);
}
try{
userService.deleteById(id);
}catch (Exception e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
return ResponseEntity.ok().build();
}
}
4.4 查询用户列表
创建一个Controller类,通过@GetMapping("/users")注解定义查询用户列表的接口。并调用UserService中的getUsers方法查询所有的用户。
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public ResponseEntity getAll(){
log.info("get all users");
List<User> users = userService.getUsers();
return ResponseEntity.ok(users);
}
}
4.5 模糊查询用户列表
创建一个Controller类,通过@GetMapping("/users/search")注解定义模糊查询用户列表的接口。并定义一个Query对象,包含搜索关键词、页码、每页记录数。然后调用UserService中的search方法模糊查询用户列表。
package com.example.demo.controller;
import com.example.demo.domain.query.UserSearchQuery;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/search")
public ResponseEntity search(@ModelAttribute UserSearchQuery query){
log.info("search user with keyword={}, page={}",query.getKeyword(),query.getPage());
Pageable pageable = PageRequest.of(query.getPage()-1,query.getSize(), Sort.Direction.ASC,"username");
Page<User> page = userService.searchByKeyword(pageable,query.getKeyword());
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/v1/users/search");
return ResponseEntity.ok().headers(headers).body(page.getContent());
}
}
4.6 数据分页显示
创建一个工具类,实现分页相关的功能,比如生成分页链接Header,解析分页查询条件,计算分页信息等。
package com.example.demo.utils;
import org.springframework.data.domain.*;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public final class PaginationUtil {
/**
* 生成分页链接header
*/
public static HttpHeaders generatePaginationHttpHeaders(Page<?> page, String url){
HttpHeaders headers = new HttpHeaders();
int totalPages = page.getTotalPages();
long limit = page.getSize()+1L;//加1是因为超过总数量的记录也算做一页
for (long i=0L ;i <totalPages ;i++){
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
MultiValueMap<String, String> params = builder.queryParams().toMultiValueMap();
//修改页码的参数
params.set("page",Long.toString(i));
//判断是否已经到达最后一页
boolean isLastPage = false;
if((limit*i + page.getSize()) >= page.getTotalElements()){
isLastPage = true;
}
//如果已到达最后一页,则不再增加page参数,防止后续参数丢失
if (!isLastPage) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < params.size(); ++j) {
sb.append(params.keySet().toArray()[j]).append('=').append(params.values().toArray()[j][0]);
if (j!= params.size() - 1) {
sb.append('&');
}
}
uri = URLDecoder.decode(sb.toString());
}else{
uri = "";
}
headers.add("link", "<" + uri + ">; rel=\"self\", ");
}
return headers;
}
}