说起Spring Cloud那肯定要带上Spring Boot,业内人士对这两个东西必定不陌生。关于Spring Cloud的介绍,这里就不再过多的介绍。关于Spring Cloud搜索引擎搜索出来的资料并不乐观,可能向我一样的初学者,最需要的就是一份demo,先跑起来,至少做到麻雀虽小五脏俱全。
在这里还是要介绍以下Spring Cloud整个的工作流程。首先看一下Spring Cloud的工作流程,
Service Consumer -> Proxy Server -> Client(Load Balancer) -> Servie Discovery -> Target Service
这里就是要一个简单Service API调用的流程。下面看一下Spring Cloud中的组件与上面流程的对应关系。
下面一步步介绍完成以上的简单demo
###数据结构及基础服务:
在这里使用Spring Boot快速的创建三个基础服务,并且其中三者之间存在逻辑依赖关系。
Company Service
Java
public class Company {
private Long id;
private String name;
// Constructor、Getter、Setter
}
@RestControllerpublic class CompanyService {
@RequestMapping("/company/{id}")public Company getCompanyById(@PathVariable("id") Long id){
sleep();
return new Company(id, "Company");
}
//利用时间等待模拟Serivce调用时长private void sleep() {
Random rand = new Random();
int time = rand.nextInt(2000);
try {
Thread.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Company {
private Long id;
private String name;
// Constructor、Getter、Setter
}
@RestControllerpublic class CompanyService {
@RequestMapping("/company/{id}")public CompanygetCompanyById(@PathVariable("id") Long id){
sleep();
return new Company(id, "Company");
}
//利用时间等待模拟Serivce调用时长private void sleep() {
Randomrand = new Random();
int time = rand.nextInt(2000);
try {
Thread.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Employee Service
Java
public class Employee {
private Long id;
private Long companyId;
private String name;
// Constructor、Getter、Setter
}
@RestControllerpublic class EmployeeService {
@RequestMapping("/employee/{id}")public Employee getEmployeeById(@PathVariable("id") Long id) {
sleep();
return new Employee(id,1L,"张三");
}
@RequestMapping("/employee")public List<Employee> getEmployeesByCompanyId(@RequestParam("companyId") Long companyId){
List<Employee> employees = new ArrayList<>();
employees.add(new Employee(1L, companyId, "张三"));
employees.add(new Employee(2L, companyId, "李四"));
employees.add(new Employee(3L, companyId, "王五"));
sleep();
return employees;
}
private void sleep() {
Random rand = new Random();
int time = rand.nextInt(2000);
try {
Thread.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Employee {
private Long id;
private Long companyId;
private String name;
// Constructor、Getter、Setter
}
@RestControllerpublic class EmployeeService {
@RequestMapping("/employee/{id}")public EmployeegetEmployeeById(@PathVariable("id") Long id) {
sleep();
return new Employee(id,1L,"张三");
}
@RequestMapping("/employee")public List<Employee> getEmployeesByCompanyId(@RequestParam("companyId") Long companyId){
List<Employee> employees = new ArrayList<>();
employees.add(new Employee(1L, companyId, "张三"));
employees.add(new Employee(2L, companyId, "李四"));
employees.add(new Employee(3L, companyId, "王五"));
sleep();
return employees;
}
private void sleep() {
Randomrand = new Random();
int time = rand.nextInt(2000);
try {
Thread.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Product Service
Java
public class Product {
private Long id;
private Long companyId;
private String sku;
// Constructor、Getter、Setter
}
@RestControllerpublic class ProductService {
private static final Logger LOG = LoggerFactory.getLogger(ProductService.class);
@RequestMapping("/product/{id}")public Product getProductById(@PathVariable("id") Long id) {
sleep();
return new Product(id, 1L, "T001");
}
@RequestMapping("/product")public List<Product> getProductsByCompanyId(@RequestParam("companyId") Long companyId) {
List<Product> products = new ArrayList<>();
products.add(new Product(1L, companyId, "T001"));
products.add(new Product(2L, companyId, "T002"));
products.add(new Product(3L, companyId, "T003"));
return products;
}
private void sleep() {
Random rand = new Random();
int time = rand.nextInt(3000);
try {
Thread.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Product {
private Long id;
private Long companyId;
private String sku;
// Constructor、Getter、Setter
}
@RestControllerpublic class ProductService {
private static final LoggerLOG = LoggerFactory.getLogger(ProductService.class);
@RequestMapping("/product/{id}")public ProductgetProductById(@PathVariable("id") Long id) {
sleep();
return new Product(id, 1L, "T001");
}
@RequestMapping("/product")public List<Product> getProductsByCompanyId(@RequestParam("companyId") Long companyId) {
List<Product> products = new ArrayList<>();
products.add(new Product(1L, companyId, "T001"));
products.add(new Product(2L, companyId, "T002"));
products.add(new Product(3L, companyId, "T003"));
return products;
}
private void sleep() {
Randomrand = new Random();
int time = rand.nextInt(3000);
try {
Thread.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这三个服务都应该是正常独立服务的。
所有的服务都需要在注册中心进行对当前服务的注册,因此要在三个服务的启动程序上加上注解 @EnableDiscoveryClient
在配置文件bootstrap.yml中加入以下配置
YAML
spring: application: name: company-service
cloud: config: uri: ${vcap.services.${PREFIX:}configserver.credentials.uri:http://user:password@localhost:8888}
spring: application: name: company-service
cloud: config: uri: ${vcap.services.${PREFIX:}configserver.credentials.uri:http://user:password@localhost:8888}
###服务中心
注册中心,在Demo中只用到最简单的服务发现与注册,只需要在启动程序加上注解 @EnableEurekaServer
@EnableDiscoveryClient
在application.yml中对注册服务进行配置
YAML
server: port: 8761security: user: password: ${eureka.password}
eureka: client: registerWithEureka: false fetchRegistry: false server: waitTimeInMsWhenSyncEmpty: 0 password: ${SECURITY_USER_PASSWORD:password}
server:
port: 8761security:
user:
password: ${eureka.password}
eureka:
client:
registerWithEureka: false
fetchRegistry: false
server:
waitTimeInMsWhenSyncEmpty: 0
password: ${SECURITY_USER_PASSWORD:password}
###远程调用Client Adapter
使用服务发现来寻找真实的服务地址,并获取API返回的数据。
在这里需要创建三个model,用来转换API结构
Java
public class CompanyAll {
private Long id;
private String name;
private List<ProductDetail> productList;
private List<EmployeeDetail> employeeList;
public CompanyAll(Company company, List<Product> productList, List<Employee> employeeList) {
this.id = company.getId();
this.name = company.getName();
if (employeeList != null) {
this.productList = productList.stream().map(product ->
new ProductDetail(product.getId(), product.getSku())
).collect(Collectors.toList());
}
if (employeeList != null) {
this.employeeList = employeeList.stream().map(employee ->
new EmployeeDetail(employee.getId(), employee.getName())
).collect(Collectors.toList());
}
}
//Getter、Setter
}
public class EmployeeDetail {
private Long id;
private String name;
// Constructor、Getter、Setter
}
public class ProductDetail {
private Long id;
private String sku;
// Constructor、Getter、Setter
}
public class CompanyAll {
private Long id;
private String name;
private List<ProductDetail> productList;
private List<EmployeeDetail> employeeList;
public CompanyAll(Companycompany, List<Product> productList, List<Employee> employeeList) {
this.id = company.getId();
this.name = company.getName();
if (employeeList != null) {
this.productList = productList.stream().map(product ->
new ProductDetail(product.getId(), product.getSku())
).collect(Collectors.toList());
}
if (employeeList != null) {
this.employeeList = employeeList.stream().map(employee ->
new EmployeeDetail(employee.getId(), employee.getName())
).collect(Collectors.toList());
}
}
//Getter、Setter
}
public class EmployeeDetail {
private Long id;
private String name;
// Constructor、Getter、Setter
}
public class ProductDetail {
private Long id;
private String sku;
// Constructor、Getter、Setter
}
创建工具类AppUtil
在工具类中会调用到LoadBalancerClient,这个就是有Netflix Ribbon提供的Client。他会根据ServiceId(配置文件中的Service Name)向Eureka(注册服务器)获取服务地址。在这里也可以提供一个容错机制,极限情况下,全宕机时使用fallbackUri。
Java
@Component
public class AppUtil {
@Autowired
private LoadBalancerClient loadBalancer;
public URI getRestUrl(String serviceId, String fallbackUri) {
URI uri = null;
try {
ServiceInstance instance = loadBalancer.choose(serviceId);
uri = instance.getUri();
} catch (RuntimeException e) {
uri = URI.create(fallbackUri);
}
return uri;
}
public <T> ResponseEntity<T> createOkResponse(T body) {
return createResponse(body, HttpStatus.OK);
}
public <T> ResponseEntity<T> createResponse(T body, HttpStatus httpStatus) {
return new ResponseEntity<>(body, httpStatus);
}
public <T> T json2Object(ResponseEntity<String> response, Class<T> clazz) {
try {
return (T) JSON.parseObject(response.getBody(), clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public <T> List<T> json2Objects(ResponseEntity<String> response, Class<T> clazz) {
try {
return JSON.parseArray(response.getBody(), clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Component
public class AppUtil {
@Autowired
private LoadBalancerClientloadBalancer;
public URIgetRestUrl(String serviceId, String fallbackUri) {
URIuri = null;
try {
ServiceInstanceinstance = loadBalancer.choose(serviceId);
uri = instance.getUri();
} catch (RuntimeException e) {
uri = URI.create(fallbackUri);
}
return uri;
}
public <T> ResponseEntity<T> createOkResponse(T body) {
return createResponse(body, HttpStatus.OK);
}
public <T> ResponseEntity<T> createResponse(T body, HttpStatushttpStatus) {
return new ResponseEntity<>(body, httpStatus);
}
public <T> T json2Object(ResponseEntity<String> response, Class<T> clazz) {
try {
return (T) JSON.parseObject(response.getBody(), clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public <T> List<T> json2Objects(ResponseEntity<String> response, Class<T> clazz) {
try {
return JSON.parseArray(response.getBody(), clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
当然这里还需要一个Client来对Api进行访问及Response数据处理
Java
@Component
public class RemoteServiceClient {
@Autowired
AppUtil appUtil;
private RestTemplate restTemplate = new RestTemplate();
public ResponseEntity<Company> getCompanyById(Long id) {
URI uri = appUtil.getRestUrl("COMPANY-SERVICE", "http://localhost:57773/");
String url = uri.toString() + "/company/" + id;
ResponseEntity<String> resultStr = restTemplate.getForEntity(url, String.class);
Company company = appUtil.json2Object(resultStr, Company.class);
return appUtil.createOkResponse(company);
}
public ResponseEntity<List<Employee>> getEmployeesByCompanyId(Long id) {
try {
URI uri = appUtil.getRestUrl("EMPLOYEE-SERVICE", "http://localhost:58017");
String url = uri.toString() + "/employee?companyId=" + id;
ResponseEntity<String> resultStr = restTemplate.getForEntity(url, String.class);
List<Employee> employees = appUtil.json2Objects(resultStr, Employee.class);
return appUtil.createOkResponse(employees);
} catch (Throwable t) {
throw t;
}
}
public ResponseEntity<List<Product>> getProductsByCompanyId(Long id) {
try {
URI uri = appUtil.getRestUrl("PRODUCT-SERVICE", "http://localhost:57750");
String url = uri.toString() + "/product?companyId=" + id;
ResponseEntity<String> resultStr = restTemplate.getForEntity(url, String.class);
List<Product> employees = appUtil.json2Objects(resultStr, Product.class);
return appUtil.createOkResponse(employees);
} catch (Throwable t) {
throw t;
}
}
}
@Component
public class RemoteServiceClient {
@Autowired
AppUtilappUtil;
private RestTemplaterestTemplate = new RestTemplate();
public ResponseEntity<Company> getCompanyById(Long id) {
URIuri = appUtil.getRestUrl("COMPANY-SERVICE", "http://localhost:57773/");
String url = uri.toString() + "/company/" + id;
ResponseEntity<String> resultStr = restTemplate.getForEntity(url, String.class);
Companycompany = appUtil.json2Object(resultStr, Company.class);
return appUtil.createOkResponse(company);
}
public ResponseEntity<List<Employee>> getEmployeesByCompanyId(Long id) {
try {
URIuri = appUtil.getRestUrl("EMPLOYEE-SERVICE", "http://localhost:58017");
String url = uri.toString() + "/employee?companyId=" + id;
ResponseEntity<String> resultStr = restTemplate.getForEntity(url, String.class);
List<Employee> employees = appUtil.json2Objects(resultStr, Employee.class);
return appUtil.createOkResponse(employees);
} catch (Throwable t) {
throw t;
}
}
public ResponseEntity<List<Product>> getProductsByCompanyId(Long id) {
try {
URIuri = appUtil.getRestUrl("PRODUCT-SERVICE", "http://localhost:57750");
String url = uri.toString() + "/product?companyId=" + id;
ResponseEntity<String> resultStr = restTemplate.getForEntity(url, String.class);
List<Product> employees = appUtil.json2Objects(resultStr, Product.class);
return appUtil.createOkResponse(employees);
} catch (Throwable t) {
throw t;
}
}
}
最终还是需要对外暴露一个端口来返回数据,这里也是一个Restful接口
Java
@RestControllerpublic class RemoteAdapter {
private static final Logger LOG = LoggerFactory.getLogger(RemoteAdapter.class);
@Autowired
RemoteServiceClient client;
@Autowired
AppUtil appUtil;
@RequestMapping("/")public String welcome() {
return "welcome to use my demo";
}
@RequestMapping("/company/{id}")public ResponseEntity<CompanyAll> getCompany(@PathVariable("id") Long id) {
ResponseEntity<Company> companyResult = client.getCompanyById(id);
if (!companyResult.getStatusCode().is2xxSuccessful()) {
return appUtil.createResponse(null, companyResult.getStatusCode());
}
List<Product> products = null;
try {
ResponseEntity<List<Product>> productsResult = client.getProductsByCompanyId(id);
if (!productsResult.getStatusCode().is2xxSuccessful()) {
LOG.error("远程调用Product API 失败");
} else {
products = productsResult.getBody();
}
} catch (Throwable t) {
LOG.error("远程调用Product API 异常 ", t);
throw t;
}
List<Employee> employees = null;
try {
ResponseEntity<List<Employee>> employeeResult = null;
employeeResult = client.getEmployeesByCompanyId(id);
if (!employeeResult.getStatusCode().is2xxSuccessful()) {
LOG.error("远程调用Employee API 失败");
} else {
employees = employeeResult.getBody();
}
} catch (Throwable t) {
LOG.error("远程调用Employee API 失败");
throw t;
}
return appUtil.createOkResponse(new CompanyAll(companyResult.getBody(), products, employees));
}
}
@RestControllerpublic class RemoteAdapter {
private static final LoggerLOG = LoggerFactory.getLogger(RemoteAdapter.class);
@Autowired
RemoteServiceClientclient;
@Autowired
AppUtilappUtil;
@RequestMapping("/")public String welcome() {
return "welcome to use my demo";
}
@RequestMapping("/company/{id}")public ResponseEntity<CompanyAll> getCompany(@PathVariable("id") Long id) {
ResponseEntity<Company> companyResult = client.getCompanyById(id);
if (!companyResult.getStatusCode().is2xxSuccessful()) {
return appUtil.createResponse(null, companyResult.getStatusCode());
}
List<Product> products = null;
try {
ResponseEntity<List<Product>> productsResult = client.getProductsByCompanyId(id);
if (!productsResult.getStatusCode().is2xxSuccessful()) {
LOG.error("远程调用Product API 失败");
} else {
products = productsResult.getBody();
}
} catch (Throwable t) {
LOG.error("远程调用Product API 异常 ", t);
throw t;
}
List<Employee> employees = null;
try {
ResponseEntity<List<Employee>> employeeResult = null;
employeeResult = client.getEmployeesByCompanyId(id);
if (!employeeResult.getStatusCode().is2xxSuccessful()) {
LOG.error("远程调用Employee API 失败");
} else {
employees = employeeResult.getBody();
}
} catch (Throwable t) {
LOG.error("远程调用Employee API 失败");
throw t;
}
return appUtil.createOkResponse(new CompanyAll(companyResult.getBody(), products, employees));
}
}
Ok,这里最后还是需要application.yml配置文件,对相关进行配置。当然bootstrap.yml的注册配置还是不能少的
YAML
server: port: 0eureka: instance: leaseRenewalIntervalInSeconds: 10 metadataMap: instanceId: ${vcap.application.instance_id:${spring.application.name}:${spring.application.instance_id:${random.value}}}
client: registryFetchIntervalSeconds: 5
server:
port: 0
eureka:
instance:
leaseRenewalIntervalInSeconds: 10
metadataMap:
instanceId: ${vcap.application.instance_id:${spring.application.name}:${spring.application.instance_id:${random.value}}}
client:
registryFetchIntervalSeconds: 5
###代理(边界)服务
边界服务在这里也demo中没有运用到太多的功能,这里只使用了路由转发的作用,因此也不用做过多的配置。只需要在启动程序上添加注解 @Controller @EnableZuulProxy即可
在application.yml需要对服务进行一些配置,由于在上述代码中提到了线程等待时间来模拟API调用时间,因此需要对ribbon的TimeOut时间设置到60秒。对zuul的配置最常用的是路由,对Client暴漏的服务接口进行路由转发
YAML
info: component: Zuul Server
endpoints: restart: enabled: true shutdown: enabled: true health: sensitive: falsehystrix: command: default: execution: timeout: enabled: falseribbon: ReadTimeout: 60000 ConnectTimeout: 6000zuul: ignoredServices: "*" routes: service-adapter: path: /app/**
server: port: 8765logging: level: ROOT: INFO
org.springframework.web: INFO
info:
component: Zuul Server
endpoints:
restart:
enabled: true
shutdown:
enabled: true
health:
sensitive: false
hystrix:
command:
default:
execution:
timeout:
enabled: false
ribbon:
ReadTimeout: 60000ConnectTimeout: 6000zuul:
ignoredServices: "*"routes:
service-adapter:
path: /app/**
server:
port: 8765
logging:
level:
ROOT: INFO
org.springframework.web: INFO
## 结果预览