【方向盘】版本历史&代码示例之:Bean Validation、JPA

开发 前端
在任何时候,当你想要处理一个应用程序的逻辑时,确保数据的正确性是你必须要考虑和面对的事情。也就说我们必须通过某种手段,确保输入进来的数据是正确的。

[[427388]]

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

正文

Bean Validation

数据校验:在任何时候,当你想要处理一个应用程序的逻辑时,确保数据的正确性是你必须要考虑和面对的事情。也就说我们必须通过某种手段,确保输入进来的数据是正确的。

然而,应用程序一般是分层的,同样的验证逻辑往往会出现在不同的层,这样就会给代码组织管理上带来冗余负担。为了避免这类情况的发生,最好就是做一层抽象:将验证逻辑与响应的模型进行绑定,这就是Bean Validation。

Bean Validation简直就是业务开发中祛掉坏味道代码的利器,完美的实现契约式编程,大大提高开发效率,降低出错概率。

注意:Bean Validation它是一种通用规范,并不只属于Web层技术,即使大概率你可能只在Spring MVC中使用过它~

  1. <!-- javax命名空间版本(Tomcat 9.x及以下版本支持) --> 
  2. <dependency> 
  3.     <groupId>javax.validation</groupId> 
  4.     <artifactId>validation-api</artifactId> 
  5.     <version>2.0.1.Final</version> 
  6. </dependency> 
  7.  
  8. <!-- jakarta命名空间版本(Tomcat 10.x及以上版本支持) --> 
  9. <dependency> 
  10.     <groupId>jakarta.validation</groupId> 
  11.     <artifactId>jakarta.validation-api</artifactId> 
  12.     <version>3.0.0</version> 
  13.     <!-- <version>2.0.2</version> 此版本命名空间同javax --> 
  14. </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.0 2017.08 JSR 380 Java 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.0 6.x
3.0 7.x

说明:Hibernate Validator 7.x专为Jakarta Bean Validation 3.0打造,适配jakarta.*命名空间

代码示例

导入BV的实现框架:

  1. <dependency> 
  2.     <groupId>org.hibernate.validator</groupId> 
  3.     <artifactId>hibernate-validator</artifactId> 
  4. </dependency> 

 

