Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您使用 Spring 创建一个包含跨域资源共享(CORS)响应头的“Hello, World” RESTful Web 服务。您可以在这篇博客文章中找到有关 Spring CORS 支持的更多信息。

您将构建什么

您将构建一个服务,该服务在 http://localhost:8080/greeting 接受 HTTP GET 请求,并以 JSON 格式返回问候语,如下代码清单所示:

{"id":1,"content":"Hello, World!"}

您可以通过在查询字符串中使用可选的 name 参数来自定义问候语,如下面的代码清单所示:

http://localhost:8080/greeting?name=User

name 参数值覆盖了默认值 World,并在响应中体现,如下面的代码清单所示:

{"id":1,"content":"Hello, User!"}

该服务与构建RESTful Web服务中描述的服务略有不同,因为它使用了Spring Framework的CORS支持来添加相关的CORS响应头。

所需条件

如何完成本指南

与大多数 Spring 入门指南一样,您可以从头开始完成每个步骤,也可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都能获得可运行的代码。

从头开始,请继续阅读从 Spring Initializr 开始

跳过基础知识,请执行以下操作:

完成后,您可以通过与 gs-rest-service-cors/complete 中的代码进行对比来检查结果。

使用 Spring Initializr 开始

您可以使用这个预初始化项目,点击生成以下载一个 ZIP 文件。该项目已配置为适合本教程中的示例。

要手动初始化项目:

  1. 访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置工作。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 Spring Web

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置好的 Web 应用程序归档文件。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此过程。

您还可以从 Github 上 fork 该项目,并在您的 IDE 或其他编辑器中打开它。

添加 httpclient5 依赖 {#_adding_the_httpclient5_dependency}

测试代码(位于complete/src/test/java/com/example/restservicecors/GreetingIntegrationTests.java中)需要Apache httpclient5库。

要将Apache httpclient5库添加到Maven中,请添加以下依赖项:

<dependency>
  <groupId>org.apache.httpcomponents.client5</groupId>
  <artifactId>httpclient5</artifactId>
  <scope>test</scope>
</dependency>

以下清单展示了完整的 pom.xml 文件:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>rest-service-cors-complete</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rest-service-cors-complete</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

要在 Gradle 中添加 Apache httpclient5 库,请添加以下依赖:

testImplementation 'org.apache.httpcomponents.client5:httpclient5'

以下清单展示了最终的 build.gradle 文件:

plugins {
    id 'org.springframework.boot' version '3.3.0'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.apache.httpcomponents.client5:httpclient5'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

创建资源表示类

现在您已经设置好了项目和构建系统,接下来可以创建您的Web服务了。

首先,从思考服务交互开始。

该服务将处理对 /greetingGET 请求,查询字符串中可以选择性地包含一个 name 参数。GET 请求应在响应体中返回一个带有 JSON 的 200 OK 响应,以表示问候语。它应该类似于以下代码清单:

{
    "id": 1,
    "content": "Hello, World!"
}

id 字段是问候语的唯一标识符,content 是问候语的文本表示。

要为问候语表示建模,需要创建一个资源表示类。提供一个带有字段、构造函数以及 idcontent 数据访问器的普通 Java 对象,如下面的清单所示(来自 src/main/java/com/example/restservicecors/Greeting.java):

package com.example.restservicecors;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting() {
        this.id = -1;
        this.content = "";
    }

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

Spring 使用 Jackson JSON 库自动将 Greeting 类型的实例序列化为 JSON。

创建资源控制器

在 Spring 构建 RESTful Web 服务的方法中,HTTP 请求由控制器处理。这些组件很容易通过 @Controller 注解来识别,如下所示的 GreetingController(来自 src/main/java/com/example/restservicecors/GreetingController.java)通过返回 Greeting 类的新实例来处理 /greetingGET 请求:

package com.example.restservicecors;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";

    private final AtomicLong counter = new AtomicLong();
    @CrossOrigin(origins = "http://localhost:9000")
    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(required = false, defaultValue = "World") String name) {
        System.out.println("==== get greeting ====");
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }

}

这个控制器简洁明了,但背后却有很多工作在进行。我们逐步分解它。

@RequestMapping 注解确保了对 /greeting 的 HTTP 请求会被映射到 greeting() 方法上。

前面的示例使用了 @GetMapping 注解,它是 @RequestMapping(method = RequestMethod.GET) 的快捷方式。我们在这里使用 GET 是因为它便于测试。如果源与 CORS 配置不匹配,Spring 仍会拒绝 GET 请求。浏览器不需要发送 CORS 预检请求,但如果我们想触发预检检查,可以使用 @PostMapping 并在请求体中接受一些 JSON 数据。

@RequestParamname 查询字符串参数的值绑定到 greeting() 方法的 name 参数上。该查询字符串参数不是 required 的。如果请求中缺少它,则会使用默认值 World

