19.springcloud_openfeign之案例
文章目录
- 一、前言
- 二、案例
- 通用配置
- @EnableFeignClients注解上的配置
- 1、包扫描
- 2、指定clients
- 3、使用EnableFeignClients#defaultConfiguration指定全局配置
- @FeignClient注解配置
- 属性文件配置
- 注解使用
- 类注解
- @CollectionFormat
- 方法注解
- @CollectionFormat
- @RequestMapping
- 参数注解
- 上传文件
- 熔断器
- 缓存
- 拦截器
- 三、总结
一、前言
前面已经介绍完了feign的基本功能以及springcloud_openfeign的扩展功能, 本节我们将介绍springcloud_openfeign的使用案例
二、案例
通用配置
pom gav
<dependencies><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId><version>13.3</version></dependency><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-jackson</artifactId><version>13.3</version></dependency><!-- 提供处理java8的日期和时间类的扩展模块,如(LocalDate, LocalDateTime, ZonedDateTime 等) --><dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId><version>2.17.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>4.1.3</version></dependency><!-- https://github.com/OpenFeign/feign-form --><dependency><groupId>io.github.openfeign.form</groupId><artifactId>feign-form</artifactId><version>3.8.0</version></dependency></dependencies>
配置文件
application.yml
spring:cloud:openfeign:cache:enabled: false # 关闭请求缓存
@EnableFeignClients注解上的配置
1、包扫描
启动类
@SpringBootApplication
@EnableFeignClients(basePackages = "per.qiao.feign.starter.remote.interfacepkg", basePackageClasses={Interface1.class, Interface2.class})
public class FeignStudyApplication {public static void main(String[] args) {SpringApplication.run(FeignStudyApplication.class, args);}
}
2、feign接口定义
package per.qiao.feign.starter.remote.clientspkg;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;@FeignClient(contextId = "interface1", name = "interface1", url = "http://localhost:8080", path = "/packageInterface")
public interface Interface1 {@GetMapping(value = "/scanTest")String scanTest();
}package per.qiao.feign.starter.remote.clientspkg;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;@FeignClient(contextId = "interface2", name = "interface2", url = "http://localhost:8080", path = "/packageInterface")
public interface Interface2 {@GetMapping(value = "/scanTest")String scanTest();
}package per.qiao.feign.starter.remote.interfacepkg;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;@FeignClient(contextId = "package-interface", name = "package-interface", url = "http://localhost:8080", path = "/packageInterface")
public interface PackageInterface {@GetMapping(value = "/scanTest")String scanTest();}
其中Interface1
和Interface2
是通过basePackageClasses指定的, PackageInterface
是通过basePackages包扫描的
controller
@RestController
@RequestMapping("packageInterface")
public class Controller2 {@GetMapping("scanTest")public String scanTest() {System.out.println("=== scanTest");return "scanTest";}
}
2、指定clients
修改启动类上注解即可, 此时包扫描将会失效
@SpringBootApplication
@EnableFeignClients(clients = {Interface1.class, Interface2.class})
public class FeignStudyApplication {public static void main(String[] args) {SpringApplication.run(FeignStudyApplication.class, args);}
}
3、使用EnableFeignClients#defaultConfiguration指定全局配置
启动类
@SpringBootApplication
@EnableFeignClients(basePackages = "per.qiao.feign.starter.remote.clientspkg", defaultConfiguration = {GlobalInterceptor.class})
public class FeignStudyApplication {public static void main(String[] args) {SpringApplication.run(FeignStudyApplication.class, args);}
}
全局拦截器
public class GlobalInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {System.out.println("全局拦截器生效...");}
}
测试类
@SpringBootTest
public class FeignDemo {@Autowiredprivate Interface1 interface1;@Autowiredprivate Interface2 interface2;@Testpublic void scanTest() {System.out.println(interface1.scanTest());System.out.println(interface2.scanTest());}
}
它会将配置的组件注册到每个子容器中, 多例对象, 而不是共享对象
defaultConfiguration可以指定的类型有
序号 | 类名 | 说明 |
---|---|---|
1 | FeignLoggerFactory.class | 定义如何创建 Feign 的日志记录器工厂,控制日志的输出逻辑。 |
2 | Feign.Builder.class | Feign 客户端的构建器,用于创建 Feign 客户端实例。 |
3 | Encoder.class | 负责将请求对象编码为 HTTP 请求的实现类。 |
4 | Decoder.class | 负责将 HTTP 响应解码为目标对象的实现类。 |
5 | Contract.class | 定义 Feign 如何解析接口上的注解并生成 HTTP 请求的契约。 |
6 | FeignClientConfigurer.class | 是否启用属性中的配置 |
7 | Logger.Level.class | 定义 Feign 的日志级别(NONE、BASIC、HEADERS、FULL)。 |
8 | Retryer.class | 定义 Feign 客户端请求失败后的重试逻辑。 |
9 | ErrorDecoder.class | 用于处理 Feign 请求过程中发生的异常,并生成自定义的错误信息。 |
10 | FeignErrorDecoderFactory.class | 用于创建 ErrorDecoder 实例的工厂类。 |
11 | Request.Options.class | 配置请求超时参数(连接超时和读取超时)。 |
12 | RequestInterceptor.class | 请求拦截器,用于在请求发送前进行自定义操作(如添加 Header)。 |
13 | ResponseInterceptor.class | 响应拦截器,用于在响应处理后进行额外操作。 |
14 | QueryMapEncoder.class | @QueryMap,@SpringQueryMap,@HeaderMap的编码器 |
15 | ExceptionPropagationPolicy.class | 重试异常的传播机制, 是抛出重试异常还是原始异常。 |
16 | Capability.class | Feign 的扩展能力类,用于增强Feign.Builder中各组件的功能 。 |
17 | Client.class | 请求客户端,例如okhttpClient |
18 | Targeter.class | feign.Target的包装类 |
@FeignClient注解配置
序号 | 属性名称 | 说明 |
---|---|---|
1 | value | 指定 Feign 客户端的名称,用于在上下文中唯一标识该客户端(与 name 等效)。 |
2 | contextId | springcloud_openfeign子容器的名称, 这个要唯一, 如果为空, 它将取name名称, 非必须 |
3 | name | 定义 Feign 客户端的名称,与 value 等效,优先推荐使用 name 。唯一且必须; 可以是占位符 |
4 | qualifiers | 别名 |
5 | url | 明确指定服务的基础 URL,跳过服务发现(如 nacos等)。非必须; 可以是占位符 |
6 | dismiss404 | 是否忽略 HTTP 404 响应,如果设置为 true ,则不抛出异常。 |
7 | configuration | 引入自定义配置类,用于定制 Feign 客户端的行为(如编码器、拦截器等)。子容器独享; 与上面defaultConfiguration可选值相同 |
8 | fallback | 指定降级处理类的实现,当服务不可用时提供备用逻辑。与fallbackFactory两者取其一。优先级高于fallbackFactory; 需要开启熔断配置 |
9 | fallbackFactory | 指定降级工厂类,用于生成降级处理的实例,可以包含更多逻辑。与fallback两者取其一; 需要开启熔断配置 |
10 | path | 在请求路径前添加公共前缀。一般对应contoller的路径; 非必须 |
11 | primary | 设置该 Bean 是否为注入的首选。如果按照类型注入可以有多个的时候, 会注入带有@primary属性的 |
需要注意: 如果url为空, 那么url将取值name属性, 并且name的值会从环境上下文中获取
yml配置
spring:cloud:openfeign:cache:enabled: false # 关闭请求缓存circuitbreaker:enabled: true # 开启熔断器
feign接口
@FeignClient(contextId = "package-interface", name = "package-interface", url = "http://localhost:8080", path = "/packageInterface", qualifiers = {"remote1"}, dismiss404 = false, configuration={PackageInterfaceInterceptor.class}, fallback = PackageInterfaceFallback.class)
public interface PackageInterface {@GetMapping(value = "/scanTest")String scanTest();
}
拦截器
public class PackageInterfaceInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {System.out.println("客户端私有拦截器");}
}
熔断配置, 它需要注入到spring容器
/*** * 熔断器工厂*/
@Configuration
public class MyCircuitBreakerFactory extends CircuitBreakerFactory {@Overridepublic CircuitBreaker create(String id) {System.out.println("熔断器id=" + id);return new MyCircuitBreaker();}@Overrideprotected ConfigBuilder configBuilder(String id) {return null;}@Overridepublic void configureDefault(Function defaultConfiguration) {}
}/*** 熔断器*/
public class MyCircuitBreaker implements CircuitBreaker {private AtomicLong failureCount = new AtomicLong();@Overridepublic <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {try {return toRun.get();}catch (Throwable throwable) {failureCount.incrementAndGet();return fallback.apply(throwable);}}
}/*** 熔断回调*/
@Component
public class PackageInterfaceFallback implements PackageInterface {@Overridepublic String scanTest() {return "异常了";}
}
controller
@RestController
@RequestMapping("packageInterface")
public class Controller2 {@GetMapping("scanTest")public String scanTest() {System.out.println("=== scanTest");return "scanTest";}
}
请求
@SpringBootTest
public class FeignDemo {/** 这里用别名 */@Autowired@Qualifier("remote1")private PackageInterface packageInterface;@Testpublic void scanTest() {System.out.println(packageInterface.scanTest());}
}
正常情况下响应
熔断器id=PackageInterfacescanTest
客户端私有拦截器
全局拦截器生效...
scanTest
异常情况响应(这里直接将服务端关闭就行)
熔断器=PackageInterfacescanTest
客户端私有拦截器
全局拦截器生效...
熔断异常了
属性文件配置
属性配置文件是否启动的开关配置如下
@Bean
public FeignClientConfigurer feignClientConfigurer() {return new FeignClientConfigurer() {@Overridepublic boolean inheritParentConfiguration() {// 为true则启动, false则禁用return true;}};
}
属性配置如下
spring:cloud:openfeign:cache:enabled: falsecircuitbreaker:enabled: false # 开启熔断器client: # feign的组件配置defaultToProperties: true # 优先使用配置文件中的配置defaultConfig: default # 指定全局配置是下面config中的那一个,默认是default, 它将给所有子容器添加多例对象config:default: # 默认配置, 将对所有的feign客户端生效loggerLevel: NONE # 默认关闭日志打印retryer: feign.Retryer.Default # 设置重试机制interface1: # interface1的配置型logger-level: basic # 指定日志级别dismiss404: trueinterface2:logger-level: headers # 指定日志级别dismiss404: true
logback.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><!-- 开启debug日志级别 --><root level="debug"><appender-ref ref="CONSOLE" /></root>
</configuration>
注意这里需要开启debug日志级别
执行
@SpringBootTest
public class FeignDemo {@Autowiredprivate Interface1 interface1;@Autowiredprivate Interface2 interface2;@Testpublic void propertiesTest() {System.out.println(interface1.scanTest());System.out.println(interface2.scanTest());}
}
结果
interface1使用的basic
日志级别; interface2使用的是headers
日志级别
-- interface1.scanTest()的日志打印结果
[Interface1#scanTest] ---> GET http://localhost:8080/packageInterface/scanTest HTTP/1.1
[Interface1#scanTest] <--- HTTP/1.1 200 (6ms)-- interface2.scanTest()的日志打印
[Interface2#scanTest] ---> GET http://localhost:8080/packageInterface/scanTest HTTP/1.1
[Interface2#scanTest] ---> END HTTP (0-byte body)
[Interface2#scanTest] <--- HTTP/1.1 200 (0ms)
[Interface2#scanTest] connection: keep-alive
[Interface2#scanTest] content-length: 8
[Interface2#scanTest] content-type: text/plain;charset=UTF-8
[Interface2#scanTest] date: Sat, 28 Dec 2024 20:44:08 GMT
[Interface2#scanTest] keep-alive: timeout=60
[Interface2#scanTest] <--- END HTTP (8-byte body)
注解使用
类注解
@CollectionFormat
用于指定集合参数添加到url请求上时的分隔符号
// controller
@GetMapping("classCollectionFormatTest")
public String classCollectionFormatTest(@RequestParam String names) {System.out.println("=== classCollectionFormatTest: " + names);return "classCollectionFormatTest";
}// feign接口 这里指定分隔符为|
@FeignClient(contextId = "interface1", name = "interface1", url = "http://localhost:8080", path = "/packageInterface")
@CollectionFormat(feign.CollectionFormat.PIPES)
public interface Interface1 {@GetMapping(value = "/classCollectionFormatTest")String classCollectionFormatTest(@RequestParam List<String> names);
}// 测试
@Test
public void collectionFormatTest() {System.out.println(interface1.classCollectionFormatTest(List.of("uncleqiao", "小杜同学")));
}// 结果
=== classCollectionFormatTest: uncleqiao|小杜同学
方法注解
@CollectionFormat
// controller
@GetMapping("methodCollectionFormatTest")
public String methodCollectionFormatTest(@RequestParam String names) {System.out.println("=== methodCollectionFormatTest: " + names);return "methodCollectionFormatTest";
}// feign接口 这里指定分隔符为|
@FeignClient(contextId = "interface1", name = "interface1", url = "http://localhost:8080", path = "/packageInterface")
@CollectionFormat(feign.CollectionFormat.PIPES)
public interface Interface1 {// 这里指定分隔符为,@GetMapping(value = "/classCollectionFormatTest")@CollectionFormat(feign.CollectionFormat.CSV)String methodCollectionFormatTest(@RequestParam List<String> names);
}// 测试
@Test
public void collectionFormatTest() {System.out.println(interface1.methodCollectionFormatTest(List.of("uncleqiao", "小杜同学")));
}// 结果
=== classCollectionFormatTest: uncleqiao,小杜同学
@RequestMapping
// controller
@PostMapping("requestMappingTest")
public Person requestMappingTest(@RequestBody Person person, @RequestHeader HttpHeaders headers) {System.out.println("=== requestMappingTest: " + person);System.out.println("=== requestMappingTest 收到请求头myhearders: " + headers.getFirst("myhearders"));person.setName("小杜同学");return person;
}// feign接口
@RequestMapping(value = "/requestMappingTest", method = RequestMethod.POST, produces = "application/json", consumes = "application/json", headers = "myhearders=aaa")
Person requestMappingTest(Person person);// 测试
@Test
public void requestMappingTest() {System.out.println(interface1.requestMappingTest(new Person("uncleqiao", 18, 1, LocalDate.now())));
}// 服务度结果
=== requestMappingTest: Person(name=uncleqiao, age=18, gender=1, birthday=2024-12-29)
=== requestMappingTest 收到请求头myhearders: aaa// 客户端结果
Person(name=小杜同学, age=18, gender=1, birthday=2024-12-29)
关于@GetMapping
和@PostMapping
, 它们实际就是用的@RequestMapping
, 只是指定了对应的method, 然后用@AliasFor
指向到RequestMapping
上, 使用起来和@RequestMapping一样
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {/*** Alias for {@link RequestMapping#name}.*/@AliasFor(annotation = RequestMapping.class)String name() default "";
}@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.POST)
public @interface PostMapping {/*** Alias for {@link RequestMapping#name}.*/@AliasFor(annotation = RequestMapping.class)String name() default "";// ....
}
参数注解
- @PathVariable: url上的path变量
- @MatrixVariable: 矩阵参数; 需要注意的是feign会将对应的参数进行u8编码,
;=
将会编码成%3B
和%3D
,需要额外处理一下 - @RequestHeader: 请求头
- @RequestBody: 请求体
- @SpringQueryMap: url上的path参数
- @RequestPart: body参数
- @CookieValue: 请求头上的cookie, 它会覆盖@RequestHeader中的cookie参数
@RestController
@RequestMapping("paramAnnonController")
public class ParamAnnonController {@GetMapping("/matrixVariableTest/{name}")public String matrixVariableTest(@PathVariable(name = "name") String name,@MatrixVariable(name = "age") Integer age) {System.out.println("uncleqiao 收到name:" + name);System.out.println("uncleqiao 收到age:" + age);return "matrixVariableTest";}@PostMapping("/pathVariableTest/{name}/{age}")public String pathVariableTest(@PathVariable(name = "name", required = false) String name,@PathVariable(name = "age") Integer age,@RequestBody Person person) {System.out.println("uncleqiao 收到name:" + name);System.out.println("uncleqiao 收到age:" + age);System.out.println("uncleqiao 收到person:" + person);return "pathVariableTest";}@PostMapping("/requestHeaderTest")public String requestHeaderTest(@RequestHeader HttpHeaders headers) {System.out.println("uncleqiao 收到请求头们:" + headers);return "requestHeaderTest";}@GetMapping("/springQueryMapTest")public String springQueryMapTest(@RequestParam String name, @RequestParam Integer age) {System.out.println("springQueryMapTest 收到:" + name);System.out.println("springQueryMapTest 收到:" + age);return "springQueryMapTest";}@GetMapping("/springQueryMapTest2")public String springQueryMapTest2(Person person) {System.out.println("springQueryMapTest2 收到:" + person);return "springQueryMapTest2";}@PostMapping("/requestPartTest")public String requestPartTest(@RequestBody Person person) {System.out.println("requestPartTest 收到:" + person);return "requestPartTest";}@PostMapping("/requestPartTest2")public String requestPartTest2(@RequestPart String name, @RequestPart Integer age) {System.out.println("requestPartTest2 收到:" + name);System.out.println("requestPartTest2 收到:" + age);return "requestPartTest2";}@GetMapping("/cookieTest")public String cookieTest(@CookieValue String name, @CookieValue String session) {System.out.println("cookieTest 收到:" + name);System.out.println("cookieTest 收到:" + session);return "cookieTest";}}
feign接口
@FeignClient(contextId = "paramAnnoRemote", name = "paramAnnoRemote", path = "/paramAnnonController", url = "localhost:8080")
public interface ParamAnnoRemote {@GetMapping(value = "/matrixVariableTest/{name}{age}")String matrixVariableTest(@PathVariable String name, @MatrixVariable Integer age);@PostMapping(value = "/pathVariableTest/{name}/{age}")String pathVariableTest(@PathVariable("name") String name, @PathVariable("age") Integer age, @PathVariable("gender") Integer gender);@PostMapping(value = "/requestHeaderTest")String requestHeaderTest(@RequestHeader("Content-Type") String contentType, @RequestHeader("myHeader") Map<String, String> myHeader);@GetMapping(value = "/springQueryMapTest")String springQueryMapTest(@SpringQueryMap Map<String, Object> map);@GetMapping(value = "/springQueryMapTest2")String springQueryMapTest2(@SpringQueryMap Map<String, Object> map);@PostMapping(value = "/requestPartTest")String requestPartTest(@RequestPart("name") String name, @RequestPart("age") Integer age);@PostMapping(value = "/requestPartTest2")String requestPartTest2(@RequestPart String name, @RequestPart Integer age);@GetMapping(value = "/cookieTest")String cookieTest(@CookieValue("name") String name, @CookieValue("session") String session);
}
测试类
@SpringBootTest
public class ParamAnnoRemoteTest {@Autowiredprivate ParamAnnoRemote paramAnnoRemote;@Testvoid matrixVariableTest() {String person = paramAnnoRemote.matrixVariableTest("小杜同学", 18);System.out.println(person);}@Testvoid pathVariableTest() {String person = paramAnnoRemote.pathVariableTest( "小杜同学", 18, 1);System.out.println(person);}@Testvoid requestHeaderTest() {Map<String, String> myHeaderMap = Map.of("myHeader1", "abc", "myHeader2", "def");String person = paramAnnoRemote.requestHeaderTest("application/json", myHeaderMap);System.out.println(person);}@Testvoid springQueryMapTest() {Map<String, Object> param = Map.of("name", "小乔同学", "age", 18);String person = paramAnnoRemote.springQueryMapTest(param);System.out.println(person);}@Testvoid springQueryMapTest2() {Map<String, Object> param = Map.of("name", "小乔同学", "age", 18);String person = paramAnnoRemote.springQueryMapTest2(param);System.out.println(person);}@Testvoid requestPartTest() {String person2 = paramAnnoRemote.requestPartTest("小乔同学", 20);System.out.println(person2);}@Testvoid cookieTest() {String person2 = paramAnnoRemote.cookieTest("zs", "abc");System.out.println(person2);}
}
对于@MatrixVariable
注解需要处理分号和逗号
@Configuration
public class InterceptorConfig {@Beanpublic RequestInterceptor matrixVariableInterceptor() {return requestTemplate -> {// 避免;和=被编码String url = requestTemplate.url();url = url.replaceAll("%3B", ";");url = url.replaceAll("%3D", "=");requestTemplate.uri(url);System.out.println("requestTemplate.url()===" + url);};}
}
上传文件
// controller
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam("file") MultipartFile file) {if (file.isEmpty()) {return "文件为空";}// 示例逻辑:打印文件信息String fileInfo = "File uploaded: " + file.getOriginalFilename() + ", size: " + file.getSize();System.out.println(fileInfo);return fileInfo;
}@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String uploadFile(@RequestPart("file") MultipartFile file);// 测试类
@Test
void uploadFile() throws Exception {File file = new File("your file path");FileInputStream input = new FileInputStream(file);MultipartFile multipartFile = new MockMultipartFile("file", file.getName(), "text/plain", input);String result = paramAnnoRemote.uploadFile(multipartFile);System.out.println(result);
}
说明: 这里实际依赖的是feign-form-spring
底层就是就是io.github.openfeign.form:feign-form-spring:3.8.0
这里测试类也可以是客户端controller接口接收的MultipartFile
文件作为feign接口的调用参数
熔断器
1、开启熔断器
spring:cloud:openfeign:cache:enabled: falsecircuitbreaker:enabled: true # 开启熔断器
2、配置熔断器
需要指定CircuitBreakerFactory对象 以及 创建熔断器对象
@Configuration
public class MyCircuitBreakerFactory extends CircuitBreakerFactory {@Overridepublic CircuitBreaker create(String id) {return new MyCircuitBreaker();}@Overrideprotected ConfigBuilder configBuilder(String id) {return null;}@Overridepublic void configureDefault(Function defaultConfiguration) {}
}/*** 熔断器*/
public class MyCircuitBreaker implements CircuitBreaker {private AtomicLong failureCount = new AtomicLong();@Overridepublic <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {try {return toRun.get();}catch (Throwable throwable) {failureCount.incrementAndGet();return fallback.apply(throwable);}}
}
3、接口与调用
@FeignClient(contextId = "circuitBreakerRemote", name = "circuitBreakerRemote", url = "localhost:8080", path = "/circuitBreakerController", fallback = CircuitBreakerRemoteFallback.class)
public interface CircuitBreakerRemote {@GetMapping(value = "/circuitBreakerTest")String circuitBreakerTest();
}// 熔断回调
@Component
public class CircuitBreakerRemoteFallback implements CircuitBreakerRemote {@Overridepublic String circuitBreakerTest() {return "circuitBreakerTest fallback";}
}// 测试
@SpringBootTest
public class CircuitBreakerTest {@Autowiredprivate CircuitBreakerRemote circuitBreakerRemote;@Testpublic void circuitBreakerTest() {System.out.println(circuitBreakerRemote.circuitBreakerTest());}
}// 结果
circuitBreakerTest fallback
缓存
1、开启缓存
spring:cloud:openfeign:cache:enabled: true
2、定义缓存拦截器
@Configuration
public class MyCacheInterceptorConfig {@Bean@ConditionalOnMissingBean(CacheOperationSource.class)public CacheOperationSource cacheOperationSource() {return new AnnotationCacheOperationSource(false);}@Beanpublic org.springframework.cache.interceptor.CacheInterceptor myCacheInterceptor(CacheOperationSource cacheOperationSource) {CacheInterceptor cacheInterceptor = new CacheInterceptor();cacheInterceptor.setCacheOperationSource(cacheOperationSource);return cacheInterceptor;}}
3、接口与调用
// controller
@RestController
@RequestMapping("/cacheableController")
public class CacheableController {@RequestMapping("/cacheableTest")public Person cacheableTest(@RequestBody Person person) {System.out.println("cacheableTest:" + person);person.setName("小杜同学");return person;}
}// feign接口
@FeignClient(contextId = "cacheableRemote", name = "cacheableRemote", path = "/cacheableController", url = "localhost:8080")
public interface CacheableRemote {@PostMapping(value = "/cacheableTest", consumes = "application/json", produces = "application/json")@Cacheable(cacheNames = "demoCache", key = "'person'+#p0.name")Person cacheableTest(Person person);}// 调用
@Test
public void cacheableTest() {Person person = this.cacheableRemote.cacheableTest(new Person("uncleqiao", 18, 1, LocalDate.now()));Person person2 = this.cacheableRemote.cacheableTest(new Person("uncleqiao", 18, 1, LocalDate.now()));System.out.println(person);System.out.println(person2);
}// 服务端打印
cacheableTest:Person(name=uncleqiao, age=18, gender=1, birthday=2024-12-29)// 客户端打印
Person(name=小杜同学, age=18, gender=1, birthday=2024-12-29)
Person(name=小杜同学, age=18, gender=1, birthday=2024-12-29)
多次调用, 只要指定的缓存key一样, 就不会真正发起调用请求
拦截器
1、请求拦截器定义
public class InterfaceConfigInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {System.out.println("feign接口指定私有拦截器生效...");}
}public class PropertiesContextIdInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {System.out.println("properties指定子容器私有拦截器生效...");}
}@Configuration
public class InterceptorConfig {@Beanpublic RequestInterceptor beanGlobalInterceptor() {return requestTemplate -> {System.out.println("全局注入拦截器beanGlobalInterceptor===");};}
}public class PropertiesGlobalInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {System.out.println("properties配置文件全局拦截器生效...");}
}public class EnableInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {System.out.println("EnableFeignClients指定全局拦截器生效...");}
}
2、feign接口
使用configuration
属性指定拦截器
@FeignClient(contextId = "interceptorRemote", name = "interceptorRemote", path = "/interceptorController", url = "localhost:8080", configuration = {InterfaceConfigInterceptor.class})
public interface InterceptorRemote {@GetMapping(value = "/interceptorTest")String interceptorTest(@RequestParam String name);}
3、配置文件
spring:cloud:openfeign:client: # feign的组件配置defaultToProperties: true # 优先使用配置文件中的配置defaultConfig: default # 指定全局配置是下面config中的那一个,默认是default, 它将给所有子容器添加多例对象config:default: # 默认配置, 将对所有的feign客户端生效loggerLevel: NONErequest-interceptors:- per.qiao.feign.starter.interceptors.PropertiesGlobalInterceptorinterceptorRemote:request-interceptors:- per.qiao.feign.starter.interceptors.PropertiesContextIdInterceptor
这里使用配置文件配置了 全局拦截器和子容器(单个feign客户端/feign接口)对应的拦截器
4、启动类指定全局拦截器
@EnableFeignClients(clients = {InterceptorRemote.class}, defaultConfiguration = {EnableInterceptor.class})
public class FeignStudyApplication {@Autowired(required = false)private UrlDemoRemote urlDemoRemote;private ApplicationContext parent;public static void main(String[] args) {SpringApplication.run(FeignStudyApplication.class, args);}
}
执行结果
feign接口指定私有拦截器生效...
EnableFeignClients指定全局拦截器生效...
全局注入拦截器beanGlobalInterceptor===
properties配置文件全局拦截器生效...
properties指定子容器私有拦截器生效...
注意上面spring.cloud.openfeign.client.defaultToProperties=false
会让全局配置失效, 因为子容器中获取的组件项采取的是覆盖的做法; 详见FeignClientFactoryBean#configureFeign->#configureUsingConfiguration
三、总结
- feign框架本身较为简单, 但是扩展点是非常多的, 使用者可以几乎对每一个功能点做定制化;
- 利用springcloud的父子容器特性将每个feign接口(客户端)隔离成单独的子容器,有点类似类加载器的隔离; 这里不同于spring的作用域的隔离机制
- 写通用并且扩展性强的代码是非常重要的, 同时也需要一定的技术功底
至此整个feign的介绍就完毕了。