目录
OpenFeign简介
Feign能干什么
Feign和OpenFeign的区别
Spring Cloud Alibaba中OpenFeign的使用
1、OpenFeign简单使用
1.1、新建shop-common子模块
1.2、新建shop-order子模块编辑添加配置文件
1.3、引入依赖
1.4、在其他模块中引用common公共模块
1.5、 shop-order模块代码
1.6、shop-user模块代码
1.7、启动服务
1.8、通过feign接口访问服务
2、OpenFeign的进阶使用
2.1、openfeign接口添加请求头信息
2.2、fallback的使用
总结
Feign是一个声明式WebService客户端,使用Feign能让编写Web Service客户端更简单
它的使用方法是定义一个服务接口然后在上面添加注解,Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡
Feign旨在使编写Java Http客户端变得更容易
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量
Feign集成了Ribbon,通过feign只需要定义服务绑定接口并且以声明式的方法,优雅而简单的实现了服务调用
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequestMaping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMaping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
前文中已经建立了一个标识店铺用户信息的shop-user模块,本文将新建一个管理店铺订单信息的shop-order模块和公共管理shop-common模块(其中shop-common模块下主要是一些工具类,被shop-common模块和shop-order模块依赖)。这样,我们就可以用两个服务测试feign接口的功能。
其中shop-common的引入如下依赖:
org.apache.commons
commons-lang3
3.12.0
com.alibaba
fastjson
1.2.83
commons-codec
commons-codec
1.15
org.springframework.boot
spring-boot-starter-log4j2
在common中添加工具类,一些字符串操作和非空判断操作。
application.yml配置文件
server:
port: 9091
spring:
application:
name: shop-order
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: alibaba
group: alibaba
bootstrap.properties配置文件
spring.application.name=shop-order
spring.cloud.nacos.config.server-addr=localhost:8848
spring.cloud.nacos.config.extension-configs[0].data-id=shop-order.yaml
spring.cloud.nacos.config.extension-configs[0].group=alibaba
spring.cloud.nacos.config.extension-configs[0].refresh=true
spring.cloud.nacos.config.file-extension=yml
spring.cloud.nacos.config.namespace=alibaba
spring.cloud.nacos.config.group=alibaba
在父工程pom中引入starter-openfeign的依赖
org.springframework.cloud
spring-cloud-starter-openfeign
3.1.1
在order服务中先创建一个获取订单号的测试接口,建立一个controller层和service层,controller层代码分别如下:
package com.alibabashop.order.controller;
import com.alibabashop.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("getOrderNo")
public String getOrderNo(String userId){
return orderService.getOrderNo(userId);
}
}
service层实现类,目前没有查DB,做简单的判断
package com.alibabashop.order.service.impl;
import com.alibabashop.order.service.OrderService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements OrderService {
@Override
public String getOrderNo(String userId) {
if (StringUtils.isNotEmpty(userId) && userId.equals("testID12345")){
return "当前的订单号是:12345";
}else {
throw new RuntimeException("单号不存在");
}
}
}
启动类开启支持feign的远程调用的注解 @EnableFeignClients
package com.alibabashop.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableFeignClients
@ComponentScan(basePackages = {"com.alibabashop.*"})
public class ShopOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShopOrderApplication.class, args);
}
}
代码结构如下:
然后在user服务中创建一个OrderFeign接口用来调用order服务
package com.alibabashop.user.fegin;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "shop-order")
@Component
public interface OrderFeign {
@GetMapping("order/getOrderNo")
String getOrderNo(@RequestParam String userId);
}
@FeignClient标签的常用属性如下:
name/value:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
url: url一般用于调试,可以手动指定@FeignClient调用的地址
decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
path: 定义当前FeignClient的统一前缀,当我们项目中配置了server.context-path,server.servlet-path时使用
value 属性的值是order服务的服务名称,也就是注册到注册中心中的服务名称。
注:需要注意的是接口的参数添加一个@RequestParam注解,其作用是如果不加默认的注解,Feign则会对参数默认加上@RequestBody注解,而RequestBody一定是包含在请求体中的,GET方式无法包含,将执行报错。
我们在user服务中建立一个controller和service调用feign接口来测试一下
controller层
package com.alibabashop.user.controller;
import com.alibabashop.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("getOrderNo")
public String getOrderNo(String userId){
return userService.getOrderNo(userId);
}
}
service层
package com.alibabashop.user.service.impl;
import com.alibabashop.user.fegin.OrderFeign;
import com.alibabashop.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private OrderFeign orderFeign;
@Override
public String getOrderNo(String userId) {
return orderFeign.getOrderNo(userId);
}
}
代码结构如下:
启动user服务的时候发现报错了,是因为SpringCloud Feign在Hoxton.M2 RELEASED版本之后抛弃了Ribbon,使用了spring-cloud-loadbalancer,所以我们这里还需要引入spring-cloud-loadbalancer的依赖。
org.springframework.cloud
spring-cloud-loadbalancer
3.1.1
在父工程pom中引入负载均衡依赖之后重新启动项目,并查看nacos,发现服务已经注册成功。
通过postman或者浏览器访问user服务,结果如下:
有时候会在接口请求中传递一些头信息,其中有5种方式可以实现传递请求头信息:
在@RequestMapping注解里添加headers属性
在方法参数前面添加@RequestHeader注解
在方法或者类上添加@Headers的注解
在方法参数前面添加@HeaderMap注解
实现RequestInterceptor接口
本文只演示一个最简单的,在方法参数前面添加@RequestHeader注解。
修改OrderFeign接口
单个参数:
@GetMapping("order/getOrderNo")
String getOrderNo(@RequestParam String userId,@RequestParam String tenantId,@RequestHeader("Authorization") String token);
多个参数使用MultiValueMap:
@GetMapping("order/getOrderNo")
String getOrderNo(@RequestParam String userId, @RequestParam String tenantId, @RequestHeader MultiValueMap headers);
我们来测试下传递单个参数的情况
user服务代码修改
controller(添加HttpServletRequest参数):
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("getOrderNo")
public String getOrderNo(String userId, String tenantId, HttpServletRequest request){
return userService.getOrderNo(userId,tenantId,request);
}
}
service实现类(获取request中的头信息并传递给feign接口)
@Service
public class UserServiceImpl implements UserService {
@Autowired
private OrderFeign orderFeign;
@Override
public String getOrderNo(String userId, String tenantId, HttpServletRequest request) {
return orderFeign.getOrderNo(userId,tenantId, request.getHeader("token"));
}
}
order服务代码修改:
controller(获取头信息中的参数):
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("getOrderNo")
public String getOrderNo(String userId, String tenantId, HttpServletRequest request){
System.out.println("Authorization:" + request.getHeader("Authorization"));
return orderService.getOrderNo(userId,tenantId);
}
}
service(将传入的参数修改):
@Service
public class OrderServiceImpl implements OrderService {
@Override
public String getOrderNo(String userId,String tenantId) {
System.out.println(tenantId);
if (StringUtils.isNotEmpty(userId) && userId.equals("testID12345")){
return "当前的订单号是:12345";
}else {
throw new RuntimeException("单号不存在");
}
}
}
重启服务并用postman调用接口
成功获取到头信息
fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
然后我们看下怎么用。
先定义一个容错的处理类 OrderFeignHandler:
package com.alibabashop.user.handler;
import com.alibabashop.user.fegin.OrderFeign;
import org.springframework.stereotype.Component;
@Component
public class OrderFeignHandler implements OrderFeign {
@Override
public String getOrderNo(String userId, String tenantId, String token) {
String fallback = "当前查询人数过多,请稍后重试!";
return fallback;
}
}
@FeignClient 注解添加fallback属性 fallback = OrderFeignHandler.class
import com.alibabashop.user.handler.OrderFeignHandler;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "shop-order",fallback = OrderFeignHandler.class)
@Component
public interface OrderFeign {
@GetMapping("order/getOrderNo")
String getOrderNo(@RequestParam String userId,@RequestParam String tenantId
,@RequestHeader("Authorization") String token);
}
重启user服务,先获取一下正常的数据
正常返回订单号
我们在模拟一下异常的情况 修改order服务中的获取单号接口,找不到单号则抛异常
@Service
public class OrderServiceImpl implements OrderService {
@Override
public String getOrderNo(String userId,String tenantId) {
System.out.println(tenantId);
if (StringUtils.isNotEmpty(userId) && userId.equals("mdx123456")){
return "O111222333444";
}else {
throw new RuntimeException("单号不存在");
}
}
}
重启order服务,参数中传递一个不存在的订单号
发现并没有返回我们想要的信息
检查代码,发现我们少了配置sentinel,因为我们使用的是alibaba微服务体系,所以我们使用sentinel来做熔断 sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性(sentinel如何使用我们后面的章节会讲到)。
在父工程pom下添加sentinel 依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
${spring-cloud-alibaba.version}
yml配置文件添加feign开启sentinel的配置
feign:
sentinel:
enabled: true
重启user服务,再次访问接口
我们可以看到order服务抛出了订单号不存在的异常
但是我们的接口成功返回了容错接口定义的信息
至此,本文简单使用openfegin的目的就完成了。在本文的第一阶段,通过在项目中新建一个shop-order的子模块,在该模块中建立订单查询的逻辑,同时修改之前文章中的shop-user子模块下的代码,通过新建OrderFegin接口,配置该接口FeignClient的参数,指定需要访问的服务,即可实现调用该服务。此外,本文下半部分介绍了openfeign接口添加请求头信息和fallback属性的使用。同时提到了SpringCloud Alibaba用来做熔断的sentinel组件,下一篇我们将进行学习总结。