【译】Spring 6 入参数据校验: 综合指南

原文地址:Spring 6 Programmatic Validator: A Comprehensive Guide

一、前言

在 Spring 6.1 中,有一个非常值得注意的重要改进——编程式验证器实现。Spring 长期以来一直通过注解支持声明式验证,而 Spring 6.1 则通过提供专用的编程式验证方法引入了这一强大的增强功能。

编程式验证允许开发人员对验证过程进行细粒度控制,实现动态和有条件的验证场景,超越了声明式方法的能力。在本教程中,我们将深入探讨实现编程式验证并将其与 Spring MVC 控制器无缝集成的细节。

二、声明式验证与编程式验证的区别

对于数据验证,Spring 框架有两种主要方法:声明式验证和编程式验证。

"声明式验证(Declarative validation)"通过域对象上的元数据或注解指定验证规则。Spring 利用 JavaBean Validation (JSR 380) 注释(如 @NotNull、@Size 和 @Pattern)在类定义中直接声明验证约束。

Spring 会在数据绑定过程中自动触发验证(例如,在 Spring MVC 表单提交过程中)。开发人员无需在代码中明确调用验证逻辑。

public class User {@NotNullprivate String username;@Size(min = 6, max = 20)private String password;// ...
}

另一方面,“编程式验证(Programmatic validation)” 在代码中编写自定义验证逻辑,通常使用 Spring 提供的 Validator 接口。这种方法可以实现更动态、更复杂的验证场景。

开发人员负责显式调用验证逻辑,通常在服务层或控制器中进行。

public class UserValidator implements Validator {@Overridepublic boolean supports(Class<?> clazz) {return User.class.isAssignableFrom(clazz);}@Overridepublic void validate(Object target, Errors errors) {User user = (User) target;// 自定义验证逻辑, 可以读取多个字段进行混合校验,编程的方式灵活性大大增加}
}

三、何时使用程序化验证

在声明式验证和编程式验证之间做出选择取决于用例的具体要求。

声明式验证通常适用于比较简单的场景,验证规则可以通过注释清晰地表达出来。声明式验证很方便,也符合惯例,即重于配置的原则。

程序化验证提供了更大的灵活性和控制力,适用于超出声明式表达范围的复杂验证场景。当验证逻辑取决于动态条件或涉及多个字段之间的交互时,程序化验证尤其有用。

我们可以将这两种方法结合起来使用。我们可以利用声明式验证的简洁性来处理常见的情况,而在面对更复杂的要求时,则采用编程式验证。

四、程序化验证器 API 简介

Spring 中的编程式验证器 API 的核心是允许创建自定义验证器类,并定义仅靠注解可能无法轻松捕获的验证规则。

以下是创建自定义验证器对象的一般步骤。

