Spring Boot AOP Service 层参数校验

文章目录

      • 1 摘要
      • 2 Maven 依赖
      • 3 核心代码
        • 3.1 参数校验注解
        • 3.2 参数校验工具类
        • 3.3 包含校验注解的参数接收 bean
        • 3.4 自定义参数校验异常
        • 3.5 Service 层 AOP 切点
        • 3.6 Controller 层拦截自定义参数校验异常
      • 4 测试
      • 5 参考资料推荐
      • 6 Gtihub 源码
      • X 注意事项

​ ​

1 摘要

参数校验是保证程序可以正常运行、防止恶意参数攻击的一个重要手段,但是在业务层重复书写校验代码会造成代码的臃肿,本文将介绍在 Spring boot 项目中使用 Spring AOP 进行 Service 层参数校验

Hibernate validator 官方文档: https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints

2 Maven 依赖

../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>

3 核心代码

3.1 参数校验注解

../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;

}

这里只列举了一个注解属性,如果有需要,可以自行添加

3.2 参数校验工具类

../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;
    }
}

注意 该参数校验的两个方法代码基本相同,只是作用不同,第一个没有返回待校验的参数名,只返回错误信息,第二个返回了参数名以及错误信息。第一种是可以配合多语言的自定义错误信息使用。

3.3 包含校验注解的参数接收 bean

../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;


}

3.4 自定义参数校验异常

../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();
    }

}

3.5 Service 层 AOP 切点

../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();
    }

}

3.6 Controller 层拦截自定义参数校验异常

../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;
    }

}

4 测试

本地测试接口地址

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)

5 参考资料推荐

参数校验-1-validation 注解

​ ​

6 Gtihub 源码

Gtihub 源码地址: https://github.com/Flying9001/springBootDemo/

X 注意事项

  • 必须在 Service 层抛出Exception 异常,否则,在 Controller 层拦截到的异常将不是自定义参数校验异常(ParamsCheckException),而是AOP代理异常(UndeclaredThrowableException)

你可能感兴趣的:(Java)