SpringCloud - RPC (3) 集成 OpenFeign

介绍

Feign 是 Netflix 开发的基于 Ribbon 的声明式、模板化 HTTP 客户端。而 openfeign 则是 Spring 官方 对 Feign 的增强,使其能够支持 SpringMVC 的注解。feign 最大的特点在于通过 feign 调用其他服务的接口,就像调用本地方法一样。

集成

在 my-mall 项目中创建一个新的服务 openfeign-service,端口为 6500:

openfeign-service-project

稍微解释一下,openfeign-service 是 client 与 core 的父项目。这么划分是为了演示后面我司正在使用的项目模块划分。当前将 core 项目理解为具体的业务服务项目即可。

core 中引入 feign 依赖

<dependencies>
    <!-- spring cloud -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- client 依赖被定义在了父项目 openfeign-service 中 -->
    <dependency>
        <groupId>priv.cqq</groupId>
        <artifactId>openfeign-service-client</artifactId>
    </dependency>

    <!-- springboot starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

启动类上加上扫描注解,完整测试代码如下:

// 扫描 FeignClient,可以指定扫描路径
@EnableFeignClients // ("priv.cqq.**.feign")
@SpringBootApplication
public class OpenfeignServiceCoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(OpenfeignServiceCoreApplication.class, args);
    }
    
    // FeignClient,name 为在注册中心中的服务的名称
    @FeignClient(name = "nacos-order-service")
    public interface OrderFeignService {

		// 具体服务的对外接口定义
        @GetMapping(value = "/echo/{string}")
        String echo(@PathVariable String string);
    }

    @RestController
    public class OpenfeignController {

        @Resource
        private OrderFeignService orderFeignService;

        @GetMapping(value = "/openfeign/order/{string}")
        public String echo(@PathVariable String string) {
            return orderFeignService.echo(string);
        }
    }
}

nacos-order-service 中的接口定义:

@RestController
public class OrderController {

    @Value("${server.port}")
    private String port;

    @GetMapping(value = "/echo/{string}")
    public String echo(@PathVariable String string) {
        return String.format("Order service %s %s", port, string);
    }
}

启动服务后,访问:http://localhost:6500/openfeign/order/cqq 查看打印结果。

简单的 Hello World 就完成了。其实 openfeign 的 FeignClient 接口很像 mybatis 中的 mapper 接口,都是在生成的代理类中封装了一些操作。比如 FeignClient 就是根据指定的服务名、调用方调用的接口 Method 实例(获取 SpringMVC 注解)等进行 HTTP 请求的组装。最后进行 RPC 调用,并将结果转为方法的返回参数。

feign 的项目模块划分

前面提到了我司使用的项目模块划分方式,除了 core 之外还有一个 client 项目,这么项目是做什么的,又为什么要划分出这么一个模块?会一一进行解释。

对于每个业务服务,比如订单服务,会创建一个对应的父工程:order-service(order-service 可能还会继承其他父项目)

...
<artifactId>order-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
...

<modules>
     <module>order-service-core</module>
     <module>order-service-client</module>
</modules>
...

并在此项目下在分别创建 core 与 client 子项目:

...
<artifactId>order-service-core</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
    <groupId>priv.cqq</groupId>
    <artifactId>order-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
...
...
<artifactId>order-service-client</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
    <groupId>priv.cqq</groupId>
    <artifactId>order-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
...

core 项目的职责就是负责订单服务的所有业务,可以理解为就是一个 SpringBoot 项目。

client 项目的职责有两个:

  1. 定义对其他内部服务暴露的接口(注意是对其他内部服务,而不是对外公开开放),以及接口的入参、出参,并在对应的 core 项目中对该接口进行实现(即编写对应的 Controller)
  2. 以依赖的形式提供给其他内部服务,方便其调用 core 项目中的 Feign 接口

这样划分可以清晰的划分出项目结构,定义出 Feign 接口(不同于普通的 RestController),并能更方便其他服务的引入。

举个栗子:

在 openfeign-service 中模拟订单服务,common 包中的内容可以被拆分到一个 common 项目中,为了方便演示临时放到 client 中了:

模拟订单服务-1

OrderFeignService 如下(FeignClient name 应该为 order-service,为了在本项目中演示,临时进行了修改):

@FeignClient(name = "nacos-openfeign-service")
public interface OrderFeignService {

    @GetMapping("/order/feign/selectById/{orderId}")
    R<OrderFeignVO> selectById(@PathVariable Long orderId);
}

在 core 项目中对 Feign 接口进行实现:

模拟订单服务-2

OrderFeignController 如下:

@RestController
public class OrderFeignController implements OrderFeignService {

    @Override
    public R<OrderFeignVO> selectById(Long orderId) {
        return R.success(new OrderFeignVO().setOrderId(orderId).setOrderCode("S" + orderId));
    }
}

