Spring Cloud Alibaba实践——使用Feign

Feign的使用

之前内容中心调用用户中心的接口是直接通过RestTemplate的方式调用的,但是当接口数量变多,接口需要的参数过多时,这种方式会变得很难维护,因此需要引入Feign组件来完成服务间的接口调用。

为内容中心引入Feign

首先引入依赖,因为Feign是属于Spring Cloud的组件而不是Spring Cloud Alibaba的组件,所以需要额外引入Spring Cloud的依赖项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <!--整合spring cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
<dependencyManagement>

然后在Spring Boot的启动类上添加上Feign的注解:@EnableFeignClients

再开发内容中心调用用户中心的客户端代理类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package tech.punklu.contentcenter.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import tech.punklu.contentcenter.domain.dto.user.UserDTO;

/**
 * 调用用户中心的Feign客户端代理类
 */
@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

可以看到这里用到的都是Spring MVC的注解,所以最后生成的URL会是:http://user-center/users/{id},和之前直接写死在ShareService中的如下这行代码中的URL一致:

1
UserDTO userDTO = restTemplate.getForObject("http://user-center/users/{userId}",UserDTO.class,userId);

将ShareService中的这行代码替换为调用刚写好的调用用户中心的客户端,,方法最终代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Autowired
private UserCenterFeignClient userCenterFeignClient;

/**
* 根据内容id获取分享内容的详情
* @param id
* @return
*/
public ShareDTO findById(Integer id){
    // 获取分享详情
    Share share = this.shareMapper.selectByPrimaryKey(id);
    // 获取发布人id
    Integer userId = share.getUserId();

    // 从ribbon负载均衡中心获得用户中心的地址

    UserDTO userDTO = this.userCenterFeignClient.findById(userId);
    // 消息装备
    ShareDTO shareDTO = new ShareDTO();
    BeanUtils.copyProperties(share,shareDTO);
    shareDTO.setWxNickname(userDTO.getWxNickname());
    return shareDTO;
}

启动项目,访问http://127.0.0.1:8082/shares/1,可以发现依然可以访问,且当具有多个用户中心实例时,依然可以支持之前的Ribbon相关的功能。

Feign的组成

接口 作用 默认值
Feign.Builder Feign的入口 Feign.Builder
Client Feign底层用什么去请求 和Ribbon配合时:
LoadBalancerFeignClient
不会和Ribbon配合时feign.Client.Default
Contract 契约,注解支持 SpringMvcContract
Encoder 编码器,用于将对象转换成HTTP请求消息体 SpringEncoder
Decoder 解码器,将响应消息体转换成对象 ResponseEntityDecoder
Logger 日志管理器 Slf4jLogger
RequestInterceptor 用于为每个请求添加通用逻辑

需要注意的是SpringMvcContract,Feign默认是不使用Spring MVC形式的写法的,为了方便使用,通过SpringMvcContract来扩展为Spring MVC形式的写法。

代码方式细粒度配置Feign日志级别

Feign默认是不打印日志的,而且Feign自定义了四种不同的日志级别,需要单独设置才能开启Feign日志的打印。

级别 打印内容
NONE(默认) 不记录任何日志
BASIC 仅记录请求方法、URL、响应状态代码以及执行时间
HEADERS 记录BASIC级别的基础上,记录请求和响应的header
FULL 记录请求和响应的header、body和元数据

首先添加配置类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package tech.punklu.contentcenter.configuration;

import feign.Logger;
import org.springframework.context.annotation.Bean;

/**
 * 配置Feign的日志级别
 */
public class UserCenterFeignConfiguration {

    @Bean
    public Logger.Level level(){
        // 让Feign打印所有请求的细节
        return Logger.Level.FULL;
    }
}

要注意的是,不能加@Configuration注解(原因同Ribbon配置类),否则会被当成Feign的全局日志级别配置,如果加上@Configuration注解又不想被当成全局配置,需要将此类移到Spring Boot启动类所在的目录之外的地方,避免被Spring Boot扫描到导致父子上下文问题。

然后,在Feign Cient类上的Feign注解里加上配置属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package tech.punklu.contentcenter.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import tech.punklu.contentcenter.configuration.UserCenterFeignConfiguration;
import tech.punklu.contentcenter.domain.dto.user.UserDTO;

/**
 * 调用用户中心的Feign客户端代理类
 */
@FeignClient(name = "user-center",configuration = UserCenterFeignConfiguration.class)
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

最后,还需要将Client的日志设置为Debug并附加到Spring Boot本身的日志里去:

1
2
3
logging:
  level:
    tech.punklu.contentcenter.feignclient.UserCenterFeignClient: debug

启动内容中心服务,访问http://127.0.0.1:8082/shares/1可以发现内容中心服务的日志里已经有了Feign调用日志的信息了。

属性方式细粒度配置Feign日志级别

除了代码方式,也可通过配置文件属性来配置Feign的日志级别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
logging:
  level:
    tech.punklu.contentcenter.feignclient.UserCenterFeignClient: debug
feign:
  client:
    config:
      # 想要调用的微服务的名称
      user-center:
        loggerLevel: full

虽然是通过属性配置了,但是将Client的日志设置为Debug并附加到Spring Boot本身的日志里去的这段配置依然必须要有。

将代码方式的配置注解注释掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package tech.punklu.contentcenter.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import tech.punklu.contentcenter.configuration.UserCenterFeignConfiguration;
import tech.punklu.contentcenter.domain.dto.user.UserDTO;

/**
 * 调用用户中心的Feign客户端代理类
 */
//@FeignClient(name = "user-center",configuration = UserCenterFeignConfiguration.class)
@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

