整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Java 21 新特性:String Templates(字符串模版)

日常写Java的时候,对于字符串的操作是非常普遍的,其中最常见的就是对字符串的组织。也因为这个操作非常普遍,所以诞生了很多方案,总下来大概有这么几种:

  • 使用+拼接
  • 使用StringBufferSpringBuilder
  • String::format and String::formatted
  • 使用java.text.MessageFormat

下面,我们一起来学习一下Java 21中的新方案!如果您对于上面这些还不熟悉的话,建议可以先看一下这篇《Java自带的4种字符串组织和格式化方法》,了解以前的机制,这样与最新的处理方案做对比,理解会更深刻。

模版表达式

在Java 21中处理字符串的新方法称为:Template Expressions,即:模版表达式。

模版表达式是Java语言中的一种新表达式。它可以执行字符串插值,帮助开发人员通过编程的方式安全高效地组织字符串。此外,模板表达式不仅仅可以用于组织字符串,它还可以根据特定模型的规则将结构化文本转换为任何类型的对象。

下面通过一个简单的案例来认识一下它:

String blog = "blog.didispace.com";
String str = STR."My blog is \{blog}";

上述代码中的第2行就是一个模版表达式,其中主要包含三个部分:

  • 模版处理器:STR
  • 包含内嵌表达式(\{blog})的模版
  • 通过.把前面两部分组合起来,形式上类似方法调用

当模版表达式运行的时候,模版处理器会将模版内容与内嵌表达式的值组合起来,生成结果,所以上面案例中的字符串str在运行时的结果为:My blog is blog.didispace.com

多行模版表达式

还记得之前我们的Java新特性专栏中,介绍过Java 15中的文本块特性吗?类似下面这样的写法:

String html = """
    <html>
    <body>
      <h1>Java 15 新特性:文本块 | 程序猿DD</h1>
      <p>didispace.com</p>
    </body>
    </html>
    """;

模版表达式也支持类似的多行字符串处理,所以开发者可以用它来方便的组织html、json、xml等字符串内容,比如下面这样:

var json = STR."""
{
    "user": "\{name}",
    "age: \{age}
}
""";

STR模版处理器

上面案例中我们所用的STR模版处理器中的内嵌表达式都采用了字符串内容,而实际上STR模版处理器还有更多的用途。

  1. 内嵌表达式中还可以之前数学运算,比如:
int x = 10, y = 20;
String s = STR."\{x} + \{y} = \{x + y}";

最终s字符串结果为:10 + 20 = 30

  1. 内嵌表达式中还可以调用方法,比如:
String s = STR."My blog is \{getMyBlog()}";

假设getMyBlog方法返回的内容为blog.didispace.com,那么最终s字符串结果为:My blog is blog.didispace.com

  1. 内嵌表达式中还可以访问对象的成员变量,比如:
User u = new User("didi", "blog.didispace.com"); // 构造函数参数为name属性和blog属性
String s = STR."\{u.name}的博客地地址为:+ \{u.blog}";

最终s字符串结果为:didi的博客地址为:blog.didispace.com

FMT模版处理器

除了STR模版处理器之外,Java中还提供了另外一个模版处理器:FMT。FMT模版处理器除了与STR模版处理器一样提供插值能力之外,还提供了左侧的格式化处理。下面通过例子来直接理解FMT的功能:

record Rectangle(String name, double width, double height) {
    double area() {
        return width * height;
    }
}
Rectangle[] zone = new Rectangle[] {
    new Rectangle("Alfa", 17.8, 31.4),
    new Rectangle("Bravo", 9.6, 12.4),
    new Rectangle("Charlie", 7.1, 11.23),
};
String table = STR."""
    Description  Width  Height  Area
    \{zone[0].name}  \{zone[0].width}  \{zone[0].height}     \{zone[0].area()}
    \{zone[1].name}  \{zone[1].width}  \{zone[1].height}     \{zone[1].area()}
    \{zone[2].name}  \{zone[2].width}  \{zone[2].height}     \{zone[2].area()}
    Total \{zone[0].area() + zone[1].area() + zone[2].area()}
    """;

上面这块是由STR模版处理器组织字符串表格数据,从模版表达式来看是非常整齐的,但是由于模版中内嵌表达式的值长短不一,最终输出结果很有可能差强人意,比如STR模版处理器处理后的结果是这样的:

