跳到主要内容

使用可插拔架构

DeepSeek V3 中英对照 Using the Pluggable Architecture

在某些情况下,您可能会遇到合约以其他格式定义的情况,例如 YAML、RAML 或 PACT。在这些情况下,您仍然希望受益于测试和存根的自动生成。您可以添加自己的实现来生成测试和存根。此外,您还可以自定义测试的生成方式(例如,可以为其他语言生成测试)以及存根的生成方式(例如,可以为其他 HTTP 服务器实现生成存根)。

自定义合约转换器​

ContractConverter 接口允许您注册自定义的合约结构转换器实现。以下代码清单展示了 ContractConverter 接口:

import java.io.File;
import java.util.Collection;

/**
* Converter to be used to convert FROM {@link File} TO {@link Contract} and from
* {@link Contract} to {@code T}.
*
* @param <T> - type to which we want to convert the contract
* @author Marcin Grzejszczak
* @since 1.1.0
*/
public interface ContractConverter<T> extends ContractStorer<T>, ContractReader<T> {

/**
* Should this file be accepted by the converter. Can use the file extension to check
* if the conversion is possible.
* @param file - file to be considered for conversion
* @return - {@code true} if the given implementation can convert the file
*/
boolean isAccepted(File file);

/**
* Converts the given {@link File} to its {@link Contract} representation.
* @param file - file to convert
* @return - {@link Contract} representation of the file
*/
Collection<Contract> convertFrom(File file);

/**
* Converts the given {@link Contract} to a {@link T} representation.
* @param contract - the parsed contract
* @return - {@link T} the type to which we do the conversion
*/
T convertTo(Collection<Contract> contract);

}

你的实现必须定义开始转换的条件。同时,你必须定义如何执行双向转换。

important

创建实现后,必须在 /META-INF/spring.factories 文件中提供实现的完全限定名称。

以下示例展示了一个典型的 spring.factories 文件:

org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.converter.YamlContractConverter

使用自定义测试生成器​

如果你想为Java以外的语言生成测试,或者你对验证器构建Java测试的方式不满意,你可以注册自己的实现。

SingleTestGenerator 接口允许您注册自己的实现。以下代码清单展示了 SingleTestGenerator 接口:

import java.nio.file.Path;
import java.util.Collection;

import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;

/**
* Builds a single test.
*
* @since 1.1.0
*/
public interface SingleTestGenerator {

/**
* Creates contents of a single test class in which all test scenarios from the
* contract metadata should be placed.
* @param properties - properties passed to the plugin
* @param listOfFiles - list of parsed contracts with additional metadata
* @param generatedClassData - information about the generated class
* @param includedDirectoryRelativePath - relative path to the included directory
* @return contents of a single test class
*/
String buildClass(ContractVerifierConfigProperties properties, Collection<ContractMetadata> listOfFiles,
String includedDirectoryRelativePath, GeneratedClassData generatedClassData);

class GeneratedClassData {

public final String className;

public final String classPackage;

public final Path testClassPath;

public GeneratedClassData(String className, String classPackage, Path testClassPath) {
this.className = className;
this.classPackage = classPackage;
this.testClassPath = testClassPath;
}

}

}

同样,你必须提供一个 spring.factories 文件,如下例所示:

org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
com.example.MyGenerator

使用自定义存根生成器​

如果你想为 WireMock 以外的存根服务器生成存根,你可以插入自己的 StubGenerator 接口实现。以下代码清单展示了 StubGenerator 接口:

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;