方法体的实现创建并返回一个新的 Greeting 对象,其中 id 属性的值基于 counter 的下一个值,而 content 的值基于查询参数或默认值。它还通过使用问候语 template 来格式化给定的 name

传统 MVC 控制器与之前展示的 RESTful Web 服务控制器之间的一个关键区别在于 HTTP 响应体的创建方式。与依赖视图技术执行服务器端渲染将问候数据转换为 HTML 不同,这个 RESTful Web 服务控制器填充并返回一个 Greeting 对象。对象数据直接以 JSON 格式写入 HTTP 响应。

为了实现这一点,@RestController 注解默认假定每个方法都继承了 @ResponseBody 的语义。因此,返回的对象数据会直接插入到响应体中。

得益于 Spring 的 HTTP 消息转换器支持,Greeting 对象会被自然地转换为 JSON。由于 Jackson 位于类路径中,Spring 的 MappingJackson2HttpMessageConverter 会自动被选中以将 Greeting 实例转换为 JSON。

启用 CORS

您可以在单个控制器中或全局范围内启用跨域资源共享 (CORS)。以下主题将介绍如何进行操作:

Controller 方法的 CORS 配置

为了让 RESTful 网络服务在响应中包含 CORS 访问控制头信息,您需要在处理方法上添加一个 @CrossOrigin 注解,如下面的代码清单(来自 src/main/java/com/example/restservicecors/GreetingController.java)所示:

    @CrossOrigin(origins = "http://localhost:9000")
    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(required = false, defaultValue = "World") String name) {
        System.out.println("==== get greeting ====");
        return new Greeting(counter.incrementAndGet(), String.format(template, name));

这个 @CrossOrigin 注解仅为这个特定方法启用了跨域资源共享。默认情况下,它允许所有来源、所有头信息以及 @RequestMapping 注解中指定的 HTTP 方法。此外,还使用了 30 分钟的 maxAge。您可以通过指定以下注解属性之一的值来自定义此行为:

  • origins

  • originPatterns

  • methods

  • allowedHeaders

  • exposedHeaders

  • allowCredentials

  • maxAge.

在这个示例中,我们只允许 http://localhost:9000 发送跨域请求。

您也可以在控制器类级别添加 @CrossOrigin 注解,以便为该类的所有处理器方法启用 CORS。

全局 CORS 配置

除了(或作为替代方案)基于细粒度注解的配置外,您还可以定义一些全局的CORS配置。这类似于使用Filter,但可以在Spring MVC中声明,并与细粒度的@CrossOrigin配置结合使用。默认情况下,允许所有来源以及GETHEADPOST方法。

以下代码(来自src/main/java/com/example/restservicecors/GreetingController.java)展示了GreetingController类中的greetingWithJavaconfig方法:

    @GetMapping("/greeting-javaconfig")
    public Greeting greetingWithJavaconfig(@RequestParam(required = false, defaultValue = "World") String name) {
        System.out.println("==== in greeting ====");
        return new Greeting(counter.incrementAndGet(), String.format(template, name));

greetingWithJavaconfig 方法与 greeting 方法(用于控制器级别的 CORS 配置)之间的区别在于路由(/greeting-javaconfig 而不是 /greeting)以及 @CrossOrigin 注解的存在。

以下清单(来自 src/main/java/com/example/restservicecors/RestServiceCorsApplication.java)展示了如何在应用程序类中添加 CORS 映射:

    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:9000");
            }
        };
    }

您可以轻松更改任何属性(例如示例中的 allowedOrigins),并将此 CORS 配置应用于特定的路径模式。

您可以结合全局和控制器级别的 CORS 配置。

创建应用程序类

Spring Initializr 为您创建了一个基础的应用类。以下代码清单(来自 initial/src/main/java/com/example/restservicecors/RestServiceCorsApplication.java)展示了这个初始类:

package com.example.restservicecors;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RestServiceCorsApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestServiceCorsApplication.class, args);
    }

}

您需要添加一个方法来配置如何处理跨域资源共享。以下代码片段(来自 complete/src/main/java/com/example/restservicecors/RestServiceCorsApplication.java)展示了如何实现:

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:9000");
            }
        };
    }

以下清单展示了完整的应用类:

package com.example.restservicecors;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class RestServiceCorsApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestServiceCorsApplication.class, args);
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:9000");
            }
        };
    }

}

@SpringBootApplication 是一个便捷的注解,它包含了以下所有内容:

  • @Configuration: 将该类标记为应用程序上下文的 Bean 定义来源。

  • @EnableAutoConfiguration: 告诉 Spring Boot 根据类路径设置、其他 Bean 和各种属性设置开始添加 Bean。例如,如果 spring-webmvc 在类路径上,此注解会将应用程序标记为 Web 应用程序并激活关键行为,例如设置 DispatcherServlet

  • @ComponentScan: 告诉 Spring 在 com/example 包中查找其他组件、配置和服务,使其能够找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用程序。您是否注意到没有一行 XML 代码?也没有 web.xml 文件。这个 Web 应用程序是 100% 纯 Java 的,您无需处理任何配置管道或基础设施。

