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 {
@Override
public void apply(RequestTemplate template) {
System.out.println("全局拦截器生效...");
}
}
测试类
@SpringBootTest
public class FeignDemo {
@Autowired
private Interface1 interface1;
@Autowired
private Interface2 interface2;
@Test
public 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 {
@Override
public void apply(RequestTemplate template) {
System.out.println("客户端私有拦截器");
}
}
熔断配置, 它需要注入到spring容器
/**
*
* 熔断器工厂
*/
@Configuration
public class MyCircuitBreakerFactory extends CircuitBreakerFactory {
@Override
public CircuitBreaker create(String id) {
System.out.println("熔断器id=" + id);
return new MyCircuitBreaker();
}
@Override
protected ConfigBuilder configBuilder(String id) {
return null;
}
@Override
public void configureDefault(Function defaultConfiguration) {
}
}
/**
* 熔断器
*/
public class MyCircuitBreaker implements CircuitBreaker {
private AtomicLong failureCount = new AtomicLong();
@Override
public <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 {
@Override
public 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;
@Test
public void scanTest() {
System.out.println(packageInterface.scanTest());
}
}
正常情况下响应
熔断器id=PackageInterfacescanTest
客户端私有拦截器
全局拦截器生效...
scanTest
异常情况响应(这里直接将服务端关闭就行)
熔断器=PackageInterfacescanTest
客户端私有拦截器
全局拦截器生效...
熔断异常了
属性文件配置
属性配置文件是否启动的开关配置如下
@Bean
public FeignClientConfigurer feignClientConfigurer() {
return new FeignClientConfigurer() {
@Override
public boolean inheritParentConfiguration() {
// 为true则启动, false则禁用
return true;
}
};
}
属性配置如下
spring:
cloud:
openfeign:
cache:
enabled: false
circuitbreaker:
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: true
interface2:
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 {
@Autowired
private Interface1 interface1;
@Autowired
private Interface2 interface2;
@Test
public 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 {
@Autowired
private ParamAnnoRemote paramAnnoRemote;
@Test
void matrixVariableTest() {
String person = paramAnnoRemote.matrixVariableTest("小杜同学", 18);
System.out.println(person);
}
@Test
void pathVariableTest() {
String person = paramAnnoRemote.pathVariableTest( "小杜同学", 18, 1);
System.out.println(person);
}
@Test
void requestHeaderTest() {
Map<String, String> myHeaderMap = Map.of("myHeader1", "abc", "myHeader2", "def");
String person = paramAnnoRemote.requestHeaderTest("application/json", myHeaderMap);
System.out.println(person);
}
@Test
void springQueryMapTest() {
Map<String, Object> param = Map.of("name", "小乔同学", "age", 18);
String person = paramAnnoRemote.springQueryMapTest(param);
System.out.println(person);
}
@Test
void springQueryMapTest2() {
Map<String, Object> param = Map.of("name", "小乔同学", "age", 18);
String person = paramAnnoRemote.springQueryMapTest2(param);
System.out.println(person);
}
@Test
void requestPartTest() {
String person2 = paramAnnoRemote.requestPartTest("小乔同学", 20);
System.out.println(person2);
}
@Test
void cookieTest() {
String person2 = paramAnnoRemote.cookieTest("zs", "abc");
System.out.println(person2);
}
}
对于@MatrixVariable
注解需要处理分号和逗号
@Configuration
public class InterceptorConfig {
@Bean
public 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: false
circuitbreaker:
enabled: true # 开启熔断器
2、配置熔断器
需要指定CircuitBreakerFactory对象 以及 创建熔断器对象
@Configuration
public class MyCircuitBreakerFactory extends CircuitBreakerFactory {
@Override
public CircuitBreaker create(String id) {
return new MyCircuitBreaker();
}
@Override
protected ConfigBuilder configBuilder(String id) {
return null;
}
@Override
public void configureDefault(Function defaultConfiguration) {
}
}
/**
* 熔断器
*/
public class MyCircuitBreaker implements CircuitBreaker {
private AtomicLong failureCount = new AtomicLong();
@Override
public <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 {
@Override
public String circuitBreakerTest() {
return "circuitBreakerTest fallback";
}
}
// 测试
@SpringBootTest
public class CircuitBreakerTest {
@Autowired
private CircuitBreakerRemote circuitBreakerRemote;
@Test
public 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);
}
@Bean
public 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 {
@Override
public void apply(RequestTemplate template) {
System.out.println("feign接口指定私有拦截器生效...");
}
}
public class PropertiesContextIdInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
System.out.println("properties指定子容器私有拦截器生效...");
}
}
@Configuration
public class InterceptorConfig {
@Bean
public RequestInterceptor beanGlobalInterceptor() {
return requestTemplate -> {
System.out.println("全局注入拦截器beanGlobalInterceptor===");
};
}
}
public class PropertiesGlobalInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
System.out.println("properties配置文件全局拦截器生效...");
}
}
public class EnableInterceptor implements RequestInterceptor {
@Override
public 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: NONE
request-interceptors:
- per.qiao.feign.starter.interceptors.PropertiesGlobalInterceptor
interceptorRemote:
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的介绍就完毕了。