本指南介绍如何使用Apache Geode的数据管理系统来缓存应用程序代码中的某些调用。
要更全面地了解 Apache Geode 的概念并学习如何从 Apache Geode 访问数据,请参阅指南使用 Apache Geode 访问数据。 |
你要建造什么
你将构建一个服务,该服务从CloudFoundry托管的报价服务请求报价,并将其缓存到Apache Geode中。
然后,你会发现再次获取相同报价时,由于 Spring 的 Cache Abstraction(由 Apache Geode 支持),对 Quote 服务的昂贵调用会被消除,并且会缓存给定请求的结果。
Quote服务的位置是……
https://quoters.apps.pcfone.io
Quote服务提供了以下API……
GET /api - get all quotes GET /api/random - get random quote GET /api/{id} - get specific quote
所需内容
-
大约 15 分钟
-
一个常用的文本编辑器或 IDE
-
Java 1.8 或以上版本
-
你也可以直接将代码导入到你的 IDE 中:
如何完成本指南
和大多数 Spring 入门指南一样,你可以从零开始并完成每一步,也可以跳过已熟悉的基本设置步骤。不管选择哪种方式,你最终都能获得可运行的代码。
要从头开始,请参阅使用 Spring Initializr 开始。
若要跳过基础知识,请执行以下操作:
-
下载并解压此指南的源代码仓库,或者使用Git进行克隆:
git clone https://github.com/spring-guides/gs-caching-gemfire.git
-
进入
gs-caching-gemfire/initial
目录 -
跳到 创建一个用于获取数据的可绑定对象。
完成之后,你可以将你的结果与 gs-caching-gemfire/complete
中的代码进行对比。
从 Spring Initializr 开始
以下是一个使用 Maven 时的示例 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>2.7.0</version> </parent> <groupId>org.springframework</groupId> <artifactId>gs-caching-gemfire</artifactId> <version>0.1.0</version> <properties> <spring-shell.version>1.2.0.RELEASE</spring-shell.version> </properties> <dependencies> <dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-geode</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.shell</groupId> <artifactId>spring-shell</artifactId> <version>${spring-shell.version}</version> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
以下是一个使用 Gradle 的示例 build.gradle
文件:
plugins { id 'org.springframework.boot' version '2.7.0' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'io.freefair.lombok' version '6.3.0' id 'java' } apply plugin: 'eclipse' apply plugin: 'idea' group = "org.springframework" version = "0.1.0" sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation "org.springframework.boot:spring-boot-starter" implementation "org.springframework.data:spring-data-geode" implementation "com.fasterxml.jackson.core:jackson-databind" implementation "org.projectlombok:lombok" runtimeOnly "javax.cache:cache-api" runtimeOnly "org.springframework.shell:spring-shell:1.2.0.RELEASE" }
创建一个用于获取数据的绑定对象
现在你已经设置好了项目和构建系统,可以专注于定义所需的领域对象,以捕获从Quote服务获取报价的数据。
src/main/java/hello/Quote.java
package hello;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.util.ObjectUtils;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@SuppressWarnings("unused")
public class Quote {
private Long id;
private String quote;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Quote)) {
return false;
}
Quote that = (Quote) obj;
return ObjectUtils.nullSafeEquals(this.getId(), that.getId());
}
@Override
public int hashCode() {
int hashValue = 17;
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getId());
return hashValue;
}
@Override
public String toString() {
return getQuote();
}
}
Quote
域类具有 id
和 quote
属性。这两个属性是你将在本指南后续部分收集的主要信息。通过使用Project Lombok,Quote
类的实现得到了简化。
除了Quote
之外,QuoteResponse
还捕获了报价服务在报价请求中返回的整个响应负载。它包括请求的状态(也称为type
)以及quote
。此类还使用Project Lombok 来简化实现。
src/main/java/hello/QuoteResponse.java
package hello;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class QuoteResponse {
@JsonProperty("value")
private Quote quote;
@JsonProperty("type")
private String status;
@Override
public String toString() {
return String.format("{ @type = %1$s, quote = '%2$s', status = %3$s }",
getClass().getName(), getQuote(), getStatus());
}
}
报价服务的典型响应如下:
{
"type":"success",
"value": {
"id":1,
"quote":"Working with Spring Boot is like pair-programming with the Spring developers."
}
}
这两个类都标注了@JsonIgnoreProperties(ignoreUnknown=true)
。这意味着即使存在其他的JSON属性,它们也会被忽略。
通过查询报价服务获取数据
你的下一步是创建一个用于查询引用的服务类。
src/main/java/hello/QuoteService.java
package hello;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@SuppressWarnings("unused")
@Service
public class QuoteService {
protected static final String ID_BASED_QUOTE_SERVICE_URL = "https://quoters.apps.pcfone.io/api/{id}";
protected static final String RANDOM_QUOTE_SERVICE_URL = "https://quoters.apps.pcfone.io/api/random";
private volatile boolean cacheMiss = false;
private final RestTemplate quoteServiceTemplate = new RestTemplate();
/**
* Determines whether the previous service method invocation resulted in a cache miss.
*
* @return a boolean value indicating whether the previous service method invocation resulted in a cache miss.
*/
public boolean isCacheMiss() {
boolean cacheMiss = this.cacheMiss;
this.cacheMiss = false;
return cacheMiss;
}
protected void setCacheMiss() {
this.cacheMiss = true;
}
/**
* Requests a quote with the given identifier.
*
* @param id the identifier of the {@link Quote} to request.
* @return a {@link Quote} with the given ID.
*/
@Cacheable("Quotes")
public Quote requestQuote(Long id) {
setCacheMiss();
return requestQuote(ID_BASED_QUOTE_SERVICE_URL, Collections.singletonMap("id", id));
}
/**
* Requests a random quote.
*
* @return a random {@link Quote}.
*/
@CachePut(cacheNames = "Quotes", key = "#result.id")
public Quote requestRandomQuote() {
setCacheMiss();
return requestQuote(RANDOM_QUOTE_SERVICE_URL);
}
protected Quote requestQuote(String URL) {
return requestQuote(URL, Collections.emptyMap());
}
protected Quote requestQuote(String URL, Map<String, Object> urlVariables) {
return Optional.ofNullable(this.quoteServiceTemplate.getForObject(URL, QuoteResponse.class, urlVariables))
.map(QuoteResponse::getQuote)
.orElse(null);
}
}
QuoteService
使用 Spring 的 RestTemplate
来查询 Quote 服务的 API。Quote 服务返回一个 JSON 对象,但 Spring 使用 Jackson 将数据绑定到 QuoteResponse
对象,并最终绑定到一个 Quote
对象。
这个服务类的关键部分是 requestQuote
方法被注解为 @Cacheable("Quotes")
。Spring 的缓存抽象 会拦截对 requestQuote
的调用,检查服务方法是否已经被调用过。如果是这样,Spring 的缓存抽象 就直接返回缓存的副本。否则,Spring 继续执行该方法,将响应存储在缓存中,并将结果返回给调用者。
我们在 requestRandomQuote
服务方法上使用了 @CachePut
注解。由于该方法返回的引用是随机的,我们无法在调用前查询缓存(即 Quotes
)。不过,我们可以缓存调用的结果,这将对后续的 requestQuote(id)
调用产生积极影响,前提是感兴趣的引用已被先前随机选择并缓存。
@CachePut
使用 SpEL 表达式(“#result.id”)来访问服务方法调用的结果,并提取 Quote
的 ID 作为缓存键。你可以在这里了解更多关于 Spring 的 Cache 抽象 SpEL 上下文。
你必须提供缓存的名称。为了演示目的,我们将它命名为 "Quotes",但在生产环境中,建议选择一个描述性更强的名称。这也意味着不同的方法可以与不同的缓存关联起来。如果你为每个缓存设置了不同的配置(如过期或驱逐策略等),这将非常有用。 |
稍后当你运行代码时,会看到每个调用的执行时间,并能辨别出缓存对服务响应时间的影响。这说明了缓存特定调用的重要性。如果应用程序频繁查询相同数据,缓存这些结果可以大幅提高性能。
使应用具备可执行性
虽然 Apache Geode 缓存可以嵌入到 Web 应用和 WAR 文件中,但下面演示的方法更为简单,即创建一个独立的应用程序。所有内容被打包在一个可执行的 JAR 文件中,并通过传统的 Java main()
方法来驱动。
src/main/java/hello/Application.java
package hello;
import java.util.Optional;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.gemfire.cache.config.EnableGemfireCaching;
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
import org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions;
@SpringBootApplication
@ClientCacheApplication(name = "CachingGemFireApplication")
@EnableCachingDefinedRegions(clientRegionShortcut = ClientRegionShortcut.LOCAL)
@EnableGemfireCaching
@SuppressWarnings("unused")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
ApplicationRunner runner(QuoteService quoteService) {
return args -> {
Quote quote = requestQuote(quoteService, 12L);
requestQuote(quoteService, quote.getId());
requestQuote(quoteService, 10L);
};
}
private Quote requestQuote(QuoteService quoteService, Long id) {
long startTime = System.currentTimeMillis();
Quote quote = Optional.ofNullable(id)
.map(quoteService::requestQuote)
.orElseGet(quoteService::requestRandomQuote);
long elapsedTime = System.currentTimeMillis();
System.out.printf("\"%1$s\"%nCache Miss [%2$s] - Elapsed Time [%3$s ms]%n", quote,
quoteService.isCacheMiss(), (elapsedTime - startTime));
return quote;
}
}
@SpringBootApplication
是一个便利的注解,包含以下所有内容:
-
@Configuration
: 表示该类是应用上下文中 bean 定义的来源。 -
@EnableAutoConfiguration
: 告诉 Spring Boot 根据类路径设置、其他 Bean 以及各种属性设置自动配置 Bean。例如,如果类路径上存在spring-webmvc
,此注解会将应用程序标识为 Web 应用程序,并激活一些关键行为,如设置DispatcherServlet
。 -
@ComponentScan
: 告诉 Spring 在hello
包中扫描并查找其他组件、配置和服务,以便找到控制器。
main()
方法使用 Spring Boot 的 SpringApplication.run()
方法来启动应用。你有没有注意到没有一行 XML 配置?也没有 web.xml
文件。这个 Web 应用是 100% 纯 Java,无需配置任何基础设施。
配置的顶部有一个重要的注解:@EnableGemfireCaching
。这个注解启用了缓存(即通过Spring的@EnableCaching
注解进行元注解),并在后台声明了一些支持使用Apache Geode作为缓存提供程序的重要bean。
第一个 bean 是 QuoteService
的实例,用于访问 Quotes RESTful Web 服务。
其他两个用于缓存报价并执行应用程序的操作。
-
quotesRegion
定义了一个 Apache GeodeLOCAL
客户端 Region,用于在缓存中存储报价。它被特别命名为 "Quotes",以便与我们在QuoteService
方法上使用的@Cacheable("Quotes")
注解相匹配。 -
runner
是一个实现了Spring BootApplicationRunner
接口的实例,用于运行我们的应用程序。
当第一次请求报价(使用 requestQuote(id)
)时,会发生缓存未命中,并且服务方法会被调用,产生明显的延迟,这个延迟远非零毫秒。在这种情况下,缓存是通过服务方法的输入参数(即 id
)来链接的,也就是说,id
是缓存键。后续使用相同的 ID 请求同一报价时,将导致缓存命中,从而避免昂贵的服务调用。
为了演示目的,对 QuoteService
的调用被封装在一个单独的方法(即 Application
类中的 requestQuote
方法)中,以便捕获服务调用所需的时间。这样你可以精确地看到每个请求所花费的具体时间。
构建可执行的 JAR 包
你可以通过 Gradle 或 Maven 从命令行运行应用程序。你也可以构建一个包含所有必需依赖项、类和资源的单个可执行 JAR 文件并运行它。这样可以轻松地在整个开发生命周期中,在不同的环境中打包、版本控制和部署服务。
如果你使用 Gradle,可以通过运行 ./gradlew bootRun
来启动应用程序。或者,你也可以通过运行 ./gradlew build
构建 JAR 文件,并按如下方式运行该 JAR 文件:
java -jar build/libs/gs-caching-gemfire-0.1.0.jar
如果你使用 Maven,可以通过运行 ./mvnw spring-boot:run
来启动应用。或者,你可以通过执行 ./mvnw clean package
构建 JAR 文件,然后按照以下方式运行该 JAR 文件:
java -jar target/gs-caching-gemfire-0.1.0.jar
这里描述的步骤创建了一个可执行的 JAR 文件。你也可以构建一个经典的 WAR 文件。 |
显示日志输出,服务将在几秒钟内启动并开始运行。
"@springboot with @springframework is pure productivity! Who said in #java one has to write double the code than in other langs? #newFavLib" Cache Miss [true] - Elapsed Time [776 ms] "@springboot with @springframework is pure productivity! Who said in #java one has to write double the code than in other langs? #newFavLib" Cache Miss [false] - Elapsed Time [0 ms] "Really loving Spring Boot, makes stand alone Spring apps easy." Cache Miss [true] - Elapsed Time [96 ms]
从这里可以看出,第一次调用报价服务获取报价耗时776毫秒,导致缓存未命中。然而,第二次请求相同报价的调用仅耗时0毫秒,并且缓存命中。这清楚地表明第二次调用被缓存了,并没有实际访问报价服务。但是,当最终为一个特定的、未缓存的报价请求进行服务调用时,它耗时96毫秒,并且由于这个新报价在调用之前不在缓存中而导致缓存未命中。
摘要
恭喜!你刚刚创建了一个执行昂贵操作的服务,并标记了它以缓存结果。
参见
以下指南可能也会有所帮助: