什么都不会也能开发Spring Cloud!
教练,我想……
- 什么是Spring Cloud?
- Spring Cloud和Dubbo有什么不同?
- 怎么搭建Spring Cloud?
1. 什么是Spring Cloud?
“Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。”——度娘百科
简单来讲,Spring Cloud就是一个基于Spring Boot架设的可以用来开发分布式微服务应用的框架。
但是,它不仅仅可以用来开发微服务。
2. Spring Cloud和Dubbo有什么不同?
目前说到Spring Cloud就不得不提到Alibaba开源,最后贡献给Apache托管维护的分布式框架Dubbo,他们二者在目的上十分相近,但是实现原理以及开发方式又有许多不同。比如Dubbo是RPC,Spring Cloud是Http。
如果从理论上说,Dubbo的数据传输方式为二进制码,而Spring Cloud则是序列化的Json字符串,所以效率后者肯定比前者低一些,但是曾有人测试这个差距在现代设备上已经微乎其微,所以我们可以忽略。
从开发角度说,Dubbo的jar依赖过于繁杂,版本也是十分混乱,可能是由于之前断更了许多年,之后又交给了Apache管理,所以你在网上看到的关于Dubbo的教程,依赖一会儿是Alibaba的,一会儿是Apache的,出现冲突十分普遍。相反,Spring Cloud的依赖则十分统一,即使版本繁杂,只要你指定了一个版本,也基本上不会出现jar包依赖冲突的问题。
撇开依赖问题,从开发角度说,两者各有优势,但总体上Dubbo难度略高一丢丢。
3. 怎么搭建Spring Cloud?
3.1 创建一个注册中心
首先我用的IDE是idea,点File > new > Project...
,弹出窗口后,左侧选择Spring Initializr
(Spring初始化),右侧选择JDK,Next
。
下一页填写项目相关信息,创建一个名为spring-cloud-demo
的,空的Spring Boot项目,作为整个工程的父项目。你可以删掉src,因为父项目只是作为一个壳,留下一个pom.xml
即可。
为什么我不之间创建Maven项目呢?很简单,懒。因为我们的父项目要以Spring Boot为父项目,方便后续开发。
另外提一下,我这里创建的Spring Boot版本号为2.1.6.RELEASE
,你可以选用其他版本,但后续其他引用版本请自行更改。
现在右键刚才创建的项目,new一个Module
,还是选择Spring Initializr
,名字叫scdemo-server
,作为服务的注册中心。
scdemo-server
请修改pom.xml
,将parent修改为刚才创建的spring-cloud-demo
。
例如我的如下:
<parent>
<groupId>com.yq</groupId>
<artifactId>scdemo-cloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
请确保你填写的groupId和其他信息都符合自己几分钟前命名的。
请在dependencies
标签下引入eureka的pom,例如:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
这时你的maven可能会报错,因为我们没有指定版本,这里之所以创建一个父项目,就是为了统一一个全局的版本号,避免因为各个项目引入不同版本的jar包引发麻烦。
别着急,打开刚才父项目的pom.xml
,添加一个dependencyManagement
,这个标签属于project
的直接子标签,请不要写错了位置。并且dependencyManagement
中只是声明全局引用jar包的版本,并非真的把jar引入了项目。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在这里我们限定了spring-cloud全局版本号为Greenwich.RELEASE
,即未来子项目中只要不特意声明<version>
,都会默认遵循此版本。
如此一来,应该可以成功导入spring-cloud-starter-netflix-eureka-server
这个jar包了,并且从idea的MavenProjects窗口可以看到其版本号为2.1.0.RELEASE
。
此时,请在你的Spring Boot启动入口类上加上
@EnableEurekaServer
注解,以启动注册中心服务。打开
application.yml
(如果你没有这个文件,请在resources
目录下新建这个文件,当然你也可以自行改成properties格式)文件,添加如下配置:
server:
port: 8761
spring:
application:
name: spring-cloud-server
eureka:
instance:
hostname: 127.0.0.1
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
run你的程序,然后浏览器访问http://127.0.0.1:8761/就可以看到注册中心的页面,这个页面显示了当前已注册服务和其他一些系统信息,当然,现在啥服务都没有。
这就相当于Dubbo你启动了一个Zookeeper或者Nacos作为注册中心一样。
3.2 创建一个服务提供者Provider
为了统一消费者和提供者的接口定义,避免不必要麻烦的发生概率,这里我们写controller时会才用接口和实现的方式,将controller作为实现类,与接口分开打包,这样在需要调用的地方会方便很多。你现在看不懂这一段,很正常。
再到父工程下new一个Module
,名字就叫scdemo-api
吧,同样请将父项目指向spring-cloud-demo
。
在pom.xml
中添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
其中spring-boot-starter-web
是引入了web框架,因为我们的服务提供者对外提供了http服务。spring-cloud-starter-openfeign
是一个访问http请求的客户端,原属于Netflix,后被整合到Spring Cloud集合下,衍生出openfeign,其实是一个东西。
在src下创建一个名为ITestCloudService
的接口,形如:
@FeignClient(name = "spring-cloud-provider", path = "/api/v1/test")
public interface ITestCloudService {
@GetMapping("/t")
Map<String, Object> testFnFeign();
}
其中@FeignClient
声明这是一个Feign客户端可访问的接口,参数name指的是注册中心服务的名称,path指的是接口映射的前缀,类似于RequestMapping。
然后删除这个项目的启动类,因为这个项目只作为接口依赖,并不会单独启动服务。同时请修改父项目的pom.xml
,将这个接口声明依赖添加到全局版本声明中,至此你的父项目应该包含如下定义:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.yq</groupId>
<artifactId>scdemo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
紧接着,再新建一个Module
,这里就叫scdemo-provider
吧。
这个项目的pom.xml
也请修改父项目,并且引入如下依赖:
<dependency>
<groupId>com.yq</groupId>
<artifactId>scdemo-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
请在src下新建一个TestCloudServiceImpl
类,实现ITestCloudService
接口,如果你无法importITestCloudService
接口,请检查pom依赖是否正确。
TestCloudServiceImpl
类内容形如:
@RestController
@RequestMapping("/api/v1/test")
public class TestCloudServiceImpl implements ITestCloudService {
@Value("${server.port}")
private String serverPort;
@Value("${eureka.instance.appname}")
private String eurekaInstanceAppName;
@Override
public Map<String, Object> testFnFeign() {
Map<String, Object> map = new HashMap<>();
map.put("serverPort", serverPort);
map.put("eurekaInstanceAppName", eurekaInstanceAppName);
map.put("me", "provider");
return map;
}
}
这里注入的2个Value是为了鉴别测试结果,接口的内容也很简单,主要说一下Spring的@RestController
和@RequestMapping
这两个注解,并非只能写到controller上。联系上刚才写的Interface,这个‘Controller’的接口映射是@RequestMapping("/api/v1/test")
+@GetMapping("/t")
,也就是/api/v1/test/t
。
接下来是provider的配置文件,application.yml
:
server:
port: 8762
spring:
application:
name: spring-cloud-provider
eureka:
instance:
appname: ${spring.application.name}
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/
请启动Spring Boot的程序入口,然后访问http://127.0.0.1:8762/api/v1/test/t
如果没问题,你将拿到一个返回结果:
{"me":"provider","serverPort":"8762","eurekaInstanceAppName":"spring-cloud-provider"}
至此服务提供者已经部署完成。
3.3 创建一个消费者consumer
重复内容就不多说了,修改pom.xml
符项目,添加如下依赖:
<dependency>
<groupId>com.yq</groupId>
<artifactId>scdemo-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
可以看到,这里也引入了scdemo-api
这个依赖,但并不是为了去实现它,而是调用它。
消费者配置文件application.xml
如下:
server:
port: 8763
spring:
application:
name: spring-cloud-consumer
eureka:
instance:
appname: ${spring.application.name}
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/
请注意,消费者调用服务有两种可选方式。
第一种是RestTemplate
,如需要使用,请在Spring Boot的配置类中注入Bean,可参考我的注入:
@Bean
@LoadBalanced // 开启负载均衡
public RestOperations restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
第二种是使用Feign客户端,请在Spring Boot的配置类上加上@EnableFeignClients(basePackages = "com.yq.scdemo.api.*")
,用来启用Feign客户端,其中basePackages指明扫描API接口的包名,即之前在那个scdemo-api
项目下创建的interface的包名。
因为Spring Boot默认扫描本项目启动类目录下的注解,如果API interface的包名在此之外,需要额外指定。
本示例同时演示了2种客户端工具,实际情况请自行选择。
最后来编写消费者的测试controller:
@RestController
public class TestConsumerController {
@Autowired
private RestOperations restTemplate;
@Autowired
private ITestCloudService testCloudService;
@GetMapping("/rest")
public Map testFnRest() {
Map map = restTemplate.getForObject("http://spring-cloud-provider/api/v1/test/t", Map.class);
map.put("me", "consumer");
return map;
}
@GetMapping("/feign")
public Map testFnFeign() {
Map<String, Object> map = testCloudService.testFnFeign();
map.put("me", "consumer");
return map;
}
}
/rest
和/feign
分别演示了两种客户端的调用方式。
其中rest的调用是写死的url,不方便维护,url中spring-cloud-provider为服务名,后面是接口地址。而feign的调用方式在我们提取出公共API Interface之后,极其类似Dubbo的开发模式,消费端无需关心接口地址和其他声明,因为这些都是服务端开发controller时顺便定义的。
接下来启动消费者,如果没问题,
访问http://127.0.0.1:8763/rest
你将通过restTemplate方式调用云服务。
访问http://127.0.0.1:8763/feign
使用的则是feign客户端方式。
当然,在这里因为调用的是同一个服务,所以返回结果都是:
{"me":"consumer","serverPort":"8762","eurekaInstanceAppName":"spring-cloud-provider"}
3.4 测试
当这里,基本的Spring Cloud服务已经搭建完成,但是既然是微服务,怎么能不测试分布式呢?
我们没有钱买许多电脑部署provider,但是我们可以模拟。
接下来,请自行打开scdemo-provider
项目的application.yml,修改
server:
port: 填写任何一个未被占用的端口号
然后依据自己电脑的性能,开启多个provider服务。
访问Eureka注册中心页面http://127.0.0.1:8761/,你可以看到自己已经启动的各项服务,请至少启动2个吧?
第一行是消费者,第二行是服务提供者,这里我只启动了2个provider,一个是8762端口,一个是8764端口。
由于之前的配置中已经开启了负载均衡,所以此时再访问http://127.0.0.1:8763/rest或者http://127.0.0.1:8763/feign,你都将按照一定规则请求到2个不同的服务上,可以通过端口号来判断。至于负载均衡的规则,有兴趣的话可以自行研究并配置尝试。
附录
项目源码已提交至github,欢迎star
。
https://github.com/lemtasev/spring-cloud-demo