  • 创建一个实现 org.springframework.validation.Validator 接口的类。
  • 重载 supports() 方法,以指定该验证器支持哪些类。
  • 实现 validate()validateObject() 方法,以定义实际的验证逻辑。
  • 使用 ValidationUtils.rejectIfEmpty()ValidationUtils.rejectIfEmptyOrWhitespace() 方法,以给定的错误代码拒绝给定字段。
  • 我们可以直接调用 Errors.rejectValue() 方法来添加其他类型的错误。
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;@Component
public class UserValidator implements Validator {@Overridepublic boolean supports(Class<?> clazz) {return User.class.isAssignableFrom(clazz);}@Overridepublic void validate(Object target, Errors errors) {User user = (User) target;// 例如: 校验 username 不能为空ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "field.required", "Username must not be empty.");// 添加更多的自定义验证逻辑}
}

要使用自定义验证器,我们可以将其注入 @Controller 或 @Service 等 Spring 组件,或者直接将其实例化。然后,我们调用验证方法,传递要验证的对象和 Errors 对象以收集验证错误。

public class UserService {private Validator userValidator;public UserService(Validator userValidator) {this.userValidator = userValidator;}public void someServiceMethod(User user) {Errors errors = new BeanPropertyBindingResult(user, "user");userValidator.validate(user, errors);if (errors.hasErrors()) {// 处理数据校验错误}}
}

五、初始化安装

5.1. Maven 配置

要使用编程式验证器,我们需要 Spring Framework 6.1 或 Spring Boot 3.2,因为这些是最低支持的版本。

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/>
</parent>

5.2. 领域对象

本教程的领域对象是雇员(Employee) 和部门(Department)对象。我们不会创建复杂的结构,因此可以专注于核心概念。

Employee.java

package demo.customValidator.model;@Data
@Builder
public class Employee {Long id;String firstName;String lastName;String email;boolean active;Department department;
}

Department.java

package demo.customValidator.model;@Data
@Builder
public class Department {Long id;String name;boolean active;
}

六、 实现程序化验证器

以下 EmployeeValidator 类实现了 org.springframework.validation.Validator 接口并实现了必要的方法。它将根据需要在 Employee 字段中添加验证规则。

package demo.customValidator.validator;import demo.customValidator.model.Employee;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;public class EmployeeValidator implements Validator {@Overridepublic boolean supports(Class<?> clazz) {return Employee.class.isAssignableFrom(clazz);}@Overridepublic void validate(Object target, Errors errors) {ValidationUtils.rejectIfEmpty(errors, "id", ValidationErrorCodes.ERROR_CODE_EMPTY);ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "First name cannot be empty");ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "Last name cannot be empty");ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "Email cannot be empty");Employee employee = (Employee) target;if (employee.getFirstName() != null && employee.getFirstName().length() < 3) {errors.rejectValue("firstName", "First name must be greater than 2 characters");}if (employee.getLastName() != null && employee.getLastName().length() < 3) {errors.rejectValue("lastName", "Last name must be greater than 3 characters");}}
}

同样,我们为 Department 类定义了验证器。如有必要,您可以添加更复杂的验证规则。

package demo.customValidator.model.validation;import demo.customValidator.model.Department;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;public class DepartmentValidator implements Validator {@Overridepublic boolean supports(Class<?> clazz) {return Department.class.equals(clazz);}@Overridepublic void validate(Object target, Errors errors) {ValidationUtils.rejectIfEmpty(errors, "id", ValidationErrorCodes.ERROR_CODE_EMPTY);ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "Department name cannot be empty");Department department = (Department) target;if(department.getName() != null && department.getName().length() < 3) {errors.rejectValue("name", "Department name must be greater than 3 characters");}}
}

现在我们可以验证 EmployeeDepartment 对象的实例,如下所示:

Employee employee = Employee.builder().id(2L).build();
//Aurowire if needed
EmployeeValidator employeeValidator = new EmployeeValidator();Errors errors = new BeanPropertyBindingResult(employee, "employee");
employeeValidator.validate(employee, errors);if (!errors.hasErrors()) {System.out.println("Object is valid");
} else {for (FieldError error : errors.getFieldErrors()) {System.out.println(error.getCode());}
}

程序输出:

First name cannot be empty
Last name cannot be empty
Email cannot be empty

Department 对象也可以进行类似的验证。

七、链式多个验证器

在上述自定义验证器中,如果我们验证了雇员对象,那么 API 将不会验证部门对象。理想情况下,在验证特定对象时,应针对所有关联对象执行验证。

程序化验证 API 允许调用其他验证器,汇总所有错误,最后返回结果。使用 ValidationUtils.invokeValidator() 方法可以实现这一功能,如下所示:

