欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringBoot中自定义注解实现参数非空校验的示例

程序员文章站 2022-03-02 16:33:13
前言由于刚写项目不久,在写 web 后台接口时,经常会对前端传入的参数进行一些规则校验,如果入参较少还好,一旦需要校验的参数比较多,那么使用 if 校验会带来大量的重复性工作,并且代码看起来会非常冗余...

前言

由于刚写项目不久,在写 web 后台接口时,经常会对前端传入的参数进行一些规则校验,如果入参较少还好,一旦需要校验的参数比较多,那么使用 if 校验会带来大量的重复性工作,并且代码看起来会非常冗余,所以我首先想到能否通过一些手段改进这点,让 controller 层减少参数校验的冗余代码,提升代码的可阅读性。

经过阅读他人的代码,发现使用 annotation 注解是一个比较方便的手段,springboot 自带的 @requestparam 注解只会校验请求中该参数是否存在,但是该参数是否符合一些规格比如不为 null 且不为空就无法进行判断的,所以我们可以尝试一下增强请求参数中的注解。

准备工作

有了前面的思路,我们先搭一个架子出来。

  • springboot 2.3.5.realease
  • jdk 1.8

pom.xml 文件如下:

<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
   xsi:schemalocation="http://maven.apache.org/pom/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelversion>4.0.0</modelversion>
 <parent>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-parent</artifactid>
  <version>2.3.5.release</version>
  <relativepath/> <!-- lookup parent from repository -->
 </parent>
 <groupid>cn.bestzuo</groupid>
 <artifactid>springboot-annotation</artifactid>
 <version>0.0.1-snapshot</version>
 <name>springboot-annotation</name>
 <description>demo project for spring boot</description>

 <properties>
  <java.version>1.8</java.version>
 </properties>

 <dependencies>
  <dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-thymeleaf</artifactid>
  </dependency>
  <dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-web</artifactid>
  </dependency>

  <dependency>
   <groupid>org.projectlombok</groupid>
   <artifactid>lombok</artifactid>
   <optional>true</optional>
  </dependency>
  <!--引入aop相应的注解-->
  <dependency>
   <groupid>org.aspectj</groupid>
   <artifactid>aspectjweaver</artifactid>
   <version>1.8.5</version>
  </dependency>
  <dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-test</artifactid>
   <scope>test</scope>
   <exclusions>
    <exclusion>
     <groupid>org.junit.vintage</groupid>
     <artifactid>junit-vintage-engine</artifactid>
    </exclusion>
   </exclusions>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-maven-plugin</artifactid>
   </plugin>
  </plugins>
 </build>

</project>

其中 aspectjweaver 用于引入 aop 的相关的注解,如 @aspect、@pointcut 等.

使用自定义注解实现统一非空校验

总体思路:自定义一个注解,对必填的参数加上该注解,然后定义一个切面,校验该参数是否为空,如果为空则抛出自定义的异常,该异常被自定义的异常处理器捕获,然后返回相应的错误信息。

1.自定义注解

创建一个名为 paramcheck 的注解,代码如下:

package cn.bestzuo.springbootannotation.annotation;

import java.lang.annotation.elementtype;
import java.lang.annotation.retention;
import java.lang.annotation.retentionpolicy;
import java.lang.annotation.target;

/**
 * 参数不能为空注解,作用于方法参数上
 *
 * @author zuoxiang
 * @since 2020-11-11
 */
@target(elementtype.parameter)
@retention(retentionpolicy.runtime)
public @interface paramcheck {
 /**
  * 是否非空,默认不能为空
  */
 boolean notnull() default true;
}

其中 @target 注解中的 elementtype.parameter 表示该注解的作用范围,我们查看源码可以看到,注解的作用范围定义比较广泛,可以作用于方法、参数、构造方法、本地变量、枚举等等。

public enum elementtype {
 /** class, interface (including annotation type), or enum declaration */
 type,

 /** field declaration (includes enum constants) */
 field,

 /** method declaration */
 method,

 /** formal parameter declaration */
 parameter,

 /** constructor declaration */
 constructor,

 /** local variable declaration */
 local_variable,

 /** annotation type declaration */
 annotation_type,

 /** package declaration */
 package,

 /**
  * type parameter declaration
  *
  * @since 1.8
  */
 type_parameter,

 /**
  * use of a type
  *
  * @since 1.8
  */
 type_use
}

当然,我们定义的注解可以扩展,不仅仅去校验参数是否为空,比如我们可以增加字符串长度的校验。

2.自定义异常类

我们在这里自定义异常的原因,是为了配合自定义注解使用,一旦校验出不符合我们自定义注解规格的参数,可以直接抛出自定义异常返回。代码如下:

