服务注册与消费

服务注册

服务注册就是把一个微服务注册到 Eureka Server 上,这样,当其他服务需要调用该服务时,只需要从Eureka Server 上查询该服务的信息即可。
这里我们创建一个 provider,作为我们的服务提供者,创建项目时,选择 Eureka Client 依赖,这样,当服务创建成功后,简单配置一下,就可以被注册到 Eureka Server 上了:

项目创建成功后,我们只需要在 application.properties 中配置一下项目的注册地址即可。注册地址的配置,和 Eureka Server 集群的配置很像。配置如下:

spring.application.name=provider
server.port=1113
eureka.client.service-url.defaultZone=http://localhost:1111/eureka

三行配置,分别表示当前服务的名称、端口号以及服务地址。
接下来,启动 Eureka Server,待服务注册中心启动成功后,再启动 provider。
两者都启动成功后,浏览器输入 http://localhost:1111,就可以查看 provider 的注册信息:

服务消费

首先在 provider 中提供一个接口,然后创建一个新的 consumer 项目,消费这个接口。
在 provider 中,提供一个 hello 接口,如下:

@RestController
public class HelloController {
      @GetMapping("/hello")
      public String hello() {
      return "hello javaboy";
}
}

接下来,创建一个 consumer 项目,consumer 项目中,去消费 provider 提供的接口。consumer 要能够获取到 provider 这个接口的地址,他就需要去 Eureka Server 中查询,如果直接在 consumer 中写死 provider 地址,意味着这两个服务之间的耦合度就太高了,我们要降低耦合度。首先我们来看一
个写死的调用。创建一个 consumer 项目,添加 web 和 eureka client 依赖:

创建完成后,我们首先也在 application.properties 中配置一下注册信息:

spring.application.name=consumer
server.port=1115
eureka.client.service-url.defaultZone=http://localhost:1111/eureka

配置完成后,假设我们现在想在 consumer 中调用 provider 提供的服务,我们可以直接将调用写死,
就是说,整个调用过程不会涉及到 Eureka Server。