public class EmployeeValidator implements Validator {DepartmentValidator departmentValidator;public EmployeeValidator(DepartmentValidator departmentValidator) {if (departmentValidator == null) {throw new IllegalArgumentException("The supplied Validator is null.");}if (!departmentValidator.supports(Department.class)) {throw new IllegalArgumentException("The supplied Validator must support the Department instances.");}this.departmentValidator = departmentValidator;}@Overridepublic void validate(Object target, Errors errors) {//...try {errors.pushNestedPath("department");ValidationUtils.invokeValidator(this.departmentValidator, employee.getDepartment(), errors);} finally {errors.popNestedPath();}}
}
  • pushNestedPath() 方法允许为子对象设置临时嵌套路径。在上例中,当对部门对象进行验证时,路径被设置为 employee.department
  • 在调用 pushNestedPath() 方法之前,popNestedPath() 方法会将路径重置为原始路径。在上例中,它再次将路径重置为 employee

现在,当我们验证 Employee 对象时,也可以看到 Department 对象的验证错误。

Department department = Department.builder().id(1L).build();
Employee employee = Employee.builder().id(2L).department(department).build();EmployeeValidator employeeValidator = new EmployeeValidator(new DepartmentValidator());Errors errors = new BeanPropertyBindingResult(employee, "employee");
employeeValidator.validate(employee, errors);if (!errors.hasErrors()) {System.out.println("Object is valid");
} else {for (FieldError error : errors.getFieldErrors()) {System.out.println(error.getField());System.out.println(error.getCode());}
}

程序输出:

firstName
First name cannot be emptylastName
Last name cannot be emptyemail
Email cannot be emptydepartment.name
Department name cannot be empty

注意打印出来的字段名称是 department.name。由于使用了 pushNestedPath() 方法,所以添加了 department. 前缀。

八、使用带消息解析功能的 MessageSource

使用硬编码的消息并不是一个好主意,因此我们可以将消息添加到资源文件(如 messages.properties)中,然后使用 MessageSource.getMessage() 将消息解析为所需的本地语言,从而进一步改进此代码。

例如,让我们在资源文件中添加以下消息:

error.field.empty={0} cannot be empty
error.field.size={0} must be between 3 and 20

为了统一访问,请在常量文件中添加以下代码。请注意,这些错误代码是在自定义验证器实现中添加的。

public class ValidationErrorCodes {public static String ERROR_CODE_EMPTY = "error.field.empty";public static String ERROR_CODE_SIZE = "error.field.size";
}

现在,当我们解析信息时,就会得到属性文件的信息。

MessageSource messageSource;//...if (!errors.hasErrors()) {System.out.println("Object is valid");
} else {for (FieldError error : errors.getFieldErrors()) {System.out.println(error.getCode());System.out.println(messageSource.getMessage(error.getCode(), new Object[]{error.getField()}, Locale.ENGLISH));}
}

程序输出:

error.field.empty
firstName cannot be emptyerror.field.empty
lastName cannot be emptyerror.field.empty
email cannot be emptyerror.field.empty
department.name cannot be empty

九、将编程式验证器与 Spring MVC/WebFlux 控制器集成

将编程式验证器与 Spring MVC 控制器集成,包括将编程式验证器注入控制器、在 Spring 上下文中配置它们,以及利用 @Valid 和 BindingResult 等注解简化验证。

令人欣慰的是,这种集成还能解决 Ajax 表单提交和控制器单元测试问题。

下面是一个使用我们在前面章节中创建的 EmployeeValidator 对象的 Spring MVC 控制器的简化示例。