Description  Width  Height  Area
Alfa  17.8  31.4     558.92
Bravo  9.6  12.4     119.03999999999999
Charlie  7.1  11.23     79.733
Total 757.693

为了解决这个问题,就可以采用FMT模版处理器,在每一列左侧定义格式

String table = FMT."""
    Description     Width    Height     Area
    %-12s\{zone[0].name}  %7.2f\{zone[0].width}  %7.2f\{zone[0].height}     %7.2f\{zone[0].area()}
    %-12s\{zone[1].name}  %7.2f\{zone[1].width}  %7.2f\{zone[1].height}     %7.2f\{zone[1].area()}
    %-12s\{zone[2].name}  %7.2f\{zone[2].width}  %7.2f\{zone[2].height}     %7.2f\{zone[2].area()}
    \{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()}
    """;

这样的结果将是如下这样,获得一个整齐的结果:

Description     Width    Height     Area
Alfa            17.80    31.40      558.92
Bravo            9.60    12.40      119.04
Charlie          7.10    11.23       79.73
                             Total  757.69

关于Java 21中推出的String Templates特性还有很多高级能力,比如:自定义模版处理器等。因为该特性还处于Preview阶段,所以DD也还没有深入研究,仅尝鲜了一下基本功能,如果您对其他高级能力感兴趣,也可以通过官网学习。等该功能正式发布之后,我会在 Java新特性专栏 中更新解读所有内容,欢迎关注与收藏。

者 | Lyndsey Padget

译者 |月满西楼

本文来自公众号“EAWorld”,ID:eaworld

今天我们来聊聊 Java中的微服务。虽说 Java EE提供了一个强大的平台,供我们创建、部署和管理企业级微服务,但在本文中,我将展示如何创建一个尽可能小的 RESTful微服务。

放心,在这个过程中,我们不会浪费时间精力去重复做些数据处理之类的事情。我们会通过 JBoss RESTEasy来进行搭建。而确保该微服务的轻量级,目的是为了向大家展示,在一个全新或者现存的微服务前端,建立一个 RESTful接口,真的非常简单。

与此同时,我会进一步证明,通过 RESTEasy构建的微服务具备很大的灵活性,不仅可以兼容包括 JSON,XML在内的多种数据传输格式,还支持将其部署到 Apache Tomcat[1]服务器而非 JBoss企业应用平台 (EAP)[2]上。诚然,每个工具都有自己的优势,但是我认为先在 KISS原则 [3]下探讨技术可用性会很有帮助,然后才是根据软件的长期目标和需求来决定应该为服务架构添加哪些特性。

本文中提到的代码示例都可以在 GitHub[4]上查阅,包括“starter” 和 “final”这两个分支。下面是我采用的环境,当然你的实际情况可能有所不同:

  • Java Development Kit[5] (JDK)1.8.0-131 (amd64)

  • apache Tomcat[6] 9

  • apache Maven[7] 3.5.0

  • Eclipse Java EE IDE[8] 4.7.0 (Oxygen)

  • Linux Mint[9] 18.2 (Sonya)64位

就技术而言...

微服务 [10]是一种体积小、更为精炼的服务,其目标是“做好一件事”。微服务之间通过一些接口进行交互是很普遍的现象。如果该接口可以通过 web访问 (使用 HTTP),那么它就是一个 web服务。部分 web服务是基于 RESTful这种架构风格的,另一些则不是。注意,微服务并不都是 web服务,web服务并不都是 RESTful web服务,RESTful web服务也并不都是微服务!

REST和 XML……能否共存?

如果你此前在使用 RESTful web服务时,没用过除 JSON 以外的文本数据交换格式 [11]来进行内容传输,那么你可能会认为二者是不相关的。但是回想下,REST是定义 API的一种架构风格,REST和 JSON这两者又碰巧一起流行起来 (注意,这并非偶然)。XML多年的发展使其拥有大量的客户群,能够接纳和提供 XML数据传输格式的 RESTful web服务, 不管是对那些已经依赖于这类内容交互系统的组织,还是对仅仅是更熟悉 XML的用户来说,都非常有用。当然,通常情况下,JSON依然是首选,因为其消息体更小,但有时 XML只是一个更简单的“sell”。拥有一个能同时支持这两种格式的 RESTful微服务是最理想的 ;从部署的角度来说,它不仅简洁,具备可扩展性,还有足够的灵活性,可以支持不同类型的内容,从而满足那些其他有调用需求的应用程序。

