Springboot使用@Valid 和AOP做參數(shù)校驗(yàn)及日志輸出問題
項(xiàng)目背景
最近在項(xiàng)目上對接前端的的時(shí)候遇到了幾個(gè)問題
1.經(jīng)常要問前端要請求參數(shù)
2.要根據(jù)請求參數(shù)寫大量if...else,代碼散步在 Controller 中,影響代碼質(zhì)量
3.為了解決問題1,到處記日志,導(dǎo)致到處改代碼
解決方案
為了解決這類問題,我使用了@Valid 做參數(shù)校驗(yàn),并使用AOP記錄前端請求日志
1.Bean實(shí)體類增加注解
對要校驗(yàn)的實(shí)體類增加注解,如果實(shí)體類中有List結(jié)構(gòu),就在List上加@Valid
@Valid注解
| 注解 | 備注 |
|---|---|
| @Null | 只能為null |
| @NotNull | 必須不為null |
| @Max(value) | 必須為一個(gè)不大于 value 的數(shù)字 |
| @Min(value) | 必須為一個(gè)不小于 value 的數(shù)字 |
| @AssertFalse | 必須為false |
| @AssertTrue | 必須為true |
| @DecimalMax(value) | 必須為一個(gè)小于等于 value 的數(shù)字 |
| @DecimalMin(value) | 必須為一個(gè)大于等于 value 的數(shù)字 |
| @Digits(integer,fraction) | 必須為一個(gè)小數(shù),且整數(shù)部分的位數(shù)不能超過integer,小數(shù)部分的位數(shù)不能超過fraction |
| @Past | 必須是 日期 ,且小于當(dāng)前日期 |
| @Future | 必須是 日期 ,且為將來的日期 |
| @Size(max,min) | 字符長度必須在min到max之間 |
| @Pattern(regex=,flag=) | 必須符合指定的正則表達(dá)式 |
| @NotEmpty | 必須不為null且不為空(字符串長度不為0、集合大小不為0) |
| @NotBlank | 必須不為空(不為null、去除首位空格后長度不為0),不同于@NotEmpty,@NotBlank只應(yīng)用于字符串且在比較時(shí)會(huì)去除字符串的空格 |
| 必須為Email,也可以通過正則表達(dá)式和flag指定自定義的email格式 |
UserInfo
package com.zero.check.query;
import lombok.Data;
import org.hibernate.validator.constraints.EAN;
import org.springframework.stereotype.Component;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 15:05
* @history: 1.2019/11/21 created by wei.wang
*/
@Component
@Data
public class UserInfo {
@NotBlank(message = "主鍵不能為空")
@Pattern(regexp = "^[1-9]\\d*$",message = "主鍵范圍不正確")
private String id;
@Valid
@NotEmpty(message = "用戶列表不能為空")
private List<User> userList;
@NotNull(message = "權(quán)限不能為空")
@Min(value = 1, message = "權(quán)限范圍為[1-99]")
@Max(value = 99, message = "權(quán)限范圍為[1-99]")
private Long roleId;
}
User
package com.zero.check.query;
import lombok.Data;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 16:03
* @history: 1.2019/11/21 created by wei.wang
*/
@Component
@Data
public class User {
@NotBlank(message = "用戶工號不能為空")
private String userId;
@NotBlank(message = "用戶名稱不能為空")
private String userName;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
2.Controller層
在需要校驗(yàn)的pojo前邊添加@Validated,在需要校驗(yàn)的pojo后邊添加BindingResult br接收校驗(yàn)出錯(cuò)信息,需要注意的是, BindingResult result一定要跟在 @Validated 注解對象的后面(必須是實(shí)體類),而且當(dāng)有多個(gè)@Validated注解時(shí),每個(gè)注解對象后面都需要添加一個(gè) BindingResult,而實(shí)際使用時(shí)由于在WebLogAspect切點(diǎn)讀取了請求數(shù)據(jù),會(huì)導(dǎo)致在Controller層請求參數(shù)中讀不到數(shù)據(jù),這里需要修改其他內(nèi)容,詳見Git
DataCheckController
package com.zero.check.controller;
import com.zero.check.query.User;
import com.zero.check.query.UserInfo;
import com.zero.check.utils.Response;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 14:57
* @history: 1.2019/11/21 created by wei.wang
*/
@RestController
@RequestMapping(value = "/check")
public class DataCheckController {
@PostMapping(value = "/userValidPost")
public Response queryUserPost(@Valid @RequestBody UserInfo userInfo, BindingResult result) {
return Response.ok().setData("Hello " + userInfo.getId());
}
@GetMapping(value = "/userValidGet")
public Response queryUserGet(@Valid User user, BindingResult result) {
return Response.ok().setData("Hello " + user.getUserName());
}
}
3.AOP
定義切點(diǎn)@Pointcut("execution( com.zero.check.controller.. (..))"),定義后可監(jiān)控com.zero.check.controller包和子包里任意方法的執(zhí)行
如果輸入?yún)?shù)不能通過校驗(yàn),就直接拋出異常,由于定義了UserInfoHandler攔截器,可以攔截處理校驗(yàn)錯(cuò)誤,這樣就可以省略大量的非空判斷,讓Controller層專注業(yè)務(wù)代碼,并且將日志集中在WebLogAspect中處理,不會(huì)因?yàn)橛涗浫罩緦?dǎo)致要到處改代碼
if (bindingResult.hasErrors()) {
FieldError error = bindingResult.getFieldError();
throw new UserInfoException(Response.error(error.getDefaultMessage()).setData(error));
}
UserInfoHandler
package com.zero.check.handler;
import com.zero.check.exception.UserInfoException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 15:04
* @history: 1.2019/11/21 created by wei.wang
*/
@RestControllerAdvice
public class UserInfoHandler {
/**
* 校驗(yàn)錯(cuò)誤攔截處理
*
* @param e 錯(cuò)誤信息集合
* @return 錯(cuò)誤信息
*/
@ExceptionHandler(UserInfoException.class)
public Object handle(UserInfoException e) {
return e.getR();
}
}
WebLogAspect
package com.zero.check.aspect;
import com.alibaba.fastjson.JSON;
import com.zero.check.exception.UserInfoException;
import com.zero.check.utils.Response;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 13:47
* @history: 1.2019/11/21 created by wei.wang
*/
@Aspect
@Component
public class WebLogAspect {
private Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
private final String REQUEST_GET = "GET";
private final String REQUEST_POST = "POST";
/**
* 定義切點(diǎn),切點(diǎn)為com.zero.check.controller包和子包里任意方法的執(zhí)行
*/
@Pointcut("execution(* com.zero.check.controller..*(..))")
public void webLog() {
}
/**
* 前置通知,在切點(diǎn)之前執(zhí)行的通知
*
* @param joinPoint 切點(diǎn)
*/
@Before("webLog() &&args(..,bindingResult)")
public void doBefore(JoinPoint joinPoint, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
FieldError error = bindingResult.getFieldError();
throw new UserInfoException(Response.error(error.getDefaultMessage()).setData(error));
}
//獲取請求參數(shù)
try {
String reqBody = this.getReqBody();
logger.info("REQUEST: " + reqBody);
} catch (Exception ex) {
logger.info("get Request Error: " + ex.getMessage());
}
}
/**
* 后置通知,切點(diǎn)后執(zhí)行
*
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) {
//處理完請求,返回內(nèi)容
try {
logger.info("RESPONSE: " + JSON.toJSONString(ret));
} catch (Exception ex) {
logger.info("get Response Error: " + ex.getMessage());
}
}
/**
* 返回調(diào)用參數(shù)
*
* @return ReqBody
*/
private String getReqBody() {
//從獲取RequestAttributes中獲取HttpServletRequest的信息
HttpServletRequest request = this.getHttpServletRequest();
//獲取請求方法GET/POST
String method = request.getMethod();
Optional.ofNullable(method).orElse("UNKNOWN");
if (REQUEST_POST.equals(method)) {
return this.getPostReqBody(request);
} else if (REQUEST_GET.equals(method)) {
return this.getGetReqBody(request);
}
return "get Request Parameter Error";
}
/**
* 獲取request
* Spring對一些(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態(tài)的bean采用ThreadLocal進(jìn)行處理
* 讓它們也成為線程安全的狀態(tài)
*
* @return
*/
private HttpServletRequest getHttpServletRequest() {
//獲取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
}
/**
* 獲取GET請求數(shù)據(jù)
*
* @param request
* @return
*/
private String getGetReqBody(HttpServletRequest request) {
Enumeration<String> enumeration = request.getParameterNames();
Map<String, String> parameterMap = new HashMap<>(16);
while (enumeration.hasMoreElements()) {
String parameter = enumeration.nextElement();
parameterMap.put(parameter, request.getParameter(parameter));
}
return parameterMap.toString();
}
/**
* 獲取POST請求數(shù)據(jù)
*
* @param request
* @return 返回POST參數(shù)
*/
private String getPostReqBody(HttpServletRequest request) {
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} catch (IOException e) {
logger.info("get Post Request Parameter err : " + e.getMessage());
}
return stringBuilder.toString();
}
}
4.測試
POST接口
localhost:9004/check/userValidPost
請求參數(shù)
{
"id":"12",
"userList": [
{
"userId": "Google",
"userName": "http://www.google.com"
},
{
"userId": "S",
"userName": "http://www.SoSo.com"
},
{
"userId": "SoSo",
"userName": "http://www.SoSo.com"
}
],
"roleId":"11"
}
返回結(jié)果
{
"code": "ok",
"data": "Hello 12",
"requestid": "706cd81db49d4c9795e5457cebb1ba8c"
}
請求參數(shù)
{
"id":"1A2",
"userList": [
{
"userId": "Google",
"userName": "http://www.google.com"
},
{
"userId": "S",
"userName": "http://www.SoSo.com"
},
{
"userId": "SoSo",
"userName": "http://www.SoSo.com"
}
],
"roleId":"11"
}
返回結(jié)果
{
"code": "error",
"message": "主鍵范圍不正確",
"data": {
"codes": [
"Pattern.userInfo.id",
"Pattern.id",
"Pattern.java.lang.String",
"Pattern"
],
"arguments": [
{
"codes": [
"userInfo.id",
"id"
],
"arguments": null,
"defaultMessage": "id",
"code": "id"
},
[],
{
"defaultMessage": "^[1-9]\\d*$",
"arguments": null,
"codes": [
"^[1-9]\\d*$"
]
}
],
"defaultMessage": "主鍵范圍不正確",
"objectName": "userInfo",
"field": "id",
"rejectedValue": "1A2",
"bindingFailure": false,
"code": "Pattern"
},
"requestid": "076c899495b448b59f1b133efd130061"
}
控制臺(tái)輸出
可以看到第一次請求時(shí)WebLogAspect成功打印了請求數(shù)據(jù)和返回結(jié)果,而第二次因?yàn)闆]有通過校驗(yàn),沒有進(jìn)入WebLogAspect,所以沒有打印數(shù)據(jù)
2019-11-21 22:50:43.283 INFO 94432 --- [nio-9004-exec-2] com.zero.check.aspect.WebLogAspect : REQUEST: {
"id":"1",
"userList": [
{
"userId": "Google",
"userName": "http://www.google.com"
},
{
"userId": "S",
"userName": "http://www.SoSo.com"
},
{
"userId": "SoSo",
"userName": "http://www.SoSo.com"
}
],
"roleId":"11"
}
2019-11-21 22:50:43.345 INFO 94432 --- [nio-9004-exec-2] com.zero.check.aspect.WebLogAspect : RESPONSE: {"code":"ok","data":"Hello 1","requestid":"286174a075c144eeb0de0b8dbd7c1851"}
GET接口
localhost:9004/check/userValidGet?userId=a&userName=zero
返回結(jié)果
{
"code": "ok",
"data": "Hello zero",
"requestid": "9b5ea9bf1db64014b0b4d445d8baf9dc"
}
localhost:9004/check/userValidGet?userId=a&userName=
返回結(jié)果
{
"code": "error",
"message": "用戶名稱不能為空",
"data": {
"codes": [
"NotBlank.user.userName",
"NotBlank.userName",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"user.userName",
"userName"
],
"arguments": null,
"defaultMessage": "userName",
"code": "userName"
}
],
"defaultMessage": "用戶名稱不能為空",
"objectName": "user",
"field": "userName",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
},
"requestid": "5677d93c084d418e88cf5bb8547c5a2e"
}
控制臺(tái)輸出
可以看到第一次請求時(shí)WebLogAspect成功打印了請求和返回結(jié)果,而第二次因?yàn)闆]有通過校驗(yàn),沒有進(jìn)入WebLogAspect,所以沒有打印數(shù)據(jù)
2019-11-21 23:18:50.755 INFO 94432 --- [nio-9004-exec-9] com.zero.check.aspect.WebLogAspect : REQUEST: {userName=zero, userId=a}
2019-11-21 23:18:50.756 INFO 94432 --- [nio-9004-exec-9] com.zero.check.aspect.WebLogAspect : RESPONSE: {"code":"ok","data":"Hello zero","requestid":"422edc9cd59d45bea275e579a67ccd0c"}
5.代碼Git地址
git@github.com:A-mantis/SpringBootDataCheck.git
總結(jié)
以上所述是小編給大家介紹的Springboot使用@Valid 和AOP做參數(shù)校驗(yàn)及日志輸出問題,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
- springboot使用自定義注解實(shí)現(xiàn)aop切面日志
- SpringBoot使用AOP記錄接口操作日志詳解
- SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)計(jì)全局接口訪問次數(shù)詳解
- 在springboot中使用AOP進(jìn)行全局日志記錄
- SpringBoot使用AOP,內(nèi)部方法失效的解決方案
- 詳解基于SpringBoot使用AOP技術(shù)實(shí)現(xiàn)操作日志管理
- SpringBoot使用AOP+注解實(shí)現(xiàn)簡單的權(quán)限驗(yàn)證的方法
- SpringBoot中使用AOP打印接口日志的方法
- SpringBoot項(xiàng)目中使用AOP的方法
- Springboot 中使用 Aop代碼實(shí)戰(zhàn)教程
相關(guān)文章
Spring源碼學(xué)習(xí)之動(dòng)態(tài)代理實(shí)現(xiàn)流程
這篇文章主要給大家介紹了關(guān)于Spring源碼學(xué)習(xí)之動(dòng)態(tài)代理實(shí)現(xiàn)流程的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
SpringBoot使用Validation包進(jìn)行輸入?yún)?shù)校驗(yàn)
Spring Boot 自帶的 spring-boot-starter-validation 包支持以標(biāo)準(zhǔn)注解的方式進(jìn)行輸入?yún)?shù)校驗(yàn),本文即關(guān)注 spring-boot-starter-validation 包所涵蓋的標(biāo)準(zhǔn)注解的使用、校驗(yàn)異常的捕獲與展示、分組校驗(yàn)功能的使用,以及自定義校驗(yàn)器的使用,需要的朋友可以參考下2024-05-05
Java?Chassis3應(yīng)用視角的配置管理技術(shù)解密
這篇文章主要為大家介紹了Java?Chassis3應(yīng)用視角的配置管理相關(guān)的機(jī)制和背后故事,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01