重启内容中心服务,访问http://127.0.0.1:8082/shares/1可以发现内容中心服务的日志里依然有Feign调用日志的信息了。

代码方式全局配置Feign日志级别

将之前内容中心的这段细粒度配置用户中心日志级别的配置注释掉:

1
2
3
4
5
6
#feign:
#  client:
#    config:
#      # 想要调用的微服务的名称
#      user-center:
#        loggerLevel: full

然后在Spring Boot启动类上的Feign注解@EnableFeignClients添加属性配置默认全局Feign配置信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package tech.punklu.contentcenter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import tech.punklu.contentcenter.configuration.UserCenterFeignConfiguration;
import tk.mybatis.spring.annotation.MapperScan;

@MapperScan("tech.punklu.contentcenter.dao")
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = UserCenterFeignConfiguration.class)
public class ContentCenterApplication {

    public static void main(String[] args) {
        SpringApplication.run(ContentCenterApplication.class, args);
    }

}

启动后,访问http://127.0.0.1:8082/shares/1,发现Feign的日志级别依然是Full。

属性方式全局配置Feign日志级别

将上面的Spring Boot启动类上的Feign注解@EnableFeignClients的属性注释掉:

 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
package tech.punklu.contentcenter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import tech.punklu.contentcenter.configuration.UserCenterFeignConfiguration;
import tk.mybatis.spring.annotation.MapperScan;

@MapperScan("tech.punklu.contentcenter.dao")
@SpringBootApplication
@EnableFeignClients//(defaultConfiguration = UserCenterFeignConfiguration.class)
public class ContentCenterApplication {

    public static void main(String[] args) {
        SpringApplication.run(ContentCenterApplication.class, args);
    }


    /**
     * 在Spring容器中,创建一个对象,类型是RestTemplate,
     * 名称/id是方法名
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

然后添加配置:

1
2
3
4
5
6
7
feign:
  client:
    config:
      # 想要调用的微服务的名称
      default:
        loggerLevel: full

可以发现,和细粒度属性配置的区别在于将服务的名称换成了default就成了默认的全局配置。

启动项目,访问http://127.0.0.1:8082/shares/1,可以看到Feign的日志级别依然是Full

Feign属性配置支持的配置项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
feign.client.config:
	<feignName>:
		connectTimeout: 5000 #连接超时时间
		readTimeout: 5000 #读取超时时间
		loggerLevel: full #日志级别
		errorDecoder: com.example.SimpleErrorDecoder #错误解码器
		retryer: com.example.SimpleRetryer 			# 重试策略
		requestInterceptors:
			- com.example.FooRequestInterceptor		# 拦截器
		# 是否对404错误码解码
		# 处理逻辑详见feign.SynchronousMethodHandler #executeAndDecode
		decode404: false
		encoder: com.example.SimpleEncoder #解码器
		decoder: com.example.SimpleDecoder #解码器
		contract: com.example.SimpleContract #契约

Feign两种配置方式的区别

配置方式 优点 缺点
代码配置 基于代码,更加灵活 如果Feign的配置类加了Configuration注解,需要注意父子上下文,线上修改得重新打包发布
属性配置 易上手、配置直观、线上修改无需打包发布、优先级更高 极端场景下没有代码配置方式灵活

优先级:全局代码 < 全局属性 < 细粒度代码 < 细粒度属性配置

应优先使用属性配置,且应尽量使用一种配置方式,避免不必要的优先级问题。

Feign多参数构造请求

对于Get请求,Feign可以使用:

  1. 在类参数上添加@SpringQueryMap注解
  2. 使用@RequestParam注解
  3. 使用Map来构建参数

以第一种方式为例,在用户中心中新增方法:

1
2
3
4
@GetMapping("/testFeignParam")
public User testFeignParam(User user){
	return user;
}

在内容中心中新增对该方法的Feign代理方法:

1
2
@GetMapping("testFeignParam")
UserDTO testFeignParam(@SpringQueryMap UserDTO userDTO);

即可实现多参数请求构造。

对于Post请求,直接使用Spring MVC中自带的@RequestBody注解即可。

Feign脱离Ribbon使用

之前的Feign调用都是通过Ribbon调用注册在Nacos上的实例的接口,有时需要脱离Ribbon直接调用没注册在Nacos上的服务下的接口。比如,直接调用百度的网址。

新建Feign代理类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package tech.punklu.contentcenter.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "baidu",url = "http://www.baidu.com")
public interface TestBaiduFeignClient {

    @GetMapping("")
    String index();
}

添加Controller接口:

1
2
3
4
5
6
7
@Autowired
private TestBaiduFeignClient testBaiduFeignClient;

@GetMapping("/baidu")
public String baidu(){
	return this.testBaiduFeignClient.index();
}

启动服务后,访问http://127.0.0.1:8082/baidu,可以看到已经访问到了百度的首页。

需要注意的是,虽然通过FeignClient注解里的url属性添加了百度的地址,但是name属性依然必须要有,否则会导致项目无法启动。

Feign性能优化

Feign的性能比RestTemplate的性能要差很多,主要有两个原因:

  1. 默认使用UrlConnection,而不是连接池
  2. 日志级别过高导致的性能损失

对于第二个原因,将Feign的日志级别设置为BASIC即可,对于第一个原因,可以通过配置连接池的方式解决:

首先添加依赖:

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

然后添加配置项:

1
2
3
4
5
6
7
8
feign:
  httpclient:
    # 让feign使用apache httpclient做请求;而不是默认的urlconnection,提高性能
    enabled: true
    # feign的最大连接数
    max-connections: 200
    # feign单个路径的最大连接数
    max-connections-per-route: 50