为什么选择 RESTEasy?

RESTEasy[12]是 Jboss的一个框架,可以用来构建 RESTful web服务。通过 RESTEasy构建的 RESTful web服务,可以根据四个函数库来实现对 XML和 JSON这两种数据传输格式的支持:

  • resteasy-jaxrs,实现了 JAX-RS 2.0 (用于 RESTful Web服务的 Java API) [13]

  • resteasy-jaxb-provider,其 JAXB[14]绑定能有效支持 XML

  • resteasy-jettison-provider,用 Jettison[15]将 XML转换为 JSON

  • resteasy-servlet-initializer,将服务部署到 Servlet 3.0容器 (在 Tomcat服务器上)

首先,创建一个内含 pom.xml数据包的 web服务项目:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lyndseypadget</groupId> <artifactId>resteasy</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>resteasy</name> <repositories> <repository> <id>org.jboss.resteasy</id> <url>http://repository.jboss.org/maven2/</url> </repository> </repositories> <dependencies> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxrs</artifactId> <version>3.1.4.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxb-provider</artifactId> <version>3.1.4.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jettison-provider</artifactId> <version>3.1.4.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-servlet-initializer</artifactId> <version>3.1.4.Final</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> <finalName>resteasy</finalName> </build> </project>

(左右滑动可查看全部代码,下同)

这些数据库的大小大概在 830KB。当然,这些直接的依赖环境(dependency),运用 Maven一起构建项目也会带来部分传递性依赖。

接下来,我将用“Maven方法”来构建这个项目,例如在 src/main/java中,使用 Maven构建命令等,不想用 Maven的话,你也可以直接从下载页面 [16]下载 RESTEasy jar数据包。下载的时候不用理会 RESTEasy站点上弹出的这个提示:JBoss仅仅是在尝试引导你采用更“企业化”的方法。你只需点击“继续下载”,来开展后面的操作。

项目设计

下面这个微服务可以用非常简单的方法来演示一些基本概念。如下图所示,它包括 5个等级。

此处,FruitApplication是微服务的切入点。FruitService提供了主要的路径 (/fruits),同时它也充当了路由器的功能。苹果和水果都是模型;水果包含一些抽象的功能,苹果则会具体地扩展它的功能。

和你设想一致的是,FruitComparator可以提供比较功能。不熟悉 Java comparator的读者,可以在这篇文章中了解一下对象的等同性和比较,这里我用字符来取代。虽然 FruitComparator不是一个模型,但我更喜欢将比较器与它想要比较的对象类型保持相类似的命名。

模型

让我们从 Fruit这级开始

package com.lyndseypadget.resteasy.model; import javax.xml.bind.annotation.XmlElement; public abstract class Fruit { private String id; private String variety; @XmlElement public String getId { return id; } public void setId(String id) { this.id = id; } @XmlElement public String getVariety { return variety; } public void setVariety(String variety) { this.variety = variety; } }

然后 Apple这级对其展开:

package com.lyndseypadget.resteasy.model; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "apple") public class Apple extends Fruit { private String color; @XmlElement public String getColor { return color; } public void setColor(String color) { this.color = color; } }

以上并不是什么特别惊人的代码,你可能会觉得都不值得拿出来炫耀,就是一个 Java继承的简单实例。但重点在于这两个注释 @XmlElement 和 @XmlRootElement,它们定义了 XML apple结构的样子:

<apple> <id>1</id> <variety>Golden delicious</variety> <color>yellow</color> </apple>

因为没有约定明显的构造函数:Java使用了隐式的、无参数的默认构造函数,所以一些更微妙的事情在发生。这个无参数的构造函数对 JAXB 施展魔法般效果的工作是十分必要的(本文解释了这一点,以及必要的话,如何用 XMLAdapter来让它工作)。

现在我们有了一个对象:被定义的苹果。它有三个属性: ID、多样性和颜色。

服务

FruitService 被用来作为与微服务交互的主要路径 (/fruits)。在本例中,我使用 @path注释直接在该层级中定义了第一个路径,/fruits/apples。随着 RESTful微服务的扩展,你可能希望在自己的层级中定义多个最终路径 (例如 /apples, /bananas, /oranges)。

