从record类型 思考 Java反射、内省、序列化、反序列化

5 篇文章 0 订阅
1 篇文章 0 订阅

  java 在14版本引入了新特性record关键字,,它是一种轻量级的数据封装类型,用于定义不可变的数据对象。在对类型了解的过程中考虑到了其适用场景,百度了解一下后发现,他适用于DTO的数据接收。既然其试用与DTO的数据接收,那么必然需要面两DTO、PO的数据转化。
  通过查询资料了解到,目前实体类之间的转化一般采用下面三种方式:

  • 反射
  • 使用内省获取beaninfo进行设置读取属性
  • 在编译的过程根据属性及配置信息自动生成get、set方法调用设值

本文从record引入,将针对反射、内省、BeanUtils、Jackson、mapstruct进行思考和说明。

record介绍

  使用record增强 Java 编程语言,record是充当不可变数据的透明载体的类。record可以被认为是名义元组。其具体试用方式如下:

public record UserRecord(String name) {
}
// 等价于
public final class UserRecord{
	private final String name;
	public User(String name){
		this.name = name;
	}
	public String name(){
		return name;
	}
}
// record 对象编译后的class文件如下
public record UserRecord(String name) {
    public UserRecord(String name) {
        this.name = name;
    }

    public String name() {
        return this.name;
    }
}

  通过上述代码比较不难看出record类型本质上将生成一个class类型,该class中包含了一个参数全的构造方法和若干个非get前缀开头的get方法。
  了解到record类型可以作为DTO,那么推断出其需要具备两个功能:

  • 允许json通过jackson反序列化到record对象
  • 可以进行DTO、PO之间的转化

  为了方便后续我们对反序列化、实体类转化深入了解和研究,首先我们需要对反射、内省有一定的了解,下面将简单的介绍反射和内省机制;

反射

  Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。根据反射描述可知反射具备以下特点:

  • 运行状态下
  • 所有方法和属性对象

  所谓所有方法和属性对象是指反射获取某个类的属性或者方法的过程中可以绕开该方法或者属性声明的访问控制,例如针对private访问控制声明的属性在反射的过程中依旧可以通过field.setAccessible(true);设置可访问权限进行设置。反射使用的一般源码如下:

public void reflex(){
 	// 获取Class对象
	Class<?> clazz = obj.getClass();
	// 获取属性
	clazz.getFields();
	// 获取方法
	clazz.getMethods();
	//处理属性或者方法
	// 1.一般逻辑为获取该属性或者方法上的注解然后通过注解进行判断
	// 2.针对属性可以通过field.set方法想某个对象的某个field设值
	// 3.针对方法可以通过method.invoke方法调用某个对象的执行方法并声明入参信息
}

内省

  内省通常会使用反射作为其底层实现机制,主要用于获取和操作类的属性、方法和事件等信息;内省对访问级别有一定的限制。当使用内省获取和操作类的属性或方法时,它会遵循类的访问控制规则,只能访问到公共的(public)属性和方法;内省是基于特定的API,其性能可能比反射更高效。因为内省在编译时已经生成了类的描述信息,所以在运行时获取类的信息时速度较快。内省具备如下特点:

  • 编译时已经生成类的描述信息
  • 对类的属性和方法有访问级别控制
  • 底层基于反射

  从上述描述中不难看出,内省比反射性能高但是反射比内省更灵活。内省的特别很适合对象的序列化和反序列化以及实体类之间的转化;因此后续说明的Jackson就是使用内省机制的。内省的使用逻辑如下:

public void introspection(){
	// 本质上在编译的过程中beanInfo信息已经存在,因此获取效率更高
	BeanInfo beanInfo = Introspector.getBeanInfo(Person.class);
	// 遍历属性描述符
    for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {          
    	// 获取 Getter 方法并调用
        if (propertyDescriptor.getReadMethod() != null) {
        	// 也是通过反射调用的
        	System.out.println("Value: " + propertyDescriptor.getReadMethod().invoke(person));
        }
     }
}

  需要说明的是:要使用内省机制正确识别和操作 JavaBean 的属性,getter 方法必须按照 JavaBean 规范进行命名,即以 get 开头(boolean 类型可以使用 is 开头)。这种命名方式是内省机制能够识别属性的关键。也就是说:约定俗成的是在使用内省进行获取读写方法的过程中方法名必须符合JavaBean规范进行命名。

Jackson-序列化和反序列化

  Spring boot或者Spring MVC 默认使用的序列化和反序列化工具是Jackson,因此在使用record类型作为DTO时,其实是使用了Jackson进行反序列化。通过查询资料了解到Jackson的序列化和反序列化具备以下特点:

  • 反序列化时:无参构造和有参构造方法都有的时候先走无参构造;
  • 序列化和反序列化过程中使用到了内省技术【Gson使用的是反射】