package cn.bestzuo.springbootannotation.exception;

public class paramisnullexception extends runtimeexception {
 private final string parametername;
 private final string parametertype;

 public paramisnullexception(string parametername, string parametertype) {
  super("");
  this.parametername = parametername;
  this.parametertype = parametertype;
 }

 /**
  * 重写了该方法
  *
  * @return 异常消息通知
  */
 @override
 public string getmessage() {
  return "required " + this.parametertype + " parameter \'" + this.parametername + "\' must be not null !";
 }

 public final string getparametername() {
  return this.parametername;
 }

 public final string getparametertype() {
  return this.parametertype;
 }
}

该异常继承 runtimeexception,并定义了两个成员属性、重写了 getmessage() 方法
之所以自定义该异常,而不用现有的 org.springframework.web.bind.missingservletrequestparameterexception 类,是因为 missingservletrequestparameterexception为checked 异常,在动态代理过程中,很容易引发 java.lang.reflect.undeclaredthrowableexception 异常。

3.自定义 aop

代码如下:

package cn.bestzuo.springbootannotation.aop;

import cn.bestzuo.springbootannotation.annotation.paramcheck;
import cn.bestzuo.springbootannotation.exception.paramisnullexception;
import org.aspectj.lang.joinpoint;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.methodsignature;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.stereotype.component;

import java.lang.annotation.annotation;
import java.lang.reflect.method;

@component
@aspect
public class paramcheckaop {
 private static final logger logger = loggerfactory.getlogger(paramcheckaop.class);

 /**
  * 定义有一个切入点,范围为 controller 包下的类
  */
 @pointcut("execution(public * cn.bestzuo.controller..*.*(..))")
 public void checkparam() {

 }

 @before("checkparam()")
 public void dobefore(joinpoint joinpoint) {
 }

 /**
  * 检查参数是否为空
  *
  * @param pjp 连接点
  * @return 对象
  * @throws throwable 异常
  */
 @around("checkparam()")
 public object doaround(proceedingjoinpoint pjp) throws throwable {
  methodsignature signature = ((methodsignature) pjp.getsignature());
  //得到拦截的方法
  method method = signature.getmethod();
  //获取方法参数注解,返回二维数组是因为某些参数可能存在多个注解
  annotation[][] parameterannotations = method.getparameterannotations();
  if (parameterannotations.length == 0) {
   return pjp.proceed();
  }
  //获取方法参数名
  string[] paramnames = signature.getparameternames();
  //获取参数值
  object[] paramvalues = pjp.getargs();
  //获取方法参数类型
  class<?>[] parametertypes = method.getparametertypes();
  for (int i = 0; i < parameterannotations.length; i++) {
   for (int j = 0; j < parameterannotations[i].length; j++) {
    //如果该参数前面的注解是paramcheck的实例,并且notnull()=true,则进行非空校验
    if (parameterannotations[i][j] != null && parameterannotations[i][j] instanceof paramcheck && ((paramcheck) parameterannotations[i][j]).notnull()) {
     paramisnull(paramnames[i], paramvalues[i], parametertypes[i] == null ? null : parametertypes[i].getname());
     break;
    }
   }
  }
  return pjp.proceed();
 }

 /**
  * 在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
  *
  * @param joinpoint 连接点
  */
 @afterreturning("checkparam()")
 public void doafterreturning(joinpoint joinpoint) {
 }

 /**
  * 参数非空校验,如果参数为空,则抛出paramisnullexception异常
  *
  * @param paramname  参数名称
  * @param value   参数值
  * @param parametertype 参数类型
  */
 private void paramisnull(string paramname, object value, string parametertype) {
  if (value == null || "".equals(value.tostring().trim())) {
   throw new paramisnullexception(paramname, parametertype);
  }
 }
}

4.全局异常处理器

该异常处理器捕获在 paramcheckaop 类中抛出的 paramisnullexception 异常,并进行处理,代码如下:

import cn.bestzuo.springbootannotation.common.result;
import cn.bestzuo.springbootannotation.enums.enumresultcode;
import cn.bestzuo.springbootannotation.utils.responsemsgutil;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.web.bind.missingservletrequestparameterexception;
import org.springframework.web.bind.annotation.exceptionhandler;

import javax.servlet.http.httpservletrequest;

public class globalexceptionhandler {
 private static final logger logger = loggerfactory.getlogger(globalexceptionhandler.class);