在其他内部服务中引入依赖:

<dependency>
    <groupId>priv.cqq</groupId>
    <artifactId>openfeign-service-client</artifactId>
</dependency>

在其他服务中新增一个测试类:

@RestController
public class OrderController {

    @Resource
    private priv.cqq.openfeign.feign.OrderFeignService orderFeignService;

    @GetMapping(value = "/order/{orderId}")
    public R<OrderFeignVO> echo(@PathVariable Long orderId) {
        return orderFeignService.selectById(orderId);
    }
}

就可以正常使用其他服务提供的接口了。

关于 SpringMVC 如何解析接口方法

可以发现一个特别点(考察 SpringMVC 熟悉程度了),对于 Fegin 接口的实现类:OrderFeignController 上的接口方法,并没有显示声明 SpringMVC 的一些注解,如@RequestMapping、@GetMapping等,但依然被 SpringMVC 进行了接口绑定。也就是说,SpringMVC 会去尝试从 Controller 的继承链中的各个父类、接口节点搜索源重写方法,并读取搜索到的源重写方法 Method 实例上的 SpringMVC 注解作用到当前无注解方法上。具体实现可以从源码类 RequestMappingHandlerMapping 作为入口,该源码类会被注入到 Spring中,且该类实现了 Spring 中的初始化接口 InitializingBean。在初始化方法中会去搜索所有的 HandlerMethod,即接口方法:

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements MatchableHandlerMapping, EmbeddedValueResolverAware {
	...
	
	@Override
	@SuppressWarnings("deprecation")
	public void afterPropertiesSet() {
		...
		super.afterPropertiesSet();
	}
	
	...
}

public abstract class AbstractHandlerMethodMapping<T> 
	extends AbstractHandlerMapping implements InitializingBean {

	@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}

	protected void initHandlerMethods() {
		for (String beanName : getCandidateBeanNames()) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				processCandidateBean(beanName);
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}
	
	protected void processCandidateBean(String beanName) {
			Class<?> beanType = null;
			
			...
			
			if (beanType != null && isHandler(beanType)) {
				// ============================== 这里!!!!!==============================
				detectHandlerMethods(beanName);
			}
		}
	}

全部与局部配置

全局配置

@Configuration // @Configuration 表示为全局 Feign 配置
public class FeignConfig {

    @Bean
    // xx 配置

    @Bean
    // xx 配置

    @Bean
    // xx 配置
}

局部配置

public class OrderFeignServiceConfig{

    @Bean
    // xx 配置

    @Bean
    // xx 配置

    @Bean
    // xx 配置
}

@FeignClient(name = "nacos-openfeign-service", 
		     configuration = OrderFeignServiceConfig.class)
public interface OrderFeignService {
    ....
}

日志配置

spring 默认的日志级别是 INFO,而 Feign 日志是 DEBUG 级别。为了能正常输出日志,需要先修改一下配置中的日志级别:

logging:
  level:
    priv.cqq.openfeign.feign.*: debug

Feign 的日志级别

  public enum Level {
    /**
     * No logging.
     */
    NONE,
    /**
     * Log only the request method and URL and the response status code and execution time.
     */
    BASIC,
    /**
     * Log the basic information along with request and response headers.
     */
    HEADERS,
    /**
     * Log the headers, body, and metadata for both requests and responses.
     */
    FULL
  }
  1. NONE:适用于生产,性能最佳
  2. BASIC:适用于生产环境追踪问题
  3. FULL:适用于开发及测试环境定位问题
public class OrderFeignServiceConfig {

    // 日志配置
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

契约配置

兼容 Feign 接口中的 Feign 原生注解。项目中一般都是使用 SpringMVC 提供的注解,减少学习成本。但由于 SpringCloud 早期版本都是使用的 netflix 的 feign 而不是后面的 openfeign,为了使用低版本 SpringCloud 的老项目的升级而引入的兼容配置。

public class OrderFeignServiceConfig {

    // 契约配置:兼容 Feign 原生注解
    @Bean
    public Contract orderFeignContract() {
        return new Contract.Default();
    }
}

对应的 Feign 接口中必须使用原生注解:

@FeignClient(name = "nacos-openfeign-service", configuration = OrderFeignServiceConfig.class)
public interface OrderFeignService {

	 @RequestLine("GET /order/feign/selectById/{orderId}")
	 R<OrderFeignVO> selectById(@Param("orderId") Long orderId);
}

超时时间配置

public class OrderFeignServiceConfig {

