# 服务调用
# 基本介绍
- Feign
Feign 是Spring Cloud Netflix
组件中的一量级Restful
的 HTTP 服务客户端,实现了负载均衡和 Rest 调用的开源框架,封装了Ribbon
和RestTemplate
, 实现了WebService
的面向接口编程,进一步降低了项目的耦合度。
- 什么是服务调用
顾名思义,就是服务之间的接口互相调用,在微服务架构中很多功能都需要调用多个服务才能完成某一项功能。
- 为什么要使用Feign
Feign 旨在使编写 JAVA HTTP 客户端变得更加简单,Feign 简化了RestTemplate
代码,实现了Ribbon
负载均衡,使代码变得更加简洁,也少了客户端调用的代码,使用 Feign 实现负载均衡是首选方案,只需要你创建一个接口,然后在上面添加注解即可。
Feign 是声明式服务调用组件,其核心就是:像调用本地方法一样调用远程方法,无感知远程 HTTP 请求。让开发者调用远程接口就跟调用本地方法一样的体验,开发者完全无感知这是远程方法,无需关注与远程的交互细节,更无需关注分布式环境开发。
- Feign vs OpenFeign
Feign 内置了Ribbon
,用来做客户端负载均衡调用服务注册中心的服务。
Feign 支持的注解和用法参考官方文档:https://github.com/OpenFeign/feign
官方文档,使用 Feign 的注解定义接口,然后调用这个接口,就可以调用服务注册中心的服务。
Feign
本身并不支持Spring MVC
的注解,它有一套自己的注解,为了更方便的使用Spring Cloud
孵化了OpenFeign
。并且支持了Spring MVC
的注解,如@RequestMapping
,@PathVariable
等等。
OpenFeign
的@FeignClient
可以解析Spring MVC
的@RequestMapping
注解下的接口,并通过动态代理方式产生实现类,实现类中做负载均衡调用服务。
# 如何使用
1、添加依赖
<!-- spring cloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2
3
4
5
2、新建RemoteUserService.java
服务接口
package com.ruoyi.system.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.ruoyi.common.core.constant.ServiceNameConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.api.factory.RemoteUserFallbackFactory;
import com.ruoyi.system.api.model.LoginUser;
/**
* 用户服务
*
* @author ruoyi
*/
@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class)
public interface RemoteUserService
{
/**
* 通过用户名查询用户信息
*
* @param username 用户名
* @return 结果
*/
@GetMapping(value = "/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
3、新建RemoteUserFallbackFactory.java
降级实现
package com.ruoyi.system.api.factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.api.RemoteUserService;
import com.ruoyi.system.api.model.LoginUser;
import feign.hystrix.FallbackFactory;
/**
* 用户服务降级处理
*
* @author ruoyi
*/
@Component
public class RemoteUserFallbackFactory implements FallbackFactory<RemoteUserService>
{
private static final Logger log = LoggerFactory.getLogger(RemoteUserFallbackFactory.class);
@Override
public RemoteUserService create(Throwable throwable)
{
log.error("用户服务调用失败:{}", throwable.getMessage());
return new RemoteUserService()
{
@Override
public R<LoginUser> getUserInfo(String username)
{
return R.fail("获取用户失败:" + throwable.getMessage());
}
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
4、消费者TestUserController.java
新增info
查询用户方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestUserController
{
@Autowired
private RemoteUserService remoteUserService;
/**
* 获取当前用户信息
*/
@GetMapping("/user/{username}")
public Object info(@PathVariable("username") String username)
{
return remoteUserService.getUserInfo(username);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
5、启动类添加@EnableRyFeignClients
注解,默认的@EnableRyFeignClients
扫描范围com.ruoyi
。
6、启动后访问http://localhost:8888/user/admin
,返回正确数据表示测试通过。
提示
目前已经存在ruoyi-api-system
系统接口模块,用于服务调用。
# 负载均衡
Feign
默认集成了Ribbon
,Nacos
也很好的兼容了Feign
,默认实现了负载均衡的效果。
# 请求传参
Get
方式传参,使用@PathVariable
、@RequestParam
注解接收请求参数
@GetMapping(value = "/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username);
2
Post
方式传参,使用@RequestBody
注解接收请求参数。
@PostMapping("/operlog")
public R<Boolean> saveLog(@RequestBody SysOperLog sysOperLog);
2
# 性能优化
# Gzip压缩
gzip
是一种数据格式,采用deflate
算法压缩数据。gzip
大约可以帮我们减少70%
以上的文件大小。
全局配置
server:
compression:
# 是否开启压缩
enabled: true
# 配置支持压缩的 MIME TYPE
mime-types: text/html,text/xml,text/plain,application/xml,application/json
2
3
4
5
6
局部配置
feign:
compression:
request:
# 开启请求压缩
enabled: true
# 配置压缩支持的 MIME TYPE
mime-types: text/xml,application/xml,application/json
# 配置压缩数据大小的下限
min-request-size: 2048
response:
# 开启响应压缩
enabled: true
2
3
4
5
6
7
8
9
10
11
12
提示
开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点。
# Http连接池
两台服务器建立HTTP
连接的过程涉及到多个数据包的交换,很消耗时间。采用HTTP
连接池可以节约大量的时间提示吞吐量。
Feign
的HTTP
客户端支持3种框架:HttpURLConnection
、HttpClient
、OkHttp
。
默认是采用java.net.HttpURLConnection
,每次请求都会建立、关闭连接,为了性能考虑,可以引入httpclient
、okhttp
作为底层的通信框架。
例如将Feign
的HTTP
客户端工具修改为HttpClient
。
1、添加依赖
<!-- feign httpclient -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2
3
4
5
2、全局配置
feign:
httpclient:
# 开启httpclient
enabled: true
2
3
4
3、测试验证
// RemoteUserService FeignClient
@GetMapping("/user/pojo")
public Object selectUser(SysUser user);
// 消费端
@Autowired
private RemoteUserService remoteUserService;
@GetMapping("/user/pojo")
public Object UserInfo(SysUser user)
{
return remoteUserService.selectUser(user);
}
// 服务端
@GetMapping("/pojo")
public R<SysUser> selectUser(@RequestBody SysUser user)
{
return R.ok(userService.selectUserByUserName(user.getUserName()));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
4、启动后访问http://localhost:8888/user/pojo?userName=ry
,返回正确数据表示测试通过。
# 日志配置
浏览器发起的请求可以通过F12
查看请求和响应信息。如果想看微服务中每个接口我们可以使用日志配置方式进行查看详细信息。
配置文件logback.xml
设置com.ruoyi
日志级别为debug
全局配置
@Bean
public Logger.Level getLog()
{
return Logger.Level.FULL;
}
2
3
4
5
局部配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import feign.Logger;
/**
* Feign 客户端配置
*
* @author ruoyi
*/
@Configuration
public class FeignConfiguration
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
// ====== 在客户端接口指定此配置 ======
/**
* 用户服务
*
* @author ruoyi
*/
@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class, configuration = FeignConfiguration.class)
public interface RemoteUserService
{
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 请求超时
Feign
的负载均衡底层用的就是Ribbon
,所以请求超时其实就只需要配置Ribbon
参数。
全局配置
# 请求处理的超时时间
ribbon:
ReadTimeout: 10000
ConnectTimeout: 10000
2
3
4
局部配置
# ruoyi-xxxx 为需要调用的服务名称
ruoyi-xxxx:
ribbon:
ReadTimeout: 10000
ConnectTimeout: 10000
2
3
4
5
# 异常配置
1、配置开启
feign:
hystrix:
enabled: true
2
3
2、FeignClient
接口服务加入fallbackFactory
@FeignClient(fallbackFactory = RemoteUserFallbackFactory.class)
3、添加接口实现异常类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import feign.hystrix.FallbackFactory;
/**
* 用户服务降级处理
*
* @author ruoyi
*/
@Component
public class RemoteUserFallbackFactory implements FallbackFactory<RemoteUserService>
{
private static final Logger log = LoggerFactory.getLogger(RemoteUserFallbackFactory.class);
@Override
public RemoteUserService create(Throwable throwable)
{
log.error("用户服务调用失败:{}", throwable.getMessage());
return new RemoteUserService()
{
@Override
public Object getUserInfo(String username)
{
return "获取用户失败:" + throwable.getMessage();
}
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 请求拦截器
在微服务应用中,通过feign
的方式实现http
的调用,可以通过实现feign.RequestInterceptor
接口在feign
执行后进行拦截,对请求头等信息进行修改。
例如项目中利用feign
拦截器将本服务的userId
、userName
、authentication
传递给下游服务
package com.ruoyi.common.security.feign;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* feign 请求拦截器
*
* @author ruoyi
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor
{
@Override
public void apply(RequestTemplate requestTemplate)
{
HttpServletRequest httpServletRequest = ServletUtils.getRequest();
if (StringUtils.isNotNull(httpServletRequest))
{
Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
// 传递用户信息请求头,防止丢失
String userId = headers.get(CacheConstants.DETAILS_USER_ID);
if (StringUtils.isNotEmpty(userId))
{
requestTemplate.header(CacheConstants.DETAILS_USER_ID, userId);
}
String userName = headers.get(CacheConstants.DETAILS_USERNAME);
if (StringUtils.isNotEmpty(userName))
{
requestTemplate.header(CacheConstants.DETAILS_USERNAME, userName);
}
String authentication = headers.get(CacheConstants.AUTHORIZATION_HEADER);
if (StringUtils.isNotEmpty(authentication))
{
requestTemplate.header(CacheConstants.AUTHORIZATION_HEADER, authentication);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# InnerAuth注解
接口访问分为两种,一种是由Gateway
网关访问进来,一种是内网通过Feign
进行内部服务调用。
如果开放了内部访问的接口,例如
ruoyi-api-system服务调用
@GetMapping("/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username);
2
ruoyi-modules-system系统模块
@GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
{
....
return R.ok(...);
}
2
3
4
5
6
这段代码也能通过网关(任意用户token
携带)访问到localhost:8080/system/info/ry
,这样暴露出去就很危险了,所以对于这种情况我们可以使用@InnerAuth
内部注解。
ruoyi-api-system服务调用(新增Header
参数from-source
)
@GetMapping("/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
2
ruoyi-modules-system系统模块
@InnerAuth
@GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
{
....
return R.ok(...);
}
2
3
4
5
6
7
加了@InnerAuth
注解每次会先进去到InnerAuthAspect.java
处理,验证请求头是否为from-source
,且携带内部标识参数inner
。如果非内部请求访问会直接抛出异常。
但是网关访问的时候,也可以手动带上这个from-source
参数,来达到这个目的。所以在网关AuthFilter.java
过滤器中对来源参数做了清除,防止出现安全问题。
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
2