/**
* Converts contracts into their stub representation.
*
* @param <T> - type of stub mapping
* @since 1.1.0
*/
public interface StubGenerator<T> {

/**
* @param mapping - potential stub mapping mapping
* @return {@code true} if this converter could have generated this mapping stub.
*/
default boolean canReadStubMapping(File mapping) {
return mapping.getName().endsWith(fileExtension());
}

/**
* @param rootName - root name of the contract
* @param content - metadata of the contract
* @return the collection of converted contracts into stubs. One contract can result
* in multiple stubs.
*/
Map<Contract, String> convertContents(String rootName, ContractMetadata content);

/**
* Post process a generated stub mapping.
* @param stubMapping - mapping of a stub
* @param contract - contract for which stub was generated
* @return the converted stub mapping
*/
default T postProcessStubMapping(T stubMapping, Contract contract) {
List<StubPostProcessor> processors = StubPostProcessor.PROCESSORS.stream()
.filter(p -> p.isApplicable(contract))
.collect(Collectors.toList());
if (processors.isEmpty()) {
return defaultStubMappingPostProcessing(stubMapping, contract);
}
T stub = stubMapping;
for (StubPostProcessor processor : processors) {
stub = (T) processor.postProcess(stub, contract);
}
return stub;
}

/**
* Stub mapping to chose when no post processors where found on the classpath.
* @param stubMapping - mapping of a stub
* @param contract - contract for which stub was generated
* @return the converted stub mapping
*/
default T defaultStubMappingPostProcessing(T stubMapping, Contract contract) {
return stubMapping;
}

/**
* @param inputFileName - name of the input file
* @return the name of the converted stub file. If you have multiple contracts in a
* single file then a prefix will be added to the generated file. If you provide the
* {@link Contract#getName} field then that field will override the generated file
* name.
*
* Example: name of file with 2 contracts is {@code foo.groovy}, it will be converted
* by the implementation to {@code foo.json}. The recursive file converter will create
* two files {@code 0_foo.json} and {@code 1_foo.json}
*/
String generateOutputFileNameForInput(String inputFileName);

/**
* Describes the file extension of the generated mapping that this stub generator can
* handle.
* @return string describing the file extension
*/
default String fileExtension() {
return ".json";
}

}

同样,你必须提供一个 spring.factories 文件,如下例所示:

# Stub converters
org.springframework.cloud.contract.verifier.converter.StubGenerator=\
org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter

默认实现是 WireMock 桩代码生成。

提示

你可以提供多个存根生成器实现。例如,从单个 DSL 中,你可以同时生成 WireMock 存根和 Pact 文件。

使用自定义存根运行器​

如果你决定使用自定义存根生成,你还需要一种自定义方式来运行不同存根提供者的存根。

假设你使用 Moco 来构建你的存根,并且你已经编写了一个存根生成器,并将你的存根放在一个 JAR 文件中。

为了让Stub Runner知道如何运行你的存根,你必须定义一个自定义的HTTP存根服务器实现,这可能类似于以下示例:

import com.github.dreamhead.moco.bootstrap.arg.HttpArgs
import com.github.dreamhead.moco.runner.JsonRunner
import com.github.dreamhead.moco.runner.RunnerSetting
import groovy.transform.CompileStatic
import groovy.util.logging.Commons

import org.springframework.cloud.contract.stubrunner.HttpServerStub
import org.springframework.cloud.contract.stubrunner.HttpServerStubConfiguration

@Commons
@CompileStatic
class MocoHttpServerStub implements HttpServerStub {

private boolean started
private JsonRunner runner
private int port

@Override
int port() {
if (!isRunning()) {
return -1
}
return port
}

@Override
boolean isRunning() {
return started
}

@Override
HttpServerStub start(HttpServerStubConfiguration configuration) {
this.port = configuration.port
return this
}

@Override
HttpServerStub stop() {
if (!isRunning()) {
return this
}
this.runner.stop()
return this
}

@Override
HttpServerStub registerMappings(Collection<File> stubFiles) {
List<RunnerSetting> settings = stubFiles.findAll { it.name.endsWith("json") }
.collect {
log.info("Trying to parse [${it.name}]")
try {
return RunnerSetting.aRunnerSetting().addStream(it.newInputStream()).
build()
}
catch (Exception e) {
log.warn("Exception occurred while trying to parse file [${it.name}]", e)
return null
}
}.findAll { it }
this.runner = JsonRunner.newJsonRunnerWithSetting(settings,
HttpArgs.httpArgs().withPort(this.port).build())
this.runner.run()
this.started = true
return this
}

@Override
String registeredMappings() {
return ""
}

@Override
boolean isAccepted(File file) {
return file.name.endsWith(".json")
}
}

