feign是一个声明式的Web服务客户端。这使得Web服务客户端的写入更加方便 要使用Feign创建一个界面并对其进行注释。它具有可插入注释支持,包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。Spring Cloud增加了对Spring MVC注释的支持,并使用Spring Web中默认使用的HttpMessageConverters
。Spring Cloud集成Ribbon和Eureka以在使用Feign时提供负载均衡的http客户端。
在分布式横行的时代,服务与服务之间的通信也越来越频繁,而spring cloud作为分布式的佼佼者,服务通信这一块当然也不再话下,oprnFeign就是cloud提供服务通信的组建。
需求
在订单模块通过用户id查询用户信息
1.新建eureka
请参考:springcloud(一)注册中心eureka
2.新建config配置中心
请参考:springcloud(二)配置中心config
3.打开cloud-order,引入openFeign依赖
org.springframework.cloud
spring-cloud-starter-openfeign
4.在cloud-user中提供一个根据用户id查询用户信息的接口
package com.ymy.coulduser.controller;
import com.ymy.coulduser.vo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RefreshScope
@Slf4j
public class UserController {
private static Map map = new ConcurrentHashMap();
static {
User user1 = new User();
user1.setId(1);
user1.setUserName("张三");
user1.setAge(18);
map.put(1,user1);
User user2 = new User();
user2.setId(2);
user2.setUserName("李四");
user2.setAge(20);
map.put(2,user2);
}
@Value("${test.name}")
private String name;
@RequestMapping(value = "/test",method = RequestMethod.GET)
public String getUserName(){
return "hello:"+name+" 人生总是起起落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落!";
}
@GetMapping(value = "userInfo")
public User getUserInfo(@RequestParam("userId") Integer userId){
if(null == userId){
log.info("用户id为空!");
return null;
}
User user = map.get(userId);
return user;
}
}
这里我是模拟的,数据都是在Map中造的,这里的重点不是数据,而是服务调用的这个过程。
5.cloud-order中新建一个与cloud-user通信的接口类
内容为:
package com.ymy.feign;
import com.ymy.entity.vo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "cloud-user")
public interface UserClient {
@GetMapping(value = "userInfo")
User getUserInfo(@RequestParam("userId") final Integer userId);
}
@FeignClient:表示该类是一个远程调用类,交给jdk动态代理处理。
value:需要调用的服务名。
TestController:
package com.ymy.controller;
import com.ymy.entity.vo.Order;
import com.ymy.entity.vo.User;
import com.ymy.feign.UserClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
@Slf4j
@RefreshScope
public class TestController {
@Value("${test.name}")
private String name;
private UserClient userClient;
TestController(UserClient userClient){
this.userClient = userClient;
}
@GetMapping(value = "/test")
public String test(){
return "hello:"+name+" 人生总是起起落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落!";
}
@GetMapping(value = "orderInfo")
public Order getOrderInfo(){
Order order = new Order();
order.setId(1);
order.setUserId(2);
order.setOorderPrice(BigDecimal.valueOf(100l));
order.setNum(2);
User userInfo = userClient.getUserInfo(order.getUserId());
if(null == userInfo){
log.info("没有查询到用户信息");
}
order.setUser(userInfo);
return order;
}
}
6.在启动类中开启feign远程调用
package com.ymy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CloudOrderApplication {
public static void main(String[] args) {
SpringApplication.run(CloudOrderApplication.class, args);
}
}
@SpringBootApplication:springboot启动注解
@EnableDiscoveryClient:将服务注册到注册中心
@EnableFeignClients:开启feign,如果不加此注解,服务调用将不会成功。
7.启动cloud-eureka、cloud-config、cloud-user、cloud-order,测试
其中user则是cloud-order通过调用cloud-user服务拿到的,这是理想状态,如果出现意外情况呢?比如cloud-user服务宕机了会发生什么呢?,现在我停掉cloud-user服务,再次请求:
这是你会发现,服务器出现了报错,这种错误是不能出现在用户眼中的,有没有方法解决呢?
feign自带熔断,所以我们不需要在引入其他的依赖,feign的熔断有两种实现方式,如下:
UserClient是一个接口,这时候我们需要用一个类来实现这个接口
UserClientHystrix.java
package com.ymy.feign.hystrix;
import com.ymy.entity.vo.User;
import com.ymy.feign.UserClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class UserClientHystrix implements UserClient {
@Override
public User getUserInfo(Integer userId) {
//这里就是当远程调用失败时,feign会自动进入接口的实现类,就是之前所说的熔断,
// 在这里,你可以做对应的处理,比如返回一个空的User对象,或者启用备用方案,调用联外一台服务等等。
//这里我们直接返回空User对象
log.info("请求cloud-user服务发生错误");
return null;
}
}
还需要一步那就是在接口指明实现类:
@FeignClient(value = "cloud-user",fallback = UserClientHystrix.class)
重启cloud-order,cloud-user保持宕机状态:
发现报错了,而且还是和第一次的错误不一致,这是什么问题导致的呢?这是因为我们没有启动feign的熔断机制,默认时关闭状态,找到bootstrap.yml文件:
feign:
hystrix:
enabled: true #开启feign熔断
第二种:指定回调
指定实现类这种方式需要对每个接口做实现,如果所有的请求接口都只需要返回空的话,这样就会重复写很多一样的实现方法,所以这里引入了回调。
1.注释掉实现类,引入hystrix依赖:
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
2.引入注解:@HystrixCommand
@HystrixCommand:这里指明服务调用失败时需要做什么。
fallbackMethod:指明失败之后进入哪个回调方法。
/**
* 失败之后走这里
* @return
*/
public Order defaultBack(){
log.info("请求cloud-user服务发生错误");
Order order = new Order();
order.setId(110);
order.setUserId(120);
order.setOorderPrice(BigDecimal.valueOf(1000l));
order.setNum(20);
return order;
}
3.在启动类中加入:@EnableCircuitBreaker
以上两种方式都可以做熔断处理,具体选择哪种,还要看业务的需求。
@EnableFeignClients做了什么?
进入FeignClientsRegistrar类
一看,蒙蔽了,这么多方法,这要怎么看,其实我们需要注意的并不多,我们重点来看一下registerFeignClients方法,打上断点,启动服务:
basePackages:指的是扫描的包,下一步对扫描出来的包做遍历
这里是一个集合,找到com.ymy下面有多少个被FeignClient注解标识的类。
在UserClient类中只指定了value属性,所以这里只展示value和name,随后就会将这些被@FeignClient注解标识的类交由jdk动态代理。
这是启动部分,我们再来看看调用过程
spring-cloud-openfeign源码:https://github.com/spring-cloud/spring-cloud-openfeign
Feign源码:https://github.com/OpenFeign/feign
spring-cloud-openfeign是我们项目中使用的依赖,而spring-cloud-openfeign又引入了feign依赖,这里我们只看调用过程,真正的调用过程在Feign源码中,请看:SynchronousMethodHandler.java
Object[] argv :参数,这里我只传入了一个userId 值为2。
我们再来看看buildTemplateFromArgs.create(argv):
varBuilder:
参数列表,Map集合,回到invoke()方法,
Options options = findOptions(argv) :拿到连接超时等信息
这是默认配置,如果我们在配置文件配置了超时时间,那么这里获取到的就是我们配置的。
进入 executeAndDecode()
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
boolean shouldClose = true;
try {
if (logLevel != Logger.Level.NONE) {
response =
logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
}
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
if (response.body().length() == null ||
response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
shouldClose = false;
return response;
}
// Ensure the response body is disconnected
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
}
throw errorReading(request, response, e);
} finally {
if (shouldClose) {
ensureClosed(response.body());
}
}
}
Request request = targetRequest(template); 拿到一个完整的request域:
进入 client.execute(request, options)方法:
看到这里是不是看到了熟悉的味道,没错,这里就是通过http协议发送请求,所以cloud的feign与dubbo是由区别的,dubbo采用rpc,feign则采用http,rpc基于netty,各有各的优点,选型的时候选择适合公司的才是最好的,其实到这里基本上可以不用往下说了,如果感兴趣的朋友可以跟着源码走一下。
FeignClientsRegistrar.java:项目启动是调用,处理被FeignClient标识的接口
SynchronousMethodHandler:feign的动态代理,远程调用的调用。
feign可以让程序以非常少的代码就能实现服务与服务之间的通信,还能再调用出错时给出熔断机制,是一个很强大的组件。
关于使用dubbo还是使用spring cloud,我觉得还是具体系统具体分析,比如开发人员熟悉dubbo,那还是用dubbo比较好,学习新的框架需要时间成本,还要根据项目的大小,开发的难易程度等等做出对应的选择,不是最新的就是最好的,选择适合自己的才是最好的。