    // 超时配置
    @Bean
    public Request.Options orderFeignRequestOptions() {
        // 1. connectTimeout: 建立连接超时时长
        // 2. readTimeout: 建立连接超时后最多等待被调用方响应时长
        // 3. followRedirects: 若响应 3xx 是否进行重定向
        return new Request.Options(
                5, TimeUnit.SECONDS,
                4, TimeUnit.SECONDS, 
                false
        );
    }
}

拦截器配置

public class OrderFeignServiceConfig {

    // 拦截器配置 (执行顺序根为 Bean 的配置顺序)
    @Bean
    public RequestInterceptor orderFeignAuthRequestInterceptor() {
        return new OrderFeignAuthRequestInterceptor();
    }

    @Bean
    public RequestInterceptor orderFeignLogRequestInterceptor() {
        return new OrderFeignLogRequestInterceptor();
    }
}
public class OrderFeignAuthRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        String auth = UUID.randomUUID().toString();
        template.header("Authorization", auth);
    }
}

@Slf4j
public class OrderFeignLogRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        log.info("Order feign config");
    }
}

负载均衡配置

Feign 底层的负载均衡依赖的依旧是 Ribbon,所以我们配置 Ribbon 的负载均很即可生效在 Feign 上。

public class OrderFeignServiceConfig {

	// 负载均衡配置
    @Bean
    public IRule feignLoadbalancer() {
        return new RandomRule();
    }
}

降级配置

feign 调用失败可以进行降级处理。需要注意,此时仅是因为提供方发生了业务异常,而并不是因为触发了配置的熔断规则(熔断规则将会在后面讲解 sentinel 中详细说明)。但,无论因为什么引发的异常,配置降级处理的步骤都是大致一样的。

bootstrap.yaml 中开启 openfeign 对 hystrix 的依赖(openfeign 不仅依赖 ribbon 也依赖 hystrix):

feign:
  hystrix:
    enabled: true

在配置 FeignClient 中配置 fallback,有两种方式:

  1. 指定具体的 fallback 处理器的 class 实例
  2. 指定可以返回 fallback 处理器的工厂类的 class 实例
@FeignClient(name = "nacos-openfeign-service", 
			 configuration = OrderFeignServiceConfig.class,
			 // Here !!!
			 // fallback = OrderFeignService.FallbackHandler.class
			 // fallbackFactory = OrderFeignService.FallbackHandlerFactory.class
)
public interface OrderFeignService {

     // 模拟订单服务
     @GetMapping("/order/feign/selectById/{orderId}")
     R<OrderFeignVO> selectById(@PathVariable Long orderId);

	@Component
    class FallbackHandler implements OrderFeignService {

        @Override
        public R<OrderFeignVO> selectById(Long orderId) {
        	log.error("降级处理,后续进行补偿");
            // 1. return redis cache data
            // 2. send mq message
            // 3. return specific business data
            return R.success(null);
        }
    }

    @Slf4j
    @Component
    class FallbackHandlerFactory implements FallbackFactory<OrderFeignService> {

        @Autowired
        @Qualifier("orderFeignService.FallbackHandler")
        private FallbackHandler fallbackHandler;

        @Override
        public OrderFeignService create(Throwable cause) {
            log.error(cause.getMessage());
            return fallbackHandler;
        }
    }
}

配置都很简单,更推荐使用工厂类的方式,因为可以获取到异常信息。 关于工厂类有两点需要说明一下:
1. 工厂类实现的接口是:feign.hystrix.FallbackFactoryorg.springframework.cloud.openfeign.FallbackFactory

2. 为什么降级处理写在了 client 模块而不是 core 模块?client 模块不应该是越精简越好吗?如果依赖 core 模块还可以避免引入一些为了支持补偿而引入的中间件或者数据库的依赖。确实是这样,如果强行写在 core 模块技术上是也可以的,我们在工厂类中返回的不再是具体的处理降级的 Bean,而是通过下面的方式进行 core 模块降级处理器的注入:

@Autowired
@Qualifier("fallbackHandler") // OrderFeignController 也实现了 OrderFeignService 
private OrderFeignService fallbackHandler; 

但这样会引发一个问题,引入该 client 模块的服务是没有这个降级处理 Bean 的。另外换个角度想一下,降级处理本身也是 feign 调用的一部分,实际上和 core 模块没有直接的关系。写在 core 模块主要是为了使用已引入的依赖,就像为了功能而进行继承,实际上二者并没有派生关系。

通过 Feign 请求外部 API

Feign 不仅仅可以用于内部服务间的通信,也可以像 RestTemplate 一样作为一个 HTTP 请求客户端调用外部接口。栗子:

@RestController
public class BaiduController {

    @Resource
    private BaiduFeignService baiduFeignService;

    @GetMapping(value = "/baidu")
    public String baidu() {
        return baiduFeignService.get();
    }
}

@FeignClient(name = "Baidu", url = "http://www.baidu.com")
public interface BaiduFeignService {
    @GetMapping
    String get();
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值