然后你可以在 spring.factories 文件中注册它,如下例所示:

org.springframework.cloud.contract.stubrunner.HttpServerStub=\
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub

现在你可以使用 Moco 运行存根。

important

如果您不提供任何实现,将使用默认(WireMock)实现。如果您提供了多个实现,则使用列表中的第一个。

使用自定义存根下载器​

你可以通过实现 StubDownloaderBuilder 接口来自定义存根下载的方式,如下例所示:

class CustomStubDownloaderBuilder implements StubDownloaderBuilder {

@Override
public StubDownloader build(final StubRunnerOptions stubRunnerOptions) {
return new StubDownloader() {
@Override
public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration config) {
File unpackedStubs = retrieveStubs();
return new AbstractMap.SimpleEntry<>(
new StubConfiguration(config.getGroupId(), config.getArtifactId(), version,
config.getClassifier()), unpackedStubs);
}

File retrieveStubs() {
// here goes your custom logic to provide a folder where all the stubs reside
}
}
}
}

然后你可以在 spring.factories 文件中注册它,如下例所示:

# Example of a custom Stub Downloader Provider
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
com.example.CustomStubDownloaderBuilder

现在你可以选择一个包含存根源代码的文件夹。

important

如果你不提供任何实现,将使用默认实现(扫描类路径)。如果你提供了 stubsMode = StubRunnerProperties.StubsMode.LOCAL 或 stubsMode = StubRunnerProperties.StubsMode.REMOTE,则会使用 Aether 实现。如果你提供了多个实现,将使用列表中的第一个。

使用 SCM 存根下载器​

当 repositoryRoot 以 SCM 协议开头时(目前我们仅支持 git://),存根下载器会尝试克隆该仓库,并将其用作生成测试或存根的合约源。

通过环境变量、系统属性,或在插件或合约仓库配置中设置的属性,你可以调整下载器的行为。下表描述了可用的属性:

表 1. SCM Stub Downloader 属性

属性类型属性名称描述
* git.branch (插件属性)

* spring.cloud.contract.stubrunner.properties.git.branch (系统属性)

* SPRING_CLOUD_CONTRACT_STUBRUNNER_PROPERTIES_GIT_BRANCH (环境变量)
master要检出的分支
* git.username (插件属性)

* spring.cloud.contract.stubrunner.properties.git.username (系统属性)

* SPRING_CLOUD_CONTRACT_STUBRUNNER_PROPERTIES_GIT_USERNAME (环境变量)
Git 克隆用户名
* git.password (插件属性)

* spring.cloud.contract.stubrunner.properties.git.password (系统属性)

* SPRING_CLOUD_CONTRACT_STUBRUNNER_PROPERTIES_GIT_PASSWORD (环境变量)
Git 克隆密码
* git.no-of-attempts (插件属性)

* spring.cloud.contract.stubrunner.properties.git.no-of-attempts (系统属性)

* SPRING_CLOUD_CONTRACT_STUBRUNNER_PROPERTIES_GIT_NO_OF_ATTEMPTS (环境变量)
10将提交推送到 origin 的尝试次数
* git.wait-between-attempts (插件属性)

* spring.cloud.contract.stubrunner.properties.git.wait-between-attempts (系统属性)

* SPRING_CLOUD_CONTRACT_STUBRUNNER_PROPERTIES_GIT_WAIT_BETWEEN_ATTEMPTS (环境变量)
1000在尝试将提交推送到 origin 之间等待的毫秒数