# 服务调用

# 基本介绍

  • Feign

Feign 是Spring Cloud Netflix组件中的一量级Restful的 HTTP 服务客户端,实现了负载均衡和 Rest 调用的开源框架,封装了RibbonRestTemplate, 实现了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>
1
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);
}
1
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());
            }
        };
    }
}
1
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);
    }
}
1
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默认集成了RibbonNacos也很好的兼容了Feign,默认实现了负载均衡的效果。

# 请求传参

Get方式传参,使用@PathVariable@RequestParam注解接收请求参数

@GetMapping(value = "/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username);
1
2

Post方式传参,使用@RequestBody注解接收请求参数。

@PostMapping("/operlog")
public R<Boolean> saveLog(@RequestBody SysOperLog sysOperLog);
1
2

# 性能优化

# Gzip压缩

gzip是一种数据格式,采用deflate算法压缩数据。gzip大约可以帮我们减少70%以上的文件大小。

全局配置

server:
  compression:
    # 是否开启压缩
    enabled: true
    # 配置支持压缩的 MIME TYPE
    mime-types: text/html,text/xml,text/plain,application/xml,application/json
1
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  
1
2
3
4
5
6
7
8
9
10
11
12

提示

开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点。

# Http连接池

两台服务器建立HTTP连接的过程涉及到多个数据包的交换,很消耗时间。采用HTTP连接池可以节约大量的时间提示吞吐量。

FeignHTTP客户端支持3种框架:HttpURLConnectionHttpClientOkHttp

默认是采用java.net.HttpURLConnection,每次请求都会建立、关闭连接,为了性能考虑,可以引入httpclientokhttp作为底层的通信框架。

例如将FeignHTTP客户端工具修改为HttpClient

1、添加依赖

<!-- feign httpclient -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
1
2
3
4
5

2、全局配置

feign:
  httpclient:
    # 开启httpclient
    enabled: true
1
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()));
}
1
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;
}
1
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
{
} 
1
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
1
2
3
4

局部配置

# ruoyi-xxxx 为需要调用的服务名称
ruoyi-xxxx:
  ribbon:
    ReadTimeout: 10000
    ConnectTimeout: 10000
1
2
3
4
5

# 异常配置

1、配置开启

feign:
  hystrix:
    enabled: true
1
2
3

2、FeignClient接口服务加入fallbackFactory

@FeignClient(fallbackFactory = RemoteUserFallbackFactory.class)
1

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();
            }
        };
    }
}
1
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拦截器将本服务的userIduserNameauthentication传递给下游服务

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);
            }
        }
    }
}
1
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);
1
2

ruoyi-modules-system系统模块

@GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
{
    ....
	return R.ok(...);
}
1
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);
1
2

ruoyi-modules-system系统模块

 







@InnerAuth
@GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
{
    ....
	return R.ok(...);
}
1
2
3
4
5
6
7

加了@InnerAuth注解每次会先进去到InnerAuthAspect.java处理,验证请求头是否为from-source,且携带内部标识参数inner。如果非内部请求访问会直接抛出异常。 但是网关访问的时候,也可以手动带上这个from-source参数,来达到这个目的。所以在网关AuthFilter.java过滤器中对来源参数做了清除,防止出现安全问题。

// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
1
2