Spring Data----对象映射基础Object Mapping Fundamentals


本节介绍Spring Data对象映射、对象创建、字段和属性访问、可变性和不变性的基本原理。请注意,本节仅适用于不使用底层数据存储(如JPA)的对象映射的Spring Data模块。此外,请务必了解特定于存储对象的映射,如索引、自定义列名或字段名等。
SpringData对象映射的核心职责是创建域对象的实例,并将store-native数据结构映射到这些实例上。这意味着我们需要两个基本步骤:

  1. 使用公开的构造函数之一创建实例。
  2. 实例填充以物化(materialize)所有公开的属性。

一、对象创建

Spring Data会自动尝试检测用于物化该类型对象的持久实体的构造函数。解析算法的工作原理如下:

  1. 如果有一个用@PersistenceCreator注解的静态工厂方法,那么就使用它。
  2. 如果存在单个构造函数,则使用它。
  3. 如果有多个构造函数,并且恰好有一个构造函数是用@PersistenceCreator注解的,则使用它。
  4. 如果类型是Java Record,则使用规范构造函数。
  5. 如果存在无参数构造函数,则使用它。其他构造函数将被忽略。

值解析假定构造函数/工厂方法参数名称与实体的属性名称匹配,即解析将像填充属性一样执行,包括映射中的所有自定义项(不同的数据存储列或字段名等)。这还需要类文件中可用的参数名称信息或构造函数上存在的@ConstructorProperties注解。
值解析可以通过使用Spring Framework的@Value值注解(使用特定于存储的SpEL表达式)进行自定义。有关更多详细信息,请参阅有关特定于存储的映射的部分。

1.1 对象创建内部机制Object creation internals

为了避免反射的开销,Spring Data对象创建默认使用运行时生成的工厂类,它将直接调用域类构造函数。例如,对于这个示例类型:

class Person {
  Person(String firstname, String lastname) {}
}

框架将在运行时创建一个语义上等同于这个的工厂类:

class PersonObjectInstantiator implements ObjectInstantiator {

  Object newInstance(Object... args) {
    return new Person((String) args[0], (String) args[1]);
  }
}

这使我们的性能比反射提高了10%。对于有资格进行此类优化的域类,它需要遵守一组约束:

  1. 它不能是private class
  2. 它不能是非静态的内部类
  3. 它不能是CGLib代理类
  4. Spring Data要使用的构造函数不能是私有的

如果这些条件中没有任何一个匹配,Spring Data将返回到通过反射实例化实体。

二、属性填充Property population

一旦创建了实体的实例,Spring Data就会填充该类的所有剩余持久属性。除非已经由实体的构造函数填充(即使用其构造函数参数列表),否则将首先填充标识符属性,以允许解析循环对象引用。之后,在实体实例上设置所有尚未由构造函数填充的非瞬态(non-transient)属性。为此,框架使用以下算法:

  1. 如果属性是不可变的,但公开了with…方法(见下文),框架将使用with…方法创建一个具有新属性值的新实体实例。
  2. 如果定义了属性访问(即通过getters和setters进行访问),框架将调用setter方法。
  3. 如果属性是可变的,框架直接设置字段。
  4. 如果属性是不可变的,框架将使用持久性操作(请参见5.1对象创建)使用的构造函数来创建实例的副本。
  5. 默认情况下,框架直接设置字段值。

2.1 属性填充内部机制Property population internals

与对象构造方面的优化(5.1.1章节)类似,框架也使用Spring Data运行时生成的访问器类与实体实例进行交互。

class Person {

  private final Long id;
  private String firstname;
  private @AccessType(Type.PROPERTY) String lastname;

  Person() {
    this.id = null;
  }

  Person(Long id, String firstname, String lastname) {
    // Field assignments
  }

  Person withId(Long id) {
    return new Person(id, this.firstname, this.lastame);
  }

  void setLastname(String lastname) {
    this.lastname = lastname;
  }
}