 /**
  * 参数为空异常处理
  *
  * @param ex 异常
  * @return 返回的异常
  */
 @exceptionhandler({missingservletrequestparameterexception.class, paramisnullexception.class})
 public result<string> requestmissingservletrequest(exception ex) {
  logger.error("request exception:", ex);
  return responsemsgutil.builderresponse(enumresultcode.fail.getcode(), ex.getmessage(), null);
 }

 /**
  * 特别说明: 可以配置指定的异常处理,这里处理所有
  *
  * @param request 请求
  * @param e  异常体
  * @return 返回的异常
  */
 @exceptionhandler(value = exception.class)
 public result<string> errorhandler(httpservletrequest request, exception e) {
  logger.error("request exception:", e);
  return responsemsgutil.exception();
 }
}

5.测试

首先定义一个 controller 进行测试:

@restcontroller
public class hellocontroller {
 /**
  * 测试@requestparam注解
  *
  * @param name 测试参数
  * @return 包装结果
  */
 @getmapping("/hello1")
 public result<string> hello1(@requestparam string name) {
  return responsemsgutil.builderresponse(enumresultcode.success.getcode(), "请求成功", "hello," + name);
 }

 /**
  * 测试@paramcheck注解
  *
  * @param name 测试参数
  * @return 包装结果
  */
 @getmapping("/hello2")
 public result<string> hello2(@paramcheck string name) {
  return responsemsgutil.builderresponse(enumresultcode.success.getcode(), "请求成功", "hello," + name);
 }

 /**
  * 测试@paramcheck与@requestparam一起时
  *
  * @param name 测试参数
  * @return 包装结果
  */
 @getmapping("/hello3")
 public result<string> hello3(@paramcheck @requestparam string name) {
  return responsemsgutil.builderresponse(enumresultcode.success.getcode(), "请求成功", "hello," + name);
 }
}

测试访问 http://localhost:8080/hello1,此时只有 @requestparam 注解,如果不加 name 参数,会请求得到一个异常:

SpringBoot中自定义注解实现参数非空校验的示例

并且控制台会报 missingservletrequestparameterexception: required string parameter 'name' is not present] 异常

如果访问 http://localhost:8080/hello2?name=,此时使用的是我们自定义的 @paramcheck 注解,此时没有参数输入,那么也会捕获输入的异常:

SpringBoot中自定义注解实现参数非空校验的示例

如果访问 http://localhost:8080/hello3?name=,此时既有参数存在校验,又有我们自定义的 paramcheck 不为空校验,所以此时访问不加参数会抛出异常:

SpringBoot中自定义注解实现参数非空校验的示例

控制台抛出我们自定义的异常:

测试总结:

当参数名为空时,分别添加两个注解的接口都会提示参数不能为空
当参数名不为空,值为空时,@requestparam注解不会报错,但@paramcheck注解提示参数'name'的值为空

6.总结

  • 经过以上的测试也验证了 @requestparam 只会验证对应的参数是否存在,而不会验证值是否为空
  • paramcheck 还可以进行拓展,比如参数值长度、是否含有非法字符等校验

7.代码附录

上述使用到的代码:

package cn.bestzuo.springbootannotation.common;

import lombok.getter;
import lombok.setter;

@getter
@setter
public class result<t> {
 private integer rescode;
 private string resmsg;
 private t data;
}
package cn.bestzuo.springbootannotation.enums;

/**
 * 枚举参数结果
 *
 * @author zuoxiang
 * @since 2020-11-11
 */
public enum enumresultcode {
 success(200),

 fail(400),

 unauthorized(401),

 not_found(404),

 internal_server_error(500);

 private final int code;

 enumresultcode(int code) {
  this.code = code;
 }

 public int getcode() {
  return code;
 }
}
package cn.bestzuo.springbootannotation.utils;

import cn.bestzuo.springbootannotation.common.result;
import cn.bestzuo.springbootannotation.enums.enumresultcode;

public class responsemsgutil {
 /**
  * 根据消息码等生成接口返回对象
  *
  * @param code 结果返回码
  * @param msg 结果返回消息
  * @param data 数据对象
  * @param <t> 泛型
  * @return 包装对象
  */
 public static <t> result<t> builderresponse(int code, string msg, t data) {
  result<t> res = new result<>();
  res.setrescode(code);
  res.setresmsg(msg);
  res.setdata(data);
  return res;
 }

 /**
  * 请求异常返回结果
  *
  * @param <t> 泛型
  * @return 包装对象
  */
 public static <t> result<t> exception() {
  return builderresponse(enumresultcode.internal_server_error.getcode(), "服务异常", null);
 }
}

以上就是springboot中自定义注解实现参数非空校验的示例的详细内容,更多关于springboot 参数非空校验的资料请关注其它相关文章!