Spring Cloud :3 . RestTemplate和OpenFeign

Spring Cloud :3 . RestTemplate和OpenFeign

    • Restful风格API
    • 为什么要是用Spring Cloud基于Restful的远程调用?
    • RestTemplate的常用API
        • 简单调用
        • 传参调用
        • post调用
        • 拦截器
    • Feign和OpenFeign
    • 使用OpenFeign直连服务(不经过Eureka)
    • 使用OpenFeign通过Eureka访问远程服务
    • OpenFeign的连接超时机制

Restful风格API

Spring Cloud是基于HTTP协议的,远程调用都是使用HTTP的URL进行调用的,这样对于URL的定义就显得尤为重要,服务的提供方不能经常的更换,Restful风格API就是对URL暴露的一种约束。

RestTemplateOpenFeign 都可以使用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呢?

Spring Cloud :3 . RestTemplate和OpenFeign_第1张图片

一共有两种方式:

  • 一个是使用swagger生成API文档,comsumer编码的时候对照着文档发起调用。
  • 写一个类,把这个类加载到consumer,这个类里面描述了所有资源的URL,顺便还可以把注释写好。

差服务远程调用为啥要是用spring cloud 呢?spring cloud是基于http的restful风格,调用起来远比Soap和Dubbo要麻烦,服务消费者和服务提供者需要共同约定API(后续要需要维护),有以下几点原因:

  1. Spring 生态完整
  2. 可以实现异构系统。provider端可以使其他语言编写(php 或 .net以及其他)。
  3. 虽然http属于无状态的短连接,相对于Dubbo中rpc协议会更消耗资源,但是正因为无状态,所以数据弱连接,对previder端集群的依赖性更小(突然previder集群中有一个节点挂了对consumer无影响),更适合微服务,解决了网络抖动的问题,可用性更高。
  4. 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

Spring Cloud :3 . RestTemplate和OpenFeign_第2张图片

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.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        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.xjgroupId>
            <artifactId>user-apiartifactId>
            <version>0.0.1-SNAPSHOTversion>
        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的连接超时机制

在项目上线后,微服务之间相互调用,不可能一点问题都不出。如果出了问题,请求超时,应该怎么去解决?

首先请求超时可能会发生在两个阶段:

  1. 在客户端和服务器端建立连接的时候。
  2. 服务器端在处理业务逻辑的时候。

我们可以在 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

这篇文章是本人的个人理解,不保证准确性,如果有错误的地方希望大家留言指正,一起学习共同进步!
如果转载请标明出处。谢谢

你可能感兴趣的:(Spring,Cloud,restful,java)