生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;   --------2           

  private Person person;                         --------1  

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

    if ("firstname".equals(name)) {
      firstname.invoke(person, (String) value);  --------2          
    } else if ("id".equals(name)) {
      this.person = person.withId((Long) value); --------3           
    } else if ("lastname".equals(name)) {
      this.person.setLastname((String) value);   --------4           
    }
  }
}

1. PropertyAccessor持有基础对象的可变实例。这是为了实现其他不可变属性的变化。
2. 默认情况下,Spring Data使用字段访问来读取和写入属性值。根据私有字段的可见性规则,MethodHandles用于与字段交互。
3. 该类公开了一个用于设置标识符的withId()方法,例如,当一个实例插入到数据存储中并生成了标识符时。调用withId()将创建一个新的Person对象。所有后续的变化(mutations)都将发生在新的实例中,而不影响先前的实例。
4. 使用属性访问允许在不使用MethodHandles的情况下直接调用方法。

这使我们的性能比反射提高了25%。对于有资格进行此类优化的域类,它需要遵守一组约束:

  • 类型不能位于默认包中或java包下。
  • 类型及其构造函数必须是public
  • 作为内部类的类型必须是static。
  • 所使用的Java运行时必须允许在原始ClassLoader中声明类。Java 9和更新版本会带来某些限制。

默认情况下,Spring Data会尝试使用生成的属性访问器,如果检测到限制,则会返回到基于反射的访问器。
让我们来看看以下实体:
一个示例实体

class Person {

  private final @Id Long id;                            --------1                    
  private final String firstname, lastname;             --------2                    
  private final LocalDate birthday;                     
  private final int age;                                --------3                    

  private String comment;                               --------4                    
  private @AccessType(Type.PROPERTY) String remarks;    --------5                    

  static Person of(String firstname, String lastname, LocalDate birthday) { --------6

    return new Person(null, firstname, lastname, birthday,
      Period.between(birthday, LocalDate.now()).getYears());
  }

  Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { --------6

    this.id = id;
    this.firstname = firstname;
    this.lastname = lastname;
    this.birthday = birthday;
    this.age = age;
  }

  Person withId(Long id) {                               --------1                   
    return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
  }

  void setRemarks(String remarks) {                      --------5                   
    this.remarks = remarks;
  }
}

1. identifier属性是final,但在构造函数中设置为null。该类公开了一个用于设置标识符的withId()方法,例如,当一个实例插入到数据存储中并生成了标识符时。原始Person实例在创建新实例时保持不变。同样的模式通常应用于存储管理的其他属性,但可能必须更改这些属性才能进行持久性操作。wither方法是可选的,因为持久性构造函数(请参见6)实际上是一个复制构造函数,设置属性将转化为创建一个应用了新标识符值的新实例。
2. firstname和lastname属性是通过getter公开的普通不可变属性。
3. age属性是不可变的,但派生自birthday属性。在显示的设计中,数据库值将胜过默认值,因为Spring Data使用了唯一声明的构造函数。即使目的是首选(preferred)计算,重要的是该构造函数也要将年龄作为参数(可能会忽略它),否则属性填充步骤将试图设置年龄字段,但由于它是不可变的,并且不存在with…方法,因此失败。
4. comment属性是可变的,可以通过直接设置其字段来填充。
5. remarks属性是可变的,并且通过调用setter方法来填充。
6. 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是额外的构造函数,以避免通过@PersistenceCreator消除构造函数的歧义。相反,属性的默认设置是在工厂方法中处理的。如果你希望Spring Data使用工厂方法进行对象实例化,请使用@PersistenceCreator对其进行注解。

