有過dubbo/dubbox使用經驗的朋友,看到下面這張圖,一定很熟悉,就是SOA架構的最基本套路。

與dubbo對比,上圖的3大要素中,spring cloud是借助以下組件來實現的:
1、注冊中心:
spring cloud默認使用eureka server來做注冊中心,而dubbo默認使用的是zookeeper。eureka的注冊信息是保存在一個雙層的Map對象中的,換句話說在內存中,不象zookeeper是長久保存在節點中。
2、服務提供方:
spring-web(Spring MVC)提供了完善的http rest服務框架,用這一套就能提供rest服務。(目前spring cloud官方提供的示例基本上都是http rest服務,理論上講,應該也可以擴展成rpc服務,而dubbo是以rpc為主的,這點有些區別)
3、服務消費方:
依賴於spring-web,負載均衡采用ribbon組件來完成,大致原理是從注冊中心發現可用服務的信息,緩存在本地,然后按一定的負載均衡算法進行調用。(跟dubbo類似,只不過dubbo是自己實現的負載均衡)
下面是這三方的最基本示例:
一、項目結構
注:spring-cloud是完全基於Spring Boot來構建項目的,所以對spring boot不熟悉的,建議先看本博客的spring boot系列。

register-center 即 eureka 注冊中心
service-api 為服務契約
service-consumer 為服務消費方
service-provider 為服務提供方
二、register-center
2.1 依賴項
buildscript {
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE")
}
}
apply plugin: 'spring-boot'
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE"
}
}
dependencies {
compile 'org.springframework.cloud:spring-cloud-starter-eureka-server'
compile 'org.springframework.boot:spring-boot-starter-actuator'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
2.2 main入口程序
package com.cnblogs.yjmyzz.spring.cloud.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* Created by 菩提樹下的楊過 on 2017/6/17.
*/
@SpringBootApplication
@EnableEurekaServer
public class RegisterServer {
public static void main(String[] args) {
SpringApplication.run(RegisterServer.class, args);
}
}
主要是靠最上面的@EnableEurekaServer這個注解,其它完全沒有花頭。
2.3 配置
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8000/eureka
解釋一下:
注冊中心本身也是一個服務,也可以當成普通服務向其它注冊中心來注冊,由於本示例中,只有一個eureka server自己就充當注冊中心,也不需要跟其它注冊中心同步注冊信息,所以都設置成false。最后一行的defaultZone,初次接觸可以先不管,先理解成注冊中心對外暴露的地址即可。
2.4 啟動
啟動后,瀏覽http://localhost:8000/,可以看到類似下圖:

現在沒有任何服務注冊,所以在Application里,顯示No instances available.
三、service-api
為了方便后面講解,先定義一個服務接口,以及對應的DTO
package com.cnblogs.yjmyzz.spring.cloud.study.api;
import com.cnblogs.yjmyzz.spring.cloud.study.dto.UserDTO;
/**
* Created by 菩提樹下的楊過 on 2017/6/17.
*/
public interface UserService {
UserDTO findUser(Integer userId);
}
以及
package com.cnblogs.yjmyzz.spring.cloud.study.dto;
import lombok.Data;
/**
* Created by 菩提樹下的楊過 on 2017/6/17.
*/
@Data
public class UserDTO {
private Integer userId;
private String userName;
}
四、service-provider
4.1 依賴項
buildscript {
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE")
}
}
apply plugin: 'spring-boot'
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE"
}
}
dependencies {
compile(project(":service-api"))
compile 'org.springframework.cloud:spring-cloud-starter-eureka'
compile 'org.springframework.boot:spring-boot-starter-actuator'
compile 'org.springframework.boot:spring-boot-starter-web'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
4.2 接口實現
package com.cnblogs.yjmyzz.spring.cloud.study.service.impl;
import com.cnblogs.yjmyzz.spring.cloud.study.api.UserService;
import com.cnblogs.yjmyzz.spring.cloud.study.dto.UserDTO;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public UserDTO findUser(Integer userId) {
UserDTO user = new UserDTO();
user.setUserId(userId);
user.setUserName("菩提樹下的楊過");
return user;
}
}
這里只是隨便示意一下,直接返回一個固定的UserDTO實例。
4.3 controller
package com.cnblogs.yjmyzz.spring.cloud.study.controller;
import com.cnblogs.yjmyzz.spring.cloud.study.api.UserService;
import com.cnblogs.yjmyzz.spring.cloud.study.dto.UserDTO;
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.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public UserDTO findUser(@PathVariable Integer id) {
return userService.findUser(id);
}
}
這里用了一個新的注解GetMapping,相當於之前SpringMVC中@RequestMapping(method = RequestMethod.GET),更簡潔而已。
到目前為止,都跟常規的SpringMVC無異。
4.4 main入口
package com.cnblogs.yjmyzz.spring.cloud.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* Created by yangjunming on 2017/6/17.
*/
@EnableDiscoveryClient
@SpringBootApplication
public class ServiceProvider {
public static void main(String[] args) {
SpringApplication.run(ServiceProvider.class, args);
}
}
依舊還是@EnableDiscoveryClient挑大梁,表明這是一個eureka的客戶端程序(即:能向eureka server注冊)
4.5 配置
server:
port: 8001
spring:
application:
name: "service-provider-demo"
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8000/eureka/
應該不難理解,最后那幾行,表示用自己IP地址向 http://localhost:8000/eureka/注冊
4.6 啟動
啟動成功后,再看eureka 剛才的頁面,會發現已經注冊進來了。

