参数校验是保证程序可以正常运行、防止恶意参数攻击的一个重要手段,但是在业务层重复书写校验代码会造成代码的臃肿,本文将介绍在 Spring boot 项目中使用 Spring AOP 进行 Service 层参数校验
Hibernate validator 官方文档: https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints
../pom.xml
../demo-model/pom.xml
../demo-web/pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>${springboot.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
<version>${springboot.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
<version>${springboot.version}version>
dependency>
其中 ${springboot.version}
为项目的 springboot 版本,本示例中的版本为:
<springboot.version>2.0.6.RELEASEspringboot.version>
../demo-common/src/main/java/com/ljq/demo/springboot/common/annotation/ParamsCheck.java
package com.ljq.demo.springboot.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description: 参数校验注解
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Target({ ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamsCheck {
/**
* 是否忽略
* 当 ignore = true 时,其他属性设置无效
*
* @return
*/
boolean ignore() default false;
}
这里只列举了一个注解属性,如果有需要,可以自行添加
../demo-common/src/main/java/com/ljq/demo/springboot/common/validate/BeanValidateUtil.java
package com.ljq.demo.springboot.common.validate;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Iterator;
import java.util.Set;
/**
* @Description: java bean 参数校验工具类
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
public class BeanValidateUtil {
private BeanValidateUtil(){}
/**
* java bean 数据校验
* 参数符合要求,返回 null,否则返回错误原因(不包含参数名)
*
* @param target 目标 bean
* @param
* @return
*/
public static <T> String validate(T target){
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<T>> constraintViolations = validator.validate(target);
Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> error = iterator.next();
return error.getMessage();
}
return null;
}
/**
* java bean 数据校验
* 参数符合要求,返回 null,否则返回错误原因(包含参数名)
*
* @param target 目标 bean
* @param
* @return
*/
public static <T> String validateWithParamter(T target){
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<T>> constraintViolations = validator.validate(target);
Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> error = iterator.next();
StringBuffer buffer = new StringBuffer().append("[")
.append(error.getPropertyPath().toString()).append("]")
.append(error.getMessage());
return buffer.toString();
}
return null;
}
}
注意 该参数校验的两个方法代码基本相同,只是作用不同,第一个没有返回待校验的参数名,只返回错误信息,第二个返回了参数名以及错误信息。第一种是可以配合多语言的自定义错误信息使用。
../demo-model/src/main/java/com/ljq/demo/springboot/BaseBean.java
package com.ljq.demo.springboot;
import lombok.Data;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
/**
* @Description: 基础 bean
* @Author: junqiang.lu
* @Date: 2018/12/24
*/
@Data
public class BaseBean implements Serializable {
private static final long serialVersionUID = 6877955227522370690L;
/**
* 语言类型
*/
@Pattern(regexp = "^\\w{2,10}$", message = "api.response.code.languageTypeError")
private String language;
}
../demo-model/src/main/java/com/ljq/demo/springboot/vo/UserSignUpBean.java
package com.ljq.demo.springboot.vo;
import com.ljq.demo.springboot.BaseBean;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
/**
* @Description: 用户注册接收参数bean
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Data
public class UserSignUpBean extends BaseBean {
private static final long serialVersionUID = 6970430558841356592L;
/**
* 账号
*/
@NotNull(message = "api.response.code.user.accountNullError")
@Pattern(regexp = "^\\S{5,50}$", message = "api.response.code.user.accountFormatError")
private String userName;
/**
* 密码
*/
@NotNull(message = "api.response.code.user.passwordNullError")
@Pattern(regexp = "^\\w{5,80}$", message = "api.response.code.user.passwordFormatError")
private String userPasscode;
}
../demo-common/src/main/java/com/ljq/demo/springboot/common/exception/ParamsCheckException.java
package com.ljq.demo.springboot.common.exception;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import lombok.Data;
/**
* @Description: 自定义参数校验异常
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Data
public class ParamsCheckException extends Exception{
private static final long serialVersionUID = 2684099760669375847L;
/**
* 异常编码
*/
private int code;
/**
* 异常信息
*/
private String message;
public ParamsCheckException(){
super();
}
public ParamsCheckException(int code, String message){
this.code = code;
this.message = message;
}
public ParamsCheckException(String message){
this.message = message;
}
public ParamsCheckException(ResponseCodeI18n responseCodeI18n){
this.code = responseCodeI18n.getCode();
this.message = responseCodeI18n.getMsg();
}
}
../demo-web/src/main/java/com/ljq/demo/springboot/web/acpect/ServiceValidateAspect.java
package com.ljq.demo.springboot.web.acpect;
import com.ljq.demo.springboot.BaseBean;
import com.ljq.demo.springboot.common.annotation.ParamsCheck;
import com.ljq.demo.springboot.common.exception.ParamsCheckException;
import com.ljq.demo.springboot.common.validate.BeanValidateUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @Description: service 层入参校验切点
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Aspect
@Component
public class ServiceValidateAspect {
/**
* service 层切点
*/
@Pointcut("execution(* com.ljq.demo.springboot.service.impl..*.*(..))")
public void servicePointcut() {
}
/**
* service 层入参校验
*
* @param joinPoint 切点
* @return
* @throws Throwable
*/
@Around(value = "servicePointcut()")
public Object serviceParamCheckAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 判断是否需要校验
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ParamsCheck paramsCheckAnnotation = method.getAnnotation(ParamsCheck.class);
if (paramsCheckAnnotation != null && paramsCheckAnnotation.ignore()) {
return joinPoint.proceed();
}
/**
* 参数校验
*/
Object[] objects = joinPoint.getArgs();
for (Object arg : objects) {
if (arg == null) {
break;
}
/**
* 判断是否为 com.ljq.demo.springboot.BaseBean.class 的子类
*/
boolean isChildClass = BaseBean.class.isAssignableFrom(arg.getClass());
if (isChildClass) {
// 参数校验
String validResult = BeanValidateUtil.validate(arg);
if (validResult != null && validResult.length() > 0) {
throw new ParamsCheckException(validResult);
}
}
}
return joinPoint.proceed();
}
}
../demo-web/src/main/java/com/ljq/demo/springboot/web/controller/UserController.java
package com.ljq.demo.springboot.web.controller;
import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ApiResultI18n;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import com.ljq.demo.springboot.common.exception.ParamsCheckException;
import com.ljq.demo.springboot.service.UserService;
import com.ljq.demo.springboot.vo.UserSignUpBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 用户控制中心
* @Author: junqiang.lu
* @Date: 2018/10/9
*/
@RestController
@RequestMapping("api/user")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
/**
* 用户注册
*
* @param userSignUpBean 注册信息
* @return
*/
@RequestMapping(value = "signup", method = RequestMethod.POST)
public ApiResultI18n signUp(@RequestBody UserSignUpBean userSignUpBean){
ApiResultI18n apiResultI18n= null;
try {
apiResultI18n = userService.signUp(userSignUpBean);
} catch (Exception e) {
if (ParamsCheckException.class.isAssignableFrom(e.getClass())){
logger.error("注册失败,参数错误");
return apiResultI18n.failure(ResponseCodeI18n.PARAM_ERROR.getCode(), e.getMessage(),
userSignUpBean.getLanguage());
}
logger.error("注册失败,未知异常",e);
return apiResultI18n.failure(ResponseCodeI18n.UNKNOWN_ERROR, userSignUpBean.getLanguage());
}
return apiResultI18n;
}
}
本地测试接口地址
http://127.0.0.1:8088/api/user/signup
参数
{
"language" : "zh_cn",
"userName" : "tom",
"userPasscode" : "123456"
}
接口返回结果
{
"code": 1001,
"msg": "账号(格式)错误",
"data": null
}
接口请求日志
2019-01-28 11:30:46:390 [http-nio-8088-exec-3] INFO com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 66) -[AOP-LOG-START]
requestMark: cc866c67-5fdc-4adb-855e-a79d67d31344
requestIP: 127.0.0.1
contentType:application/json
requestUrl: http://127.0.0.1:8088/api/user/signup
requestMethod: POST
requestParams: {"language":"zh_cn","userName":"tom","userPasscode":"123456"}
targetClassAndMethod: com.ljq.demo.springboot.web.controller.UserController#signUp
2019-01-28 11:30:46:405 [http-nio-8088-exec-3] ERROR c.l.demo.springboot.web.controller.UserController(UserController.java 91) -注册失败,参数错误
2019-01-28 11:30:46:407 [http-nio-8088-exec-3] INFO com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 74) -[AOP-LOG-END]
ApiResultI18n(code=1001, msg=账号(格式)错误, data=null)
参数校验-1-validation 注解
Gtihub 源码地址: https://github.com/Flying9001/springBootDemo/
Exception
异常,否则,在 Controller 层拦截到的异常将不是自定义参数校验异常(ParamsCheckException
),而是AOP代理异常(UndeclaredThrowableException
)