Традиционная проверка параметров
Я считаю, что у всех в процессе разработки болит голова по проверке параметров, передаваемых из фронтенда, потому что иногда интерфейсу нужно очень много параметров.В древности наш метод проверки должен был быть таким:
@PostMapping("/order/submit")
public void submit(@RequestBody OrderRequest order){
if(order.getParam1() == null){
throw new XXXException("xxx不能为空");
} else if(order.getParam2() == null){
throw new XXXException("xxx不能为空");
}
...
}
Молодец, если интерфейс имеет десятки параметров, я думаю, это может сломать написанное людьми... Если параметры запроса не проверены, проблема более серьезная, и различные параметры могут не совпадать при отладке с интерфейсом. , Незаконно проблема, в этом случае, я думаю, передняя и задняя части рухнут. . . . Может быть, вы скажете, разве нет такого документа, как чванство? Знак "*" означает, что он должен пройти. Но вы должны знать, что это только сообщает внешнему интерфейсу, что это должно быть передано, и на самом деле не ограничивает интерфейс.Что, если внешний интерфейс просто забыл передать его? Поэтому необходимо делать проверку параметров в фоновом режиме.
попробуй оптимизировать
Умные друзья, возможно, подумали, что нет необходимости писать эти суждения для каждого интерфейса.Я сам определяю аннотацию, перехватываю эти запросы в перехватчике и сужу, есть ли в полях объекта запроса аннотации, которые я определил. аннотация себя
/**
* 自定义注解
* */
@Documented
@Target( {ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
}
Используйте аннотацию, которую мы определили в классе объекта параметра запроса.
@Data
public class OrderRequest {
//自己定义的注解
@NotNull
private String merchantCode;
@NotNull
private String memberCode;
}
Сделать единую проверку в перехватчике
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
//先获取参数列表
Parameter[] parameters = handlerMethod.getMethod().getParameters();
for (Parameter parameter : parameters) {
//获取参数字段
Field[] declaredFields = parameter.getType().getDeclaredFields();
for (Field declaredField : declaredFields) {
NotNull annotation = declaredField.getAnnotation(NotNull.class);
//如果该字段有NotNull注解
if(annotation != null){
declaredField.setAccessible(true);
String s = declaredField.get(getTargetObject(parameter,request)).toString();//实际值
if(s == null){
//这里应该组装信息返回给前端,为了简单就直接 throw
throw new RuntimeException(declaredField.getName()+":不能为NULL");
}
}
}
}
}
Вот такая простая проверка NotNull завершена.Вроде просто, но я не реализовал метод getTargetObject.Получить целевой объект (то есть объект заказа вышеописанным методом контроллера) довольно хлопотно, потому что мы получаем только такую информацию, как типы и поля могут быть получены путем отражения. Если вы хотите получить целевой объект, вы должны получить поток байтов из HttpServletRequest для преобразования, что является работой, выполняемой синтаксическим анализом параметров SpringMVC и привязкой данных.
Мощный спящий валидатор
Возможно, вы обнаружили, что пользовательская проверка, которую мы только что реализовали, очень проста, только одна проверка не пуста, и то, что мы реализовали, находится в перехватчике, когда мы переходим к перехватчику, привязка данных фактически завершена. В связи с этим существует фреймворк hibernate-validator, который поможет нам решить эту проблему.
Прежде чем говорить об этом, нам нужно понять спецификацию JSR (Java Specification Requests).JSR 303 — это официальная спецификация проверки данных, предложенная Java под названием Bean Validation.Теперь она достигла версии 2.0, соответствующей JSR 380. Как и в случае с сервлетом, упоминается только одна спецификация, которую мы реализуем. hibernate-validator сделал набор реализаций, соответствующих этой спецификации.
В этой статье в качестве примера взят SpringBoot.Это очень удобно использовать в SpringBoot.Достаточно ввести непосредственно стартер,и SpringBoot автоматически настроит его для нас.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
После введения вы можете использовать ряд предоставленных им проверочных аннотаций.Теперь мы заменим пользовательские аннотации аннотациями, предоставленными фреймворком, и удалим предыдущий перехватчик.
@Data
public class OrderRequest {
//hibernate-validator 提供的注解
@javax.validation.constraints.NotNull
private String merchantCode;
@javax.validation.constraints.NotNull
private String memberCode;
}
Код контроллера
@PostMapping("/order/submit")
public void submit(@RequestBody @Validated OrderRequest request){}
Используйте Postman для отправки запроса Post и преднамеренно передайте пустую строку JSON для тестирования, вы можете видеть, что Postman получит следующие возвращаемые результаты:
{
"timestamp": "2021-06-09T11:25:49.136+00:00",
"status": 400,
"error": "Bad Request",
"path": "/order/submit"
}
Это показывает, что аннотация работает, но это не тот результат, которого мы хотим.Что мы определенно хотим, так это иметь возможность сообщить внешнему интерфейсу, какое поле или поля не прошли проверку, и знать, какая проверка не прошла, например, должно быть, я не передал переданные параметры, или переданное число больше максимального предела и так далее.
Поэтому нам нужно разобраться с сообщением об ошибке ниже. Перед этим нам нужно знать, что это действие проверки параметра на самом деле выдается SpringMVC. Конкретные правила проверки предоставляются hibernate-validator. Вы можете прочитать исходный код SpringMVC:
В том месте, где я прерываю рисунок, SpringMVC решает, требуется ли проверка, что в конечном итоге вызывает правила реализации hibernate-validator, а затем генерирует исключение типа MethodArgumentNotValidException на основе возвращаемого результата. Таким образом, нам нужно только поймать это исключение, а затем разработчик может ответить сам. Чтобы поймать это исключение, я считаю, что каждый должен знать унифицированную обработку исключений SpringMVC.
Унифицированная обработка исключений SpringMVC
Унифицированная обработка исключений относительно проста, вам нужно только написать класс с @RestControllerAdvice Annotated with @ExceptionHandler Annotated методами, но SpringMVC предоставляет нам класс ResponseEntityExceptionHandler, наследует его и переопределяет метод handleExceptionInternal
@RestControllerAdvice
public class GlobalExceptionController extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
if(ex instanceof MethodArgumentNotValidException){
String message = ((MethodArgumentNotValidException) ex).getFieldErrors().stream().map(v -> v.getField()+":"+v.getDefaultMessage()).collect(Collectors.joining(";"));
JSONObject obj = new JSONObject();
obj.put("status",status.value());
obj.put("error",status.getReasonPhrase());
obj.put("message",message);
obj.put("path",((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class).getRequestURI());
body = obj;
}
return super.handleExceptionInternal(ex, body, headers, status, request);
}
}
Как мы видели выше, исключение MethodArgumentNotValidException будет вызвано для проверки недопустимого параметра, поэтому мы судим о типе ex выше.Если он имеет этот тип, поместите нужную нам информацию в тело и верните ее во внешний интерфейс, и снова используйте тест Postman. получите следующий результат
{
"path": "/order/submit",
"error": "Bad Request",
"message": "memberCode:不能为null;merchantCode:不能为null",
"status": 400
}
Такой результат нам обычно и нужен. Так почему же мы можем перехватывать исключения, переопределяя этот метод handleExceptionInternal? Вы можете взглянуть на исходный код унаследованного нами класса ResponseEntityExceptionHandler, который сам определяет метод, аннотируется @ExceptionHandler и перехватывает почти все возможные исключения.
Эти возвраты обрабатывают разные типы исключений по-разному, но, в конце концов, все внутренние вызовы — это наш переопределенный метод handleExceptionInternal.
Тогда вы, возможно, обнаружили, что он может обрабатывать только эти типы исключений, определенные в @ExceptionHandler, Мы обязательно определим некоторые классы исключений сами в процессе разработки, так как же захватить наши пользовательские классы исключений?
SpringMVC обрабатывает пользовательские исключения
Следуя тому, как написан ResponseEntityExceptionHandler, мы также можем написать метод, помеченный @ExceptionHandler.Во-первых, настроить класс исключения ClientException
public class ClientException extends RuntimeException {
public ClientException(String msg) {
super(msg);
}
public ClientException(String msg, Object... objs) {
super(MessageFormatter.arrayFormat(msg, objs).getMessage());//实现日志占位符,类似 Slf4j 的log
}
}
Затем в нашем унифицированном классе обработки настройте метод обработки исключений.handleClientExceptionиметь дело сClientExceptionтипа, плюс@ExceptionHandlerТолько что
@RestControllerAdvice
@Slf4j
public class GlobalExceptionController extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
if(ex instanceof MethodArgumentNotValidException){
String message = ((MethodArgumentNotValidException) ex).getFieldErrors().stream().map(v -> v.getField()+":"+v.getDefaultMessage()).collect(Collectors.joining(";"));
JSONObject obj = new JSONObject();
obj.put("status",status.value());
obj.put("error",status.getReasonPhrase());
obj.put("message",message);
obj.put("path",((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class).getRequestURI());
body = obj;
} else if (ex instanceof ClientException){ //捕捉自定义异常
body = "1111";
}
return super.handleExceptionInternal(ex, body, headers, status, request);
}
/**
* 处理自定义异常
*/
@ExceptionHandler
public ResponseEntity<Object> handleClientException(ClientException ex, NativeWebRequest request) {
HttpHeaders headers = new HttpHeaders();
HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
return handleExceptionInternal(ex, null, headers, status, request);
}
}
Затем мы тестируем в коде, выбрасываяClientException,Например
if (user == null) {
throw new ClientException("手机号不存在");
}
----------------------
if (count > 0) {
throw new ClientException("名称为:{} 的权限已经存在", permission.getName());
}
Таким образом, исключение будет перехвачено нашим пользовательским методом обработки, и ответная информация будет возвращена во внешний интерфейс.
каскадная проверка
Бывает такой сценарий, что объект запроса может быть вложенным, например, интерфейс создания заказа обычно передает информацию о товаре
@Data
public class OrderRequest {
@NotNull
private String merchantCode;
@NotNull
private String memberCode;
@Valid
@NotNull
private List<OrderProduct> orderProduct;
@Data
public class OrderProduct {
@NotNull
private String skuName;
@Positive
private BigDecimal price;
}
}
В этот момент нам нужноOrderProductКласс должен использовать проверку поля@ValidАннотация поддерживает каскадную проверку, и отрицательный результат проверки суммы передается в
{
"path": "/order/submit",
"error": "Bad Request",
"message": "orderProduct[0].price:必须是正数",
"status": 400
}
Это выглядит идеально, но кажется, что это ложка дегтя, потому что тогда мы будем много писать в бизнес-слое в будущем.ifЗатем бросьте исключение, что не очень элегантно в бизнес-методах.
Элегантные подсказки на стороне клиента с помощью Assert
На самом деле, после небольшого мозгового штурма, мы можем подумать об использованииAssertреализовать. Это знакомо? для написания модульных тестовAssert! это не совсем реализация этихisNull,boolean expressionЯвляется ли проверка ~ ~ ноSpringизAssertчто брошеноIllegalArgumentExceptionНенормально, не соответствует требованиям. Так что настройтеAssertКласс в порядке~
public abstract class Assert {
public static void isNull(@Nullable Object object, String message, Object... args) {
if (object == null) {
throw new ClientException(message, args);
}
}
@Nullable
private static <T> T nullSafeGet(@Nullable Supplier<T> messageSupplier) {
return (messageSupplier != null ? messageSupplier.get() : null);
}
public static <T> void isTrue(boolean expression, String message, Supplier<T> supplier) {
if (expression) {
throw new ClientException(message, nullSafeGet(supplier));
}
}
//...继续扩展
}
Таким образом, суждение на деловом уровне можно использовать следующим образом: так ли она элегантна, как 18-летняя девушка?
Assert.isNull(user, "手机号不存在");
Assert.isTrue(count > 0, "名称为:{} 的权限已经存在", permission::getName);
ответ, полученный клиентом
{
"path": "/user/login",
"error": "Bad Request",
"message": "手机号不存在",
"status": 400
}
-----------------
{
"path": "/admin/permission",
"error": "Bad Request",
"message": "名称为:admin 的权限已经存在",
"status": 400
}
запрос функции аннотации hibernate-validator
Если вы используете его реже, вы можете не знать, какие проверки аннотаций доступны и какие функции проверяются, поэтому, как теплый человек, я перечислю все аннотации проверки здесь~~
| аннотация | имея в виду | аннотация | имея в виду | аннотация | имея в виду |
|---|---|---|---|---|---|
| @NotNull | Убедитесь, что поле не пустое | @NotEmpty | Убедитесь, что поле не пусто, часто используется для проверки того, что элемент коллекции не пуст. | @NotBlank | Убедитесь, что поле не пусто, часто используется для проверки того, что строка не является пустой строкой. |
| @Max | Проверить максимальное значение поля | @Min | Проверить минимальное значение поля | @Digits | (целое число = целые цифры, дробь = десятичные цифры) проверить целые цифры поля и верхний предел десятичных цифр |
| @DecimalMax | Подобно @Max, разница в том, что он ограничивает значение десятичными знаками, обычно используемыми для типов double и Bigdecimal. | @DecimalMin | Подобно @Min,... | @Range | Проверка того, что значение поля числового типа находится между минимальным и максимальным значениями |
| @Size | Убедитесь, что значение поля находится в пределах указанного интервала от минимального до максимального (включительно), например длина символа, размер набора | @Length | Убедитесь, что длина строкового значения находится в пределах минимального и максимального интервала. | ||
| @AssertFalse | Подтвердить, что логическое значение является ложным | @AssertTrue | Убедитесь, что логическое значение истинно | @Future | Проверить значение поля типа даты позже текущего времени |
| Значением поля проверки является электронная почта | @Pattern | (regex=регулярное выражение) Убедитесь, что значение элемента аннотации не соответствует указанному регулярному выражению. | @Past | Проверить значение поля типа даты раньше, чем текущее время | |
| @Negative | проверка должна быть отрицательной | @Positive | проверка должна быть положительной | @PastOrPresent | Убедитесь, что значение поля типа даты предшествует текущему времени или текущей дате. |
| @NegativeOrZero | Чек должен быть отрицательным или 0 | @PositiveOrZero | контрольная сумма должна быть положительной или 0 | @FutureOrPresent | Убедитесь, что значение поля типа даты более позднее, чем текущее время или текущая дата. |
проверить пакет
В реальной разработке мы пишем класс, который принимает параметры запроса, которые можно использовать для различных бизнес-проверок. Например, у меня есть класс, который используется для сохранения и обновления одновременно, обычно для сохранения не требуется id, но поле id должно быть передано при обновлении. Что, если мы хотим использовать один класс для проверки обоих типов? hibernate-validator предоставляет нам схему группировки.Мы можем разделить хранилище на группу и определить, какие поля хранятся в группе действий, а какие поля находятся в группе действий обновления, чтобы можно было выполнить требования проверки двух предприятий. одновременно доволен..
На самом деле все аннотации, предоставляемые hibernate-validator, имеют атрибут групп
@Data
public class ReqPremiumLevelRights {
@NotNull(groups = {UpdateGroup.class})//只用于更新
private Long id;
@Length(max = 8, groups = {SaveGroup.class, UpdateGroup.class})
@NotNull(groups = {SaveGroup.class, UpdateGroup.class})
private String name;
@NotNull(groups = {SaveGroup.class})
@Size(min = 1, message = "必须至少选择一个会员权益", groups = {SaveGroup.class, UpdateGroup.class})
private List<Long> rightsIds;
public interface SaveGroup {}
public interface UpdateGroup {}
}
Затем мы указываем группировку при использовании @Validated в контроллере.
@PostMapping
@Operation(summary = "新增会员级别(关联权益)")
public void save(@RequestBody @Validated(ReqPremiumLevelRights.SaveGroup.class) ReqPremiumLevelRights levelRights) {
premiumMemberLevelService.save(levelRights);
}
@PutMapping
@Operation(summary = "编辑会员级别")
public void edit(@RequestBody @Validated(ReqPremiumLevelRights.UpdateGroup.class) ReqPremiumLevelRights levelRights) {
premiumMemberLevelService.update(levelRights);
}
Таким образом, решается проблема использования одного класса для нескольких бизнес-проверок одновременно.
Не полагайтесь на SpringMVC для запуска действий проверки.
Есть такой сценарий, нам нужно определить, какую проверку группировки использовать на основе значения определенного поля.Выше мы используем разные интерфейсы для их разделения, чтобы мы могли выбрать, какое правило группировки использовать для проверки.Если мы сейчас только хочу использовать Как использовать интерфейс для проверки различных групп для различных ситуаций?
Например, например, теперь у нас есть требование выставления счетов. Существует набор правил проверки для персонального выставления счетов и набор правил проверки для выставления счетов предприятия. Обычно мы не используем два интерфейса для реализации этой функции выставления счетов. Затем нам нужно решить, какой набор правил проверки использовать в коде в зависимости от типа пользователя.
@Autowired
private Validator validator;
@PostMapping
public void apply(@RequestBody @Validated AppInvoiceApplyRequest request) {
Set<ConstraintViolation<FundApplyVerifyRequest>> constraintViolations = null;
if (isCorporation) { //企业开票
constraintViolations = validator.validate(request,AppInvoiceApplyRequest.CompanyInvoiceGroup.class);
} else { //个人开票
constraintViolations = validator.validate(request,AppInvoiceApplyRequest.PersonalCompanyInvoiceGroup.class);
}
if (CollectionUtils.isNotEmpty(constraintViolations)) { //如果没有通过校验,抛出异常
throw new ConstraintViolationException(constraintViolations);
}
appInvoiceService.save(request);
}
Фактически исходный код SpringMVC делает то же самое при вызове валидатора валидатора, теперь мы не указываем в параметрах группу валидации @Validated, судим о типе пользователя, переданном из фронтенда в коде, и используем валидатор валидатора по к типу пользователя.Достаточно сверить группы проверки, соответствующие разным сервисам.
Расширьте аннотации, предоставляемые hibernate-validator
Если вы будете осторожны, вы, возможно, обнаружили, что hibernate-validator предоставляет только около аннотаций проверки 20. Хотя он удовлетворил использование большинства сценариев, из-за разнообразия бизнеса в процессе разработки мы можем столкнуться с тем, что он не предоставляет, например , если мы хотим убедиться, что поле должно быть действительным идентификационным номером, то у нас нет выбора hibernate-validator позволяет нам определять наши собственные аннотации
Сначала определите аннотацию проверки
/**
* 身份证校验
*/
@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityConstraintValidator.class)//用哪个校验器校验
public @interface IdentityNo {
String message() default "身份证号码不符合规则";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Для логики проверки нам нужно только реализовать интерфейс ConstraintValidator и переопределить метод isValid.
/**
* 身份证约束逻辑判断
*/
public class IdentityConstraintValidator implements ConstraintValidator<IdentityNo, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return check(value);//返回身份证号码是否符合规则(自己实现校验逻辑)
}
}
Таким образом определяется наша собственная аннотация, и ее можно использовать в поле, которое необходимо проверить.
@Data
public class OrderRequest {
//自己定义的注解
@IdentityNo
private String merchantCode;
private String memberCode;
}
Результаты теста почтальона
{
"path": "/order/submit",
"error": "Bad Request",
"message": "merchantCode:身份证号码不符合规则",
"status": 400
}
Эпилог
hibernate-validator идеально подходит для унифицированной обработки исключений, так что большинство проблем могут быть обнаружены при передаче внешних параметров~~