JAX-RS 2.0 中的内容协商

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

在 JAX-RS 中,客户端和服务器都可以指定它们期望使用或要生成的内容类型。从技术上讲内容类型是数据格式。例如,JSON 和 XML 是两种最知名的数据格式,通常用于 RESTful Web 服务。此功能有助于服务器和客户端开发人员在设计和实现上更加灵活。

与 HTTP 协议一样,JAX-RS 中的内容类型也表示为 MIME 类型。 MIME 格式是表示和分类不同内容类型的标准方式。例如,代表这篇文章的文本在 MIME 中被归类为 ( text/plain )。

在 JAX-RS 中, @Produces @Consumes 注释用于指定内容类型。顾名思义, @Consumes 注解用于指定方法期望的内容格式, @Produces 是方法期望生成的内容格式。此时区分数据和数据格式很重要。数据是方法的输入和输出,但数据格式是如何将此数据转换为标准表示形式。数据转换通常发生在传输之前或之后。

为了从 JAX-RS 的角度深入研究这个问题,我使用了一个简单的案例场景,我最近在我的一个项目中使用过。这将使这个主题更有趣和更容易理解。

考虑一个非常简单的 CRUD 场景。在这种情况下,客户端以其首选格式发送数据内容,服务器使用该数据。最后服务端将接收到的数据持久化到数据库中。在这种情况下,服务器使用实体对象将数据持久保存到数据库中。考虑 SimpleEntity 类:


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}


SimpleEntity 是 DAO(数据访问对象),服务器使用它来保存数据并从数据库中检索数据。该对象是服务器处理和管理数据的内部机制的一部分。客户不需要知道这样的机制和对象。这个场景展示了 数据格式化(转换) 如何提供一种标准机制来将客户端和服务器彼此分离并让每个人都满意。重要的关注点是格式和内容。

首先,客户端和服务器之间约定客户端只能 生成 JSON 和 XML 格式的数据。出于同样的原因,服务器应该只 使用 这两种格式的数据。

为了展示 JAX-RS 如何处理 @Consumes 注释,假设服务器有合同支持客户端开发人员使用每种格式的样本数据。为JSON数据结构提供示例数据,服务端提供了 sample/json 资源(方法):


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

结果将是一个 JSON 结构,如下所示:


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

并为 XML 数据结构提供示例数据,服务器提供 sample/xml 资源:


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

结果将是一个 XML 结构,如下所示:


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

这两个方法都是普通的java方法。它们的返回类型是 SimpleEntity 类,它是服务器的业务对象之一。通过应用 @Produces 注释,JAX-RS 运行时获取方法的输出,使用适当的 MessageBodyWriter 转换对象,最后构建响应。类型转换的负担由 JAX-RS 处理。简单快捷。稍后可以修改这些方法以接受 ID 参数并从数据库中检索相应的记录。

为了阐明这个想法,这些方法是针对每种数据格式单独实现的。对于更加集中和可扩展的设计模式,@Produce 注释提供了一种通过单一方法组合多种内容格式的功能。考虑 /sample 资源:



 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

这次 @Producer 批注接受两种 MIME 类型。客户端必须明确提供首选内容类型。基于 HTTP 的客户端通过设置 Accept 请求标头值来指定首选内容类型。服务器识别客户端的首选内容类型,调用 sampleProducer 方法并最终将有效负载转换为客户端首选的内容。

现在,如果客户端未指定首选内容类型或指定 */* 作为 Accept 标头值,会发生什么情况?在 JAX-RS 2.0 中,有一个概念称为“ 来自服务器的质量 ”或 “qs” 因素。在上述场景中,每当客户端未指定任何特定内容类型或接受所有类型的内容时, qs 因素都会指示服务器提供哪种内容类型作为默认内容格式。为了阐明这个概念, @Producer 注释可以重写如下:


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

qs 因子指定为服务器选择默认内容类型的优先级。 qs 因子可以取 0 到 1 之间的值。如果未显式设置相应的 qs 值,则 MIME 类型具有默认值。具有最高优先级的 MIME 类型具有最低优先级。所以在上面的例子中选择 json 格式是优先的,因为它的 qs 因子值大于列表中的另一个。

如果服务器没有指定 qs 值,客户端可以向服务器指示首选内容的优先级。客户可以根据他们的请求设置 “相对质量因子” “q” 值,以指定他们更喜欢接收内容的格式顺序。

例如,如果 Producer 注释保持不变(不应用 qs 因子)并且客户端将其 Accept 标头值设置为:


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

会得到同样的结果,服务器以json格式下发内容。

前面的示例显示了服务器如何根据@Producer 注释中定义的内容类型约定和质量因素生成内容并交付给客户端。对于从客户端到服务器的内容,存在相同的合同。要指定预期的内容格式,使用 @Consumes 注释。这次是服务器期望以 XML 或 JSON 格式接收来自客户端的请求。为了演示这种情况,请考虑以下代码:


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}

当服务器从客户端接收到内容时,它会获取适当的 MediatypeProvider, 解析接收到的内容并将其转换为方法参数中指定的对象。服务器在一切正常时返回 HTTP 200 OK 响应,如果数据格式与服务器期望的 MIME 类型不匹配,则返回 HTTP 400 Bad Request 消息。

到目前为止,定义内容的方式被称为 静态内容协商。 JAX-RS 还提供 运行时内容协商。 此功能有助于构建更易于维护和修改的更灵活和可扩展的服务器方法。如前所述, Variant 类表示内容格式或 MIME 类型。可以从外部源(文件、数据库等)读取变体列表,并在运行时进行检查。

考虑以下示例。之前的 Persist 方法被修改为支持运行时内容协商。


 @Entity
@XmlRootElement
public class SimpleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "NAME")
private String name;

@Column(name = "FAMILY")
private String family;

@Column(name = "AGE")
private int age;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getFamily() {
    return family;
}

public void setFamily(String family) {
    this.family = family;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Override public String toString() { return "SimpleEntity{" + "name=" + name + ", family=" + family + ", age=" + age + '}'; }

}


很明显,要实现运行时内容协商,需要更多的填充。