Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用
1、简介
官网:https://spring.io/projects/spring-cloud-openfeign
feign是一个声明式WebService 客户端,使用Feign能让编写Web Service客户端更加简单,我们只需创建一个接口并用注解的方式来配置它,就可以实现对某个服务接口的调用,简化了直接使用RestTemplate来调用服务接口的开发量。Feign具备可插拔的注解支持,同时支持Feign注解、JAX-RS注解及SpringMvc注解。当使用Feign时,Spring Cloud集成了Ribbon和Eureka以提供负载均衡的服务调用及基于Hystrix的服务容错保护功能。
他的使用方法是定义一个服务接口,然后在上面添加注解。Feign也支持可插拔式的编码和解码器。Spring Cloud 对Feign进行了封装,使其支持Spring MVC 标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
2、Feign能干什么?
Feign使编写java Http客户端更加容易
前面使用Ribbon + RestTemplate时,利用RestTemplate对Http请求的封装处理,形成了一套模板化的调用方法,但是在实际的开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多次调用。所以Feign在此基础上做了进一步的封装,由他来帮助我们定义和实现依赖服务接口。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon,自动封装服务调用客户端的工作量。
Feign集成了Ribbon
Ribbon维护了服务列表信息,并且通过轮询的方式实现客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务的调用。
Feign 与 OpenFeign的区别?
|
Feign |
OpenFeign |
|
Feign是Spring Cloud组件中的一个轻量级RestFul的Http服务客户端Feign内置了Ribbon,用来做客户端的负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口就可以调用服务注册中心的服务。 |
OpenFeign是Spring Cloud在feign的基础上支持了SpringMvc的注解,如@RequestMapping等等 OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他的服务 |
|
<dependency> <groupId>org.springframework.cloud</ groupId > <artifactId>spring-cloud-starter-feign</ artifactId> </ dependency > |
<!-- openfeign--> |
3、OpenFeign 的使用:
3.1、在启动类上添加 @EnableFeignClients 注解来启用Feign的客户端功能。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignServiceApplication {
public static void main(String[] args) {
SpringApplication.run(FeignServiceApplication.class, args);
}
}
3.2、添加UserService接口完成对user-service服务的接口绑定
@FeignClient(value = "user-service")
public interface UserService {
@PostMapping("/user/create")
CommonResult create(@RequestBody User user);
@GetMapping("/user/{id}")
CommonResult<User> getUser(@PathVariable Long id);
@GetMapping("/user/getByUsername")
CommonResult<User> getByUsername(@RequestParam String username);
@PostMapping("/user/update")
CommonResult update(@RequestBody User user);
@PostMapping("/user/delete/{id}")
CommonResult delete(@PathVariable Long id);
}
3.3、修改yml,OpenFeign默认的等待时间1秒钟,超时后将报错:
server:
port: 80
eureka:
client:
register-with-eureka: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
spring:
application:
name: feign-customer-order
# 设置feign客户端超时时间(openFeign默认支持Ribbon)
ribbon:
# 建立连接后从服务器读取到可用资源的时间
ReadTimeout: 5000
# 建立连接所用的时间,适用于网络正常的情况下,两端连接所用的时间
ConnectTimeout: 5000
4、Feign中的服务降级
Feign中的服务降级使用起来非常方便,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可,下面我们为UserService接口添加一个服务降级实现类。
4.1、添加服务降级实现类UserFallbackService, 需要注意的是它实现了UserService接口,并且对接口中的每个实现方法进行了服务降级逻辑的实现。
public class UserFallbackService implements UserService {
@Override
public CommonResult create(User user) {
User defaultUser = new User(-1L, "defaultUser", "123456");
return new CommonResult<>(defaultUser);
}
@Override
public CommonResult<User> getUser(Long id) {
User defaultUser = new User(-1L, "defaultUser", "123456");
return new CommonResult<>(defaultUser);
}
@Override
public CommonResult<User> getByUsername(String username) {
User defaultUser = new User(-1L, "defaultUser", "123456");
return new CommonResult<>(defaultUser);
}
@Override
public CommonResult update(User user) {
return new CommonResult("调用失败,服务被降级",500);
}
@Override
public CommonResult delete(Long id) {
return new CommonResult("调用失败,服务被降级",500);
}
}
4.2、修改UserService接口,设置服务降级处理类为UserFallbackService
修改@FeignClient注解中的参数,设置fallback为UserFallbackService.class即可。
@FeignClient(value = "user-service", fallback = UserFallbackService.class)
public interface UserService {
....
}
4.3、修改application.yml,开启Hystrix功能
feign:
hystrix:
enabled: true #在Feign中开启Hystrix
5、日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。
日志级别:
- NONE:默认的,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
- FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
我们通过java配置来使Feign打印最详细的Http请求日志信息。
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
在application.yml中配置需要开启日志的Feign客户端
logging:
level:
com.openapi.service: debug
这里的com.openapi.service是openFeign接口所在的包名,当然你也可以配置一个特定的openFeign接口。
6、Feign的常用配置
feign:
hystrix:
enabled: true #在Feign中开启Hystrix
compression:
request:
enabled: false #是否对请求进行GZIP压缩
mime-types: text/xml,application/xml,application/json #指定压缩的请求数据类型
min-request-size: 2048 #超过该大小的请求会被压缩
response:
enabled: false #是否对响应进行GZIP压缩
logging:
level: #修改日志级别
com.dw.cloud.service.UserService: debug
如果openFeign没有设置对应得超时时间,那么将会采用Ribbon的默认超时时间。
理解了超时设置的原理,由之产生两种方案也是很明了了,如下:
- 设置openFeign的超时时间
- 设置Ribbon的超时时间(默认1s)
1、设置Ribbon的超时时间(不推荐)
设置很简单,在配置文件中添加如下设置:
ribbon:
# 值的是建立链接所用的时间,适用于网络状况正常的情况下, 两端链接所用的时间
ReadTimeout: 5000
# 指的是建立链接后从服务器读取可用资源所用的时间
ConectTimeout: 5000
2、设置openFeign的超时时间(推荐)
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
7、OpenFeign GET 请求使用 @SpringQueryMap 传递对象参数
4.1、SpringCloud在2.1.x版本中提供了 @SpringQueryMap 注解,可以传递对象参数。 但是传递的对象参数中只能包含基本类型,如果需要在传递的参数对象中, 包含其他的对象类型, 需要自定义
类实现 QueryMapEncode 接口,自定义参数解析。具体配置如下:
@Configuration
public class FeignConfiguration {
/**
* 使用@SpringQueryMap可以解决GET请求的时候,传递对象
* 关于使用 @SpringQueryMap不能解析对象中的对象的问题
* @return
*/
@Bean
public Feign.Builder feignBuilder() {
return Feign.builder()
.queryMapEncoder(new BeanQueryMapNestEncoder())
.retryer(Retryer.NEVER_RETRY);
}
/**
* 配置feign日志级别
* @return
*/
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
4.2、自定义 BeanQueryMapNestEncoder 传递复杂参数解析配置:
import feign.Param;
import feign.QueryMapEncoder;
import feign.codec.EncodeException;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
public class BeanQueryMapNestEncoder implements QueryMapEncoder {
private final Map<Class<?>, BeanQueryMapNestEncoder.ObjectParamMetadata> classToMetadata = new HashMap();
public BeanQueryMapNestEncoder() {
}
public Map<String, Object> encode(Object object) {
try {
Map<String, Object> propertyNameToValue = new HashMap();
this.putPropertiesToMap((String)null, object, propertyNameToValue);
return propertyNameToValue;
} catch (IntrospectionException | InvocationTargetException | IllegalAccessException var3) {
throw new EncodeException("Failure encoding object into query map", var3);
}
}
private void putPropertiesToMap(String parentProperty, Object object, Map<String, Object> propertyNameToValue)
throws IntrospectionException, InvocationTargetException, IllegalAccessException {
BeanQueryMapNestEncoder.ObjectParamMetadata metadata = this.getMetadata(object.getClass());
Iterator var5 = metadata.objectProperties.iterator();
while(true) {
while(true) {
PropertyDescriptor pd;
Method method;
Object value;
StringBuffer nameBuffer;
do {
do {
if (!var5.hasNext()) {
return;
}
pd = (PropertyDescriptor)var5.next();
method = pd.getReadMethod();
value = method.invoke(object);
nameBuffer = new StringBuffer();
if (StringUtils.isNotEmpty(parentProperty)) {
nameBuffer.append(parentProperty).append('.');
}
} while(value == null);
} while(value == object);
Param alias = (Param)method.getAnnotation(Param.class);
nameBuffer.append(alias != null ? alias.value() : pd.getName());
if (BeanUtils.isSimpleProperty(value.getClass())) {
propertyNameToValue.put(nameBuffer.toString(), value);
} else if (!(value instanceof Collection)) {
this.putPropertiesToMap(nameBuffer.toString(), value, propertyNameToValue);
} else {
StringBuffer arrValueBuffer = new StringBuffer();
Object arrObj;
for(Iterator var12 = ((Collection)value).iterator(); var12.hasNext(); arrValueBuffer.append(arrObj)) {
arrObj = var12.next();
if (!BeanUtils.isSimpleProperty(arrObj.getClass())) {
break;
}
if (arrValueBuffer.length() != 0) {
arrValueBuffer.append(',');
}
}
if (arrValueBuffer.length() > 0) {
propertyNameToValue.put(nameBuffer.toString(), arrValueBuffer.toString());
}
}
}
}
}
private BeanQueryMapNestEncoder.ObjectParamMetadata getMetadata(Class<?> objectType) throws IntrospectionException {
BeanQueryMapNestEncoder.ObjectParamMetadata metadata = (BeanQueryMapNestEncoder.ObjectParamMetadata)this.classToMetadata.get(objectType);
if (metadata == null) {
metadata = BeanQueryMapNestEncoder.ObjectParamMetadata.parseObjectType(objectType);
this.classToMetadata.put(objectType, metadata);
}
return metadata;
}
private static class ObjectParamMetadata {
private final List<PropertyDescriptor> objectProperties;
private ObjectParamMetadata(List<PropertyDescriptor> objectProperties) {
this.objectProperties = Collections.unmodifiableList(objectProperties);
}
private static BeanQueryMapNestEncoder.ObjectParamMetadata parseObjectType(Class<?> type) throws IntrospectionException {
List<PropertyDescriptor> properties = new ArrayList();
PropertyDescriptor[] var2 = Introspector.getBeanInfo(type).getPropertyDescriptors();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
PropertyDescriptor pd = var2[var4];
boolean isGetterMethod = pd.getReadMethod() != null && !"class".equals(pd.getName());
if (isGetterMethod) {
properties.add(pd);
}
}
return new BeanQueryMapNestEncoder.ObjectParamMetadata(properties);
}
}
}
8、Feign 异常处理
5.1、自定义Feign解析器:
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.crecgec.baseboot.jsoncore.exception.BaseException;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class FeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
try {
// 这里直接拿到我们抛出的异常信息
String message = Util.toString(response.body().asReader());
try {
JSONObject jsonObject = JSONObject.parseObject(message);
return new BaseException(jsonObject.getString("resultMsg"), jsonObject.getInteger("resultCode"));
} catch (JSONException e) {
e.printStackTrace();
}
} catch (IOException ignored) {
}
return decode(methodKey, response);
}
}
5.2、定义系统的异常类
public class BaseException extends RuntimeException {
private int status ;
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public BaseException() {
}
public BaseException(String message, int status) {
super(message);
this.status = status;
}
public BaseException(String message) {
super(message);
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(Throwable cause) {
super(cause);
}
public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
5.3、统一异常拦截转换对应的异常信息返回前端
public class ResultSet {
/**
* 返回的状态码
*/
private Integer resultCode;
/**
* 返回的消息
*/
private String resultMsg;
/**
* 返回的数据
*/
private Object data;
public ResultSet() {
}
public ResultSet(Integer resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
public ResultSet(Integer resultCode, String resultMsg, Object data) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
this.data = data;
}
public Integer getResultCode() {
return resultCode;
}
public void setResultCode(Integer resultCode) {
this.resultCode = resultCode;
}
public String getResultMsg() {
return resultMsg;
}
public void setResultMsg(String resultMsg) {
this.resultMsg = resultMsg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
5.4、全局异常类处理配置:
@ExceptionHandler(value = BaseException.class)
public ResultSet defaultErrorHandler(HttpServletRequest req, HttpServletResponse resp, BaseException e) {
ResultSet resultSet = new ResultSet();
if (e.getStatus() == 400) {
resultSet.setResultCode(-1);
resultSet.setResultMsg(e.getMessage());
resultSet.setData(null);
resp.setStatus(400);
} else {
resp.setStatus(500);
if(logger.isErrorEnabled()){
logger.error("系统异常,请联系系统开发人员进行处理", e);
}
resultSet.setResultCode(-1);
resultSet.setResultMsg(e.getMessage());
resultSet.setData(null);
}
return resultSet;
}
9、使用OpenFeign 实现文件传输
参考 github上 的实现方式:https://github.com/OpenFeign/feign-form
6.1、引入依赖(如果引入的 OpenFeign 中没有以下依赖):
<dependencies>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>
</dependencies>
6.2、Feign 调用端接口:
/**
* @Author dw
* @ClassName TestFileService
* @Description
* @Date 2021/9/20 10:11
* @Version 1.0
*/
@FeignClient(value = "cloud-test-server")
public interface ITestFileService {
@RequestMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE )
public Object fileUpload(@RequestPart("file")MultipartFile multipartFile, @SpringQueryMap MyUser myUser);
}
6.3、调用端的Controller
@RestController
@RequestMapping("file")
public class TestFileController {
@Autowired
private ITestFileService testFileService;
@RequestMapping("upload")
public Object fileUpload(@RequestPart("file") MultipartFile multipartFile){
Role role = new Role("123", "管理員", 3);
MyUser user = new MyUser("张三", 23, role);
Object o = testFileService.fileUpload(multipartFile, user);
return o;
}
}
6.4、服务端Controller
@RestController
@RequestMapping("file")
public class TestController {
@RequestMapping("upload")
public Object fileUpload(@RequestPart("file") MultipartFile multipartFile, MyUser myUser){
System.out.println(multipartFile);
return myUser;
}
}
OK...文件传输完成了
10、如何替换默认的httpclient?
Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。
在生产环境中,通常不使用默认的http client,通常有如下两种选择:
- 使用ApacheHttpClient
- 使用OkHttp
至于哪个更好,其实各有千秋,我比较倾向于ApacheHttpClient,毕竟老牌子了,稳定性不在话下。
那么如何替换掉呢?其实很简单,下面演示使用ApacheHttpClient替换。
1、添加ApacheHttpClient依赖
在openFeign接口服务的pom文件添加如下依赖:
<!-- 使用Apache HttpClient替换Feign原生httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
为什么要添加上面的依赖呢?从源码中不难看出,请看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration这个类,代码如下:
上述红色框中的生成条件,其中的@ConditionalOnClass(ApacheHttpClient.class),必须要有ApacheHttpClient这个类才会生效,并且feign.httpclient.enabled这个配置要设置为true。
2、配置文件中开启
在配置文件中要配置开启,代码如下:
feign:
client:
httpclient:
# 开启 Http Client
enabled: true
3、如何验证已经替换成功?
其实很简单,在feign.SynchronousMethodHandler#executeAndDecode()这个方法中可以清楚的看出调用哪个client,如下图:
上图中可以看到最终调用的是ApacheHttpClient。