服务调用
Spring Cloud 入门 ---- OpenFeign 服务调用
简介
Feign 是一个声明式 WebService 客户端,使用 Feign 能让编写 WebService 客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。Feign 也支持可插拔式的编码器和解码器。Spring Cloud 对 Feign 进行了封装,使其支持 Spring MVC 标注注解和 HttpMessageConverters。OpenFeign 可以与 Eureka 和 Ribbon 组合使用以支持负载均衡。
OpenFeign 能干什么
OpenFeign 旨在使编写 Java Http 客户端变得更加容易。前面我们使用 Ribbon + RestTemplate 时,利用 RestTemplate 对 http 请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,
往往一个接口会被多次调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。
所以,OpenFeign 在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在 OpenFeign 的实现下,我们只需要创建一个接口并使用注解的方式来配置它【类似于以前 Dao 接口上标注 Mapper 注解】,现在是一个微服务接口上面标注一个 OpenFeign 注解即可,
即可完成对微服务提供方接口的绑定,简化了使用 Ribbon + RestTemplate 时,自动封装服务调用客户端的开发量。
OpenFeign 集成了 Ribbon
利用 Ribbon 维护了服务列表信息,并且通过轮询实现了客户端的负载均衡。而与 Ribbon 不同的是,
通过 OpenFeign 只需要定义服务绑定接口并且以声明式的方法
,优雅而简单的实现服务调用。
Feign 与 OpenFeign 区别
创建演示项目
沿用之前的 Eureka 注册中心,与 ribbon 一样 Open Feign 也是用在客户端的,也就是消费方。
创建服务提供者
导入 pom 依赖
<!-- 引入 eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
添加 yml 配置,由于需要演示负载均衡,需要添加两个配置文件
application-one.yml
与application-two.yml
由于这两个配置文件基本相同,这里只给出application-one.yml
的全部配置,另一个只给出不同的配置。
application-one.yml
server:
port: 8013
spring:
application:
name: feign-service-provider
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3600/cloud?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
# defaultZone: http://localhost:7001/eureka
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: feign-provider-8013
## 当调用getHostname获取实例的hostname时,返回ip而不是host名
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
lease-renewal-interval-in-seconds: 10
# Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
lease-expiration-duration-in-seconds: 30
#mybatis-plus
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.akieay.cloud.fuser.entity;
configuration:
#是否开启驼峰命名自动映射
map-underscore-to-camel-case: true
#全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。
cache-enabled: false
#指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法
call-setters-on-nulls: true
#指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
logging:
level:
root: info
application-two.yml
server:
port: 8014
eureka:
instance:
instance-id: feign-provider-8014
主启动
@SpringBootApplication
@EnableEurekaClient
@MapperScan(value = "com.akieay.cloud.fuser.mapper")
public class FeignProviderApplication {
public static void main(String[] args) {
SpringApplication.run(FeignProviderApplication.class, args);
}
}
业务类,这里只给出 UserController 的详细,其它只是整合了 MyBatis-Plus 框架,并没有具体业务实现。
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Value("${server.port}")
private String serverPort;
@PostMapping(value = "/create")
public CommonResult create(@RequestBody UserVO userVo) {
UserEntity user = new UserEntity();
user.setId(userVo.getId());
user.setUsername(userVo.getUsername());
user.setAge(userVo.getAge());
user.setCreateTime(new Date());
user.setPassword(userVo.getPassword());
boolean flag = userService.save(user);
log.info("*****插入结果:" + flag);
if (flag) {
return new CommonResult(200, "插入数据库成功, serverPort:" + serverPort, flag);
} else {
return new CommonResult(444, "插入数据库失败");
}
}
@GetMapping(value = "/get/{id}")
public CommonResult<UserVO> getUserById(@PathVariable("id") Long id) {
UserEntity userEntity = userService.getById(id);
UserVO userVO = new UserVO();
userVO.setId(userEntity.getId());
userVO.setAge(userEntity.getAge());
userVO.setUsername(userEntity.getUsername());
userVO.setCreateTime(userEntity.getCreateTime());
log.info("*****查询结果:" + userEntity);
if (null != userEntity) {
return new CommonResult(200, "查询数据成功, serverPort:" + serverPort, userVO);
} else {
return new CommonResult(400, "没有对应的记录,查询ID:" + id);
}
}
@PostMapping(value = "/update")
public CommonResult<Boolean> updateById(@RequestBody UserVO userVo) {
UserEntity user = new UserEntity();
user.setId(userVo.getId());
user.setUsername(userVo.getUsername());
user.setAge(userVo.getAge());
user.setPassword(userVo.getPassword());
boolean flag = userService.updateById(user);
if (flag) {
return new CommonResult(200, "修改数据成功,serverPort: " + serverPort, flag);
} else {
return new CommonResult(500, "修改数据库失败");
}
}
@GetMapping(value = "/remove/{id}")
public CommonResult<Long> removeById(@PathVariable("id") Long id) {
boolean flag = userService.removeById(id);
if (flag) {
return new CommonResult(200, "删除成功,serverPort: " + serverPort, id);
} else {
return new CommonResult(500, "删除失败");
}
}
}
创建两个启动服务,具体细节已经在前面的模块做了介绍,这里就不介绍了
启动这两个服务,并打卡注册中心,即可看到这两个服务已经注册到了注册中心中;
至此 服务提供者创建完成
创建服务消费者
导入 pom 依赖
<!--引入 openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 引入 eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加 application.yml 配置文件,注意指定 ribbon 客户端的超时时间【默认等待1秒钟】,一旦服务端业务所需的等待时间超过 1 秒,注意要修改 客户端超时时间。
server:
port: 80
spring:
application:
name: feign-service-concumer
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
# defaultZone: http://localhost:7001/eureka
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: feign-concumer-80
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
lease-renewal-interval-in-seconds: 10
# Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
lease-expiration-duration-in-seconds: 30
logging:
level:
#feign日志以什么级别监控哪个接口
com.akieay.cloud.feign.service.UserService: debug
# 设置服务的 ribbon 配置
FEIGN-SERVICE-PROVIDER:
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法-随机
主启动 @RibbonClient 配置 FEIGN-SERVICE-PROVIDER 服务的负载均衡策略
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class FeignConcumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConcumerApplication.class, args);
}
}
业务类,
@FeignClient
将FEIGN-SERVICE-PROVIDER
服务绑定到UserService
接口,其中要保证 接口的返回值、参数、请求路径 要与服务提供者FEIGN-SERVICE-PROVIDER
中的 UserController 的一致【如下图所示】;注意:若想使@FeignClient
生效,主启动中的@EnableFeignClients
配置不可少,它表示开启基于注解的 OpenFeign 功能。
@Component
@RequestMapping("/user")
@FeignClient(value = "FEIGN-SERVICE-PROVIDER")
public interface UserService {
/**
* 创建用户
* @param userVo
* @return
*/
@PostMapping(value = "/create")
CommonResult create(@RequestBody UserVO userVo);
/**
* 获取指定id 的用户信息
* @param id
* @return
*/
@GetMapping(value = "/get/{id}")
CommonResult<UserVO> getUserById(@PathVariable("id") Long id);
/**
* 修改指定id 的用户信息
* @param userVo
* @return
*/
@PostMapping(value = "/update")
CommonResult<Boolean> updateById(@RequestBody UserVO userVo);
/**
* 删除指定id 的用户信息
* @param id
* @return
*/
@GetMapping(value = "/remove/{id}")
CommonResult<Long> removeById(@PathVariable("id") Long id);
}
@RestController
@Slf4j
public class UserController {
@Resource
UserService userService;
@GetMapping("/concumer/user/get/{id}")
public CommonResult<UserVO> getUser(@PathVariable("id") Long id) {
CommonResult<UserVO> result = userService.getUserById(id);
return result;
}
}
重启服务,访问 Eureka 注册中心,即可看到服务已经注册成功;访问:http://localhost/concumer/user/get/1 可以看到 使用的是我们指定的
随机
的负载均衡策略【而不是默认的轮询
】来随机获取 8013 与 8014 服务提供者节点的服务。
OpenFeign 超时控制
OpenFeign默认等待1秒钟,超时后会报错;即,若是调用服务提供者提供的方法,超过1秒钟还没得到响应,会立刻抛出异常(哪怕该服务正常调用需要2秒)
解决方案:针对这种情况,可以修改超时时间,在yml添加如下配置【前面的演示中已经添加了配置】:
#设置feign客户端超时时间(openfeign默认支持ribbon)
ribbon:
#指的是建立连接所用时间,适用于网络正常情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
OpenFeign 的日志打印功能
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Http 请求的细节。说白了就是
对 Feign 接口的调用情况进行监控和输出
。
日志级别
- NONE:默认的,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:除了 BASIC 中定义的信息之外,还有请求头和响应的头信息;
- FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
启用日志功能
添加 feign 配置类
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
yml 新增配置
logging:
level:
#feign日志以什么级别监控哪个接口
com.akieay.cloud.feign.service.UserService: debug
重启服务,调用方法即可看到详细的服务调用日志,如下:
常见的应用程序属性
官网:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/appendix.html
Name | Default | Description |
---|---|---|
feign.client.config | ||
feign.client.default-config | default | |
feign.client.default-to-properties | true | |
feign.compression.request.enabled | false | Enables the request sent by Feign to be compressed. |
feign.compression.request.mime-types | [text/xml, application/xml, application/json] | The list of supported mime types. |
feign.compression.request.min-request-size | 2048 | The minimum threshold content size. |
feign.compression.response.enabled | false | Enables the response from Feign to be compressed. |
feign.compression.response.useGzipDecoder | false | Enables the default gzip decoder to be used. |
feign.httpclient.connection-timeout | 2000 | |
feign.httpclient.connection-timer-repeat | 3000 | |
feign.httpclient.disable-ssl-validation | false | |
feign.httpclient.enabled | true | Enables the use of the Apache HTTP Client by Feign. |
feign.httpclient.follow-redirects | true | |
feign.httpclient.max-connections | 200 | |
feign.httpclient.max-connections-per-route | 50 | |
feign.httpclient.time-to-live | 900 | |
feign.httpclient.time-to-live-unit | ||
feign.hystrix.enabled | false | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker. |
feign.okhttp.enabled | false | Enables the use of the OK HTTP Client by Feign. |