import demo.app.customValidator.model.Employee;
import demo.app.customValidator.model.validation.DepartmentValidator;
import demo.app.customValidator.model.validation.EmployeeValidator;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;@Controller
@RequestMapping("/employees")
public class EmployeeController {@InitBinderprotected void initBinder(WebDataBinder binder) {// 注入 编程式验证器binder.setValidator(new EmployeeValidator(new DepartmentValidator()));}@GetMapping("/registration")public String showRegistrationForm(Model model) {model.addAttribute("employee", Employee.builder().build());return "employee-registration-form";}@PostMapping("/processRegistration")public String processRegistration(@Validated @ModelAttribute("employee") Employee employee,BindingResult bindingResult) {if (bindingResult.hasErrors()) {return "employee-registration-form";}// 处理成功通过数据校验后表单的逻辑// 通常涉及数据库操作、身份验证等。return "employee-registration-confirmation"; // 重定向至成功页面}
}

之后,当表单提交时,可以使用 ${#fields.hasErrors('*')} 表达式在视图中显示验证错误。

在下面的示例中,我们在两个地方显示验证错误,即在表单顶部的列表中显示所有错误,然后显示单个字段的错误。请根据自己的要求定制代码。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Employee Registration</title>
</head>
<body><h2>Employee Registration Form</h2><!-- Employee Registration Form -->
<form action="./processRegistration" method="post" th:object="${employee}"><!-- Display validation errors, if any --><div th:if="${#fields.hasErrors('*')}"><div style="color: red;"><p th:each="error : ${#fields.errors('*')}" th:text="${error}"></p></div></div><!-- Employee ID (assuming it's a hidden field for registration) --><input type="hidden" th:field="*{id}" /><!-- Employee First Name --><label for="firstName">First Name:</label><input type="text" id="firstName" th:field="*{firstName}" required /><span th:if="${#fields.hasErrors('firstName')}" th:text="#{error.field.size}"></span><br/><!-- Employee Last Name --><label for="lastName">Last Name:</label><input type="text" id="lastName" th:field="*{lastName}" required /><span th:if="${#fields.hasErrors('lastName')}" th:text="#{error.field.size}"></span><br/><!-- Employee Email --><label for="email">Email:</label><input type="email" id="email" th:field="*{email}" required /><span th:if="${#fields.hasErrors('email')}" th:text="#{error.field.size}"></span><br/><!-- Employee Active Status --><label for="active">Active:</label><input type="checkbox" id="active" th:field="*{active}" /><br/><!-- Department Information --><h3>Department:</h3><label for="department.name">Department Name:</label><input type="text" id="department.name" th:field="*{department.name}" required /><span th:if="${#fields.hasErrors('department.name')}" th:text="#{error.field.size}"></span><br/><!-- Submit Button --><button type="submit">Register</button></form></body>
</html>

当我们运行应用程序并提交无效表单时,会出现如图所示的错误:

在这里插入图片描述

十、单元测试编程式验证器

我们可以将自定义验证器作为模拟依赖关系或单个测试对象进行测试。下面的 JUnit 测试用例将测试 EmployeeValidator

我们编写了两个非常简单的基本测试供快速参考,您也可以根据自己的需求编写更多测试。

import demo.app.customValidator.model.Department;
import demo.app.customValidator.model.Employee;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;public class TestEmployeeValidator {static EmployeeValidator employeeValidator;@BeforeAllstatic void setup() {employeeValidator = new EmployeeValidator(new DepartmentValidator());}@Testvoid validate_ValidInput_NoErrors() {// Set up a valid userEmployee employee = Employee.builder().id(1L).firstName("Lokesh").lastName("Gupta").email("admin@howtodoinjava.com").department(Department.builder().id(2L).name("Finance").build()).build();Errors errors = new BeanPropertyBindingResult(employee, "employee");employeeValidator.validate(employee, errors);Assertions.assertFalse(errors.hasErrors());}@Testvoid validate_InvalidInput_HasErrors() {// Set up a valid userEmployee employee = Employee.builder().id(1L).firstName("A").lastName("B").email("C").department(Department.builder().id(2L).name("HR").build()).build();Errors errors = new BeanPropertyBindingResult(employee, "employee");employeeValidator.validate(employee, errors);Assertions.assertTrue(errors.hasErrors());Assertions.assertEquals(3, errors.getErrorCount());}
}

最佳做法是确保测试涵盖边缘情况和边界条件。这包括输入处于允许的最小值或最大值的情况。

十一、结论

在本教程中,我们通过示例探讨了 Spring 6.1 Programmatic Validator API 及其实施指南。程序化验证允许开发人员对验证过程进行细粒度控制。

我们讨论了如何创建和使用自定义验证器类,并将其与 Spring MVC 控制器集成。我们学习了如何使用消息解析,随后还讨论了如何测试这些验证器以实现更强大的编码实践。

代码地址:programmatic-validator

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/207470.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

kafka学习笔记(一)--脑裂

我知道你想裂&#xff0c;但你先别裂 目录 脑裂Kafka脑裂实验Kafka如何防止脑裂--Leader Epochepoch的局限性ISR列表ISR列表的伸缩机制 脑裂 用集群部署的大多数的分布式系统无可避免会面临脑裂问题。简单来说&#xff0c;脑裂就是在同一时刻出现了两个“Leader&#xff08;或…

Vue+Element-ui实例_在form中动态校验tag标签

1.开发需求 在日常开发中&#xff0c;我们会遇到form表单的动态添加和校验&#xff0c;当我们需要在动态添加的内容中再次动态使用输入框的时候&#xff0c;就会变得很繁琐&#xff0c;我在网上找了很多案例&#xff0c;没有符合自己需求的内容&#xff0c;只好闲暇时间自己搞…

css加载会造成阻塞吗??

前言 前几天面试问到了这个问题&#xff0c;当时这个答得不敢确定哈哈&#xff0c;虽然一面还是过了 现在再分析下这个&#xff0c;总结下&#xff0c;等下次遇到就能自信得回答&#xff0c;666 准备工作 为了完成本次测试&#xff0c;先来科普一下&#xff0c;如何利用chr…

【开源】基于Vue和SpringBoot的农家乐订餐系统

项目编号&#xff1a; S 043 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S043&#xff0c;文末获取源码。} 项目编号&#xff1a;S043&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户2.2 管理员 三、系统展示四、核…

Peter算法小课堂—差分与前缀和

差分 Codeforces802 D2C C代码详解 差分_哔哩哔哩_bilibili 一维差分 差分与前缀和可以说成减法和加法的关系、除法和乘法的关系、积分和微分的关系&#xff08;听不懂吧&#xff09; 给定数组A&#xff0c;S为A的前缀和数组&#xff0c;则A为S的差分数组 差分数组构造 现…

openbabel 安装 生成指纹方法

今日踩坑小结&#xff1a; openbabel 安装&#xff1a; 可以装&#xff0c;但是得在 Linux 环境下&#xff0c;win 环境装会报错&#xff08;安装不会报错&#xff0c;但是生成指纹的时候会&#xff09; 指纹&#xff1a; 在下面这个链接里&#xff0c;官方给出了命令行调用 o…

这几款 idea 插件让效率起飞!

作者&#xff1a;苍何&#xff0c;前大厂高级 Java 工程师&#xff0c;阿里云专家博主&#xff0c;CSDN 2023 年 实力新星&#xff0c;土木转码&#xff0c;现任部门技术 leader&#xff0c;专注于互联网技术分享&#xff0c;职场经验分享。 &#x1f525;热门文章推荐&#xf…

Vue3-toRaw 和 markRaw 函数

Vue3-toRaw 和 markRaw 函数 toRaw(转换为原始)&#xff1a;将响应式对象转换为普通对象&#xff0c;只适用于 reactive 生成的响应式对象。markRaw(标记为原始)&#xff1a;标记某个对象&#xff0c;让这个对象永远都不具备响应式。一些集成的第三方库&#xff0c;会有大量的…

ELK分布式日志管理平台部署

目录 一、ELK概述 1、ELK概念&#xff1a; 2、其他数据收集工具&#xff1a; 3、ELK工作流程图&#xff1a; 4、ELK 的工作原理&#xff1a; 5、日志系统的特征&#xff1a; 二、实验部署&#xff1a; 1、ELK Elasticsearch 集群部署 2、安装 Elasticsearch-head 插件 …

MySQL的体系结构与SQL的执行流程

文章目录 前言体系结构SQL语句的执行流程1、连接MySQL2、查询缓存3、解析SQL语句4、优化SQL语句5、执行SQL语句 总结 前言 如果你在使用MySQL时只会写sql语句的&#xff0c;那么你应该看一下《MySQL优化的底层逻辑》。如果你只了解到sql是如何优化的&#xff0c;那么你应该通过…

Codeforces Round #911 (Div. 2) A~E

A.Cover in Water&#xff08;思维&#xff09; 题意&#xff1a; 有一个 1 n 1 \times n 1n的水池&#xff0c;里面有些格子可以加水&#xff0c;有些格子是被堵上的&#xff0c;你可以进行以下两种操作&#xff1a; 1.往一个空的格子里加水 2.移除一个有水的格子中的水&a…

RabbitMq使用与整合

MQ基本概念 MQ概述 MQ全称 Message Queue&#xff08;[kjuː]&#xff09;&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 &#xff08;队列是一种容器&#xff0c;用于存放数据的都是容器&#xff0c;存…

【3D程序软件】SideFX与上海道宁一直为设计师提供程序化 3D动画和视觉效果工具,旨在创造高质量的电影效果

Houdini是一个 从头开始构建的程序系统 使艺术家能够自由工作 创建多次迭代 并与同事快速共享工作流程 Houdini FX为 视觉特效艺术家创作故事片 广告或视频游戏 凭借其基于程序节点的工作流程 Houdini FX可让 您更快地创建更多内容 从而缩短时间并 在所有创意任务中…

SpringBoot——自定义start

优质博文&#xff1a;IT-BLOG-CN 一、Mybatis 实现 start 的原理 首先在写一个自定义的start之前&#xff0c;我们先参考下Mybatis是如何整合SpringBoot&#xff1a;mybatis-spring-boot-autoconfigure依赖包&#xff1a; <dependency><groupId>org.mybatis.spr…

中国移动联合中国华电完成基于ZETA物联网技术的风电机组主辅智能控制系统试点应用

2023年11月17日&#xff0c;中国移动联合中国华电研发的“基于ZETA物联网技术的风电机组主辅智能控制系统与风电机组叶片巡检系统”在甘肃省酒泉华电黑崖子风电场成功投运。中移物联网有限公司相关人员主导参与了本次试点。 ZETA技术是一种基于UNB的低功耗广域网&#xff08;LP…

JVM的小知识总结

加载时jvm做了这三件事&#xff1a; 1&#xff09;通过一个类的全限定名来获取该类的二进制字节流 什么是全限定类名&#xff1f; 就是类名全称&#xff0c;带包路径的用点隔开&#xff0c;例如: java.lang.String。 即全限定名 包名类型 非限定类名也叫短名&#xff0c;就…

近期知识点随笔

菜单查询&#xff08;编写权限时的细节&#xff09; 菜单查询list为了侧边框展示更完整&#xff08;不报空指针&#xff09; 登录时&#xff08;用户名&#xff09;查询出多个结果&#xff08;保证用户名唯一&#xff09; 文件上传 前端 对权限与菜单绑定的修改&#xff08;实…

【数据结构】树的概念以及二叉树

目录 1 树概念及结构 1.1 树的概念 1.3 树的存储 2 二叉树的概念及结构 2.1 概念 2.2 特殊的二叉树 2.3 二叉树的性质 2.4 二叉树的存储结构 1 树概念及结构 1.1 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组…

04 # 第一个 TypeScript 程序

初始化项目以及安装依赖 新建 ts_in_action 文件夾 npm init -y安装好 typescript&#xff0c;就可以执行下面命令查看帮助信息 npm i typescript -g tsc -h创建配置文件&#xff0c;执行下面命令就会生成一个 tsconfig.json 文件 tsc --init使用 tsc 编译一个 js 文件 新…

解决:AttributeError: ‘NoneType’ object has no attribute ‘shape’

解决&#xff1a;AttributeError: ‘NoneType’ object has no attribute ‘shape’ 文章目录 解决&#xff1a;AttributeError: NoneType object has no attribute shape背景报错问题报错翻译报错位置代码报错原因解决方法今天的分享就到此结束了 背景 在使用之前的代码时&…