07 Feign扩展之Contract接口

博客内容讲述了Feign接口的约束条件,包括不能是泛型接口,接口最多只能有一个父接口。BaseContract抽象类负责解析并验证接口及方法的元数据,处理接口和方法上的注解。Default类作为默认实现,处理了@RequestLine、@Body、@Headers等注解。Spring Cloud为支持Feign与SpringMvc接口的兼容也基于此进行扩展。
部署运行你感兴趣的模型镜像

这个接口决定了哪些注解可以标注在接口上还是方法上是有效的,并且提取处有效的信息,组装成为MethodMetaData元信息。

1 Contract接口

public interface Contract {
	// targetType:就是客户端接口的的class类型
	// 此方法来解析类中链接到HTTP请求的方法:提取有效信息到元信息存储
	// MethodMetadata:方法各种元信息,包括但不限于
	// 返回值类型returnType
	// 请求参数、请求参数的index、名称
	// url、查询参数、请求body体等等等等
	List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}

1.2 抽象基类

abstract class BaseContract implements Contract {
    @Override
    public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
		// 这些检查挺有意思的
		// 1、类上不能存在任何一个泛型变量
		... targetType.getTypeParameters().length == 0
		// 2、接口最多最多只能有一个父接口
		... targetType.getInterfaces().length <= 1
			targetType.getInterfaces()[0].getInterfaces().length == 0

		// 对该类所有的方法进行解析:包装成一个MethodMetadata
		// getMethods表示本类 + 父类的public方法
		// 因为是接口,所有肯定都是public的(当然Java8支持private、default、static等)
		Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
		for (Method method : targetType.getMethods()) {
			... // 排除掉Object的方法、static方法、default方法等
			
			// parseAndValidateMetadata是本类的一个protected方法
			MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
			// 请注意这个key是:metadata.configKey()
			result.put(metadata.configKey(), metadata);

			// 注意:这里并没有把result直接返回回去
			// 而是返回一个快照版本
			return new ArrayList<>(result.values());
		}
    }
}

可见,我们的Feign接口的是有要求的:

  • 不能是泛型接口
  • 接口最多只有一个父接口

会处理所有的接口方法,包含父接口的,但是不包含接口里面的默认方法, 私有方法,静态方法等,也会排除掉Object里面的方法

接下来就是对方法的元数据解析

BaseContract:

	protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
		MethodMetadata data = new MethodMetadata();
		// 方法返回类型是支持泛型的
		data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
		// 这里使用了Feign的一个工具方法,来生成configKey,不用过于了解细节,简单的说就是尽量唯一
		data.configKey(Feign.configKey(targetType, method));

	  // 这一步很重要:处理接口上的注解。并且处理了父接口哦
	  // 这就是为何你父接口上的注解,子接口里也生效的原因哦~~~
	  // processAnnotationOnClass()是个abstract方法,交给子类去实现(毕竟注解是可以扩展的嘛)
      if (targetType.getInterfaces().length == 1) {
        processAnnotationOnClass(data, targetType.getInterfaces()[0]);
      }
      processAnnotationOnClass(data, targetType);
	
	  // 处理标注在方法上的所有注解
	  // 若子接口override了父接口的方法,注解请以子接口的为主,忽略父接口方法
      for (Annotation methodAnnotation : method.getAnnotations()) {
        processAnnotationOnMethod(data, methodAnnotation, method);
      }
		// 简单的说:处理完方法上的注解后,必须已经知道到底是GET or POST 或者其它了
	   checkState(data.template().method() != null,


		// 方法参数,支持泛型类型的。如List<String>这种...
      Class<?>[] parameterTypes = method.getParameterTypes();
      Type[] genericParameterTypes = method.getGenericParameterTypes();

	   // 注解是个二维数组...
      Annotation[][] parameterAnnotations = method.getParameterAnnotations();
      int count = parameterAnnotations.length;
      // 一个注解一个注解的处理
      for (int i = 0; i < count; i++) {
		...
		// processAnnotationsOnParameter是抽象方法,子类去决定
        if (parameterAnnotations[i] != null) {
          isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
        }

		// 方法参数若存在URI类型的参数,那url就以它为准,并不使用全局的了
		if (parameterTypes[i] == URI.class) {
			data.urlIndex(i);
		}
		... 
		// 校验body:
		// 1、body参数不能用作form表单的parameters
		// 2、Body parameters不能太多
		
		...
		return data;
      }
		
	}