内省的BUG
  在我们开发过程中序列化和反序列化中出现的一些问题可能就是内省机制导致的,最为明显的就是序列化后多参数情况:例如存在user属性为name、age方法存在get、set;此外还有一个方法isStudent返回boolean表明该用户是否是学生,针对user类序列化后json中会多出一个student的键值对!这就是因为Jackson的反序列化使用了内省技术吗,它将获取基于JavaBean的命名方法,然后通过get、set方法调用进行序列化和反序列化;isStudent在内省看来是一个满足JavaBean命名的方法,并且student为一个boolean类型,在序列化时将执行isStudent方法获取值并设置到student中。
  上述原因也就是为什么不建议在pojo中进行除get、set方法外的其他方法定义的原因。

record的序列化和反序列化

  结合上述所有说明介绍,通过record类型的字节码,从Jackson和内省的角度上来说具备以下思考点:

  • record只有一个全参数的构造方法
  • record的不存在set方法且get方法并不满足JavaBean命名规范

  经过测试放下:record可以通过Jackson进行序列化和反序列化、但是class中如果没有get开头的方法则不能进行序列化;存在全参数构造方法可以解释为什么record可以作为一个DTO,在接收到前端的json数据后Jackson可以将其反序列化到record中通过全参构造方法;
  但是record为什么能序列化呢,他的get方法并不满足JavaBean的命名规范啊!详情请参考: jackson适配jdk14说明。简单来说Jackson为了应对record类型的序列化和反序列化进行了序列化工具的扩展。该序列化工具没有进行了解学习,如果有大神了解欢迎指导。

BeanUtils(spring 提供的类转化工具)

  既然record类型可以作为DTO、PO、VO那就避免不了实体类之间的转化;BeanUtils是实体类转化工具之一(在此仅仅介绍spring提供的工具,其他BeanUtils工具不进行考虑),其底层主要使用了反射机制进行实现的。追踪源码可知如下调用顺序:

业务 BeanUtils CachedIntrospectionResults copyProperties getPropertyDescriptor() 获取源数据的属性描述器 forClass()数据构造 new()[introspectInterfaces、introspectPlainAccessors] 返回CachedIntrospectionResults 业务 BeanUtils CachedIntrospectionResults

  上述时序图对获取源数据获取属性值的方法进行了说明,在获取CachedIntrospectionResults的过程中new对象的适合执行了introspectInterfaces和introspectPlainAccessors方法,这两个方法的源码如下:

// Explicitly check implemented interfaces for setter/getter methods as well,
// in particular for Java 8 default methods...
private void introspectInterfaces(Class<?> beanClass, Class<?> currClass, Set<String> readMethodNames)
// Check for record-style accessors without prefix: e.g. "lastName()"
// - accessor method directly referring to instance field of same name
// - same convention for component accessors of Java 15 record classes
private void introspectPlainAccessors(Class<?> beanClass, Set<String> readMethodNames)

  一个是针对get、set方法进行初始化实例,另一个是针对不带get、set的进行设置及record中的不满足JavaBean的方法。
  通过上述源码理解,可知BeanUtil可以将record对象转化成为一个class pojo对象。

mapstruct

  该工具也是一个实体类转化工具,其原理是在编码过程中自动按照配置信息生成一个方法进行类型转化,该方法内部调用两个类型转化中的大量的get、set方法。也就是说其类似于lombok,在编译的过程中会自动生成代码,在类型转化的过程中是通过这些代码实现的,需要调用实体类的get、set方法。
  mapstruct的性能要比BeanUtils高很对,因此后者是基于反射前者直接执行的方法;但遗憾的是record类型中并不存在get、set方法,因此mapstruct目前无法应用于record类型。

总结

  由于这两天新学到了record类型,针对该类型进行简单了解后便思考了一下其使用场景,根据不同场景思考后遇到一些和既有认知上的冲突,学习了解后整理本文。
  首先,就我了解Jackson基于内省,而内省需要实体类方法名遵循JavaBean命名规范,而record明显不满足,因此基于我即有的知识来讲,record并不能被作为DTO和VO;学习了解后认知到:

  • Jackson为Java 14进行适配,针对record对象实现了专门的序列化工具
  • 在我们日常开发中,DTO、VO使用的都是无参构造方法,然后Jackson通过内省调用getter、setter方法进行属性赋值,而实际上当对象中只存在一个有参构造方法时,Jackson可以通过调用有参构造方法进行赋值,这一点针对record反序列化提供了支持

  实体类之间的转化我通常使用mapstruct,对BeanUtils也有简单的了解,本次针对BeanUtils进行了扩展:

  • BeanUtils基于反射实现的,他并不是基于内省实现,因此具备更高的灵活度
  • BeanUtils处理对getter、setter方法进行调用外,他还允许直接针对参数名()形式的get方法调用,这一点很好的契合了record的get方法。

  本文对record及序列化反序列化、反射内省等内容进行了简单说明,如有错误之处欢迎斧正。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值