package com.lyndseypadget.resteasy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import com.lyndseypadget.resteasy.model.Apple; import com.lyndseypadget.resteasy.model.FruitComparator; @Path("/fruits") public class FruitService { private static Map<String, Apple> apples = new TreeMap<String, Apple>; private static Comparator comparator = new FruitComparator; @GET @Path("/apples") @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public List getApples { List retVal = new ArrayList(apples.values); Collections.sort(retVal, comparator); return retVal; } }

这张苹果的地图帮助我们根据 id跟踪苹果的数据,从而模拟某些类型的数据持久层。利用 getApples方法(常用的 HTTP请求方式)将会返回地图跟踪到的相关苹果数据。GET /apples route是用 @GET和 @path注释定义的,它可以生成数据传输格式 XML或 JSON的内容。

这个方法需要返回一个 List< apple >对象,然后用这个比较器按品种属性来对列表进行排序。

FruitComparator看起来是像这样的:

package com.lyndseypadget.resteasy.model; import java.util.Comparator; public class FruitComparator implements Comparator { public int compare(F f1, F f2) { return f1.getVariety.compareTo(f2.getVariety); } }

注意,如果想要对苹果的一个特定属性进行排序,比如颜色,我们就必须创建一个新的比较器去取代,并取个名字,比如 AppleComparator。

应用程序

在 RESTEasy3.1.x中, 你需要定义一个扩展应用的层级。RESTEasy示例文档说明这是一个单例模式注册表(singleton registry),如下所示:

package com.lyndseypadget.resteasy; import javax.ws.rs.core.Application; import java.util.HashSet; import java.util.Set; public class FruitApplication extends Application { HashSet singletons = new HashSet; public FruitApplication { singletons.add(new FruitService); } @Override public Set<Class> getClasses { HashSet<Class> set = new HashSet<Class>; return set; } @Override public Set getSingletons { return singletons; } }

如果仅为了说明本例,就不需要对这个层级做太多工作,但是我们需要在 web.xml文件中将它连接起来,这会在后面的章节“web服务连接”中进行介绍。

对象的构建集合

GET /apples调用将返回如下数据:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <collection> <apple> <id>1</id> <variety>Golden delicious</variety> <color>yellow</color> </apple> </collection>
[ { "apple": { "id": 1, "variety": "Golden delicious", "color": "yellow" } } ]

但是,我们可以将数据更改成看起来稍有点不同:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <apples> <apple> <id>1</id> <variety>Golden delicious</variety> <color>yellow</color> </apple> </apples>
 { "apples": { "apple": { "id": 1, "variety": "Golden delicious", "color": "yellow" } } }

第二个选项在 XML中看起来更好一些,但是对 JSON产生了不太好的影响。如果你喜欢这个结构,可以用它自己的类型打包 List< Apple >,并修改 FruitService.getApples方法来返回这种类型:

package com.lyndseypadget.resteasy.model; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "apples") public class Apples { private static Comparator comparator = new FruitComparator; @XmlElement(name = "apple", type = Apple.class) private List apples; public List getApples { Collections.sort(apples, comparator); return apples; } public void setApples(Collection apples) { this.apples = new ArrayList(apples); } }

这些注释有效地“重新标记”了根元素,即 collection/list。通过读取用于 javax.xml.bind.annotation的 javadoc文档,你可以尝试用它和不同的 XML Schema映射注释。

当然,如果实在不能搞定一般的方法签名(method signature),则可以编码写入不同的方法——一个用于 XML,另一个用于 JSON。

一些 web服务连接

从将该服务部署到 Tomcat开始,我用一个放在 src/main/webapp/web inf/web.xml的 web应用部署描述符文件。它所包含的内容如下:

