
本文转载自微信公众号「Java方向盘」,盘版作者方向盘 。本历转载本文请联系Java方向盘公众号。史代
正文

Bean Validation
数据校验:在任何时候,码示当你想要处理一个应用程序的盘版逻辑时,确保数据的本历正确性是你必须要考虑和面对的事情。也就说我们必须通过某种手段,史代确保输入进来的码示数据是正确的。
然而,盘版应用程序一般是本历分层的,同样的史代验证逻辑往往会出现在不同的层,这样就会给代码组织管理上带来冗余负担。码示为了避免这类情况的盘版发生,最好就是本历做一层抽象:将验证逻辑与响应的模型进行绑定,这就是史代Bean Validation。

Bean Validation简直就是业务开发中祛掉坏味道代码的利器,完美的服务器托管实现契约式编程,大大提高开发效率,降低出错概率。
注意:Bean Validation它是一种通用规范,并不只属于Web层技术,即使大概率你可能只在Spring MVC中使用过它~
<!-- javax命名空间版本(Tomcat 9.x及以下版本支持) --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <!-- jakarta命名空间版本(Tomcat 10.x及以上版本支持) --> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>3.0.0</version> <!-- <version>2.0.2</version> 此版本命名空间同javax --> </dependency> 版本历程
版本 发布日期 JSR版本 对应Java EE版本 主要特性 1.0 2009.11 JSR 303 Java EE 6 对JavaBean进行验证,提供13个注解 1.1 2013.05 JSR 349 Java EE 7 新增方法级验证(参数、返回值) 2.02017.08JSR 380Java EE 8新增9个注解达到22个。支持容器元素验证 3.0 2020.07 Jakarta管理 Jakarta EE 9 同上 截止到2.0/3.0版本,共计13 + 9 = 22个内建标准的注解:
序号 注解 支持类型 含义 null值是否校验 01 @AssertFalse bool 元素必须是false 否 02 @AssertTrue bool 元素必须是true 否 03 @DecimalMax Number的子类型(浮点数除外)以及String 元素必须是一个数字,且值必须<=最大值 否 04 @DecimalMin 同上 元素必须是一个数字,且值必须>=最大值 否 05 @Max 同上 同上 否 06 @Min 同上 同上 否 07 @Digits 同上 元素构成是否合法(整数部分和小数部分) 否 08 @Future 时间类型(包括JSR310) 元素必须为一个将来(不包含相等)的日期(比较精确到毫秒) 否 09 @Past 同上 元素必须为一个过去(不包含相等)的日期(比较精确到毫秒) 否 10 @NotNull any 元素不能为null 是11 @Null any 元素必须为null 是12 @Pattern 字符串 元素需符合指定的正则表达式 否 13 @Size String/Collection/Map/Array 元素大小需在指定范围中 否 -- -- 2.0版本新增了9个注解,如下 -- -- 14 @Email 字符串 元素必须为电子邮箱地址 否 15 @NotEmpty 容器类型 集合的Size必须大于0 是16 @NotBlank 字符串 字符串必须包含至少一个非空白的字符 是17 @Positive 数字类型 元素必须为正数(不包括0) 否 18 @PositiveOrZero 同上 同上(包括0) 否 19 @Negative 同上 元素必须为负数(不包括0) 否 20 @NegativeOrZero 同上 同上(包括0) 否 21 @PastOrPresent 时间类型 在@Past基础上包括相等 否 22 @FutureOrPresent 时间类型 在@Futrue基础上包括相等 否 值得注意的是,服务器租用还有些比较常用的注解如@DurationMin、@DurationMax、@Length、@ScriptAssert、@ParameterScriptAssert、@Range、@UniqueElements等,它们不属于标准注解,而属于Hibernate的。但正如上面所说,Hibernate Validator它几乎就是标准,所以在开发中使用也是没有任何问题的。
生存现状
Spring在数据验证这块的API设计得比较失败,Bean Validation很好的弥补了其不足。
虽然Bean Validation存在时间已经很长了,但很多程序员对其依旧“无感”。随着DDD领域驱动模型的流行和普及,它的重要性日趋凸显,毕竟它的设计思想和域模型是一致的,能起到很好的“化学反应”,源码库从而事半功倍。
简单的讲,若你是一个模块化设计爱好者、优雅代码的拥护者、声明式编程的追求者,那么Bean Validation对你的帮助绝非一点点。
实现(框架)
虽说BV规范的实现框架一般有两种:Hibernate Validator和Apache BVal,但实际上基本可认为前者是唯一实现,它就等同于标准,版本对应关系如下:
BV版本 HV实现版本 1.0 4.x 1.1 5.x 2.06.x3.0 7.x 说明:Hibernate Validator 7.x专为Jakarta Bean Validation 3.0打造,适配jakarta.*命名空间
代码示例
导入BV的实现框架:
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency> 准备一个Java Bean,并通过注解声明规则:
/** * 在此处添加备注信息 * * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> * @site https://yourbatman.cn * @date 2021/10/6 10:16 * @since 0.0.1 */ @Data public class Person { @Positive private long id; @NotBlank private String name; @NotNull @PositiveOrZero private Integer age; } 书写验证代码:
@Test public void validBean() { // 使用默认配置获得验证器 Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); // 准备(待校验的)Bean Person person = new Person(); person.setId(-1); person.setName("YourBatman"); person.setAge(18); // 使用校验器对其执行校验 Set<ConstraintViolation<Person>> violations = validator.validate(person); // 打印校验结果 if (violations.isEmpty()) { System.out.println("校验通过!"); } else { System.out.println("校验不通过!错误详情如下:"); violations.forEach(v -> System.out.println("\t" + v.getPropertyPath() + v.getMessage() + ",但你的值是:" + v.getInvalidValue())); } } 运行程序,输出为:
校验不通过!错误详情如下: id必须是正数,但你的值是:-1 若运行时碰到这个类找不着:

不要“害怕”,这不正是上篇文章提到内容吗:EL表达式用于数据校验,加入进来即可。
<dependency> <groupId>org.glassfish</groupId> <artifactId>jakarta.el</artifactId> </dependency> 为了加强理解,再来一个方法级的校验示例(参数、返回值):
public @Positive int toInt(@NotNull String numStr) { int result = Integer.parseInt(numStr); return result; } 不同于Java Bean表示状态(静态的),方法/构造器是执行期(动态的)才能进行校验,所以执行校验的代码可以这么做:
public @Positive int toInt(@NotNull String numStr) throws NoSuchMethodException { // 使用默认配置获得验证器(用于方法、构造器的校验器) ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables(); // 执行参数校验逻辑start... Method currMethod = BeanValidationDemo.class.getMethod("toInt", String.class); Set<ConstraintViolation<BeanValidationDemo>> violations = executableValidator.validateParameters(this, currMethod, new Object[]{ numStr}); // 打印校验结果 if (violations.isEmpty()) { System.out.println("校验通过!"); } else { System.out.println("校验不通过!错误详情如下:"); violations.forEach(v -> System.out.println("\t" + v.getPropertyPath() + v.getMessage() + ",但你的值是:" + v.getInvalidValue())); throw new IllegalArgumentException("校验不通过!"); //抛出异常,终止此方法 } // 执行参数校验逻辑end... int result = Integer.parseInt(numStr); // 执行返回值校验逻辑start // executableValidator.validateReturnValue(); // 执行返回值校验逻辑end return result; } 这样每次方法运行时就能触发校验逻辑了。也许,你会觉得这么做不算完美:侵入性太强了。
是的,不够优雅。但有经验的小伙伴似乎一眼就能看出来如何优化:

没错,就是用AOP来改善坏味道的代码,让校验逻辑和业务逻辑完全分离。
至于如何使用AOP,额,笔者就不用再贴代码示例了吧,给你点提示:
在非Spring场景下,可基于Java EE的@Inteceptors实现
在Spring场景下,你熟悉的场景
本专栏源代码:https://github.com/yourbatman/FXP-java-ee
JPA
Java Persistence API:通过注解或者XML描述【对象-关系表】之间的映射关系,并将实体对象持久化到数据库中。JPA规范给开发者带来了福音:开发者面向JPA规范的接口,但底层的JPA实现可以任意切换:觉得Hibernate好的,可以选择Hibernate JPA实现;觉得TopLink好的,可以选择TopLink JPA实现……这样开发者可以避免为使用Hibernate学习一套ORM框架,为使用TopLink又要再学习一套ORM框架。

如图亦可见,JPA是Java EE的野心:Sun公司希望通过JPA整合ORM技术,实现天下归一。实际情况是,它做到了,除了天朝钟爱更轻量级的MyBatis外,海外依旧是使用JPA居多。
学习过Hibernate的应当可以很轻易的上手Java persistence,因为Java persistence的开发者其实就是原hibernate的开发者。再配合以annotation,可以很轻易的开发出Entity Bean。
注意:是先有Hibernate、TopLink等ORM框架,后才有的JPA来统一天下的
<!-- javax命名空间版本(Tomcat 9.x及以下版本支持) --> <dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> <version>2.2</version> </dependency> <!-- jakarta命名空间版本(Tomcat 10.x及以上版本支持) --> <dependency> <groupId>jakarta.persistence</groupId> <artifactId>jakarta.persistence-api</artifactId> <version>3.0.0</version> <!-- <version>2.2.3</version> 此版本命名空间同javax --> </dependency> 版本历程
版本 发布日期 JSR版本 对应Java EE版本 1.0 2006.05 -- Java EE 5 2.0 2009.12 JSR 317 Java EE 6 2.22017.08JSR 338Java EE 83.0 2020.11 Jakarta管理 Jakarta EE 9 JPA 2.1已经是一个非常成熟的规范,提供了现代应用程序所需的大部分功能。2017年夏天发布的2.2版本(规范内容和2.1规范差不多),新增了对Java 8更好的支持,如它的日期时间Date/Time、重复注解@Repeatable、Stream形式等等,大大增加了其易用性。
下面列出JPA最常用的一些注解:
序号 注解 标注在哪 释义 01 @Entity 类 标识实体类是JPA实体,告诉JPA在程序运行时生成实体类对应表 02 @Table 类 定义entity主表的name,catalog,schema等属性。也就是说ORM规则自定义 03 @Id 属性 标注此属性为主键 04 @GeneratedValue 属性 JPA通用主键策略生成器,此方式依赖具体数据库(和@Id联用) 05 @Column 属性 定义了映射到数据库的列的所有属性:列名,是否唯一,是否允许为空,是否允许更新等 06 @Transient 属性 该属性并不是一个到数据库表的字段的映射,指定的这些属性不会被持久化,ORM框架将忽略该属性 07 @Temporal 属性 当为java.util中的日期/时间类型时,通过它来指定格式 08 @Enumerated 属性 标注枚举类型如何存库 09 @TableGenerator 类/属性 定义一个主键值生成器,在Id这个元数据的generate=TABLE时,generator属性中可以使用生成器的名字 10 @SequenceGenerator 类/属性 定义一个主键值生成器,在Id这个元数据的generator属性中可以使用生成器的名字 11 @SecondaryTable 类 一个entity class可以映射到多表,SecondaryTable用来定义单个从表的名字,主键名字等属性 12 @UniqueConstraint 注解元数据 定义在Table或SecondaryTable元数据里,用来指定建表时需要建唯一约束的列 13 @OneToOne 类 一对一的关联。可配置抓取策略、级联操作等 14 @ManyToOne 类 多对一的映射,该注解标注的属性通常是数据库表的外键 15 @OneToMany 类 一对多的关联,该属性应该为集体类型,在数据库中并没有实际字段 16 @ManyToMany 类 多对多的关联.多对多关联上是两个一对多关联,但是在ManyToMany描述中,中间表是由ORM框架自动处理 17 @JoinColumn 属性 如果在entity class的field上定义了关系(one2one或one2many等),通过JoinColumn来定义关系的属性 18 @IdClass 类 当entity class使用复合主键时,需要定义一个类作为id class 19 @MapKey 属性 在一对多,多对多关系中,我们可以用Map来保存集合对象。默认用主键值做key,如果使用复合主键,则用id class的实例做key,如果指定了name属性,就用指定的field的值做key 20 @OrderBy 属性 在一对多,多对多关系中,有时希望从数据库加载出来的集合对象是按一定方式排序的,这可以通过OrderBy来实现,默认是按对象的主键升序排列 21 @Version 属性 实体类在乐观事务中的version属性 22 @Lob 属性 指定一个属性作为数据库支持的大对象类型在数据库中存储。使用LobType这个枚举来定义Lob是二进制类型还是字符类型 23 @DiscriminatorColumn 属性 定义在使用SINGLE_TABLE或JOINED继承策略的表中区别不继承层次的列 @Entity和@Table有何区别?答:@Entity表示这个class是实体类,并且使用 默认的 orm规则,即class名即数据库表中表名,class字段名即表中的字段名。若想自定义规则,就要使用@Table来改变包括表名、schema等,且辅助@Column来改变class中字段名与db中表的字段名的映射规则。很明显,@Table + @Column组合方式更灵活和更常用。
典型示例:
@Entity //声明该类是和数据库表映射的实体类 @Table(name="t_user") //建立实体类与表的映射关系 public class User implements Serializable { @Id //声明当前私有属性为主键 @GeneratedValue(strategy=GenerationType.IDENTITY) //配置主键的生成策略,为自增主键 @Column(name = "user_id") private Long userId; @Column(name="user_name") private String userName; @Column(name="user_source") private String userSource; } 生存现状
国内不乐观,海外坚挺。
实现(框架)
虽说ORM框架较多,但Hibernate的市占率独步天下,几乎能等同于JPA规范。毕竟制定JPA规范的人之前在Hibernate上班呢~
Hibernate 从3.2开始,开始兼容JPA。
版本 日期 JPA版本 info 5.0 2015.08 2.1 兼容到JDK 6,提供hibernate-java8兼容到JDK 8 5.1 2016.02 2.1 小版本迭代逐步放弃6、7,最低要求8 5.2 2016.06 2.1 最低要求JDK 8 5.3 2018.05 2.2 最低要求JDK 8,全面使用maven管理器所有的模块artifacts 5.42018.122.2EntityGraph增强。最低要求8,支持11和175.5 2021.06 2.2/3.0 同上。额外通过hibernate-core-jakarta增加了对JPA 3.0规范的支持 5.6 开发中 2.2/3.0 开发中 6.0 开发中 2.2/3.0 开发中 值得注意的是,Hibernate采用模块化管理,其中最重要的当属hibernate-core,还有hibernate-tools、hibernate-jcache、hibernate-hikaricp、hibernate-ehcache等等
代码示例
在classpath下准备一个hibernate.cfg.xml标准文件(亦可不使用xml文件,完全采用编程方式设置configuration):
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <!-- 配置数据库连接 connection --> <session-factory> <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/demo</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">root</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> <!-- 连接池 hikaricp 该provider由hikari提供--> <property name="hibernate.connection.provider_class">com.zaxxer.hikari.hibernate.HikariConnectionProvider</property> <!-- 格式化输出生成的SQL语句 --> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> <!-- hibernate根据映射关系自动建表 默认: 不会创建表 create: 没有表就创建,有表就删除重建。 create-drop: 没有表就创建,有表就删除重建,使用完自动删表。 update: 没有表就创建表,否则使用现有的表。 validate: 不会创建表 --> <property name="hibernate.hbm2ddl.auto">validate</property> </session-factory> </hibernate-configuration> 准备一个entity(并在数据库创建好表结构):
/** * 在此处添加备注信息 * * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> * @site https://yourbatman.cn * @date 2021/10/6 16:11 * @since 0.0.1 */ @Data @Entity @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name") private String name; @Column(name = "age") private Integer age; } 启动Hibernate:
/** * 在此处添加备注信息 * * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> * @site https://yourbatman.cn * @date 2021/10/6 16:06 * @since 0.0.1 */ public class JPADemo { @Test public void fun1() { // 准备Hibernate的Session Configuration configure = new Configuration().configure(); SessionFactory sessionFactory = configure.buildSessionFactory(); Session session = sessionFactory.openSession(); Transaction transaction = session.beginTransaction(); // 业务逻辑start User user = new User(); user.setName("YourBatman"); user.setAge(10); session.save(user); System.out.println("保存成功,id为:" + user.getId()); // 业务逻辑end transaction.commit(); session.close(); sessionFactory.close(); } } 大功告成。
在国内,即使使用JPA,大都是使用Spring Data JPA,可大大简化开发。当然喽,企业级项目使用MyBatis还是居多~
工程源代码:https://github.com/yourbatman/FXP-java-ee
总结
本文拉齐了Java EE的两项热门技术:Bean Validation和JPA,它俩的实现恰巧都是Hibernate,所以放在本篇一起毫无违和感。
-->