Spring Cloud :3 . RestTemplate和OpenFeign
Restful风格API
Spring Cloud是基于HTTP协议的,远程调用都是使用HTTP的URL进行调用的,这样对于URL的定义就显得尤为重要,服务的提供方不能经常的更换,Restful风格API就是对URL暴露的一种约束。
RestTemplate 和 OpenFeign 都可以使用Restful风格进行远程调用服务。
其中,RestTemplate是基于rest协议单独封装煮出来的。更倾向于手动配置,调用服务的时候更灵活,但是多个项目组之间,在联调服务的时候 需要硬性的靠restful风格约束,写死的URL,不能随意修改,每个项目组需要对外公布API文档。
Feign数据声明式的服务调用,服务有哪些接口,要提前以接口的方式声明出来,支持注解,可以像Dubbo一样直接调用接口方法的形式调用远程服务,这样做的好处就是开发效率高。
在传统项目中,定义URL访问接口,如果定义一个User的模块,一般是这样的:
传统URL | Restful URL | 请求方式 | |
---|---|---|---|
获取用户列表 | http://IP:port/user/userList | http://IP:port/users | get |
新增用户 | http://IP:port/user/addUser | http://IP:port/user/addUser | post |
根据ID查询用户 | http://IP:port/user/getUserByID | http://IP:port/user/{1} | get |
根据ID删除用户 | http://IP:port/user/deleteUserByID | http://IP:port/user/{1} | delete |
根据ID更新用户 | http://IP:port/user/updateUser | http://IP:port/user/updateUser | put |
同时还可以在URL上面增添版本信息,比如:http://IP:port/v1/user/{1}
为什么要是用Spring Cloud基于Restful的远程调用?
在Consumer端调用Provider服务的时候,需要对http协议的URL硬编码写死,而URL资源地址一定是由provider提供的,那么consumer如何知道provider有提供哪些URL呢?
一共有两种方式:
- 一个是使用swagger生成API文档,comsumer编码的时候对照着文档发起调用。
- 写一个类,把这个类加载到consumer,这个类里面描述了所有资源的URL,顺便还可以把注释写好。
差服务远程调用为啥要是用spring cloud 呢?spring cloud是基于http的restful风格,调用起来远比Soap和Dubbo要麻烦,服务消费者和服务提供者需要共同约定API(后续要需要维护),有以下几点原因:
- Spring 生态完整。
- 可以实现异构系统。provider端可以使其他语言编写(php 或 .net以及其他)。
- 虽然http属于无状态的短连接,相对于Dubbo中rpc协议会更消耗资源,但是正因为无状态,所以数据弱连接,对previder端集群的依赖性更小(突然previder集群中有一个节点挂了对consumer无影响),更适合微服务,解决了网络抖动的问题,可用性更高。
- Soap是基于xml的,而Spring Cloud是基于json的。
RestFul风格最大的好处就是异构系统。
在做为消费方的时候,只要提供方暴露了RestFull接口,SpringCloud就可以使用RestTemplate或者OpenFeign进行调用和负载均衡。
在作为提供方的时候,只需要使用swagger对外提供一份URL文档,就可以使让其他语言的消费端访问服务。
RestTemplate的常用API
简单调用
getForObject(url, String.class) 和 getForEntity(url, String.class):
服务提供方:
@Value("${server.port}")
String port;
@GetMapping("/getport")
public String getPort(){
return port;
}
服务消费方:
@GetMapping("/ribbon2")
public Object ribbon2(){
String url="http://eureka-provider/getport";
//远程调用,获得返回结果
/**
* 只返回调用结果,由于服务提供方返回的是字符串,所以使用String类型接收,如果服务提供方返回的是map就用Map.class接收,
* 如果是自定义类,如 Person 就用 Person.class接收。preson类需要放到公共模块中,分别引入到调用方和提供方
*/
// String result = restTemplate.getForObject(url, String.class);
/**
* result的值:<200,8080,[Content-Type:"text/plain;charset=UTF-8", Content-Length:"4", Date:"Fri, 10 Jul 2020 04:44:52 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
* 相较于getForObject,getForEntity多出状态码,响应头等信息
*/
ResponseEntity<String> result = restTemplate.getForEntity(url, String.class);
System.out.println(result);
return result;
}
传参调用
提供方:
@GetMapping("/getName")
public String getName(String name){
System.out.println(name);
return "传递的name为:"+name;
}
消费方:
@GetMapping("/sendName")
public Object sendName(){
String url="http://eureka-provider/getName?name={1}";
ResponseEntity<String> result = restTemplate.getForEntity(url, String.class,"zhangsan");
System.out.println(result);
return result;
}
post调用
提供方:
@PostMapping("/saveUser")
public Object saveUser(@RequestBody Map map){
System.out.println(map);
return "ok";
}
消费方:
@GetMapping("/saveUser")
public Object saveUser(){
String url="http://eureka-provider/saveUser";
Map map=new HashMap();
map.put("name","zhangsan");
map.put("age",18);
String result = restTemplate.postForObject(url, map, String.class);
return result;
}
拦截器
自定义一个类实现 ClientHttpRequestInterceptor 接口
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
System.out.println("拦截啦!!!");
System.out.println(request.getURI());
ClientHttpResponse response = execution.execute(request, body);
System.out.println(response.getHeaders());
return response;
}
在修改配置类中的bean:
@Bean
@LoadBalanced
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
return restTemplate;
}
Feign和OpenFeign
Feign本身不支持Spring MVC的注解,它有一套自己的注解
OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping
等等。
OpenFeign的@FeignClient
可以解析SpringMVC的@RequestMapping
注解下的接口,
并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
其实Feign的本质是是对RestTemplate的包装,让服务远程调用变得更简单了。使用起来像是本地调用普通方法。
使用OpenFeign直连服务(不经过Eureka)
服务提供方:
@RestController
public class UesrController {
@GetMapping("/alive")
public String alive(){
return "ok";
}
}
服务调用方:
导入OpenFeign的start包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
/**
* 添加接口,并且添加上@FeignClient注解,在不通过Eureka直连的情况下,name属性没有实际意义,
* 通过url数据设置URL地址,后面拼接 @GetMapping 资源目录。
*/
@FeignClient(name = "ooxx",url = "http://localhost:7900")
public interface UesrApi {
@GetMapping("/alive")
public String alive();
}
@RestController
public class UesrControllerMain {
/**
* 可以直接使用 @Autowired 注解注入接口,而不用担心接口没有具体实现而报错。
* 因为这个接口是使用 @FeignClient 标注过了,在调用的时候可以像使用普通API
* 那样对远程发起调用。
*/
@Autowired
UesrApi uesrApi;
@GetMapping("/alive")
public String alive(){
String alive = uesrApi.alive();
return alive;
}
}
使用OpenFeign通过Eureka访问远程服务
启动Eureka,查看服务注册列表:
服务调用方:
/**
* 和之前不同的是这里的name属性有实际意义,代表注册带Eureka中的服务名
*/
@FeignClient(name = "open-feign-provider7900")
public interface UesrApi {
@GetMapping("/alive")
public String alive();
}
但是这种方式依然会感觉比较麻烦,我们可以把接口类单独封装成一个模块,然后在maven的pom文件中引入。
首先先定义一个公共模块 :user-api ,并且在模块中创建接口
//顺便把注释和注解写好,这样引入的双方都能看见使用
@RequestMapping("/user")
public interface UserAPI {
/**
* 查看服务状态
* @return
*/
@GetMapping("/alive")
public String alive();
/**
* 发送用户名
* @param name
* @return
*/
@PostMapping("/post")
public String post(@RequestBody String name);
}
需要注意的是,接收参数最好添加上注解,比如 @RequestBody
、@RequestParam
、等注解,因为如果不添天这些注解,可能会影响Feign远程调用时 参数的传递(公共接口必须添加注解)
把该模块打包,让OpenFeign的consumer 和 provider 都导入这个jar包:
<dependency>
<groupId>com.xj</groupId>
<artifactId>user-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
provider 端:
@RestController
public class UesrController implements UserAPI {
@Override //这里甚至不需要写@GetMapping注解,因为UserAPI里已经标注好注解了
public String alive(){
return "ok";
}
@Override
public String post(String name) {
System.out.println("登录名:"+name);
return name;
}
}
consumer 端:
@FeignClient(name = "open-feign-provider7900")
public interface UesrApiProxy extends UserAPI { //只需要继承即可
}
@RestController
public class UesrControllerMain {
@Autowired
UesrApiProxy uesrApi;
@GetMapping("/alive")
public String alive(){
String alive = uesrApi.alive();
return alive;
}
@PostMapping("/sendName")
public String sendName(){
String name = uesrApi.post("zhangsan");
return name;
}
}
另外,如果前端页面传递的参数不固定,可以使用Map接收参数。
OpenFeign的连接超时机制
在项目上线后,微服务之间相互调用,不可能一点问题都不出。如果出了问题,请求超时,应该怎么去解决?
首先请求超时可能会发生在两个阶段:
- 在客户端和服务器端建立连接的时候。
- 服务器端在处理业务逻辑的时候。
我们可以在 consumer端 配置文件中添加配置:
#连接超时时间(ms)
ribbon.ConnectTimeout=1000
#业务逻辑超时时间(ms)
ribbon.ReadTimeout=6000
consumer端测试代码:
@Autowired
UesrApiProxy uesrApi;
@GetMapping("/alive")
public String alive(){
String alive = uesrApi.alive();
return alive;
}
配置一个 provider 集群 (7900,7901),对两个相同的方法设置不同的休眠时间,并且做标识区分:
@Value("${server.port}")
private String port;
private AtomicInteger count=new AtomicInteger();
@Override
public String alive() {
try {
System.out.println("准备睡");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int i = count.getAndIncrement();
System.out.println("====坏的第:" + i + "次调用");
return "port:" + port;
}
请求一次 http://localhost:90/alive 会发现在睡眠时间超时的服务7900请求了一次之后(失败),又从新请求到7901服务器。
配置重试:
#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=1
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重试
ribbon.OkToRetryOnAllOperations=false
这篇文章是本人的个人理解,不保证准确性,如果有错误的地方希望大家留言指正,一起学习共同进步!
如果转载请标明出处。谢谢