下面就简单列一下order-service子项目的代码:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false username: root password: zc20020106 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: type-aliases-package: com.haiexijun.user.pojo configuration: map-underscore-to-camel-case: true logging: level: com.haiexijun: debug pattern: dateformat: MM-dd HH:mm:ss:SSS
controller层:
package com.haiexijun.order.web; import com.haiexijun.order.pojo.Order; import com.haiexijun.order.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("order") public class OrderController { @Autowired private OrderService orderService; @GetMapping("{orderId}") public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) { // 根据id查询订单并返回 return orderService.queryOrderById(orderId); } }
service层:
package com.haiexijun.order.service; import com.haiexijun.order.mapper.OrderMapper; import com.haiexijun.order.pojo.Order; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderService { @Autowired private OrderMapper orderMapper; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 4.返回 return order; } }
pojo和mapper:
package com.haiexijun.order.pojo; import lombok.Data; @Data public class User { private Long id; private String username; private String address; }
package com.haiexijun.order.pojo; import lombok.Data; @Data public class Order { private Long id; private Long price; private String name; private Integer num; private Long userId; private User user; }
package com.haiexijun.order.mapper; import com.haiexijun.order.pojo.Order; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; @Repository public interface OrderMapper { @Select("select * from tb_order where id = #{id}") Order findById(Long id); }
我们分别启动这两个项目(一定要都运行起来),分别访问不同的端口,进行数据访问:
到这来,我们就完成了项目的准备工作。同时实现了服务拆分。下一节就来学习如何做远程调用。
3.微服务远程调用
在学习之前先来引出案例需求:
我们要实现根据订单Id查询订单的同时,把订单所属的用户信息一起返回。
现在我们的订单服务还不能做到这一点,而且我们不能直接通过访问用户服务的数据库获得信息,要去访问用户服务获取才对。
但是问题来了,我们以前没有学过从一个服务到另外一个服务的远程调用。下面就来分析一下,如何进行远程调用。
远程调用方式分析:
我们的user服务通过@GetMapping(“/user/{id}”)对外暴露了一个restful的接口,只要我们在浏览器里面输入对应的地址,就可以拿到用户信息。我们的order订单服务如果也能像浏览器一样发起一个http的请求,用户服务也应该返回一个对应的信息给我们。这时候,订单模块再结合本地数据库查询出来的订单信息,就组合出了最终的目标了。
所以我们的问题就变成了如何在Java代码当中发起HTTP请求,如果能发起HTTP请求,就可以调用其他服务的restful接口了。
Spring它提供了一个工具叫做RestTemplate ,这个工具就是Spring提供给我们来发HTTP请求的。我们要使用这个工具,就要先在配置类里面注册,而启动类就是一个配置类。所以我们可以在order-service的OrderApplication中通过@Bean注解注册RestTemplate。
package com.haiexijun.order; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @MapperScan("com.haiexijun.order.mapper") @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } /** * 创建RestTemplate,并注入Spring容器 * @return */ @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
完成了这一步以后,我们接下来就可以利用它来发HTTP请求了。
所以我们下面要对订单的查询业务进行修改:
package com.haiexijun.order.service; import com.haiexijun.order.mapper.OrderMapper; import com.haiexijun.order.pojo.Order; import com.haiexijun.order.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.利用RestTemplate发起HTTP请求,查询用户 String url="http://localhost:8081/user/"+order.getUserId(); User user= restTemplate.getForObject(url, User.class); // 3.封装user 到 order里面去 order.setUser(user); // 4.返回 return order; } }
我们重启项目后,下面来运行两条查询订单试试:
到这里,我们就实现了,跨服务的远程调用。
4.服务的提供者与消费者
这一节又是讲解概念的一节,会了解什么是服务的提供者与消费者。
服务提供者:一次业务中,被其他微服务调用的服务就称之为服务的提供者。(提供接口给其他微服务)
服务消费者:一次业务中,调用其他微服务的服务就称之为服务的消费者。(调用其他微服务提供的接口)
我们上一节的案例中,user-service是服务提供者提供者,而order-service是服务消费者。
现在就会引发出一个问题了,现在服务A调用了服务B,而服务B调用了服务C,那服务B是什么角色呢?这时,比相对与A而言是提供者,而相对于C而言是消费者。所以一个服务既可以是提供者,又可以是消费者。
三. Eureka注册中心
在这一节里,我们会聊一聊之前案例里面存在的一些问题,以及Eureka如何解决这些问题,然后会介绍Eureka具体如何去使用。
1.Eureka原理分析
先来回顾一下之前的案例,在之前的案例当中,我们有一个订单服务和用户服务。订单服务需要远程调用我们的用户服务,它采用的方式是发起一次HTTP请求。不过在我们的代码当中,我们是将user-service的IP和端口硬编码在代码当中的。
但这样的写法是存在一定的问题的。比如说我们开发的时候,我们会分开发环境、测试环境和生产环境等等等。每一次环境的变更,可能服务的地址也会发生改变。如果你采用硬编码的方式写死了,难道每一次都要重新修改代码然后编译打包吗?而且,为了应对更高的并发,我们的user服务可能会部署成多实例,形成一个集群,端口和地址可能就不一样了。这时候,我们到底改写谁的地址呢?如果选择其中一个实例,那其他几个的意义岂不是就没有了吗?
所以这里一定不能采用硬编码的方式。那问题也来了,如果我不做硬编码,那这三个服务的地址我该如何去获取呢?而且万一以后又有第四台和第五台呢?如果拿到了他们的地址,我该如何挑选其中一台去使用呢?如果你挑中了一台,你怎么知道这台现在依然是健康的呢?万一它挂了呢?是不是一堆的问题啊?
但是啊,我们的 Eureka是可以解决这些问题的!
Eureka的作用:
在Eureka的结构当中,它分成了2个概念(角色),第一个角色是eureka-server 注册中心 ,它的作用是记录和管理这些微服务。而第二个角色是我们的服务提供者和服务消费者,但不管是提供者还是消费者,都是微服务,我们把它叫做eureka-client 客户端 。
我们的user-service和order-service在启动的时候会做一件事情,它会把自己的信息注册给eureka。注意:是每开一个服务启动时都会注册。eureka会把你的服务信息给记录下来。
那么全都记下来了,所有的服务信息都在注册中心里面了。如果这个时候我们有一个服务想要消费,它不需要自己去记录信息,直接找eureka就好了。如果eureka找到发现有,而且还有3个呢,就会把信息给服务消费者。
服务消费者拿到了服务提供者的信息,发现有三个,这个时候就会用到我们以前学过的负载均衡的知识点,从这三个里面挑一个出来。
那可能会想了,那你调用的这一个会不会是挂了的啊?这肯定不会,因为我们的服务每隔30秒都会向eureka发一次心跳续约 ,来确认自己的状态。如果哪一天,它不跳了。eureka就会把那个服务移除,服务消费者自然也不会得到挂掉的那个服务。
2.搭建eureka服务
我们来做三件事情,第一就是搭建eureka注册中心 ,第二步我们会完成服务的注册,把所有的服务都注册到eureka,第三步我们会去做服务发现,会让order-service取拉去服务列表,得到user-service的所有实例,利用负载均衡去挑一个。下面我们就来一步一步完成:
搭建EurekaServer 注册中心
(1)eureka的搭建需要创建一个独立的微服务,所以等一会儿我们会在Spring Cloud父项目下创建一个新的项目(new一个maven的Module),这个子项目名叫eureka-server,然后子项目会引入下面的依赖:
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
(2)创建启动类,添加@EnableEurekaServer注解
package com.haiexijun.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class,args); } }
(3)在application.yml配置文件中添加如下的配置(这里最好复制,不然容易出错):
server: port: 10086 # 服务端口 spring: application: name: eureka-server # 服务的名称 eureka: client: service-url: # eureka的地址信息,如果是eureka集群的话,用逗号隔开 defaultZone: http://127.0.0.1:10086/eureka
下面就可以运行起来了。
dea的功能很强大,我们点击之后会跳转到Eureka的界面,如下图所示:
这个界面的Instances currently registered with Eureka就是注册到Eureka中的实例的列表。上面我们看见了,eureka也注册了自己的实例。
3.服务注册
服务注册只要经过以下两个步骤就够了:
(1)在user-service的pom文件中,引入下面的eureka-client依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
(2)编写user-service的application.yml文件,内容如下:
server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false username: root password: zc20020106 driver-class-name: com.mysql.cj.jdbc.Driver application: #服务的名称 name: userservice mybatis: type-aliases-package: com.haiexijun.user.pojo configuration: map-underscore-to-camel-case: true logging: level: com.haiexijun: debug pattern: dateformat: MM-dd HH:mm:ss:SSS eureka: client: service-url: # eureka的地址 defaultZone: http://127.0.0.1:10086/eureka