构建可执行 JAR

您可以使用 Gradle 或 Maven 从命令行运行该应用程序。您还可以构建一个包含所有必要依赖项、类和资源的单个可执行 JAR 文件并运行它。构建可执行 JAR 文件使得在整个开发生命周期中、跨不同环境等情况下,更容易地发布、版本控制和部署该服务作为应用程序。

如果您使用 Gradle,可以通过 ./gradlew bootRun 来运行应用程序。或者,您可以使用 ./gradlew build 构建 JAR 文件,然后运行该 JAR 文件,如下所示:

java -jar build/libs/gs-rest-service-cors-0.1.0.jar

如果您使用 Maven,可以通过 ./mvnw spring-boot:run 来运行应用程序。或者,您可以使用 ./mvnw clean package 构建 JAR 文件,然后按如下方式运行该 JAR 文件:

java -jar target/gs-rest-service-cors-0.1.0.jar

这里描述的步骤将创建一个可运行的 JAR 文件。您也可以构建一个经典的 WAR 文件

日志输出已显示。服务应在几秒钟内启动并运行。

测试服务

现在服务已启动,请在浏览器中访问 http://localhost:8080/greeting,您应该会看到:

{"id":1,"content":"Hello, World!"}

通过访问http://localhost:8080/greeting?name=User来提供一个name查询字符串参数。content属性的值将从Hello, World!变为Hello User!,如下列代码所示:

{"id":2,"content":"Hello, User!"}

这一改动表明 GreetingController 中的 @RequestParam 配置按预期工作。name 参数被赋予了默认值 World,但始终可以通过查询字符串显式覆盖。

此外,id 属性从 1 更改为 2。这证明您在多个请求中操作的是同一个 GreetingController 实例,并且其 counter 字段在每次调用时按预期递增。

现在,您可以测试 CORS 标头是否已正确配置,并允许来自其他源的 Javascript 客户端访问该服务。为此,您需要创建一个 Javascript 客户端来使用该服务。以下清单展示了这样一个客户端:

首先,创建一个名为 hello.js 的简单 Javascript 文件(位于 complete/public/hello.js),内容如下:

$(document).ready(function() {
    $.ajax({
        url: "http://localhost:8080/greeting"
    }).then(function(data, status, jqxhr) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
       console.log(jqxhr);
    });
});

该脚本使用 jQuery 来调用位于 http://localhost:8080/greeting 的 REST 服务。它由 index.html 加载,如下面的清单(来自 complete/public/index.html)所示:

<!DOCTYPE html>
<html>
    <head>
        <title>Hello CORS</title>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script src="hello.js"></script>
    </head>

    <body>
        <div>
            <p class="greeting-id">The ID is </p>
            <p class="greeting-content">The content is </p>
        </div>
    </body>
</html>

要测试 CORS 行为,您需要从另一个服务器或端口启动客户端。这样做不仅可以避免两个应用程序之间的冲突,还能确保客户端代码是从与服务不同的源提供的。

要让客户端在 localhost 的 9000 端口上运行,请保持应用程序在 8080 端口运行,并在另一个终端中运行以下 Maven 命令:

./mvnw spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=9000'

如果您使用 Gradle,可以使用以下命令:

./gradlew bootRun --args="--server.port=9000"

应用程序启动后,在浏览器中打开 http://localhost:9000,您应该会看到以下内容,因为服务响应包含了相关的 CORS 头部信息,所以 ID 和内容会被渲染到页面中:

如果响应中包含正确的 CORS 头部信息,从 REST 服务获取的模型数据将被渲染到 DOM 中。

现在,停止运行在端口 9000 上的应用程序,保持运行在端口 8080 上的应用程序,并在另一个终端中运行以下 Maven 命令:

./mvnw spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=9001'

如果您使用 Gradle,您可以使用以下命令:

./gradlew bootRun --args="--server.port=9001"

应用程序启动后,在浏览器中打开 http://localhost:9001,您应该会看到以下内容:

如果响应中缺少 CORS 头信息(或对客户端来说不充分),浏览器将无法完成请求。数据将不会渲染到 DOM 中。

在这里,由于 CORS 头信息缺失(或对客户端来说不充分),浏览器无法完成请求,因此这些值不会渲染到 DOM 中,因为我们只允许来自 http://localhost:9000 的跨域请求,而不是 http://localhost:9001

总结

恭喜!您刚刚开发了一个包含使用 Spring 实现跨域资源共享的 RESTful Web 服务。

另请参阅

以下指南可能也会有所帮助:

本页目录