<?xml version="1.0"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>resteasy</display-name> <context-param> <param-name>javax.ws.rs.core.Application</param-name> <param-value>com.lyndseypadget.resteasy.FruitApplication</param-value> </context-param> <context-param> <param-name>resteasy.servlet.mapping.prefix</param-name> <param-value>/v1</param-value> </context-param> <listener> <listener-class> org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap </listener-class> </listener> <servlet> <servlet-name>Resteasy</servlet-name> <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class> </servlet> <servlet-mapping> <servlet-name>Resteasy</servlet-name> <url-pattern>/v1/*</url-pattern> </servlet-mapping> </web-app>

没错,“servlet-name”表示 servlet(即 Service)的名称是 Resteasy。servlet-mapping url-pattern (/v1/*)要求 Tomcat服务器将包含该模式的传入请求传输到 Resteasy服务。关于如何建立这个文件的更多信息,以及可用的不同选项,请参阅 Tomcat的应用程序部署文档 [17]。

构建及部署

从项目的根目录中,可以运行以下内容来构建 WAR(web application resource,web应用程序资源)文件:

mvn clean install

这将在 target文件夹中创建一个包含 WAR文件的新文件夹。虽然用 Maven或其他工具来部署该文件也可以,但我只用一个简单的复制命令就可以。需要注意的是,每次将 WAR重新部署到 Tomcat服务器时,应该首先暂停服务器运行,并删除服务应用程序文件夹 (在本例中,是这个文件夹:< tomcatDirectory >/webapps/resteasy)和旧的 WAR文件 (< tomcatDirectory >/webapps/resteasy.war)。

[sudo] cp target/resteasy.war <tomcatDirectory>/webapps/resteasy.war

如果此时 Tomcat服务器正在运行,那么会即刻部署 web服务。如果不是,下次服务器启动时,该服务也会被自动部署上去。然后,就可以通过如下地址访问 web服务:http://< tomcatHost >:< tomcatPort >/resteasy/v1/fruits/apples。我的范例中是这个地址 http://localhost:8080/resteasy/v1/fruits/apples。

通过“内容协商(Content negotiation)”测试服务

内容协商(Content negotiation)是一种机制,它可以提供不同资源 (URI)的表现形式。最基本的,这意味着可以:

  • 详细设置 Accept header,以指示希望从服务中接受的内容类型

  • 详细设置 Content-Type header,以指示发送给服务的内容类型

要获取更多关于内容协商(Content negotiation)和 header的信息,请参阅 RFC 2616[18]的第 12和 14章。在本例中,你真正需要了解的是:

  • @Produces annotation(注释)指明了该方法能够生成哪些内容 (这将尝试匹配请求上的 Accept header)。

  • @Consumes annotation(注释)指明了该方法能够使用哪些内容 (这将尝试匹配请求的 content-type header)。

如果您试图对一个有效端点进行 HTTP调用,但是内容不能被协商,这意味着没有 @Produces匹配该 Accept数据,或者没有 @Consumes匹配 Content-Type数据,将被返回 HTTP状态码 415:不支持的数据传输格式。

返回常见数据传输格式的 GET调用实际上可以直接进入浏览器。对于 GET /apples这样的调用,默认情况下您将获得 XML:

不过,使用像 Postman[19]这类工具可能会更有帮助,因为它明确地指定 Accept header作为 application/xml:

这两种方法都返回了一些有效但没有多大意义的 XML,即一个空的苹果列表。但是这里有一些很酷的东西。将 Accept header更改为 application/json,太好了,瞧!JSON*生效*了:

不只是“读取”

你可能会发现,很多 RESTful web服务的例子,都是只读的,部分也不会有进一步的提示,比如如何去创建、更新和删除这些操作。虽然我们现在已经有了 web服务的框架,但这是一个不能更改的空列表,这并没多大意义。所以我们应该运用一些其他方法,将苹果添加到这个列表中或从列表中将其删除。

package com.lyndseypadget.resteasy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.lyndseypadget.resteasy.model.Apple; import com.lyndseypadget.resteasy.model.FruitComparator; @Path("/fruits") public class FruitService { private static Comparator comparator = new FruitComparator; private static Map apples = new TreeMap; private static int appleCount = 0; @GET @Path("/apples") @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public List getApples { List retVal = new ArrayList(apples.values); Collections.sort(retVal, comparator); return retVal; } @GET @Path("/apples/{id}") @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response getApple(@PathParam("id") String id) { Apple found = apples.get(id); if(found == ) { return Response.status(404).build; } return Response.ok(found).build; } @DELETE @Path("/apples/{id}") public Response deleteApple(@PathParam("id") String id) { apples.remove(id); return Response.status(200).build; } @POST @Path("/apples") @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response createApple(Apple apple) { String newId = Integer.toString(++appleCount); apple.setId(newId); apples.put(newId, apple); return Response.status(201).header("Location", newId).build; } }

如此,就增加了一些新的功能:

  • 通过 id检索苹果数据 (如果在地图中没有找到,则返回状态代码 404)

  • 通过 id删除苹果数据

  • 创建新的苹果数据 (如果成功的话,返回状态代码 201)

这些方法完善了很多功能,确保了服务可以按照预期工作。更新苹果 (使用 @PUT和 /或 @PATCH),以及更多的关于端点、逻辑和管理持久性方面的功能操作,都留给读者你们来练习吧。

当我们再次进行构建和部署时会发现 (如果用 Maven或者 Tomcat来进行设置,请参阅上文“构建和部署”),现在已经可以在服务中创建、检索和删除苹果了。而且即使不在服务器上做任何重新配置,也可以在 XML和 JSON之间进行选择性调用。

来创建一个拥有“application/json”内容类型和 JSON主体的苹果,如下图所示:

这是另一个例子:创建一个具有“application/xml”内容类型和 XML主体的苹果。

在 XML中检索所有的苹果数据:

在 JSON中通过 id检索 apple 2的数据:

通过 id删除 apple 1的数据:

在 JSON中检索所有苹果的数据:

小结

在此我们已经探讨了 RESTEasy架构如何在 Java web服务中无缝支持 XML和 JSON数据传输格式。我还解释了 REST、Media type(数据传输格式)、web服务和微服务之间的技术差异,因为在这些术语中有很多容易混淆的灰色地带。

我这里列举的例子可能有点勉强,生活中我其实从来没有真正需要过水果相关的数据,我也没有在食品行业工作过。之所以用水果来举例,是因为我觉得这个“规模”能有助于大家理解微服务的概念,你也可以想象其他例如蔬菜,罐头或海鲜这样的微服务是如何共同构成一个食物分配系统。现实世界中,食品杂货店的食物分配系统实际上非常复杂,它必须考虑到包括销售、优惠券、过期日期、营养信息等各方面的问题。

当然,你可以选择其他方式去对系统进行分割,但当你需要一种快速高效、轻量级工具来支持多种数据格式时,RESTEasy真的是个非常不错的选择。

参考地址:

[1]、[2]、[6] https://tomcat.apache.org/

[3] https://en.wikipedia.org/wiki/KISS_principle

[4] https://github.com/lyndseypadget/resteasy-demo

[5] http://www.oracle.com/technetwork/java/javase/downloads/index.html

[7] https://maven.apache.org/

[8] https://www.eclipse.org/downloads/

[9] https://linuxmint.com/edition.php?id=237

[10] https://stackify.com/what-are-microservices/

[11] https://www.iana.org/assignments/media-types/media-types.xhtml

[12] http://resteasy.jboss.org/

[13] https://jcp.org/aboutJava/communityprocess/final/jsr339/index.html

[14] http://www.oracle.com/technetwork/articles/javase/index-140168.html

[15] https://github.com/jettison-json/jettison

[16] http://resteasy.jboss.org/downloads

[17] https://tomcat.apache.org/tomcat-9.0-doc/appdev/deployment.html

[18] https://www.ietf.org/rfc/rfc2616.txt

[19] https://www.getpostman.com/

期望得到更多优质技术干货,欢迎加入 EAWorld社区,与近万名技术人一起成长。入群暗号:328

elenium IDE安装

1.在线安装

1.1.Chrom在线安装

1.打开https://chrome.google.com/webstore/category/extensions网站,搜索Selenium IDE

2.进入Selenium IDE详情页,点击Add to Chrome即可

3.点击Chrome右上角的“扩展程序”按钮,点击Selenium IDE就可以打开软件,录制自动化脚本

1.2.FireFox在线安装

1.搜索selenium插件

打开FireFox浏览器,打开设置->扩展和主题,然后输入selenium点击搜索

2.添加到FireFox

选择搜索的插件并且添加到FireFox

3.点击Chrome右上角的“扩展程序”按钮,点击Selenium IDE就可以打开软件,录制自动化脚本

Selenium录制

1.插件使用

点击selenium插件弹出插件selenium ide录制插件

2.自动化录制

点击创建新的项目,并且输入项目名称

点击开始录制并输入网址

打开百度,在输入框中输入"腾讯课堂",选择“腾讯课堂”的链接跳转到腾讯课堂官网,然后再腾讯课堂的输入框中输入"车载测试"

3.脚本回放

Selenium WebDriver使用

1.创建Maven项目

2.引入依赖

<dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.9.1</version>
    </dependency>

3.编写自动化脚本

3.1.WebDriver相关API

// get(String url):访问指定网址
driver.get("https://www.selenium.dev/selenium/web/web-form.html");
// getCurrentUrl():获取当前网页Url
webDriver.getCurrentUrl();
// getTitle():获取当前页面标题
webDriver.getTitle();
// getPageSource():获取当前页面源码
webDriver.getPageSource();
// quit():关闭驱动对象以及所有相关的窗口
webDriver.quit();
// close():关闭当前窗口
webDriver.close();
// getWindowHandle():获取当前页面句柄
webDriver.getWindowHandle();
// getWindowHandles():获取打开所有窗口的句柄

//-----------Options 相关API------------
webDriver.getWindowHandles();
// manage():浏览器options相关设置
Options options = webDriver.manage();
// 窗口操作
Window window = options.window();
// 窗口全屏
window.fullscreen();
// 窗口最大化
window.maximize();
// 窗口的大小:宽度和高度
Dimension dimension = window.getSize();
System.out.println("width"+dimension.getWidth()+" height:"+dimension.getHeight());
// 窗口位置;X,Y
Point point = window.getPosition();
System.out.println("x:"+point.getX()+" y:"+point.getY());

//------------navigate对象相关API----------
// 获取navigate对象
Navigation navigation = webDriver.navigate();
// 访问指定url
navigation.to(base_url);
		
// 刷新当前页面
navigation.refresh();
		
// 浏览器回退
navigation.back();
		
// 浏览器前进
navigation.forward();

3.2.获取元素

3.2.1.id:根据dom元素ID属性值获取元素

// 根据ID属性获取元素
webDriver.findElement(By.id("kw")).sendKeys("车载测试");

3.2.2.name:根据dom元素name属性值获取元素

// 根据name属性获取元素:有可能会重复
webDriver.findElement(By.name("wd")).sendKeys("车载测试");

3.2.3.tagName:根据dom元素标签名获取元素

// 根据标签名定位元素:不推荐使用,一般会定位到多个元素,有可能会定位不准确
webDriver.findElement(By.tagName("intput")).sendKeys("车载测试");

3.2.4.className:根据dom元素类名属性值获取元素

 // 根据className定位元素:也有可能会定位到多个元素
webDriver.findElement(By.className("s_ipt")).sendKeys("车载测试");

3.2.5.linkText:根据超链接文本内容获取元素

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

// 根据超链接文本定位元素:完成文本
webDriver.findElement(By.linkText("新闻")).click();

3.2.6.partialLinkTest:根据超链接的部分文本内容来获取元素

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

// 根据超链接文本定位元素:部分或全部文本
webDriver.findElement(By.partialLinkText("123")).click();

3.2.7.根据css Selector

// 根据tagName
webDriver.findElement(By.cssSelector("input"));
// 根据ID
webDriver.findElement(By.cssSelector("#kw"));
//根据className:可以同时根据多个className定位
webDriver.findElement(By.cssSelector(".input.bg"));
// 根据标签名和属性名和值进行定位:intput[属性名='值'][属性名2='值']
webDriver.findElement(By.cssSelector("input[class='bg s_btn']"));

3.2.8.xpath定位元素

// 1.相对路径定位
// xpath定位方式:相对路径。如果多个属性时候使用" and "
webDriver.findElement(By.xpath("//input[@id='kw' and @class='s_btn']")); 
// 根据元素文本值精确定位
webDriver.findElement(By.xpath("//a[text()='新闻']")).click(); 
// 根据元素文本值模糊定位
webDriver.findElement(By.xpath("//a[contains(text(),'123')]")).click();

// 2.绝对路径定位
// xpath定位方式:绝对路径,由于前段的调整导致绝对定位失败,导致自动化脚本运行失败,所以不常用,也不推荐使用
webDriver.findElement(By.xpath("/html/body/div[1]/div[1]/div[5]/div/div/form/span[1]/input")).sendKeys("车载测试");

3.3.元素API

// sendKeys("") 设置值
webDriver.findElement(By.className("s_ipt")).sendKeys("车载测试");
// click() 元素点击事件
webDriver.findElement(By.partialLinkText("123")).click();
// clear() 清空
webDriver.findElement(By.className("s_ipt")).clear();

3.4.三大等待

说明:程序在运行过程中太快了,导致数据加载未完成或者其他操作未完成导致自动化脚本运行过程中失败,所以需要在自动化脚本中引入等待机制,在需要等待的地方加入等待机制保证脚本的正常运行

3.4.1.硬性等地

说明:固定时间长度等待。即使在等待时间内已经获取到元素依然会等待,直到时间结束才会结束等待

Thread.sleep(1000);

3.4.2.隐式等待

说明:在设置的超时范围内不断查找元素,直到找到元素或超时。优点:相对灵活,缺点:设置是全局的,在webDriver整个生命周期有效。会给所有的元素都添加超时机制

// 全局设置查找元素超时时间为5S,如果获取到结束等待;如果没有获取到元素会尝试重新获取,直到超时
Duration duration = Duration.ofSeconds(5);
webDriver.manage().timeouts().implicitlyWait(duration);

3.4.3.显式等待

说明:用来等待某个条件发生后再继续执行后面的代码

// 给单个获取元素操作添加等待。
// ExpectedConditions.visibilityOfElementLocated:元素可见
WebDriverWait wait = new WebDriverWait(webDriver, Duration.ofSeconds(implicitly_wait));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//a[text() = '腾讯课堂_专业的在线教育平台']"))).click();

3.5.特殊元素定位

3.5.1.模态框

说明:模态框是指用户想要进行后续操作时,必须先点击模态框的”确定“或”取消“按钮关闭模态框。主要由alert和confirm

Alert alert = webDriver.switchTo().alert();
// 同意
alert.accept();
// 取消
alert.dismiss();
// 获取文本内容
alert.getText();

3.5.2.Iframe

四种切换方式:

webDriver.switchTo().frame(index);

webDriver.switchTo().frame(nameOrId);

webDriver.switchTo().frame(frameElement);

切换之后,回到默认内容页面,否则会找不到元素

webDriver.switchTo().defaultContent();

3.5.3.window切换

当你想要操作另外一个窗口元素时候,一定 要写切换窗口。切换方式:传入窗口句柄或窗口名进行切换

// 获取当前窗口句柄
String currentWindowHandle = webDriver.getWindowHandle();
webDriver.switchTo().window(currentWindowHandle);
// 多个窗口同时开启的时候要先获取所有开启窗口的句柄,然后循环切换并结合窗口的标题进行确定需要切换到的窗口
for (String handle : webDriver.getWindowHandles()) {
	webDriver.switchTo().window(handle);
	if(webDriver.getTitle().equals("aaaa")) {
		break;
	}
}
// 进行后续操作

3.5.6.select定位

需要将web元素封装成为Select对象,然后调用Select的api进行操作

Select select = new Select(webDriver.findElement(By.id("")));
// 获取所有下拉元素
select.getOptions();
// 根据下标选择元素
select.selectByIndex(1);
// 根据隐藏值选择元素
select.selectByValue("");
// 根据现实文本选中元素
select.selectByVisibleText("");

3.5.7.时间控件

情况一:控件没有限制手动输入,可以直接调用sendKeys写入时间数据

// 打开飞猪,在时间框中输入“2023-06-04”时间
webDriver.findElement(By.xpath("//*[@id='J_FlightForm'] //input[@name='depDate']")).sendKeys("2023-06-04");

情况二:控件限制输入,那么只能通过js代码来改变元素值

JavascriptExecutor exception = (JavascriptExecutor) webDriver;
// 执行js代码移除元素的readonly属性
exception.executeScript("document.getElementById('').removeAttribute('readonly')", "");
webDriver.findElement(By.id("")).sendKeys("2023-06-04");

3.5.8.鼠标操作

Actions actions = new Actions(webDriver);
// 鼠标拖拽
actions.clickAndHold(null).moveToElement(null).release().build().perform();
// 鼠标按下
actions.keyDown(null).release().build().perform();
// 鼠标弹起
actions.keyUp(null).release().build().perform();
// 鼠标双击
actions.doubleClick(null).release().build().perform();
// 鼠标滚动
actions.scrollToElement(null).release().build().perform();

3.5.9.文件上传

情况一:使用input标签进行文件上传。使用sendKeys方法写入文件数据

// 获取文件上传input
webDriver.findElement(By.xpath("//input[@type='file' and @name]='file'")).sendKeys("文件路径");
//点击上传按钮
webDriver.findElement(By.id("submit")).click();

情况二:使用第三方控件。必须使用一些第三方的工具

3.5.9.验证码

主要由3中处理方式:

一:去除验证码-测试环境时候

二:使用验证码识别库进行识别-代码复杂,效率低,成功率低,只适合简单的验证码

三:使用万能验证码(推荐)-后台设置一个万能验证码