准备一个Java Bean,并通过注解声明规则:

  1. /** 
  2.  * 在此处添加备注信息 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/10/6 10:16 
  7.  * @since 0.0.1 
  8.  */ 
  9. @Data 
  10. public class Person { 
  11.  
  12.     @Positive 
  13.     private long id; 
  14.     @NotBlank 
  15.     private String name
  16.     @NotNull 
  17.     @PositiveOrZero 
  18.     private Integer age; 
  19.  

书写验证代码:

  1. @Test 
  2. public void validBean() { 
  3.     // 使用默认配置获得验证器 
  4.     Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); 
  5.  
  6.     // 准备(待校验的)Bean 
  7.     Person person = new Person(); 
  8.     person.setId(-1); 
  9.     person.setName("YourBatman"); 
  10.     person.setAge(18); 
  11.  
  12.     // 使用校验器对其执行校验 
  13.     Set<ConstraintViolation<Person>> violations = validator.validate(person); 
  14.     // 打印校验结果 
  15.     if (violations.isEmpty()) { 
  16.         System.out.println("校验通过!"); 
  17.     } else { 
  18.         System.out.println("校验不通过!错误详情如下:"); 
  19.         violations.forEach(v -> System.out.println("\t" + v.getPropertyPath() + v.getMessage() + ",但你的值是:" + v.getInvalidValue())); 
  20.     } 

运行程序,输出为:

  1. 校验不通过!错误详情如下: 
  2.  id必须是正数,但你的值是:-1 

若运行时碰到这个类找不着:

不要“害怕”,这不正是上篇文章提到内容吗:EL表达式用于数据校验,加入进来即可。

  1. <dependency> 
  2.     <groupId>org.glassfish</groupId> 
  3.     <artifactId>jakarta.el</artifactId> 
  4. </dependency> 

为了加强理解,再来一个方法级的校验示例(参数、返回值):

  1. public @Positive int toInt(@NotNull String numStr) { 
  2.     int result = Integer.parseInt(numStr); 
  3.     return result; 

不同于Java Bean表示状态(静态的),方法/构造器是执行期(动态的)才能进行校验,所以执行校验的代码可以这么做:

  1. public @Positive int toInt(@NotNull String numStr) throws NoSuchMethodException { 
  2.     // 使用默认配置获得验证器(用于方法、构造器的校验器) 
  3.     ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables(); 
  4.     // 执行参数校验逻辑start... 
  5.     Method currMethod = BeanValidationDemo.class.getMethod("toInt", String.class); 
  6.     Set<ConstraintViolation<BeanValidationDemo>> violations = executableValidator.validateParameters(this, currMethod, new Object[]{numStr}); 
  7.     // 打印校验结果 
  8.     if (violations.isEmpty()) { 
  9.         System.out.println("校验通过!"); 
  10.     } else { 
  11.         System.out.println("校验不通过!错误详情如下:"); 
  12.         violations.forEach(v -> System.out.println("\t" + v.getPropertyPath() + v.getMessage() + ",但你的值是:" + v.getInvalidValue())); 
  13.         throw new IllegalArgumentException("校验不通过!"); //抛出异常,终止此方法 
  14.     } 
  15.     // 执行参数校验逻辑end... 
  16.  
  17.     int result = Integer.parseInt(numStr); 
  18.     // 执行返回值校验逻辑start 
  19.     // executableValidator.validateReturnValue(); 
  20.     // 执行返回值校验逻辑end 
  21.     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来统一天下的

  1. <!-- javax命名空间版本(Tomcat 9.x及以下版本支持) --> 
  2. <dependency> 
  3.     <groupId>javax.persistence</groupId> 
  4.     <artifactId>javax.persistence-api</artifactId> 
  5.     <version>2.2</version> 
  6. </dependency> 
  7.  
  8. <!-- jakarta命名空间版本(Tomcat 10.x及以上版本支持) --> 
  9. <dependency> 
  10.     <groupId>jakarta.persistence</groupId> 
  11.     <artifactId>jakarta.persistence-api</artifactId> 
  12.     <version>3.0.0</version> 
  13.     <!-- <version>2.2.3</version> 此版本命名空间同javax --> 
  14. </dependency> 

版本历程

版本 发布日期 JSR版本 对应Java EE版本
1.0 2006.05 -- Java EE 5
2.0 2009.12 JSR 317 Java EE 6
2.2 2017.08 JSR 338 Java EE 8
3.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组合方式更灵活和更常用。

典型示例:

  1. @Entity  //声明该类是和数据库表映射的实体类 
  2. @Table(name="t_user")  //建立实体类与表的映射关系 
  3. public class User implements Serializable { 
  4.  
  5.  @Id  //声明当前私有属性为主键 
  6.  @GeneratedValue(strategy=GenerationType.IDENTITY)  //配置主键的生成策略,为自增主键 
  7.  @Column(name = "user_id"
  8.  private Long userId; 
  9.  @Column(name="user_name")   
  10.  private String userName; 
  11.  @Column(name="user_source"
  12.  private String userSource; 
  13.   

生存现状

国内不乐观,海外坚挺。

实现(框架)

虽说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.4 2018.12 2.2 EntityGraph增强。最低要求8,支持11和17
5.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):

  1. <?xml version="1.0" encoding="UTF-8"?> 
  2. <!DOCTYPE hibernate-configuration PUBLIC 
  3.         "-//Hibernate/Hibernate Configuration DTD 3.0//EN" 
  4.         "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"
  5.  
  6. <hibernate-configuration> 
  7.     <!-- 配置数据库连接 connection --> 
  8.     <session-factory> 
  9.         <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property> 
  10.         <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/demo</property> 
  11.         <property name="hibernate.connection.username">root</property> 
  12.         <property name="hibernate.connection.password">root</property> 
  13.         <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> 
  14.         <!-- 连接池 hikaricp 该provider由hikari提供--> 
  15.         <property name="hibernate.connection.provider_class">com.zaxxer.hikari.hibernate.HikariConnectionProvider</property> 
  16.  
  17.         <!-- 格式化输出生成的SQL语句 --> 
  18.         <property name="hibernate.show_sql">true</property> 
  19.         <property name="hibernate.format_sql">true</property> 
  20.  
  21.         <!-- hibernate根据映射关系自动建表 
  22.             默认: 不会创建表 
  23.             create: 没有表就创建,有表就删除重建。 
  24.             create-drop: 没有表就创建,有表就删除重建,使用完自动删表。 
  25.             update: 没有表就创建表,否则使用现有的表。 
  26.             validate: 不会创建表 
  27.          --> 
  28.         <property name="hibernate.hbm2ddl.auto">validate</property> 
  29.     </session-factory> 
  30. </hibernate-configuration> 

准备一个entity(并在数据库创建好表结构):

  1. /** 
  2.  * 在此处添加备注信息 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/10/6 16:11 
  7.  * @since 0.0.1 
  8.  */ 
  9. @Data 
  10. @Entity 
  11. @Table(name = "user"
  12. public class User { 
  13.  
  14.     @Id 
  15.     @GeneratedValue(strategy = GenerationType.IDENTITY) 
  16.     private Long id; 
  17.  
  18.     @Column(name = "name"
  19.     private String name
  20.     @Column(name = "age"
  21.     private Integer age; 
  22.  

启动Hibernate:

  1. /** 
  2.  * 在此处添加备注信息 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/10/6 16:06 
  7.  * @since 0.0.1 
  8.  */ 
  9. public class JPADemo { 
  10.  
  11.     @Test 
  12.     public void fun1() { 
  13.         // 准备Hibernate的Session 
  14.         Configuration configure = new Configuration().configure(); 
  15.         SessionFactory sessionFactory = configure.buildSessionFactory(); 
  16.         Session session = sessionFactory.openSession(); 
  17.         Transaction transaction = session.beginTransaction(); 
  18.  
  19.         // 业务逻辑start 
  20.         User user = new User(); 
  21.         user.setName("YourBatman"); 
  22.         user.setAge(10); 
  23.         session.save(user); 
  24.         System.out.println("保存成功,id为:" + user.getId()); 
  25.         // 业务逻辑end 
  26.  
  27.         transaction.commit(); 
  28.         session.close(); 
  29.         sessionFactory.close(); 
  30.     } 
  31.  

大功告成。

在国内,即使使用JPA,大都是使用Spring Data JPA,可大大简化开发。当然喽,企业级项目使用MyBatis还是居多~

工程源代码:https://github.com/yourbatman/FXP-java-ee

总结

本文拉齐了Java EE的两项热门技术:Bean Validation和JPA,它俩的实现恰巧都是Hibernate,所以放在本篇一起毫无违和感。

 

责任编辑:武晓燕 来源: Java方向盘
相关推荐

2021-10-11 08:51:50

JavaMailJDBCJava

2021-10-25 08:16:20

Java JAX-RS Java 基础

2021-09-15 18:54:22

BATutopia-JWebSocket

2021-10-19 08:23:43

JMXJava 管理扩展

2021-09-13 18:39:50

ServeltELJSP

2010-12-21 11:36:58

职场

2023-03-10 14:55:28

2021-11-01 07:55:39

Java EE开发项目

2018-01-15 09:32:34

无人驾驶GPU主机辅助驾驶

2021-12-20 08:15:25

SpringFeignClientSpring Clou

2022-04-11 08:34:27

IDEA代码补全

2021-11-29 08:13:41

Spring Boot环境变量Spring技术

2022-01-06 09:38:13

集度自动驾驶apollo

2021-12-02 15:19:34

自动驾驶技术苹果

2010-03-22 09:33:18

Bean Valida

2011-04-02 14:33:51

Bean ValidaJava EE 6Java

2022-03-28 08:35:33

IDEA快捷键操作视窗

2023-10-18 18:38:44

数据校验业务

2022-04-06 08:35:13

IDEADebug调试Run运行
点赞
收藏

51CTO技术栈公众号