三、一般建议

  • 尽量坚持使用不可变的对象——创建不可变对象很简单,因为物化(materializing )对象只需调用其构造函数。此外,这避免了域对象中充斥着允许客户端代码操作对象状态的setter方法。如果你需要这些,最好让它们受到包保护,这样它们只能由有限数量的共存(co-located)类型调用。仅构造函数的物化比属性填充快30%。
  • 提供一个包含全部参数的构造函数——即使你不能或不想将实体塑造(model)为不可变的值,提供一个将实体的所有属性(包括可变属性)作为参数的构造函数仍然有价值,因为这允许对象映射跳过属性填充以获得最佳性能。
  • 使用工厂方法而不是重载构造函数来避免@PersistenceCreator——对于优化性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序用例的构造函数,这些构造函数省略了自动生成的标识符等。使用静态工厂方法来公开全参数构造函数的这些变体是一种既定模式。
  • 确保遵守允许使用生成的实例化器和属性访问器类的约束——
  • 对于要生成的标识符,仍然将final字段与all-arguments持久性构造函数(首选)或with…方法结合使用——
  • 使用Lombok避免样板(boilerplate)代码 — 由于持久性操作通常需要构造函数接受所有参数,因此它们的声明变成了样板参数到字段赋值的乏味重复,最好使用Lombok的@AllArgsConstructor来避免这种情况。

3.1 覆盖属性

Java允许灵活地设计域类,其中子类可以定义已经在其超类中以相同名称声明的属性。参见下面的例子:

public class SuperType {

   private CharSequence field;

   public SuperType(CharSequence field) {
      this.field = field;
   }

   public CharSequence getField() {
      return this.field;
   }

   public void setField(CharSequence field) {
      this.field = field;
   }
}

public class SubType extends SuperType {

   private String field;

   public SubType(String field) {
      super(field);
      this.field = field;
   }

   @Override
   public String getField() {
      return this.field;
   }

   public void setField(String field) {
      this.field = field;

      // optional
      super.setField(field);
   }
}

这两个类都使用可赋值类型定义字段。然而,SubType隐藏了SuperType.field。根据类设计,使用构造函数可能是设置SuperType.field的唯一默认方法。或者,在setter中调用super.setField(…)可以在SuperType中设置字段。所有这些机制都会在一定程度上造成冲突,因为属性共享相同的名称,但可能表示两个不同的值。如果类型不可赋值,则Spring Data将跳过super-type属性。也就是说,被重写属性的类型必须可分配给要注册为重写的super-type属性类型,否则该超类型属性被视为瞬态属性。我们通常建议使用不同的属性名称。
Spring Data模块通常支持具有不同值的重写属性。从编程模型的角度来看,需要考虑以下几点:

  1. 应该持久化哪个属性(默认为所有声明的属性)?可以通过使用@Transient注解特性来排除属性(properties)。
  2. 如何在数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此应使用显式字段/列名对至少一个属性进行注解。
  3. 不能使用@AccessType(PROPERTY),因为在不对setter实现进行任何进一步假设的情况下,通常不能设置super-property。

四、Kotlin支持

Spring Data适配了Kotlin的特性,允许对象创建和变化(mutation)。

4.1 Kotlin 对象创建

Kotlin类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。
Spring Data会自动尝试检测用于物化(materialize)该类型对象的持久实体的构造函数。解析算法的工作原理如下:

  1. 如果有一个构造函数是用@PersistenceCreator注解的,那么就会使用它。
  2. 如果类型是Kotlin data cass,则使用主构造函数。
  3. 如果有一个用@PersistenceCreator注解的静态工厂方法,那么就使用它。
  4. 如果存在单个构造函数,则使用它。
  5. 如果有多个构造函数,并且恰好有一个构造函数是用@PersistenceCreator注解的,则使用它。
  6. 如果类型是Java Record,则使用规范构造函数。
  7. 如果存在无参数构造函数,则使用它。其他构造函数将被忽略。

考虑以下data类Person:

data class Person(val id: String, val name: String)

上面的类编译为带有显式构造函数的典型类。我们可以通过添加另一个构造函数来定制这个类,并用@PersistenceCreator注解它来指示构造函数的首选项:

data class Person(var id: String, val name: String) {

    @PersistenceCreator
    constructor(id: String) : this(id, "unknown")
}