@RestController
public class UserHelloController {
    @GetMapping("/hello1")
    public String hello1() {
        HttpURLConnection con = null;
        try {
            URL url = new URL("http://localhost:1113/hello");
            con = (HttpURLConnection) url.openConnection();
            if (con.getResponseCode() == 200) {
                BufferedReader br = new BufferedReader(new
                        InputStreamReader(con.getInputStream()));
                String s = br.readLine();
                br.close();
                return s;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "error";
    }
}

这是一段利用了 HttpUrlConnection 来发起的请求,请求中 provider 的地址写死了,意味着 provider和 consumer 高度绑定在一起,这个不符合微服务的思想。要改造它,我们可以借助 Eureka Client 提供的 DiscoveryClient 工具,利用这个工具,我们可以根据服务名从 Eureka Server 上查询到一个服务的详细信息,改造后的代码如下:

@Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/hello2")
    public String hello2() {
        List<ServiceInstance> list = discoveryClient.getInstances("provider");
        ServiceInstance instance = list.get(0);
        String host = instance.getHost();
        int port = instance.getPort();
        StringBuffer sb = new StringBuffer();
        sb.append("http://")
                .append(host)
                .append(":")
                .append(port)
                .append("/hello");
        HttpURLConnection con = null;
        try {
            URL url = new URL(sb.toString());
            con = (HttpURLConnection) url.openConnection();
            if (con.getResponseCode() == 200) {
                BufferedReader br = new BufferedReader(new
                        InputStreamReader(con.getInputStream()));
                String s = br.readLine();
                br.close();
                return s;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "error";
    }

注意,DiscoveryClient 查询到的服务列表是一个集合,因为服务在部署的过程中,可能是集群化部署,集合中的每一项就是一个实例。这里我们可以稍微展示一下集群化部署。首先,修改 provider 中的 hello 接口:

@RestController
public class HelloController {
    @Value("${server.port}")
    Integer port;
    @GetMapping("/hello")
    public String hello() {
        return "hello javaboy:" + port;
    }
}

因为我一会会启动多个 provider 实例,多个 provider 实例的端口不同,为了区分调用时到底是哪一个
provider 提供的服务,这里在接口返回值中返回端口。
修改完成后,对 provider 进行打包。provider 打包成功之后,我们在命令行启动两个 provider 实例:

java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1113
java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1116

启动完成后,检查 Eureka Server 上,这两个 provider 是否成功注册上来。注册成功后,在 consumer 中再去调用 provider,DiscoveryClient 集合中,获取到的就不是一个实例了,而是两个实例。这里我们可以手动实现一个负载均衡:

int count = 0;
    @GetMapping("/hello3")
    public String hello3() {
        List<ServiceInstance> list = discoveryClient.getInstances("provider");
        ServiceInstance instance = list.get((count++) % list.size());
        String host = instance.getHost();
        int port = instance.getPort();
        StringBuffer sb = new StringBuffer();
        sb.append("http://")
                .append(host)
                .append(":")
                .append(port)
                .append("/hello");
        HttpURLConnection con = null;
        try {
            URL url = new URL(sb.toString());
            con = (HttpURLConnection) url.openConnection();
            if (con.getResponseCode() == 200) {
                BufferedReader br = new BufferedReader(new
                        InputStreamReader(con.getInputStream()));
                String s = br.readLine();
                br.close();
                return s;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "error";
    }

在从集合中,获取数据时,通过一个小小举动,就可以实现线性负载均衡。

升级改造

从两个方面进行改造:

  1. Http 调用
  2. 负载均衡
    Http 调用,我们使用 Spring 提供的 RestTemplate 来实现。
    首先,在当前服务中,提供一个 RestTemplate 的实例:
@SpringBootApplication
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
    @Bean
    RestTemplate restTemplateOne(){
        return new RestTemplate();
    }
}

然后,在Http调用是,不再使用HttpUrlConnection,而是直接使用RestTemplate:

    @Autowired
    DiscoveryClient discoveryClient;
    @Autowired
    @Qualifier("restTemplateOne")
    RestTemplate restTemplateOne;

    @GetMapping("/hello2")
    public String hello2() {
        List<ServiceInstance> list = discoveryClient.getInstances("provider");
        ServiceInstance instance = list.get(0);
        String host = instance.getHost();
        int port = instance.getPort();
        StringBuffer sb = new StringBuffer();
        sb.append("http://")
                .append(host)
                .append(":")
                .append(port)
                .append("/hello");
        String s = restTemplateOne.getForObject(sb.toString(),String.class);
        return s;
    }

用 RestTemplate ,一行代码就实现了 Http 调用。
接下来,使用 Ribbon 来快速实现负载均衡。
首先,我们需要给 RestTemplate 实例添加一个 @LoadBalanced 注解,开启负载均衡:

    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }

此时的 RestTemplate 就自动具备了负载均衡的功能。
此时的调用代码如下:

    @Autowired
    @Qualifier("restTemplate")
    RestTemplate restTemplate;
    @GetMapping("/hello3")
    public String hello3() {
        return restTemplate.getForObject("http://provider/hello",String.class);
    }

RestTemplate

RestTemplate是从Spring3.0开始支持的一个Http请求工具,这个请求工具和SpringBoot无关,更和SpringCloud无关。RestTemplate提供了常见的Rest请求方法模板,例如:GET,POST,PUT,DELETE请求以及一些通用的请求方法exchange和execute方法。
RestTemplate本身实现了RestOperations接口,而在RestOperations接口中,定义了常见的RESTful操作,这些操作在RestTemplate中都得到了很好的实现。

GET

首先我们在 provider 中定义一个 hello2 接口:

@GetMapping("/hello2")
public String hello2(String name){
    return "hello "+ name;
}

接下来,我们在consumer去访问这个接口,这个接口是一个Get请求,所以,访问方式,就是调用RestTemplate中的GET请求。
可以看到,在RestTemplate中,关于Get请求,一共有两大类方法:

这两大类方法实际上是重载,唯一不同的,就是返回值类型。
getForObject返回的是一个对象,这个对象就是服务器返回的具体值。getForEntity返回的是一个ResponseEntity,这个ResponseEntity中除了服务器返回的具体数据外,还保留了http响应头的数据。

    @GetMapping("/hello4")
    public void hello4(){
        String s1= restTemplate.getForObject("http://provider/hello2?name={1}", String.class,
                "javaboy");
        System.out.println(s1);
        ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://provider/hello2" +
                "?name={1}", String.class, "你好");
        String body = responseEntity.getBody();
        System.out.println("body: " + body);
        HttpStatus statusCode = responseEntity.getStatusCode();
        System.out.println("statusCode" + statusCode);
        int statusCodeValue = responseEntity.getStatusCodeValue();
        System.out.println("statusCodeValue"+statusCodeValue);
        HttpHeaders headers = responseEntity.getHeaders();
        System.out.println("--------header----------");
        Set<String> keySet = headers.keySet();
        for (String s : keySet) {
            System.out.println(s+":"+headers.get(s));
        }
    }

这里大家可以看到,getForObject 直接拿到了服务的返回值,getForEntity 不仅仅拿到服务的返回值,还拿到 http 响应的状态码。然后,启动 Eureka Server、provider 以及 consumer ,访问 consumer中的 hello4 接口,既可以看到请求结果。

看清楚两者的区别之后,接下来看下两个各自的重载方法,getForObject和getForEntity分别有三个重载方法两者的三个重载方法基本都是一致的。所以,这里,我们主要看其中一种。三个重载方法,
其实代表了三种不同的传参方式。

@GetMapping("/hello5")
public void hello5() throws UnsupportedEncodingException {
    //第一种
    String s1 = restTemplate.getForObject("http://provider/hello2?name={1}&password={2}", String.class,
            "lisi", "123");
    System.out.println(s1);
    //第二种
    Map<String,Object> map =new HashMap<>();
    map.put("name", "lisi");
    String s2 = restTemplate.getForObject("http://provider/hello2?name={name}", String.class,
            map);
    System.out.println(s2);
    //第三种
    String url = "http://provider/hello2?name=" + URLEncoder.encode("张三", "UTF-8");
    URI uri = URI.create(url);
    String s = restTemplate.getForObject(uri, String.class);
    System.out.println(s);
}

POST

首先在provider中提供两个接口,同时,因为POST请求可能需要传递JSON,所以,这里我们创建一个普通的Maven项目作为commons模块,然后这个commons模块被provider和consumer共同引用,这样我们就可以方便的传递JOSN了。
commons 模块创建成功后,首先在 commons 模块中添加 User 对象,然后该模块分别被 provider 和 consumer 引用。
然后,我们在 provider 中,提供和两个 POST 接口:

@PostMapping("/user1")
public User addUser1(User user) {
    return user;
}

@PostMapping("/user2")
public User addUser2(@RequestBody User user){
    return user;
}

这里定义了两个User添加的方法,两个方法代表了两种不同的传参方式,第一种是以key/value形式来传参,第二种方法是以JSON形式来传参。
定义完成后,接下来,我们在consumer中调用这两个post接口。

可以看到,这里的 post 和前面的 get 非常像,只是多出来了三个方法,就是 postForLocation,另外两个 postForObject 和 postForEntiy 和前面 get 基本一致,所以这里我们主要来看 postForObject,看完之后,我们再来看这个额外的 postForLocation。

    @GetMapping("/hello5")
    public void hello5() throws UnsupportedEncodingException {
        //第一种
        String s1 = restTemplate.getForObject("http://provider/hello2?name={1}&password={2}", String.class,
                "lisi", "123");
        System.out.println(s1);
        //第二种
        Map<String,Object> map =new HashMap<>();
        map.put("name", "lisi");
        String s2 = restTemplate.getForObject("http://provider/hello2?name={name}", String.class,
                map);
        System.out.println(s2);
        //第三种
        String url = "http://provider/hello2?name=" + URLEncoder.encode("张三", "UTF-8");
        URI uri = URI.create(url);
        String s = restTemplate.getForObject(uri, String.class);
        System.out.println(s);
    }

    @GetMapping("/hello6")
    public void hello6(){
        MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
        map.add("name", "javaboy");
        map.add("password","123");
        map.add("id",99);
        User user = restTemplate.postForObject("http://provider/user1", map, User.class);
        System.out.println(user);
        user.setId(98);
        user = restTemplate.postForObject("http://provider/user2", user, User.class);
        System.out.println(user);
    }

post参数到底是key/value形式还是json形式,主要看第二个参数,如果第二个参是MultiValueMap,则参数是以key/value形式来传递的,如果一个普通对象,则参数是以json形式传递的。

最后再看看一下 postForLocation 。有的时候,当我执行完一个 post 请求之后,立马要进行重定向,
一个非常常见的场景就是注册,注册是一个 post 请求,注册完成之后,立马重定向到登录页面去登
录。对于这种场景,我们就可以使用 postForLocation。
首先我们在 provider 上提供一个用户注册接口:

@Controller
public class RegisterController {
    @PostMapping("/register")
    public String register(User user){
        return "redirect:http://provider/loginPage?username="+user.getName();
    }

    @GetMapping("/loginPage")
    @ResponseBody
    public String loginPage(String username) {
        return "loginPage:"+username;
    }
}

注意,这里的post接口,响应一定是302,否则postForLocation无效
注意,重定向的地址,一定要写成绝对路径,不要写相对路径,否则在consumer中调用时会出问题

@GetMapping("/hello7")
public void hello7(){
    MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
    map.add("username", "javaboy");
    map.add("password", "123");
    map.add("id",99);
    URI uri = restTemplate.postForLocation("http://provider/register",map);
    String s = restTemplate.getForObject(uri, String.class);
    System.out.println(s);
}

这就是postForLocation,调用该方法返回的是一个Uri,这个Uri就是重定向的地址(里边也包含了重定向的参数),拿到Uri之后,就可以直接发送新的请求了。

PUT

PUT 请求比较简单,重载的方法也比较少。
我们首先在 provider 中提供一个 PUT 接口:

@PutMapping("/user1")
public void updateUser1(User user1){
    System.out.println(user1);
}

@PutMapping("/hello2")
public void updateUser2(@RequestBody User user) {
    System.out.println(user);
}

注意,PUT 接口传参其实和 POST 很像,也接受两种类型的参数,key/value 形式以及 JSON 形式。
在 consumer 中,我们来调用该接口:

@GetMapping("/hello8")
public void hello8(){
    //key/value传参
    MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
    map.add("name", "javaboy");
    map.add("password", "123");
    map.add("id",99);
    restTemplate.put("http://provider/user1",map);
    //json传参
    User user = new User();
    user.setId(98);
    user.setName("小明");
    user.setPassword("123");
    restTemplate.put("http://provider/user2", user);
}

consumer中的写法基本和post类似,也是两种形式,可以传递两种不同的参数

DELETE

DELETE也比较容易,我们有两种方式来传递参数,key/value形式或者PathVariable(参数放在路径中),首先我们在provider中定义两个delete方法。

@DeleteMapping("/user1")
public void deleteUser1(Integer id) {
    System.out.println(id);
}

@DeleteMapping("/user2/{id}")
public void deleteUser2(@PathVariable Integer id){
    System.out.println(id);
}

然后在consumer中调用这两个删除的接口

@GetMapping("/hello9")
public void hello9() {
    restTemplate.delete("http://provider/user1?id={1}",99);
    restTemplate.delete("http://provider/user2/{1}",99);
}

delete中参数的传递,也支持map,这块实际上和get是一样的

客户端负载均衡

客户端负载均衡就是相对服务端负载均衡而言的。
服务端负载均衡,就是传统的Nginx的方式,用Nginx做负载均衡,我们称之为服务端负载均衡

这种负载均衡,我们称之为服务端负载均衡,它的一个特点就是,调用的客户端并不知道具体是哪一个Server提供的服务,它也不关心,反正请求发送给Nginx,Nginx再将请求转发到Tomcat,客户端只需要记着Nginx的地址即可。

客户端负载均衡则是另外一种情形:

客户端负载均衡,就是调用的客户端本身是知道所有的Server的详细信息的,当需要调用Server上的接口的时候,客户端从自身所维护的Server列表中,根据提前配置好的负载均衡策略,自己挑选一个Server来调用,此时,客户端知道它所调用的是哪一个Server

在RestTemplate中,要想使用负载均衡功能,只需要给RestTemplate实例上添加一个@LoadBalanced注解即可,此时,RestTemplate就会自动具备负载均衡功能,这个负载均衡就是客户端负载均衡。

负载均衡原理

在SpringCloud中,实现负载均衡非常容易,只需要添加@LoadBalanced注解即可,只要添加了该注解,一个原本普普通通做Rest请求的工具RestTemplate就会自动具备负载均衡功能,这个是怎么实现的呢?
整体上来说,这个功能的实现就是三个核心点:

  1. 从Eureke Client本地缓存的服务注册信息中,选择一个可以调用的服务
  2. 根据1中所选择的服务,重构请求URL地址
  3. 将1,2步的功能嵌入到RestTemplate中
posted @ 2020-08-02 21:40  柒丶月  阅读(128)  评论(0编辑  收藏  举报