1,RESTful API与单元测试
Restful API是目前比较成熟的一套互联网应用程序的API设计理念,Rest是一组架构约束条件和原则,如何Rest约束条件和原则的架构,我们就称为Restful架构,Restful架构具有结构清晰、符合标准、易于理解以及扩展方便等特点,受到越来越多网站的采用
- RESTful API规范
1.1 URL设计
-
URL应该清晰、简洁、易于理解和记忆。
-
URL应该使用名词而不是动词,例如/users而不是/getUsers。
-
URL应该使用小写字母和短横线分隔符,例如/users/123。
1.2 HTTP方法
-
GET:用于获取资源。
-
POST:用于创建资源。
-
PUT:用于更新资源。
-
DELETE:用于删除资源。
1.3 HTTP状态码
-
200 OK:请求成功。
-
201 Created:资源创建成功。
-
204 No Content:请求成功,但没有返回任何内容。
-
400 Bad Request:请求参数错误。
-
401 Unauthorized:未授权访问。
-
403 Forbidden:禁止访问。
-
404 Not Found:请求的资源不存在。
-
500 Internal Server Error:服务器内部错误。
- Token(令牌)
Token是一种身份验证方式,它是服务器生成的一串字符串,用于标识用户身份。当用户登录成功后,服务器会生成一个Token,并将其返回给客户端。客户端在后续的请求中需要携带该Token,以便服务器验证用户身份。
2.1 Token的生成方式
Token可以使用随机数、时间戳、用户ID等信息生成。常见的Token生成方式有JWT、OAuth等。
2.2 Token的验证方式
客户端在请求中需要携带Token,服务器在接收到请求后,需要验证Token的有效性。常见的Token验证方式有黑名单、白名单、签名等。
- MD5使用
MD5是一种常用的哈希算法,它将任意长度的消息压缩成一个128位的消息摘要。MD5常用于密码加密、文件校验等场景。
3.1 MD5的加密方式
MD5加密可以使用MD5算法库,也可以使用第三方库。
3.2 MD5的应用场景
-
密码加密:将用户密码进行MD5加密后存储到数据库中,可以增加密码的安全性。
-
文件校验:将文件内容进行MD5加密后,可以用于校验文件的完整性。
RESTful API 实现
首先回顾下Spring MVC的几个注解:
- @Controller:修饰class,用来创建处理http请求的对象
- @RestController:Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式
- @RequestMapping:配置url映射。现在更多的也会直接用以Http Method直接关联的映射注解来定义,比如:GetMapping、PostMapping、DeleteMapping、PutMapping等.
下面我们通过使用Spring MVC来实现一组对User对象操作的RESTful API,配合注释详细说明在Spring MVC中如何映射HTTP请求、如何传参、如何编写单元测试。
RESTful API具体设计如下:
(1)定义User实体
package com.example.demospringboot;
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private Integer age;
}
注意:相比1.x版本教程中自定义set和get函数的方式,这里使用@Data注解可以实现在编译器自动添加set和get函数的效果。该注解是lombok提供的,只需要在pom中引入加入下面的依赖就可以支持:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
(2)实现对User对象的操作接口
package com.example.demospringboot;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping(value = "/users") // 通过这里配置使下面的映射都在/users下
public class UserController {
// 创建线程安全的Map,模拟users信息的存储
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
/**
* 处理"/users/"的GET请求,用来获取用户列表
*
* @return
*/
@GetMapping("/")
public List<User> getUserList() {
// 还可以通过@RequestParam从页面中传递参数来进行查询条件或者翻页信息的传递
List<User> r = new ArrayList<User>(users.values());
return r;
}
/**
* 处理"/users/"的POST请求,用来创建User
*
* @param user
* @return
*/
@PostMapping("/")
public String postUser(@RequestBody User user) {
// @RequestBody注解用来绑定通过http请求中application/json类型上传的数据
users.put(user.getId(), user);
return "success";
}
/**
* 处理"/users/{id}"的GET请求,用来获取url中id值的User信息
*
* @param id
* @return
*/
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// url中的id可通过@PathVariable绑定到函数的参数中
return users.get(id);
}
/**
* 处理"/users/{id}"的PUT请求,用来更新User信息
*
* @param id
* @param user
* @return
*/
@PutMapping("/{id}")
public String putUser(@PathVariable Long id, @RequestBody User user) {
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
/**
* 处理"/users/{id}"的DELETE请求,用来删除User
*
* @param id
* @return
*/
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
users.remove(id);
return "success";
}
}
MockMvc单元测试
下面针对该Controller编写测试用例验证正确性:
package com.example.demospringboot;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplication {
private MockMvc mvc;
@Before
public void setUp() {
mvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
}
@Test
public void testUserController() throws Exception {
// 测试UserController
RequestBuilder request;
// 1、get查一下user列表,应该为空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
// 2、post提交一个user
request = post("/users/")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"name\":\"Mike\",\"age\":20}");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 3、get获取user列表,应该有刚才插入的数据
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[{\"id\":1,\"name\":\"Mike\",\"age\":20}]")));
// 4、put修改id为1的user
request = put("/users/1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Jack\",\"age\":30}");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 5、get一个id为1的user
request = get("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("{\"id\":1,\"name\":\"Jack\",\"age\":30}")));
// 6、del删除id为1的user
request = delete("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 7、get查一下user列表,应该为空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
}
}
postman测试
当然也可以通过浏览器插件等进行请求提交验证。postman 可以直接在chrome 上安装插件,当然大部分的同学是没法连接到谷歌商店的,我们可以在电脑本地安装postman 客户端工具。
postman用于测试http协议接口,无论是开发, 还是测试人员, 都有必要学习使用postman来测试接口, 用起来非常方便。
Postman官网下载地址 https://www.postman.com/downloads/:
Postman for MAC https://dl.pstmn.io/download/latest/osx
Postman for windows 64 https://dl.pstmn.io/download/latest/win64
Postman for windows X86 https://dl.pstmn.io/download/latest/win32
Postman for linux https://dl.pstmn.io/download/latest/linux64
用法也比较简单:
示例如下:
创建User:
查询User:
2,web应用中的身份认证:cookie、session、token
(1) cookie和session
首先,要思考的是为啥有这三个东西,因为HTTP协议是无状态的特性导致的,比如客户端和服务端进行了一次请求和响应,协议只负责处理请求,拒绝响应之类的,但是不负责记忆,这样就有问题产生了,协议的拒绝或者响应肯定是需要验证的,但是协议本身没有记忆的能力,那么每次请求都去做验证显然是不合理的,就好比你逛淘宝每次去到一个新的页面都需要你进行登录验证,正常人都会心态爆炸啦,所以为了维持会话的进行,cookie和session就应运而生了,这也就是他的用途了,当然token也是同理,只是手段有些许差别。
①我们需要记住的是cookie是存储在客户端的,而session是存储在服务器的,这里这么理解,cookie就是钥匙放在客户的手上,服务端就是一个酒店,而session就是每一个门的门锁,门里面都有个服务员可以和客户对话,存贮位置不同是他们一个重要的区别,比如我们要进行一个会话,那肯定需要用钥匙打开对应的门才能继续会话。
②cookie是有过期时间的,如果不设置,cookie就是当前游览器会话时间的话,游览器关了,cookie就过期了,如果设置了过期时间,cookie会被存贮在硬盘上,游览器关闭,下次打开cookie依旧存在
③cookie有大小的限制,单个cookie的大小是4kb,多数游览器限制一个站点最多可以存储20个cookie。
④这里还有个需要注意的点就是cookie是以明文的形式存储的,存储方式为键值对,这样就暴露一个问题那就是隐私数据比如账号密码是不能存在于cookie的,不然很轻易就会被他人窃取,同样cookie也比较容易伪造,可以轻易修改cookie的数值,所以如果重要信息以明文的形式存储非常容易泄露被人利用。
所以,这里我将cookie和session验证的缺点总结一下:
①session需要将每一个会话存储会话信息存到内存中,这样会占用大量的内存,增加服务器的压力
②session有局限性,如果是单服务器不会有问题,因为账单都在总店,但是如果是分布式服务就会面临无法验证的问题,因为session是存储在服务器A中的。
③安全性不高,由于是服务器将cookie发给客户端,客户端只要发送cookie即可通过验证。客户端容易伪造出示cookie,服务端就验证通过了,将返回的信息以明文的形式返回给客户端进行保存。
④app等移动端应用会使得session失效
⑤cookie不允许跨域访问
⑥cookie有大小限制4kb
(2) token
token是服务端生成的一串字符串,作为客户请求的一个令牌,当第一次登录后,服务器就会生成一个token给客户端,以后的请求带上token就可以无需登录了,其实和cookie非常相似,但是token多了一个验证的步骤,不再是出示即可通过了。
相较于cookie,token减轻了服务端的压力,不需要就session这样将大量的会话记录保存起来了,而是通过token,服务器向redis中查询用户的信息。有了redis,服务器只需要去redis查询用户信息即可,并且token并不限制大小,可以允许跨域的请求,这也是不同与cookie的,也解决了app等移动端应用会使得session失效的问题。由此可见,token在多方面是有优于cookie的。
(3) JWT和数字签名
token除了去redis中查询还有一种JWT(JSon Web Token)的形式,因为频繁的查询redis同样会加强redis的压力,所以JWT相较于查询redis也有他的一定优点,他是将数据交给了游览器存储,下次需要继续请求的时候携带JWT给服务器进行验证就可以了,这样可以避免查询redis这个步骤,在一定程度上降低redis的压力:
另外,token和cookie不同的是token加入了签名机制,有了签名机制就加强了数据的安全性。JWT一般是一个字符串类似于adb.123ccc.666ad,以.
为分隔符,由Header、Payload和Signature三部分组成。
- Header:
{
"alg": "HS256", "typ": "JWT"
}
主要有两个属性,typ属性统一写JWT,alg是表示加密使用的算法,这里的HS256就是对称加密,还有一个是RS256是非对称加密.
- payload
这里存放的就是一些类似于cookie的数据,类似于:
{
"iss":"zs",
"sub":"123",
"sex":"男" //同样允许写入自定义的字段
}
有七个默认字段可供选择:
iss (issuer):签发人/发行人
sub (subject):主题
aud (audience):用户
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
iat (Issued At):签发时间
jti (JWT ID):用于标识该 JWT
值得注意的是,JWT的json对象是采用Base64的方式简单加密的,由于这种加密是可逆,是可以通过反向编码得到原文,所以敏感数据依旧不能放在JWT中
- Signature
签名是将前面两个部分(header和payload)的信息先进行base64加密成字符串的形式再使用头部声明的加密方式(如HS256)进行加密后得到的一个数据。
可以这么理解,当数据不被篡改的时候,将secret解密后的数据通过base64反向解析后得到的和header和payload是一样的数据;如果服务器解密Signature部分发现payload和header与发来的明文payload和header对不上,那就证明信息已经被篡改了。
这里有个可以散发的点,为什么使用HS256这种对称加密的方法?因为对称加密需要对密钥进行妥善保管,这个过程客户端都没有参与,是服务器对信息进行了加密解密,也就是说类似于cookie用户登录的信息被服务器加密后,返回给用户,下次使用的时候发给服务器,服务器再通过密钥解密确认信息是否被篡改过。自始至终都是服务器在做加密和解密验证的工作,密钥也只有服务器知道,密钥不需要在客户端和服务器中传输,也就不存在安全性的问题。不考虑传输安全性的情况,对称加密的解密更快,肯定是优于非对称加密的,这也是为什么JWT大多采用HS256,而不是我们认为更安全的RS256(非对称加密RSA),主要就是因为不需要传递密钥,所以业务场景很重要。有了签名之后,即使 JWT 被泄露或者截获,如果服务端的秘钥不被泄露的话,黑客也没办法同时篡改 Signature、Header、Payload。所以密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
补充说明:
对称加密其实就是加密和解密用的都是一个钥匙,但是由于加解密用的一个钥匙,密钥的保存和传输就是个问题,如果一旦别人截获密钥的信息就能得到全部数据,同时密钥的保存也是一个问题。
非对称加密RSA数字签名算法会生成公钥和私钥,这里对于公钥和私钥的理解需要注意一下,公私的区分是公钥是发布在互联网中,或者说对外公布,而私钥是个人保存的,比如一个数据可以进行公钥加密,那么拥有私钥的人通过私钥解密就可以获得原文了;而私钥加密的前提下,如果只知道公钥是无法获得原文的。因此,非对称公钥无需保存,只保存私钥即可,但是加解密过程慢;对称加密,加解密同一密钥,密钥保存难度大,但是加解密过程快,所有优势互补,就有了以下场景:
A公司拥有自己的公钥A和私钥A,同样B公司拥有公钥B和私钥B。 A公司如果想想B公司发送一段重要隐私信息,那么可以通过B公司的公钥进行加密,将需要传输的数据进行对称加密,将加密的密钥一块发送给B公司,那么只有拥有对应私钥的B公司才能解密获得信息的内容,再通过获取的密钥将对称加密的信息进行解密,这样在保证安全性的同时,大大提升了解密的速度。
(4) JWT 实践
1,引入JWT依赖,由于是基于Java,所以需要的是java-jwt
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2,自定义两个注解
(1)用来跳过验证的PassToken:
package com.example.demospringboot;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
(2)用来使用验证的UseToken :
package com.example.demospringboot;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UseToken {
boolean required() default true;
}
自定义注解参数说明:
@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
@Retention:注解的保留位置
RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解
3,在Controller中使用token
(1)在postUser注册接口中获取token并返回;
(2)在getUser查询接口增加@UseToken注解验证token
/**
* 处理"/users/"的POST请求,用来创建User,并获取token值
*
* @param user
* @return
*/
@PostMapping("/")
public JSONObject postUser(@RequestBody User user) {
// @RequestBody注解用来绑定通过http请求中application/json类型上传的数据
users.put(user.getId(), user);
JSONObject jsonObject=new JSONObject();
String token = tokenService.getToken(user);
jsonObject.put("token", token);
jsonObject.put("user", user);
return jsonObject;
}
/**
* 处理"/users/{id}"的GET请求,用来获取url中id值的User信息
* @UseToken:该接口必须在请求头中加上token并通过验证才可以访问
*/
@UseToken
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// url中的id可通过@PathVariable绑定到函数的参数中
return users.get(id);
}
获取token的方法实现如下:
package com.example.demospringboot;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.stereotype.Service;
@Service("TokenService")
public class TokenService {
public String getToken(User user) {
String token="";
// withAudience()存入需要保存在token的信息,这里我把用户ID存入token中
// Algorithm.HMAC256():使用HS256加密生成token,这里密钥用了用户age,唯一密钥的话可以保存在服务端。
token= JWT.create().withAudience(String.valueOf(user.getId()))// 将 user id 保存到 token 里面
.sign(Algorithm.HMAC256(String.valueOf(user.getAge())));// 以 age 作为 token 的密钥
return token;
}
}
4,实现一个拦截器去获取token并验证token:
SpringMVC 中的Interceptor 拦截请求是通过HandlerInterceptor 来实现的。HandlerInterceptor接口是Spring框架中用于拦截HTTP请求的接口,它提供了三个方法,分别在请求处理之前、请求处理之后和请求处理完成之后被调用。
package com.example.demospringboot;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import static com.example.demospringboot.UserController.users;
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用token的注解
if (method.isAnnotationPresent(UseToken.class)) {
UseToken userLoginToken = method.getAnnotation(UseToken.class);
if (userLoginToken.required()) {
// 执行认证
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
if (token == null) {
throw new RuntimeException("无token!");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = users.get(Long.parseLong(userId));
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(String.valueOf(user.getAge()))).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
5,最后配置拦截器
创建拦截器配置类:InterceptorConfig------必须搭配的类,实现WebMvcConfigurer(建议使用)并且添加@Configuration注解;另一种继承WebMvcConfigurerAdapter不建议使用
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
6,post验证:
(1),注册时获取token:
(2)未使用token或错误token:
在服务侧可以看到抛出对应异常。
(3)正确使用token:
github:https://github.com/JackyZhang888/springboot/tree/main/useTokenJWT
3,使用Swagger2构建强大的API文档
本章将介绍RESTful API的重磅好伙伴Swagger2,它可以轻松的整合到Spring Boot中,并与Spring MVC程序配合组织出强大RESTful API文档。它既可以减少我们创建文档的工作量,同时说明内容又整合入实现代码中,让维护文档和修改代码整合为一体,可以让我们在修改代码逻辑的同时方便的修改文档说明。另外Swagger2也提供了强大的页面测试功能来调试每个RESTful API。
第一步:添加swagger-spring-boot-starter依赖
在pom.xml中加入依赖,具体如下:
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.9.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
第二步:应用主类中添加@EnableSwagger2Doc注解,具体如下
package com.example.demospringboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.spring4all.swagger.EnableSwagger2Doc;
@SpringBootApplication
@EnableSwagger2Doc
public class DemospringbootApplication {
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
}
第三步:application.properties中配置文档相关内容,比如
swagger.title=spring-boot-starter-swagger
swagger.description=Starter for swagger 2.x
swagger.version=1.4.0.RELEASE
swagger.license=Apache License, Version 2.0
swagger.licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.html
swagger.termsOfServiceUrl=https://github.com/xxx/spring-boot-starter-swagger
swagger.contact.name=didi
swagger.contact.url=http://blog.xxx.com
swagger.contact.email=xxx.com
swagger.base-package=com.example.demospringboot
swagger.base-path=/**
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
各参数配置含义如下:
- swagger.title:标题
- swagger.description:描述
- swagger.version:版本
- swagger.license:许可证
- swagger.licenseUrl:许可证URL
- swagger.termsOfServiceUrl:服务条款URL
- swagger.contact.name:维护人
- swagger.contact.url:维护人URL
- swagger.contact.email:维护人email
- swagger.base-package:swagger扫描的基础包,默认:全扫描
- swagger.base-path:需要处理的基础URL规则,默认:/**
更多配置说明可见官方说明:https://github.com/SpringForAll/spring-boot-starter-swagger
最后配置的spring.mvc.pathmatch.matching-strategy=ant_path_matcher
是因为Springboot 版本与 Swagger 版本不匹配:由于Spring Boot 2.6.x 请求路径与 Spring MVC 处理映射匹配的默认策略从AntPathMatcher更改为PathPatternParser。所以需要设置spring.mvc.pathmatch.matching-strategy为ant-path-matcher来改变它。
第四步:启动应用,访问:http://localhost:8080/swagger-ui.html,就可以看到如下的接口文档页面:
点击user-controller可以看到具体接口:
添加Api文档内容
在整合完Swagger之后,在http://localhost:8080/swagger-ui.html页面中可以看到,关于各个接口的描述还都是英文或遵循代码定义的名称产生的。这些内容对用户并不友好,所以我们需要自己增加一些说明来丰富文档内容。如下所示,我们通过@Api,@ApiOperation注解来给API增加说明、通过@ApiImplicitParam、@ApiModel、@ApiModelProperty注解来给参数增加说明。
比如下面的例子:
@Data
@ApiModel(description="用户实体")
public class User {
@ApiModelProperty("用户编号")
private Long id;
@ApiModelProperty("用户姓名")
private String name;
@ApiModelProperty("用户年龄")
private Integer age;
}
@Api(tags = "用户管理")
@RestController
@RequestMapping(value = "/users") // 通过这里配置使下面的映射都在/users下
public class UserController {
// 创建线程安全的Map,模拟users信息的存储
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());
@GetMapping("/")
@ApiOperation(value = "获取用户列表")
public List<User> getUserList() {
List<User> r = new ArrayList<>(users.values());
return r;
}
@PostMapping("/")
@ApiOperation(value = "创建用户", notes = "根据User对象创建用户")
public String postUser(@RequestBody User user) {
users.put(user.getId(), user);
return "success";
}
@GetMapping("/{id}")
@ApiOperation(value = "获取用户详细信息", notes = "根据url的id来获取用户详细信息")
public User getUser(@PathVariable Long id) {
return users.get(id);
}
@PutMapping("/{id}")
@ApiImplicitParam(paramType = "path", dataType = "Long", name = "id", value = "用户编号", required = true, example = "1")
@ApiOperation(value = "更新用户详细信息", notes = "根据url的id来指定更新对象,并根据传过来的user信息来更新用户详细信息")
public String putUser(@PathVariable Long id, @RequestBody User user) {
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
@DeleteMapping("/{id}")
@ApiOperation(value = "删除用户", notes = "根据url的id来指定删除对象")
public String deleteUser(@PathVariable Long id) {
users.remove(id);
return "success";
}
}
完成上述代码添加后,启动Spring Boot程序,访问:http://localhost:8080/swagger-ui.html,就能看到下面这样带中文说明的文档了(其中标出了各个注解与文档元素的对应关系以供参考):
API文档访问与调试
在上图请求的页面中,我们看到user的Value是个输入框。因为Swagger除了查看接口功能外,还提供了调试测试功能,我们可以点击上图中点击下方"Try it out!“(“Cancal”)按钮,再执行"Execure”,即可完成了一次请求调用!
3,JSR-303实现请求参数校验
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Bean Validation中内置的constraint:
Hibernate Validator附加的constraint:
在JSR-303的标准之下,我们可以通过上面这些注解,优雅的定义各个请求参数的校验。比如:校验字符串是否为空、检验字符串的长度、校验数字的大小、校验字符串格式是否为邮箱等。
第一步:在要校验的字段上添加上@NotNull、@Size、@MAx、@Min注解,具体如下:
package com.example.demospringboot;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.*;
@Data
@ApiModel(description="用户实体")
public class User {
@ApiModelProperty("用户编号")
private Long id;
@NotNull(message="姓名不能为空")
@Size(min = 2, max = 5)
@ApiModelProperty("用户姓名")
private String name;
@NotNull(message="年龄不能为空")
@Max(100)
@Min(10)
@ApiModelProperty("用户年龄")
private Integer age;
}
第二步:在需要校验的参数实体前添加@Valid注解,具体如下:
@PostMapping("/")
@ApiOperation(value = "创建用户", notes = "根据User对象创建用户")
public String postUser(@Valid @RequestBody User user) {
users.put(user.getId(), user);
return "success";
}
完成上面配置之后,启动应用,并用POST请求访问localhost:8080/users/接口,body使用一个空对象{}。用Postman等测试工具发起,或使用curl发起:
curl -X POST \
http://localhost:8080/users/ \
-H 'Content-Type: application/json' \
-H 'Postman-Token: 72745d04-caa5-44a1-be84-ba9c115f4dfb' \
-H 'cache-control: no-cache' \
-d '{
}'
可以看到输出的信息:
可以看到前端已经报错,但是并没有收到我们返回的校验信息,可以通过自定义捕获异常后的处理方式,简单实现:
package com.example.demospringboot;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomExceptionHandler {
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public String methodArgumentNotValidExceptionHandle(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getAllErrors();
List<String> errorsMessages = allErrors.stream()
.map(error -> error.getDefaultMessage())
.collect(Collectors.toList());
return errorsMessages.toString();
}
}
返回如下:
当然,为了让前端有更好的逻辑展示与页面交互处理,统一的数据返回格式必不可少。可以参考:https://www.hangge.com/blog/cache/detail_2897.html
常用的返回内容参数:
- timestamp:请求时间
- status:HTTP返回的状态码,这里返回400,即:请求无效、错误的请求,通常参数校验不通过均为400
- error:HTTP返回的错误描述,这里对应的就是400状态的错误描述:Bad Request
- errors:具体错误原因,是一个数组类型;因为错误校验可能存在多个字段的错误,比如这里因为定义了两个参数不能为Null,所以存在两条错误记录信息
- message:概要错误消息,返回内容中很容易可以知道,这里的错误原因是对user对象的校验失败,其中错误数量为2,而具体的错误信息就定义在上面的errors数组中
- path:请求路径
4,找回启动日志中的请求路径列表
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping类在启动的时候,通过扫描Spring MVC的@Controller、@RequestMapping等注解去发现应用提供的所有接口信息。然后在日志中打印,以方便开发者排查关于接口相关的启动是否正确。
从Spring Boot 2.1.0版本开始,将这些日志的打印级别做了调整:从原来的INFO调整为TRACE。所以,当我们希望在应用启动的时候打印这些信息的话,只需要在配置文件增增加对RequestMappingHandlerMapping类的打印级别设置即可,比如在application.properties中增加下面这行配置既可:
logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=tra
参考:
https://blog.didispace.com/spring-boot-learning-21-2-3/
https://blog.csdn.net/zhanghfei88/article/details/123663218
https://blog.csdn.net/weixin_54515240/article/details/129445989
https://www.jianshu.com/p/e88d3f8151db