其中还定义了三个抽象方法:

 
    protected abstract void processAnnotationOnClass(MethodMetadata data, Class<?> clz);

   
    protected abstract void processAnnotationOnMethod(MethodMetadata data,
                                                      Annotation annotation,
                                                      Method method);
    protected abstract boolean processAnnotationsOnParameter(MethodMetadata data,
                                                             Annotation[] annotations,
                                                             int paramIndex);

1.3 默认实现

Feign提供的唯一实现,也是Feign的默认实现

class Default extends BaseContract {

	static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");

	// 支持注解:@Headers
    @Override
    protected void processAnnotationOnClass(MethodMetadata data, Class<?> targetType) { ... }

	// 支持注解:@RequestLine、@Body、@Headers
    @Override
    protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { ... }

	// 支持注解:@Param、@QueryMap、@HeaderMap等
    @Override
    protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { ... }
	...
}

后面spring-cloud为了支持让Feign支持SpringMvc的一系列接口,就是通过这个实现这个接口

我们实际对这个接口的重写还是比较少,即使重写肯定也要继承默认实现,保证Feign的原先功能不丢失。

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

<think>首先,用户的问题是关于在SpringCloudContract中使用Feign时,如何正确使用参数注解进行契约定义。具体来说,用户询问“springcloudcontractfeignclientparameterannotationsusage”。我需要回顾系统级指令:-所有行内数学表达式用$...$,但这个问题没有数学内容,所以可能不需要。-独立公式用$$...$$,但同样,这里可能不适用。-LaTeX语法正确:确保在需要时使用正确的LaTeX。-使用中文回答:整个响应必须用中文。-生成相关问题:在响应末尾添加相关问题。-回答中引用的段落末尾自然地添加引用标识:例如[^1]。参考了提供的引用[1]到[5],但这些是关于Feign的配置和使用,不是直接关于SpringCloudContract的契约定义。我需要基于这些引用和一般知识来回答。用户的问题是:“我想在SpringCloudContract中使用Feign时,如何正确使用参数注解进行契约定义”。这表明用户在使用SpringCloudContractFeign时,需要定义契约(contracts),并确保参数注解正确使用。关键点:-**SpringCloudContract**:用于契约测试,确保服务提供者和消费者的契约一致。-**Feign**:一个声明式的HTTP客户端,用于简化REST客户端。-**参数注解**:在Feign中,使用注解如`@PathVariable`,`@RequestParam`,`@RequestBody`等来定义HTTP请求的参数。-**契约定义**:在SpringCloudContract中,契约定义了预期的请求和响应。在Feign中,参数注解用于映射方法参数到HTTP请求部分。例如:-`@PathVariable`:用于路径变量。-`@RequestParam`:用于查询参数。-`@RequestBody`:用于请求体。-其他如`@Header`用于头信息。在SpringCloudContract中,契约通常是用GroovyDSL或YAML定义的,描述请求(方法、路径、头、体等)和响应。当使用Feign客户端时,契约需要与Feign接口的注解对齐,以确保测试正确。回答结构:-由于指令要求结构清晰,帮助用户逐步解决问题,我会组织成步骤或部分。-确保回答真实可靠:基于SpringCloudContractFeign的官方文档或常见实践。参考提供的引用:-[^1]:关于Feign配置,自定义编码器。-[^2]:Feign注解定义契约。-[^3]:使用`@Param`注解,自定义扩展器。-[^4]:Feign仓库扫描注解。-[^5]:SwaggerCodegen命令。其中,[^2]和[^3]最相关:-[^2]:Feign的默认契约定义了注解,如`@RequestLine`,`@Param`等。-[^3]:详细说明`@Param`的用法,包括自定义扩展器。在SpringCloudContract中,契约定义需要匹配Feign客户端的注解。例如,如果Feign接口使用`@PathVariable`,契约中的路径变量应匹配。步骤:1.**定义Feign接口**:使用标准Feign注解如`@RequestLine`(如果使用OpenFeign)或SpringMVC注解如`@GetMapping`,`@PathVariable`等。2.**创建契约**:在SpringCloudContract中,使用GroovyDSL定义契约,指定请求方法、URL、路径参数、查询参数等。3.**对齐注解**:确保契约中的参数定义与Feign接口的注解一致。-例如,如果Feign方法有`@PathVariable("id")Longid`,契约中应定义路径变量`id`。4.**处理特殊情况**:如自定义序列化,使用`@Param`的expander,如引用[^3]所示。具体建议:-Feign接口中,使用`@RequestParam`forqueryparameters,`@PathVariable`forpathsegments.-在契约文件中,例如GroovyDSL:```groovyContract.make{request{method'GET'urlPath('/users/{id}'){pathParameters{parameter'id':value(consumer(regex('[0-9]+')),producer('123'))}}queryParameters{parameter'name':value(consumer(regex('.+')),producer('John'))}}response{status200}}```-对应的Feign接口可能:```java@FeignClient(name="user-service")publicinterfaceUserClient{@GetMapping("/users/{id}")UsergetUser(@PathVariable("id")Longid,@RequestParam("name")Stringname);}```注意事项:-确保注解与契约匹配,避免不匹配导致的测试失败。-对于自定义处理,如日期格式化,使用`@Param`的expander,如引用[^3]。-SpringCloudContract自动生成测试,验证契约。响应结构:1.**引言**:解释问题背景。2.**步骤**:逐步指导如何正确使用参数注解。3.**示例**:提供代码示例。4.**注意事项**:关键点提醒。5.**相关问题**:在末尾生成相关问题。生成相关问题:基于指令“生成相关问题”,在响应末尾添加几个相关问题。例如:-如何配置SpringCloudContract来测试Feign客户端?-在契约中如何处理复杂的请求体?-FeignSpringCloudContract的集成最佳实践是什么?引用标识:-在回答中,如果引用提供的引用,添加[^n]。-例如,提到自定义参数扩展器时引用[^3]。最终响应用中文。</think>### 在Spring Cloud Contract中使用Feign的参数注解进行契约定义 在Spring Cloud Contract中,Feign客户端的参数注解是确保契约测试正确性的关键。Feign通过注解(如`@PathVariable`、`@RequestParam`、`@RequestBody`)定义HTTP请求参数,而Spring Cloud Contract的契约文件(通常用Groovy DSL编写)必须与这些注解对齐,以避免测试失败。以下基于最佳实践逐步说明如何正确使用参数注解进行契约定义。 #### 步骤1: 定义Feign接口的参数注解 Feign接口中的参数注解决定了HTTP请求的结构。Spring Cloud Contract支持标准Spring MVC注解和OpenFeign的`@Param`注解(用于自定义序列化)。确保注解与契约的请求路径、查询参数或请求体一致。 - **常用注解**: - `@PathVariable`: 用于路径变量,例如URL中的`/{id}`。 - `@RequestParam`: 用于查询参数,例如`?name=value`。 - `@RequestBody`: 用于请求体,通常用于POST/PUT请求。 - `@Param`: 自定义参数序列化(如日期格式化),支持`expander`属性(参考引用[^3])。 - **示例Feign接口**: ```java @FeignClient(name = "user-service") public interface UserClient { // 路径变量和查询参数 @GetMapping("/users/{userId}") User getUser( @PathVariable("userId") Long userId, @RequestParam("name") String name ); // 自定义参数序列化(使用@Param的expander) @RequestLine("GET /search?since={date}") List<User> searchUsers( @Param(value = "date", expander = DateToMillis.class) Date date ); } ``` 在自定义序列化中,`DateToMillis`类实现`Param.Expander`接口,将日期转为毫秒值(引用[^3])。 #### 步骤2: 创建Spring Cloud Contract契约文件 契约文件(Groovy DSL)必须镜像Feign接口的注解。路径变量、查询参数和请求体需在契约中明确声明,使用`pathParameters`、`queryParameters`或`body`块。 - **关键契约元素**: - `pathParameters`: 匹配`@PathVariable`,定义路径变量的正则约束。 - `queryParameters`: 匹配`@RequestParam`,定义查询参数的预期值。 - `body`: 匹配`@RequestBody`,定义请求体的JSON结构。 - **示例契约文件(`contracts/user.groovy`)**: ```groovy Contract.make { // 匹配Feign的getUser方法 request { method 'GET' urlPath('/users/{userId}') { pathParameters { parameter 'userId': value(consumer(regex('[0-9]+')), producer('123')) // 路径变量约束 } queryParameters { parameter 'name': value(consumer(regex('.+')), producer('John')) // 查询参数约束 } } } response { status 200 body([id: 123, name: 'John']) // 响应体示例 } } // 匹配Feign的searchUsers方法(自定义参数) Contract.make { request { method 'GET' urlPath('/search') { queryParameters { parameter 'since': value(consumer(regex('[0-9]+')), producer('1625097600000')) // 匹配@Param的expander输出 } } } response { status 200 body([]) // 空列表响应 } } ``` 在契约中,`value(consumer(...), producer(...))`定义测试时的消费者和生产者行为。路径变量`userId`必须与Feign的`@PathVariable("userId")`名称完全一致。 #### 步骤3: 对齐注解与契约的常见问题处理 - **注解名称匹配**:Feign注解中的参数名(如`@PathVariable("userId")`)必须与契约中的参数名(如`parameter 'userId'`)一致。否则,契约测试会失败。 - **自定义序列化**:如果使用`@Param`的`expander`(如日期格式化),契约中的查询参数值需匹配`expander`的输出格式(引用[^3])。例如,日期转为毫秒值后,契约使用正则`regex('[0-9]+')`验证。 - **请求体处理**:当使用`@RequestBody`时,契约的`body`块需定义JSON结构。例如: ```groovy request { method 'POST' url '/users' body([ // 匹配@RequestBody name: $(consumer(regex('.+')), producer('Jane')), email: $(consumer(regex(email())), producer('jane@example.com')) ]) } ``` - **契约生成与测试**:运行`./gradlew generateContractTests`(或Maven等效命令)生成测试。Spring Cloud Contract会自动验证Feign接口是否满足契约。 #### 注意事项 - **注解冲突**:避免混合使用Spring MVC注解(如`@GetMapping`)和OpenFeign的`@RequestLine`。推荐统一使用Spring Cloud OpenFeign的默认注解(引用[^2])。 - **契约位置**:契约文件需放在`src/test/resources/contracts/`目录。确保Feign客户端扫描正确配置(引用[^4])。 - **测试覆盖**:契约测试仅验证HTTP交互,不测试业务逻辑。补充单元测试覆盖Feign接口的实现。 - **性能优化**:对于复杂参数,使用`@Param`的`expander`减少序列化开销(引用[^3])。 - **错误处理**:如果契约测试失败,检查Feign注解名称是否与契约一致,或使用`@FeignClient`的`configuration`属性自定义编码器(引用[^1])。 通过以上步骤,参数注解能正确映射到契约定义,确保服务提供者和消费者的兼容性。更多细节参考Spring Cloud Contract官方文档和Feign注解规范(引用[^2])。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值