Springboot使用@Valid 和AOP做参数校验及日志输出问题

网友投稿 488 2022-12-21


Springboot使用@Valid 和AOP做参数校验及日志输出问题

项目背景

最近在项目上对接前端的的时候遇到了几个问题

1.经常要问前端要请求参数

2.要根据请求参数写大量if...else,代码散步在 Controller 中,影响代码质量

3.为了解决问题1,到处记日志,导致到处改代码

解决方案

为了解决这类问题,我使用了@Valid 做参数校验,并使用AOP记录前端请求日志

1.Bean实体类增加注解

对要校验的实体类增加注解,如果实体类中有List结构,就在List上加@Valid

@Valid注解

注解

备注

@Null

只能为null

@NotNull

必须不为null

@Max(value)

必须为一个不大于 value 的数字

@Min(value)

必须为一个不小于 value 的数字

@AssertFalse

必须为false

@AssertTrue

必须为true

@DecimalMax(value)

必须为一个小于等于 value 的数字

@DecimalMin(value)

必须为一个大于等于 value 的数字

@Digits(integer,fraction)

必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction

@Past

必须是 日期 ,且小于当前日期

@Future

必须是 日期 ,且为将来的日期

@Size(max,min)

字符长度必须在min到max之间

@Pattern(regex=,flag=)

必须符合指定的正则表达式

@NotEmpty

必须不为null且不为空(字符串长度不为0、集合大小不为0)

@NotBlank

必须不为空(不为null、去除首位空格后长度不为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格

@Email

必须为Email,也可以通过正则表达式和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/tVLlxZvGqv21 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 userList;

@NotNull(message = "权限不能为空")

@Min(value = 1, message = "权限范围为[1-99]")

@Max(value = 99, message = "权限范围为[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层

在需要校验的pojo前边添加@Validated,在需要校验的pojo后边添加BindingResult br接收校验出错信息,需要注意的是, BindingResult result一定要跟在 @Validated 注解对象的后面(必须是实体类),而且当有多个@Validated注解时,每个注解对象后面都需要添加一个 BindingResult,而实际使用时由于在WebLogAspect切点读取了请求数据,会导致在Controller层请求参数中读不到数据,这里需要修改其他内容,详见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

定义切点@Pointcut("execution( com.zero.check.controller.. (..))"),定义后可监控com.zero.check.controller包和子包里任意方法的执行

如果输入参数不能通过校验,就直接抛出异常,由于定义了UserInfoHandler拦截器,可以拦截处理校验错误,这样就可以省略大量的非空判断,让Controller层专注业务代码,并且将日志集中在WebLogAspect中处理,不会因为记录日志导致要到处改代码

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 {

/**

* 校验错误拦截处理

*

* @param e 错误信息集合

* @return 错误信息

*/

@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";

/**

* 定义切点,切点为com.zero.check.controller包和子包里任意方法的执行

*/

@Pointcut("execution(* com.zero.check.controller..*(..))")

public void webLog() {

}

/**

* 前置通知,在切点之前执行的通知

*

* @param joinPoint 切点

*/

@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));

}

//获取请求参数

try {

String reqBody = this.getReqBody();

logger.info("REQUEST: " + reqBody);

} catch (Exception ex) {

logger.info("get Request Error: " + ex.getMessage());

}

}

/**

* 后置通知,切点后执行

*

* @param ret

*/

@AfterReturning(returning = "ret", pointcut = "webLog()")

public void doAfterReturning(Object ret) {

//处理完请求,返回内容

try {

logger.info("RESPONSE: " + JSON.toJSONString(ret));

} catch (Exception ex) {

logger.info("get Response Error: " + ex.getMessage());

}

}

/**

* 返回调用参数

*

* @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等)中非线程安全状态的bean采用ThreadLocal进行处理

* 让它们也成为线程安全的状态

*

* @return

*/

private HttpServletRequest getHttpServletRequest() {

//获取RequestAttributes

RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

return (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

}

/**

* 获取GET请求数据

*

* @param request

* @return

*/

private String getGetReqBody(HttpServletRequest request) {

Enumeration enumeration = request.getParameterNames();

Map parameterMap = new HashMap<>(16);

while (enumeration.hasMoreElements()) {

String parameter = enumeration.nextElement();

parameterMap.put(parameter, request.getParameter(parameter));

}

return parameterMap.toString();

}

/**

* 获取POST请求数据

*

* @param request

* @return 返回POST参数

*/

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

请求参数

{

"id":"12",

"userList": [

{

"userId": "Google",

"userName": "http://google.com"

},

{

"userId": "S",

"userName": "http://SoSo.com"

},

{

"userId": "SoSo",

"userName": "http://SoSo.com"

}

],

"roleId":"11"

}

返回结果

{

"code": "ok",

"data": "Hello 12",

"requestid": "706cd81db49d4c9795e5457cebb1ba8c"

}

请求参数

{

"id":"1A2",

"userList": [

{

"userId": "Google",

tVLlxZvGqv "userName": "http://google.com"

},

{

"userId": "S",

"userName": "http://SoSo.com"

},

{

"userId": "SoSo",

"userName": "http://SoSo.com"

}

],

"roleId":"11"

}

返回结果

{

"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"

}

控制台输出

可以看到第一次请求时WebLogAspect成功打印了请求数据和返回结果,而第二次因为没有通过校验,没有进入WebLogAspect,所以没有打印数据

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://google.com"

},

{

"userId": "S",

"userName": "http://SoSo.com"

},

{

"userId": "SoSo",

"userName": "http://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

返回结果

{

"code": "ok",

"data": "Hello zero",

"requestid": "9b5ea9bf1db64014b0b4d445d8baf9dc"

}

localhost:9004/check/userValidGet?userId=a&userName=

返回结果

{

"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"

}

控制台输出

可以看到第一次请求时WebLogAspect成功打印了请求和返回结果,而第二次因为没有通过校验,没有进入WebLogAspect,所以没有打印数据

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

总结


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:IntelliJ IDEA下自动生成Hibernate映射文件以及实体类
下一篇:idea hibernate jpa 生成实体类的实现
相关文章

 发表评论

暂时没有评论,来抢沙发吧~