注:大家可以把service-provider多啟動幾個實例(端口錯開,不要沖突即可),然后再觀察下這個界面,可以看到注冊了多個provider實例
五、service-consumer
5.1 依賴項
buildscript {
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE")
}
}
apply plugin: 'spring-boot'
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE"
}
}
dependencies {
compile(project(":service-api"))
compile 'org.springframework.cloud:spring-cloud-starter-eureka'
compile 'org.springframework.boot:spring-boot-starter-actuator'
compile 'org.springframework.cloud:spring-cloud-starter-ribbon'
compile 'org.springframework.boot:spring-boot-starter-web'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
5.2 建一個調用的Controller
package com.cnblogs.yjmyzz.spring.cloud.study.service.controller;
import com.cnblogs.yjmyzz.spring.cloud.study.dto.UserDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* Created by yangjunming on 2017/6/17.
*/
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/order/{userId}/{orderNo}")
public String findOrder(@PathVariable Integer userId, @PathVariable String orderNo) {
UserDTO user = restTemplate.getForEntity("http://SERVICE-PROVIDER-DEMO/user/" + userId, UserDTO.class).getBody();
if (user != null) {
return user.getUserName() + " 的訂單" + orderNo + " 找到啦!";
}
return "用戶不存在!";
}
@GetMapping("/user-instance")
public List<ServiceInstance> showInfo() {
return this.discoveryClient.getInstances("SERVICE-PROVIDER-DEMO");
}
@GetMapping("/log-instance")
public ServiceInstance chooseInstance() {
return this.loadBalancerClient.choose("SERVICE-PROVIDER-DEMO");
}
}
這里暴露了3個url,一個個來看:
a. /order/{userId}/{orderNo} 這個用來示例如何調用service-provider中的方法,注意這里我們並沒有用http://localhost:8001/user/1 來調用,而通過http://service-provider-demo/user/ 指定service-provider的application name,讓系統從注冊中心去發現服務。
b. /user-instance , /log-instance 這二個url 用來輔助輸出從注冊中心發現的服務實例相關的信息,並非必須。
這里面還有二個注入的實例:restTemplate 、loadBalancerClient ,分別用來發起rest的http請求,以及使用負載均衡從可用的服務列表中,挑出一個可用實例。
5.3 main入口
package com.cnblogs.yjmyzz.spring.cloud.study.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class ServiceConsumer {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ServiceConsumer.class, args);
}
}
依然靠二個關鍵的注解:@EnableDiscoveryClient、@LoadBalanced,特別是@LoadBalanced,經過這個修飾的restTemplate,就不是普通的restTemplate了,而是具備負載均衡能力的restTemplate,即每次都會用負載均衡算法,從可用服務列表中,挑一個進行調用。
5.3 啟動

可以從eukera中看到,service-provider與service-consumer都注冊進來了。
調用一下試試:http://localhost:8002/order/1/1000,成功的話會看到下面的輸出

注:此時可以把注冊中心eureka server停掉,然后再調用下http://localhost:8002/order/1/1000,會發現仍然可以正常調用,說明注冊中心的服務列表,在本機是有緩存的,這跟dubbo/dubbox類似。
另外還可以驗證下負載均衡,方法如下:
先把service-provider啟2個,開二個終端窗口:
java -jar xxx.jar --server.port=9001
java -jar xxx.jar --server.port=9002
這樣就能跑二個應用起來,然后看注冊中心

然后再調用下consumer的log-instance

可以看到,這次選擇的是9002端口應對的實例,然后再刷新一下:

這回選擇的是另一個端口9001的實例,說明負載均衡確實起作用了。
至此,一個最基本的SOA框架雛形搭建起來了,當然還有很多地方需要完善,比如:注冊中心如何做到HA,服務融斷如何處理,注冊中心如何安全認證(防止其它服務亂注冊)等等,后面再講。
