В процессе разработки всегда неизбежна проверка параметров, проверка параметров связана с бизнес-кодом, код становится все более и более раздутым, что сказывается на последующем сопровождении, и код недостаточно красив.
Авиатор принадлежит Google.
表达式求值引擎
. использоватьAviator
В основном для проверки параметров. Он поддерживает большинство арифметических операторов, включая арифметические операторы, операторы отношения, логические операторы, обычные операторы сопоставления (=~), троичные выражения ?:, а также поддерживает приоритет операций и приоритет применения скобок.
Так как Авиатор использовался в предыдущих проектах, а я привык использоватьAssert断言
для проверки параметров. потому чтоAssert断言
Выброшенное исключениеIllegalArgumentException
, может вызвать нежелательное для пользователя исключение. Итак, я хочу разработать функцию проверки параметров.
полагаться
<?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.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ler</groupId>
<artifactId>jcheck</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>jcheck</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--Aviator依赖-->
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Первое, что приходит на ум, — это аннотации, но поскольку обычно используется несколько проверок параметров, в одном и том же методе необходимо использовать несколько аннотаций.Но до Java 8 одну и ту же аннотацию нельзя было повторно использовать в одном и том же месте.
Хотя аннотации можно использовать повторно, на самом деле это тоже синтаксический сахар, и несколько аннотаций фактически заворачиваются в контейнер после компиляции.
Вот аннотации:
package com.ler.jcheck.annation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author lww
*/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
//这个注解就是可以让一个注解同一个方法上标注多次
@Repeatable(CheckContainer.class)
public @interface Check {
String ex() default "";
String msg() default "";
}
-
ex
является выражением, которое необходимо проверить, и можно использовать регулярное выражение. Ключ — это имя формального параметра. В случае объекта JOSN ключ — это имя формального параметра. Атрибут, подробности см. в следующем примере. -
msg
Это сообщение об ошибке, которое подсказывает и требует сотрудничестваГлобальный перехватчик исключенийиспользовать. - Порядок проверки параметров соответствует порядку аннотаций.
package com.ler.jcheck.annation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author lww
*/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckContainer {
Check[] value();
}
Это аннотация-контейнер. Когда используется несколько аннотаций, эта аннотация будет использоваться для переноса нескольких идентичных аннотаций после компиляции.
Таким образом, аспект АОП должен контролироватьсяCheck
а такжеCheckContainer
.
Основной класс AopConfig
package com.ler.jcheck.config;
import com.googlecode.aviator.AviatorEvaluator;
import com.ler.jcheck.annation.Check;
import com.ler.jcheck.annation.CheckContainer;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.util.StringUtils;
/**
* @author lww
* @date 2019-09-03 20:35
*/
@Aspect
@Configuration
public class AopConfig {
/**
* 切面,监视多个注解,因为一个注解的时候是Check 多个注解编译后是CheckContainer
*/
@Pointcut("@annotation(com.ler.jcheck.annation.CheckContainer) || @annotation(com.ler.jcheck.annation.Check)")
public void pointcut() {
}
@Before("pointcut()")
public Object before(JoinPoint point) {
//获取参数
Object[] args = point.getArgs();
//用于获取参数名字
Method method = ((MethodSignature) point.getSignature()).getMethod();
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
CheckContainer checkContainer = method.getDeclaredAnnotation(CheckContainer.class);
List<Check> value = new ArrayList<>();
if (checkContainer != null) {
value.addAll(Arrays.asList(checkContainer.value()));
} else {
Check check = method.getDeclaredAnnotation(Check.class);
value.add(check);
}
for (int i = 0; i < value.size(); i++) {
Check check = value.get(i);
String ex = check.ex();
//规则引擎中null用nil表示
ex = ex.replaceAll("null", "nil");
String msg = check.msg();
if (StringUtils.isEmpty(msg)) {
msg = "服务器异常...";
}
Map<String, Object> map = new HashMap<>(16);
for (int j = 0; j < paramNames.length; j++) {
//防止索引越界
if (j > args.length) {
continue;
}
map.put(paramNames[j], args[j]);
}
Boolean result = (Boolean) AviatorEvaluator.execute(ex, map);
if (!result) {
throw new UserFriendlyException(msg);
}
}
return null;
}
}
Комментарии очень понятны. Давайте посмотрим на конкретное использование.
Использование в контроллере
Общие параметры
@ApiOperation("测试普通参数")
@ApiImplicitParams({
@ApiImplicitParam(name = "name", value = "姓名"),
@ApiImplicitParam(name = "age", value = "年龄"),
@ApiImplicitParam(name = "phone", value = "手机号"),
@ApiImplicitParam(name = "idCard", value = "身份证号"),
})
@GetMapping("/simple")
@Check(ex = "name != null", msg = "姓名不能为空")
@Check(ex = "age != null", msg = "年龄不能为空")
@Check(ex = "age > 18", msg = "年龄要大于18岁")
@Check(ex = "phone != null", msg = "手机号不能为空")
@Check(ex = "phone =~ /^(1)[0-9]{10}$/", msg = "手机号格式错误")
@Check(ex = "string.startsWith(phone,\"1\")", msg = "手机号要以1开头")
@Check(ex = "idCard != null", msg = "身份证号不能为空")
//不先判空 com.googlecode.aviator.exception.ExpressionRuntimeException
@Check(ex = "idCard =~ /^[1-9]\\d{5}[1-9]\\d{3}((0[1-9])||(1[0-2]))((0[1-9])||(1\\d)||(2\\d)||(3[0-1]))\\d{3}([0-9]||X)$/", msg = "身份证号格式错误")
//没有,不会抛出 NoSuchMethodException 或者 NullPointerException 异常
@Check(ex = "gender == 1", msg = "性别")
@Check(ex = "date =~ /^[1-9][0-9]{3}-((0)[1-9]|(1)[0-2])-((0)[1-9]|[1,2][0-9]|(3)[0,1])$/", msg = "日期格式错误")
@Check(ex = "date > '2019-12-20 00:00:00:00'", msg = "日期要大于 2019-12-20")
public HttpResult simple(String name, Integer age, String phone, String idCard, String date) {
System.out.println("name = " + name);
System.out.println("age = " + age);
System.out.println("phone = " + phone);
System.out.println("idCard = " + idCard);
System.out.println("date = " + date);
return HttpResult.success();
}
Если вы хотите проверить параметры, вы должны сначала сделать непустое суждение.Если вы не проверите, обычные параметры не сообщат об ошибке, напримерage > 18
. Но если это регулярное выражение, оно выдаетExpressionRuntimeException
.
При проверке даты, напримерdate > '2019-12-20 00:00:00:00
, в первую очередь следует проверить формат, так как если формат параметра не может быть сопоставлен с датой, то Авиатор не будет его сравнивать. Поэтому никакой проверки не будет.
Если проверка не является аргументом, результат будет ложным, заметки будут выброшены напрямуюmsg
из.
Параметр @RequestBody
/*
{
"age": 0,
"bornDate": "string",
"idCard": "string",
"name": "string",
"phone": "string"
}
*/
@ApiOperation("测试 @RequestBody")
@PostMapping("/body")
@Check(ex = "user.name != null", msg = "姓名不能为空")
@Check(ex = "user.age != null", msg = "年龄不能为空")
@Check(ex = "user.age > 18", msg = "年龄要大于18岁")
@Check(ex = "user.phone =~ /^(1)[0-9]{10}$/", msg = "手机号格式错误")
@Check(ex = "user.name != null && user.age != null", msg = "姓名和年龄不能为空")
//先要检查日期格式,bornDate="string" 这种非正常数据,不会比较大小
@Check(ex = "user.bornDate =~ /^[1-9][0-9]{3}-((0)[1-9]|(1)[0-2])-((0)[1-9]|[1,2][0-9]|(3)[0,1])$/", msg = "日期格式错误")
@Check(ex = "user.bornDate > '2019-12-20'", msg = "日期要大于 2019-12-20")
//@Check(ex = "user.gender == 1", msg = "性别")
//Caused by: java.lang.NoSuchMethodException: Unknown property 'gender' on class 'class com.ler.jcheck.domain.User'
public HttpResult body(@RequestBody User user) {
String jsonString = JSONObject.toJSONString(user);
System.out.println(jsonString);
return HttpResult.success();
}
Параметры передаются в виде JSON, а ключом в выражении ex является формальный параметр Имя атрибута.
Ничего не передавать — это ошибка параметра. Если вы хотите передать пустое значение, передайте{}
, порядок проверки - это порядок аннотаций. В основном то же самое, что и обычные параметры выше, единственное отличие состоит в том, что если в ex нет атрибута, он выдастjava.lang.NoSuchMethodException
Используйте Сервис в
Проверка параметров заключается в использовании аспекта АОП, мониторингаCheck
а такжеCheckContainer
Эти две аннотации, если классы агента Spring могут использовать эту аннотацию для завершения проверки параметров.
код показывает, как показано ниже:
Controller
@ApiOperation("添加 在 Service 中校验")
@PostMapping("/addUser")
public HttpResult addUser(@RequestBody User user) {
userService.addUser(user);
return HttpResult.success();
}
@ApiOperation("删除 在 Service 中校验")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "id"),
})
@PostMapping("/delete")
public HttpResult delete(Long id) {
userService.deleteUser(id);
return HttpResult.success();
}
Service
package com.ler.jcheck.service;
import com.ler.jcheck.domain.User;
/**
* @author lww
*/
public interface UserService {
void addUser(User user);
void deleteUser(Long id);
}
ServiceImpl
package com.ler.jcheck.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.ler.jcheck.annation.Check;
import com.ler.jcheck.domain.User;
import com.ler.jcheck.service.UserService;
import org.springframework.stereotype.Service;
/**
* @author lww
* @date 2019-10-10 15:33
*/
@Service
public class UserServiceImpl implements UserService {
@Override
@Check(ex = "user.name != null", msg = "姓名不能为空")
public void addUser(User user) {
System.out.println(JSONObject.toJSONString(user));
}
@Override
@Check(ex = "id != null", msg = "id不能为空!")
public void deleteUser(Long id) {
System.out.println("id = " + id);
}
}
Использование его в службе на самом деле такое же, как использование контроллера.
код проектаGitHub
Вы можете пойти еще дальше, использовать этот проект как Starter и напрямую вводить зависимости во время разработки, и вы можете его использовать.
Посетите мой блогСоздайте свой собственный SpringBoot-Starter для среды проверки параметров JCheck.Здесь проект упакован вSpringBoot-Starter
, и интегрирует конфигурацию Swagger, конфигурацию рабочей среды, глобальный перехватчик исключений, междоменную конфигурацию и т. д. В конце блога есть Git-адрес проекта и несколько тестовых изображений.