Kotlin通过允许在未提供参数的情况下使用默认值来支持参数可选性。当Spring Data检测到具有参数默认值的构造函数时,如果数据存储不提供值(或只是返回null),则它将忽略这些参数,因此Kotlin可以应用参数默认值。参见以下类,该类将参数默认值应用于name

data class Person(var id: String, val name: String = "unknown")

每当name参数不是结果的一部分或其值为空时,则name默认为unknown。

4.2 Kotlin data 类的属性填充

在Kotlin中,所有的类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。参见以下data class Person:

data class Person(val id: String, val name: String)

这个类实际上是不可变的。它允许创建新实例,因为Kotlin生成一个copy(…)方法,该方法创建新对象实例,从现有对象复制所有属性值,并应用提供的属性值作为方法的参数。

4.3 Kotlin 覆盖属性

Kotlin允许声明属性覆盖来改变子类中的属性。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
	SuperType(field) {
}

这样的安排呈现带有名称为field的两个属性。Kotlin为每个类中的每个属性生成属性访问器(getter和setter)。有效代码如下所示:

public class SuperType {

   private int field;

   public SuperType(int field) {
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

public final class SubType extends SuperType {

   private int field;

   public SubType(int field) {
      super(field);
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

SubType上的Getters和setters只设置SubType.field,而不设置SuperType.field。在这种安排中,使用构造函数是设置SuperType.field的唯一默认方法。可以通过“this.SuperType.field=…”将方法添加到SubType以设置“SuperType.field”,但不在支持的约定范围内。属性重写在一定程度上会造成冲突,因为属性共享相同的名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。
Spring Data模块通常支持具有不同值的重写属性。从编程模型的角度来看,需要考虑以下几点:

  1. 应该持久化哪个属性(默认为所有声明的属性)?可以通过使用@Transient注解特性来排除这些属性。
  2. 如何在数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此应使用显式字段/列名对至少一个属性进行注解。
  3. 不能使用@AccessType(PROPERTY),因为不能设置super-property。

4.4 Kotlin Value 类

Kotlin Value类是为更具表现力的领域模型(domain model)而设计的,以使底层概念易于理解。Spring Data可以读取和写入使用Value类定义属性的类型。
参见以下领域模型:

@JvmInline
value class EmailAddress(val theAddress: String)                               --------1     

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) ---2

1. 具有不可为null的值类型的简单value类。
2. 使用EmailAddress值类定义属性的Data class

使用非基本值类型的非空属性在编译类中被展平(flattened)为value类型。可空的原始值类型或可空的value-in-value类型用其包装器类型表示,这会影响值类型在数据库中的表示方式。

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
spring-boot-starter-actuator是Spring Boot框架中的一个模块,它提供了一系列用于监控和管理应用程序的端点(endpoints),比如/health、/info、/metrics等。这些端点可以通过HTTP请求访问,返回应用程序的各种指标和状态信息。 spring-boot-starter-actuator的原理主要包括以下几个方面: 1. 自动配置:Spring Boot框架提供了自动配置功能,可以根据应用程序的依赖项和配置文件来自动配置spring-boot-starter-actuator模块。 2. 端点映射spring-boot-starter-actuator使用Spring MVC框架来处理HTTP请求。它通过端点映射(Endpoint Mapping)将HTTP请求映射到相应的端点处理器(Endpoint Handler)上。 3. 端点处理器:每个端点都有一个对应的处理器,用于处理HTTP请求并返回响应。端点处理器可以是自定义的Java类,也可以是Spring Boot框架提供的默认实现。 4. 数据源:spring-boot-starter-actuator会从应用程序的各种数据源中收集指标和状态信息,比如JVM内存使用情况、数据库连接池状态等。这些数据源可以是应用程序本身、第三方库、操作系统等。 5. 安全性:为了保护应用程序的安全性,spring-boot-starter-actuator提供了一些安全功能,比如基于角色的访问控制、IP地址过滤等。可以通过配置文件来配置这些安全功能。 总之,spring-boot-starter-actuator通过自动配置、端点映射、端点处理器、数据源和安全性等机制,实现了对应用程序的监控和管理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值