SpringBoot
1.创建SpringBoot项目

下面的包可以删掉Artifact


添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
|
编写一个测试类
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Controller public class TestController {
@RequestMapping("/test") @ResponseBody public ResultInfo test() { ResultInfo info = new ResultInfo(); info.setStatus(201); info.setMessage("请求成功"); return info; } }
|
配置数据源
application.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/management_system?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false username: root password: root server: port: 8888
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
2.JWT
添加依赖
1 2 3 4 5
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency>
|

2.1. 创建JWT工具类
创建操作JWT的工具类
jwt/JwtUtil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.JWTVerifier; import com.ep.pojo.User; import com.ep.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
import java.util.Calendar;
@Service public class JwtUtil {
@Autowired private static UserService userService;
@Autowired public void setService(UserService userService) { JwtUtil.userService = userService; }
public static String createToken(User user) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, 7); JWTCreator.Builder builder = JWT.create(); builder.withClaim("id", user.getId()) .withClaim("username", user.getUsername()) .withClaim("rid", user.getRid());
return builder.withExpiresAt(calendar.getTime()) .sign(Algorithm.HMAC256(user.getPassword())); }
public static DecodedJWT verifyToken(String token) { if (token==null){ System.out.println("token不能为空"); } Claim claim = getClaimByName(token,"id"); Integer id = claim.asInt(); User user = userService.getById(id); JWTVerifier build = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); return build.verify(token); }
public static Claim getClaimByName(String token, String name){ return JWT.decode(token).getClaim(name); } }
|
2.2. 创建JWT拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import com.auth0.jwt.exceptions.AlgorithmMismatchException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@Slf4j public class JWTInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); log.info("token:" + token);
if (token == null) { log.error("token为空"); } try { JwtUtil.verifyToken(token); } catch (SignatureVerificationException e) { log.error("无效签名"); return false; } catch (TokenExpiredException e) { log.error("token过"); return false; } catch (AlgorithmMismatchException e) { log.error("token算法不一致"); return false; } catch (Exception e) { log.error("token无效"); return false; } return true; } }
|
2.3. 创建JWT配置类
IntercaptorConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import com.ep.jwt.JWTInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class IntercaptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/*") .excludePathPatterns("/", "/login", "/register","/test"); }
}
|
3.接口返回规范
3.1状态码
状态码类别
1XX(信息性状态码)表示接收的请求正在处理
2XX(成功状态码)表示请求正常处理完毕
3XX(重定向状态码)表示需要进行附加操作以完成请求
4XX(客户端错误状态码)表示服务器无法处理请求
5XX(服务器错误状态码)表示服务器处理请求出错
100(临时响应)表示临时响应并需要请求这继续执行操作的状态码
100
-继续请求者应当继续提出请求。服务器返回此代码表示已收到请求的一部分,正在等待其余部分。
101
-切换协议 请求者已要求服务器切换协议,服务器已确认并准备切换。
200(成功)表示成功处理了请求的状态码
200
-成功 服务器已经成功处理了请求。通常,这表示服务器提供了请求的网页。
201
-已创建 请求成功并且服务器创建了新的资源。
202
-已接受 服务器已接受请求,但尚未处理。
203
-非授权信息 服务器已经成功处理了请求,但返回的信息可能来自别的资源。
204
-无内容 服务器成功处理了请求,但没有返回任何内容。
205
-重置内容 服务器成功处理了请求,但没有返回任何内容。
206
-部分内容 服务器成功处理 了部分GET请求。
300(重定向)表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向
300
-多种选择 针对请求,服务器可执行多种操作。服务器可根据请求者(user agent)选择一项操作,或提供操作列表提供请求者选择。
301
-永久移动 请求的网页已永久移动到新位置。服务器回返此响应(对GET或HEAD请求的响应)时,会自动将请求者转到新位置
302
- 临时移动 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
303
-查看其它位置 请求者应当对不同的位置使用单独的GET请求来检索响应时,服务器返回此代码。
304
-未修改 自上次请求后,请求的网页未修改过。服务器返回此响应,不会返回网页的内容。
305
-使用代理 请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理。
307
-临时性重定向 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有的位置来进行以后的请求。
400(请求错误)这些状态码表示可能出错,妨碍了服务器的处理
400
-错误请求 服务器不理解请求的语法。
401
-未授权 请求要求身份验证。对于需要登陆的网页,服务器可能返回此响应。
403
-禁止 服务器拒绝请求。
404
-未找到 服务器到不到请求的网页。
405
-方法禁用 禁用请求中指定的方法。
406
-不接受 无法使用请求的内容特性响应请求的网页。
407
-需要代理授权 此状态码与401(未授权)类似,但指定请求者应当授权使用代理。
408
-请求超时 服务器等候请求时发生超时。
409
-冲突 服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。
410
- 已删除 如果请求的资源已永久删除,服务器就会返回此响应。
411
-需要有效长度 服务器不接受不含有效内容长度标头字段的请求。
412
-未满足前提条件 服务器未满足请求者在请求者设置的其中一个前提条件。
413
-请求实体过大 服务器无法处理请求,因为请求实体过大,超出了服务器的处理能力。
414
-请求的URI过长 请求的URI(通常为网址)过长,服务器无法处理。
415
- 不支持媒体类型 请求的格式不受请求页面的支持。
416
-请求范围不符合要求 如果页面无法提供请求的范围,则服务器会返回此状态码。
417
-未满足期望值 服务器未满足“期望”请求标头字段的要求
500(服务器错误)这些状态码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错
500
-服务器内部错误 服务器遇到错误,无法完成请求。
501
- 尚未实施 服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码。
502
- 错误网关 服务器作为网关或代理,从上游服务器无法收到无效响应。
503
- 服务器不可用 服务器目前无法使用(由于超载或者停机维护)。通常,这只是暂时状态。
504
- 网关超时 服务器作为网关代理,但是没有及时从上游服务器收到请求。
505
- HTTP版本不受支持 服务器不支持请求中所用的HTTP协议版本。
3.2状态码枚举类
使用状态码枚举类可以使后端返回给前端的状态码规范,方便维护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.ep.utils;
public enum ReturnCode {
S200(200,"请求成功"), S201(201,"创建成功"), S204(204,"删除成功"),
S400(400,"请求的地址不存在或者包含不支持的参数"), S401(401,"未授权"), S403(403,"被禁止访问"), S404(404,"请求资源不存在"), S422(422,"错误"),
INVALID_TOKEN(2001,"访问令牌不合法"), ACCESS_DENIED(2003,"没有权限访问该资源"), CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"), USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"), UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式");
private final int code; private final String message;
ReturnCode(int code, String message){ this.code = code; this.message = message; }
public int getCode() { return code; }
public String getMessage() { return message; } }
|
3.3定义返回对象
一个标准的返回格式至少包含3部分:
- status 状态值:由后端统一定义各种返回结果的状态码。
- message 描述:本次接口调用的结果描述。
- data 数据:本次返回的数据。
- timestamp: 接口调用时间(可以不要)
1 2 3 4 5
| { "status":"100", "message":"操作成功", "data":"hello" }
|
方式一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| package com.ep.vo;
import com.ep.utils.ReturnCode; import lombok.Data;
@Data public class ResultInfo {
private int status;
private String message;
private Object data;
private long timestamp ;
public ResultInfo() { this.timestamp = System.currentTimeMillis(); }
public static ResultInfo success(Object data) { ResultInfo info = new ResultInfo(); info.setStatus(ReturnCode.S200.getCode()); info.setMessage(ReturnCode.S200.getMessage()); info.setData(data); return info; }
public static ResultInfo success(int code, String message, Object data) { ResultInfo info = new ResultInfo(); info.setStatus(code); info.setMessage(message); info.setData(data); return info; }
public static ResultInfo fail(int code, String message) { ResultInfo info = new ResultInfo(); info.setStatus(code); info.setMessage(message); return info; } }
|
方式二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| package com.ep.vo;
import com.ep.utils.ReturnCode; import lombok.Data;
@Data public class ResultData<T> { private int status;
private String message;
private T data;
private long timestamp ;
public ResultData() { this.timestamp = System.currentTimeMillis(); }
public static <T> ResultData<T> success(T data) { ResultData<T> resultData = new ResultData<>(); resultData.setStatus(ReturnCode.S200.getCode()); resultData.setMessage(ReturnCode.S200.getMessage()); resultData.setData(data); return resultData; }
public static <T> ResultData<T> success(int code, String message, T data) { ResultData<T> resultData = new ResultData<>(); resultData.setStatus(code); resultData.setMessage(message); resultData.setData(data); return resultData; }
public static <T> ResultData<T> fail(int code, String message) { ResultData<T> info = new ResultData<>(); info.setStatus(code); info.setMessage(message); return info; } }
|
方式一和方式二的区别:
方式二可以指定返回数据的类型,而方式一默认是Object类型
补充Object和泛型的区别
Object范围非常广,而T从一开始就会限定这个类型(包括它可以限定类型为Object)。
Object由于它是所有类的父类,所以会强制类型转换,而T从一开始在编码时(注意是在写代码时)就限定了某种具体类型,所以它不用强制类型转换。
3.4编写测试接口
如果使用了jwt拦截器,记得排除拦截测试接口
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController public class TestController {
@RequestMapping("/test") public ResultInfo test() { return ResultInfo.success(null); }
@GetMapping("/test1") public ResultData<String> test1() { return ResultData.success("hello,success"); } }
|
3.5返回数据
1 2 3 4 5 6 7 8 9 10 11 12
| { "status": 200, "message": "请求成功", "data": null, "timestamp": 1658714057846 } { "status": 200, "message": "请求成功", "data": "hello,success", "timestamp": 1658714199127 }
|
这样确实已经实现了我们想要的结果,我在很多项目中看到的都是这种写法,在Controller层通过ResultData.success()
对返回结果进行包装后返回给前端。
但是还是存在弊端,就是每写一个接口都要调用ResultData.success()
,重复代码太多
3.6ResponseBodyAdvice
快速使用直接点击跳转地址复制粘贴
ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。
3.6.1ResponseBodyAdvice
的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public interface ResponseBodyAdvice<T> {
boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);
@Nullable T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6); }
|
3.6.2编写接口实现类
我们只需要编写实现类即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import com.ep.vo.ResultData; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice<Object> { @Autowired private ObjectMapper objectMapper;
@Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; }
@SneakyThrows @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); } return ResultData.success(o); } }
|
@RestControllerAdvice
注解
@RestControllerAdvice
是@RestController
注解的增强,可以实现三个方面的功能:
- 全局异常处理
- 全局数据绑定
- 全局数据预处理
1 2 3
| if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); }
|
这段代码一定要加,如果Controller直接返回String的话,SpringBoot是直接返回,故我们需要手动转换成json。
经过上面的处理我们就再也不需要通过ResultData.success()
来进行转换了,直接返回原始数据格式,SpringBoot自动帮我们实现包装类的封装。
3.6.3接口测试
1 2 3 4
| @GetMapping("/test1") public String test1() { return "hello,ResponseAdvice"; }
|
1 2 3 4 5 6
| { "status": 200, "message": "请求成功", "data": "hello,ResponseAdvice", "timestamp": 1658715342475 }
|
3.6.4接口异常问题
此时有个问题,由于我们没对Controller的异常进行处理,当我们调用的方法一旦出现异常,就会出现问题,比如下面这个接口
1 2 3 4 5
| @GetMapping("/test1") public int test1() { int i = 1 / 0; return i; }
|
返回结果显然有问题

请接着看下面的全局异常处理
3.7全局异常处理
3.7.1SpringBoot为什么需要全局异常处理器
- 不用手写try…catch,由全局异常处理器统一捕获
使用全局异常处理器最大的便利就是程序员在写代码时不再需要手写try...catch
了,前面我们讲过,默认情况下SpringBoot出现异常时返回的结果是这样:
1 2 3 4 5 6
| { "timestamp": "2021-07-08T08:05:15.423+00:00", "status": 500, "error": "Internal Server Error", "path": "/wrong" }
|
这种数据格式返回给前端,前端是看不懂的,所以这时候我们一般通过try...catch
来处理异常
1 2 3 4 5 6 7 8 9 10 11
| @GetMapping("/wrong") public int error(){ int i; try{ i = 9/0; }catch (Exception e){ log.error("error:{}",e); i = 0; } return i; }
|
我们追求的目标肯定是不需要再手动写try...catch
了,而是希望由全局异常处理器处理。
- 对于自定义异常,只能通过全局异常处理器来处理
1 2 3 4
| @GetMapping("error1") public void empty(){ throw new RuntimeException("自定义异常"); }
|
- 当我们引入Validator参数校验器的时候,参数校验不通过会抛出异常,此时是无法用
try...catch
捕获的,只能使用全局异常处理器。
3.7.2如何实现全局异常处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Slf4j @RestControllerAdvice public class RestExceptionHandler {
@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResultData<String> exception(Exception e) { log.error("全局异常信息 ex={}", e.getMessage(), e); return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage()); }
}
|
@RestControllerAdvice
,RestController的增强类,可用于实现全局异常处理器
@ExceptionHandler
,统一处理某一类异常,从而减少代码重复率和复杂度,比如要获取自定义异常可以@ExceptionHandler(BusinessException.class)
@ResponseStatus
指定客户端收到的http状态码
3.7.3全局异常接入返回的标准格式
要让全局异常接入标准格式很简单,因为全局异常处理器已经帮我们封装好了标准格式,我们只需要直接返回给客户端即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import com.ep.vo.ResultData; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice<Object> { @Autowired private ObjectMapper objectMapper;
@Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; }
@SneakyThrows @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); } if(o instanceof ResultData){ return o; } return ResultData.success(o); } }
|
关键代码:
1 2 3
| if(o instanceof ResultData){ return o; }
|
如果返回的结果是ResultData对象,直接返回即可。
这时候我们再调用上面的错误方法,返回的结果就符合我们的要求了。
1 2 3 4 5 6
| { "status": 500, "message": "自定义异常", "data": null, "timestamp": 1625796580778 }
|

参考:https://zhuanlan.zhihu.com/p/391288136
4.参数校验
一个接口一般对参数(请求数据)都会进行安全校验,参数校验至关重要
首先我们来看一下最常见的做法,就是在业务层进行参数校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public String addUser(User user) { if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) { return "对象或者对象字段不能为空"; } if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) { return "不能输入空字符串"; } if (user.getAccount().length() < 6 || user.getAccount().length() > 11) { return "账号长度必须是6-11个字符"; } if (user.getPassword().length() < 6 || user.getPassword().length() > 16) { return "密码长度必须是6-16个字符"; } return "success"; }
|
虽然这样写是正确的但是太繁琐了。接下来使用Spring Validator和Hibernate Validator这两套Validator来进行方便的参数校验!这两套Validator依赖包已经包含在前面所说的web依赖包里了,所以可以直接使用。
本文使用的是2.7.1版本,需要导包
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
|
4.1Validator + BindResult进行校验
Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import lombok.Data;
import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.Serializable;
@Data public class TestValidator implements Serializable {
@NotNull(message="用户id不能为空") private Integer id;
@NotBlank(message = "用户账号不能为空") @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符") private String username;
@NotBlank(message = "用户密码不能为空") @Size(min = 6,max = 18,message = "密码长度必须为6-18个字符") private String password;
@NotBlank(message = "用户邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; }
|
校验规则和错误提示信息配置完毕后,接下来只需要在接口需要校验的参数上加上@Valid注解,并添加BindResult参数即可方便完成验证:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @PostMapping("/testvalidator") public ResultData<String> testValidator(@RequestBody @Valid TestValidator user, BindingResult bindingResult){
for (ObjectError error : bindingResult.getAllErrors()) { return ResultData.fail(400,error.getDefaultMessage()); } System.out.println(user); return ResultData.success("校验成功"); }
|
@Valid注解与@Validated注解功能差不多
不同点在于:
- @Valid属于javax包下,而@Validated属于Spring下
- @Valid支持嵌套校验、而@Validated不支持
- @Validated支持分组,而@Valid不支持
一般SpringBoot的项目会使用Spring Validation,它是对Hibernate Validation的二次封装。在SpringMVC模块中添加了自动校验。并将校验信息封装到特定的类中。
4.2提供的校验注解
@Null
被注释的元素必须为null
@NotNull
被注释的元素必须不为null, 不能为null,可以是空
@NotBlank(message =)
验证字符串非 null,且长度必须大于 0,字符串trim()后也不能等于“”
@NotEmpty
不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“”
@AssertTrue
被注释的元素必须为true
@AssertFalse
被注释的元素必须为false
@Min(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)
被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)
被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past
被注释的元素必须是一个过去的日期
@Future
被注释的元素必须是一个将来的日期
@Pattern(value)
被注释的元素必须符合指定的正则表达式
@Email
被注释的元素必须是电子邮箱地址
@Length(min=,max=)
被注释的字符串的大小必须在指定的范围内
@Range(min=,max=,message=)
被注释的元素必须在合适的范围内
@URL
必须是一个URL
4.3Validator + 自动抛出异常
我们完全可以将BindingResult这一步给去掉:
1 2 3 4 5 6 7
| @PostMapping("/testvalidator") public String testValidator(@RequestBody @Valid TestValidator user){
System.out.println(user); return "校验通过"; }
|
其实这样就已经达到我们想要的效果了,参数校验不通过自然就不执行接下来的业务逻辑,去掉BindingResult后会自动引发异常,异常发生了自然而然就不会执行业务逻辑。也就是说,我们完全没必要添加相关BindingResult相关操作嘛。不过事情还没有完,异常是引发了,可我们并没有编写返回错误信息的代码呀,那参数校验失败了会响应什么数据给前端呢? 我们来看一下刚才异常发生后接口响应的数据:

参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理,不然还不如用之前BindingResult方式呢。又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果!
4.3.1基本使用
首先,我们需要新建一个类,在这个类上加上@ControllerAdvice
或@RestControllerAdvice
注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller
还是@RestController
来决定) 然后在类中新建方法,在方法上加上@ExceptionHandler
注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理! 我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException
全局处理:
1 2 3 4 5 6 7
| @ExceptionHandler(MethodArgumentNotValidException.class) public ResultData<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { ObjectError objectError = e.getBindingResult().getAllErrors().get(0); return ResultData.fail(400,objectError.getDefaultMessage()); }
|
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Slf4j @RestControllerAdvice public class RestExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) public ResultData<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { ObjectError objectError = e.getBindingResult().getAllErrors().get(0); return ResultData.fail(400,objectError.getDefaultMessage()); }
@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResultData<String> exception(Exception e) { log.error("全局异常信息 ex={}", e.getMessage(), e); return ResultData.fail(ReturnCode.S500.getCode(),e.getMessage()); }
}
|

4.3.2自定义异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import lombok.Getter;
@Getter public class APIException extends RuntimeException { private int code; private String msg;
public APIException() { this(400, "错误请求"); }
public APIException(String msg) { this(400, msg); }
public APIException(int code, String msg) { super(msg); this.code = code; this.msg = msg; } }
|
这样就对异常的处理就比较规范了,当然还可以添加对Exception
的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。
在刚才的全局异常处理类中记得添加对我们自定义异常的处理:
1 2 3 4
| @ExceptionHandler(APIException.class) public String APIExceptionHandler(APIException e) { return e.getMsg(); }
|
参考:https://zhuanlan.zhihu.com/p/340620501
4.4分组校验
但是实际业务是在编辑的时候 Id
才是必填,在新增的时候 name
必填,这时候可以用groups分组功能来实现:同一个模型在不同场景下,动态区分校验模型中的不同字段。
4.4.1使用方式
- 首先我们定义一个分组接口ValidGroup,再在分组接口总定义出多个不同的操作类型,Create,Update,Query,Delete
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.ep.utils;
import javax.validation.groups.Default;
public interface ValidGroup extends Default { interface Crud extends ValidGroup{
interface Create extends Crud{
}
interface Update extends Crud{
}
interface Query extends Crud{
}
interface Delete extends Crud{
} } }
|
- 在模型中给校验参数分配分组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import com.ep.utils.ValidGroup; import lombok.Data;
import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.Serializable;
@Data public class TestValidator implements Serializable {
@NotNull(message="用户id不能为空") private Integer id;
@NotBlank(message = "用户账号不能为空",groups = ValidGroup.Crud.Update.class) @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符") private String username;
@NotBlank(groups = ValidGroup.Crud.Query.class) @Size(min = 6,max = 18,message = "密码长度必须为6-18个字符") private String password;
@NotBlank(message = "用户邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; }
|
这里@Emailh和id注解未指定分组,默认会属于Default分组,username和password指定了分组就不会再属于Default分组了。
3.在参数校验时通过value属性指定分组
1 2 3 4 5 6 7 8 9 10 11 12 13
| @PostMapping("/testvalidator") public ResultData<String> testValidator(@RequestBody @Validated(value = ValidGroup.Crud.Update.class) TestValidator user){
return ResultData.success("校验成功"); }
@PostMapping("/testvalidator") public ResultData<String> testValidator(@RequestBody @Validated({ValidGroup.Crud.Update.class,ValidGroup.Crud.Create.class}) TestValidator user){
return ResultData.success("校验成功"); }
|
这里通过 @Validated(value = ValidGroup.Crud.Update.class)
指定了具体的分组,上面提到的是否继承Default的区别在于:
- 如果继承了Default,@Validated标注的注解也会校验未指定分组或者Default分组的参数,比如email
- 如果不继承Default则不会校验未指定分组的参数,需要加上
@Validated(value = {ValidGroup.Crud.Update.class, Default.class}
才会校验
4.5快速失败(Fali Fast)
默认情况下在对参数进行校验时Spring Validation会校验完所有字段然后才抛出异常,可以通过配置开启 Fali Fast
模式,一旦校验失败就立即返回。
config/ValidatedConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import org.hibernate.validator.HibernateValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory;
@Configuration public class ValidatedConfig {
@Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory(); return validatorFactory.getValidator(); } }
|
4.6嵌套校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import lombok.Data;
import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size;
@Data public class TestDemo { @NotNull(message = "id必须非空") private Long id;
@NotBlank(message = "用户名不能为空,并且长度必须大于0") @Size(min = 6, max = 11, message = "用户名长度必须是6-11个字符") private String username;
@NotBlank(message = "用户密码不能为空,并且长度必须大于0") @Size(min = 6, max = 16, message = "密码长度必须是6-16个字符") private String password;
@Email(message = "邮箱格式错误") @NotBlank(message = "邮箱不能为空,并且长度必须大于0") private String email;
@Valid @NotNull private Test02 test02; }
|
1 2 3 4 5 6 7 8 9 10
| import lombok.Data;
import javax.validation.constraints.NotNull;
@Data public class Test02 {
@NotNull(message = "id必须非空") private Long id; }
|
4.7自定义校验注解
4.7.1编写自定义注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = PhoneValidator.class ) public @interface Phone { String message() default "手机格式不正确!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
|
4.7.2编写该注解校验器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.regex.Matcher; import java.util.regex.Pattern;
public class PhoneValidator implements ConstraintValidator<Phone, String> { @Override public boolean isValid(String phoneNum, ConstraintValidatorContext constraintValidatorContext) { if (phoneNum == null && phoneNum.length() == 0) { return true; } Pattern p = Pattern.compile("^(13[0-9]|14[5|7|9]|15[0|1|2|3|5|6|7|8|9]|17[0|1|6|7|8]|18[0-9])\\d{8}$"); Matcher matcher = p.matcher(phoneNum); return matcher.matches(); } }
|
4.7.3使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import com.ep.utils.Phone; import com.ep.utils.ValidGroup; import lombok.Data;
import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.Serializable;
@Data public class TestValidator implements Serializable {
@NotNull(message="用户id不能为空") private Integer id;
@NotBlank(message = "用户账号不能为空",groups = ValidGroup.Crud.Update.class) @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符") private String username;
@Phone(message = "手机号码不合法") private String phone; }
|

5.日志
点击直接跳转使用
5.1日志框架
小张;开发一个大型系统;
1、System.out.println(“”);将关键数据打印在控制台;去掉?写在一个文件?
2、框架来记录系统的一些运行时信息;日志框架 ; zhanglogging.jar;
3、高大上的几个功能?异步模式?自动归档?xxxx? zhanglogging-good.jar?
4、将以前框架卸下来?换上新的框架,重新修改之前相关的API;zhanglogging-prefect.jar;
5、JDBC—数据库驱动;
写了一个统一的接口层;日志门面(日志的一个抽象层);logging-abstract.jar;
给项目中导入具体的日志实现就行了;我们之前的日志框架都是实现的抽象层;
市面上的日志框架;
JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j….
日志门面 (日志的抽象层) |
日志实现 |
JCL(Jakarta Commons Logging) SLF4j(Simple Logging Facade for Java) jboss-logging |
Log4j JUL(java.util.logging) Log4j2 Logback |
左边选一个门面(抽象层)、右边来选一个实现;
日志门面: SLF4J;
日志实现:Logback;
SpringBoot:底层是Spring框架,Spring框架默认是用JCL;‘
==SpringBoot选用 SLF4j和logback;==
5.2 SLF4j使用
以后开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法;
给系统里面导入slf4j的jar和 logback的实现jar
1 2 3 4 5 6 7 8 9
| import org.slf4j.Logger; import org.slf4j.LoggerFactory;
public class HelloWorld { public static void main(String[] args) { Logger logger = LoggerFactory.getLogger(HelloWorld.class); logger.info("Hello World"); } }
|
图示;

每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件;
5.2.2遗留问题
a(slf4j+logback): Spring(commons-logging)、Hibernate(jboss-logging)、MyBatis、xxxx
统一日志记录,即使是别的框架和我一起统一使用slf4j进行输出?

如何让系统中所有的日志都统一到slf4j;
==1、将系统中其他日志框架先排除出去;==
==2、用中间包来替换原有的日志框架;==
==3、我们导入slf4j其他的实现==
5.3.SpringBoot日志关系
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
|
SpringBoot使用它来做日志功能;
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency>
|
底层依赖关系

总结:
1)、SpringBoot底层也是使用slf4j+logback的方式进行日志记录
2)、SpringBoot也把其他的日志都替换成了slf4j;
3)、中间替换包?
1 2 3 4 5 6
| @SuppressWarnings("rawtypes") public abstract class LogFactory {
static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = "http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j";
static LogFactory logFactory = new SLF4JLogFactory();
|

4)、如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉?
Spring框架用的是commons-logging;
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency>
|
==SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;==
5.4日志使用;
5.4.1默认配置
SpringBoot默认帮我们配置好了日志;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest class SpringbootLoggingApplicationTests { Logger logger = LoggerFactory.getLogger(getClass());
@Test void contextLoads() { logger.trace("trace日志..."); logger.debug("debug日志..."); logger.info("info日志..."); logger.warn("warn日志..."); logger.error("error日志..."); } }
|
不指定路径在当前项目文件夹下生成log文件

1、logging.file.name
设置具体输出的日志名称,可以是绝对路径或者基于当前运行目录的相对路径,例如:logging.file.name=app.log、logging.file.name=/var/log/hello-service/app.log
2、logging.file.path
设置输出的日志被写入到的目录,默认文件名为 spring.log
,例如:logging.file.path=D:/log
3、如果你两个都同时设置,则以 logging.file.name
为准。
本文重点,是推荐使用 logging.file.name,原因是因为这里涉及到另外一个知识点,那就是 spring 的 actuator 中的 logfile 接口(例如:http://localhost:8080/hello-service/actuator/logfile 可以直接获取日志文件,比较不错的是它还支持使用 HTTPRange 头来检索日志文件的部分内容)。
如果你只设置了 logging.file.name,不论你实际的文件名设置为什么,这个接口都可以正常获取日志文件(因为接口它能明确知道日志文件的位置路径)。
如果你只设置了 logging.file.path,并且没有在自定义的 logback.xml 文件重新定义输出的具体日志文件的名称(也就是仍然使用默认的spring.log),那么 logfile 接口也是能正常获取日志文件,因为它是按照默认文件 spring.log 获取日志的。但是但凡你在 logback.xml 中重新定义了输出的日志文件名(例如输出的日志文件为 hello-service.log),则你使用 actuator/logfile 接口就会或得一个404,因为此时框架并不知道你的真实日志文件叫什么,它去读取 spring.log 又读不到(因为你改名了)。
so,建议直接使用 logging.file.name,清晰明了。
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
SpringBoot修改日志的默认配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| logging.level.com.ep = trace
logging.file.name = springboot.log
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%n
|
5.4.2指定配置
给类路径下放上每个日志框架自己的配置文件即可;SpringBoot就不使用他默认配置的了
Logging System |
Customization |
Logback |
logback-spring.xml , logback-spring.groovy , logback.xml or logback.groovy |
Log4j2 |
log4j2-spring.xml or log4j2.xml |
JDK (Java Util Logging) |
logging.properties |
logback.xml:直接就被日志框架识别了;

logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能
application.properties中配置环境
1
| spring.profiles.active=dev
|
1 2 3 4 5
| <springProfile name="staging"> 可以指定某段配置只在某个环境下生效 </springProfile>
|
如:(logback-spring.xml中的代码加上springrofile相关。如果是logback.xml则需要去掉,只留pattern就可以了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout"> <springProfile name="dev"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern> </springProfile> <springProfile name="!dev"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern> </springProfile> </layout> </appender>
|

如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误
no applicable action for [springProfile]
5.5切换日志框架
可以按照slf4j的日志适配图,进行相关的切换;
slf4j+log4j的方式;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> <exclusion> <artifactId>log4j-over-slf4j</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency>
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </dependency>
|
切换为log4j2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-logging</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
|