译者 | 涂承烨
审校 | 孙淑娟
在本教程中,我将向你展示如何使用Spring Boot及其不同组件构建微服务。在最后一节中,我将向你展示如何使用Docker容器部署微服务。
我们将学习:
1、 实现微服务的不同组件
2、 通过容器化部署服务
微服务架构组件
1.配置服务器
为了使属性文件集中并被所有微服务共享,我们将创建一个本身就是微服务的配置服务器,并管理所有微服务属性文件,这些文件是版本控制的;属性中的任何更改都将自动发布到所有微服务,而不需要重新启动这些服务。需要记住的一件事是,每个微服务都要与配置服务器通信以获取属性值,因此配置服务器必须是一个高可用的组件;如果它宕机了,那么所有的微服务都会宕机,因为它不能确定属性的值!因此,我们应该考虑这个场景-配置服务器不应该是SPF(单点故障),所以我们将为配置服务器启动多个实例容器,用于应对这样的场景。
2.Eureka发现服务
微服务的主要目标是构建基于业务特性去中心化的不同组件,这样每个组件(也就是微服务)可以根据需要扩展,因此对于一个特定的微服务,有多个实例,我们可以根据需要添加和删除实例,因此单体式负载均衡的方式在微服务范例中是不适用的。
因为容器是动态生成的,所以容器有动态IP地址,要跟踪微服务的所有实例,就需要一个管理微服务的服务。当容器生成时,它将自己注册到此服务中,管理服务跟踪实例;如果删除了微服务,则管理服务将其从服务注册表中删除。如果其他微服务需要相互通信,它会通过服务发现以获得另一个服务的实例。同样,这是一个高可用性组件;如果服务发现宕机,微服务就不能相互通信,因此服务发现必须有多个实例。
3.组件,也称为服务
组件是微服务体系结构中的关键组成部分。我所说的组件是指可以独立管理或更新的实用工具或业务特性。它有一个预定义的边界,它公开了一个API,其他组件可以通过这个API与该服务通信。
微服务的理念是将一个完整的业务功能分解为几个独立的小功能,这些功能将相互通信以实现整个业务功能。将来,如果功能的任何部分发生了变化,我们可以更新或删除该组件,并向架构中添加一个新组件。因此,微服务架构通过适当的封装和适当定义的边界形成了适当的模块化架构。
4.网关服务
微服务是一组独立服务的集合,它们共同组成一个业务功能。每个微服务都会发布一个API,通常是一个REST API,因此作为一个客户端,管理这么多要通信的端点URL是很麻烦的。另外,请从另一个角度考虑:如果某个应用程序希望构建身份验证框架或安全检查,它们必须在多个服务中各自实现,这将违反DRY的设计原则(DRY:Don't Repeat Yourself )。如果我们有一个面向互联网的网关服务,客户端将只调用一个端点,它将调用委托给一个实际的微服务,所有的身份验证或安全检查都将在网关服务中完成。
现在我们已经基本了解了微服务的不同部分是如何一起工作的。在本教程中,我将创建一个返回员工信息的员工搜索服务、一个调用搜索服务并显示结果的EmployeeDashBoard服务、一个Eureka服务(便于这些服务可以注册)以及一个网关服务(便于从外部连接到这些服务)。然后,我们将在Docker容器中部署我们的服务,并使用DockerCompose来生成Docker容器。在本教程中,我将使用Spring Boot。
让我们开始构建我们的微服务项目,必须创建五个单独的微服务:
- 配置服务器
- Eureka服务器(注册中心)
- 员工服务
- 员工Dashboard服务
- Zuul代理(网关服务)
最好从Spring Initializr网站开始,购买所需的模块,然后点击“generate project”。
对于本教程,我们将使用Spring Boot 1.5.4。
创建配置服务器
要创建配置服务器,首先我们需要检查starting .spring.io中的配置服务器模块,也检查actuator的endpoints。然后,下载zip文件并在Eclipse中打开它。
pom文件类似这样:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>MicroserviceConfigServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MicroserviceConfigServer</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
它将会下载spring-cloud-config-server构件。
下一步,我们会创建一个bootstrap.properties文件,在该文件中提到配置服务器从哪里读取相关属性。在生产模式下,它应该是Git仓库的URL,但是由于这是一个演示,所以我将使用我的本地磁盘。所有属性文件都将放在那里,配置服务器将读取这些属性文件。
让我们来看看bootstrap.properties文件的内容:
server.port=9090
spring.cloud.config.server.native.searchLocations=file://${user.home}/MicroService/centralProperties/
SPRING_PROFILES_ACTIVE=native
在这里,我们引导Spring Boot设置系统的访问端口为9090,并使用一个centralProperties文件夹作为搜索所有属性文件的文件夹。注意,在我们的Docker容器中,你必须创建一个centralProperties文件夹,并将所有属性文件放在那里。
现在让我们看看Java部分:
package com.example.MicroserviceConfigServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@EnableConfigServer
@SpringBootApplication
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
在这里,我们使用了@EnableConfigserver注释,我们引导Spring Boot将此服务视为配置服务器应用程序。
现在,在centralProperties文件夹中放置一些测试属性文件。
现在,我们已经为配置服务器设置好了。如果我们运行这个服务并在浏览器输入URL http://localhost:9090/config/default,我们会看到以下响应:
{
"name": "config",
"profiles": [
"default"
],
"label": null,
"version": null,
"state": null,
"propertySources": [
{
"name": "file:///home/shamik/MicroService/centralProperties/config.properties",
"source": {
"application.message": "Hello Shamik"
}
},
{
"name": "file:///home/shamik/MicroService/centralProperties/application.properties",
"source": {
"welcome.message": "Hello Spring Cloud"
}
}
]
}
它显示了我放置在centralProperties文件夹中的所有文件名、键和值。
实现服务发现
下一步是创建用于服务发现的Eureka服务器。我们将使用Netflix的Eureka服务器进行服务发现。为此,我从start.spring.io中选择Eureka服务器模块,并下载项目。
pom文件类似这样:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>EmployeeEurekaServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>EmployeeEurekaServer</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
现在创建bootstrap.properties:
spring.application.name=EmployeeEurekaServer
eureka.client.serviceUrl.defaultZone:http://localhost:9091/
server.port=9091
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
在这里,我为这个应用程序的逻辑名称起名为EmployeeEurekaServer, Eureka服务器的URL是http://localhost:9091,将在端口9091上启动。请注意,Eureka服务器本身可以是一个Eureka客户端;因为可能有多个Eureka服务器的实例,它需要与其他服务器同步。通过eureka.client.register-with-eureka=false,我们明确告知Spring Boot不要把Eureka服务器当作客户端,因为我只创建了一个Eureka服务器,所以它不需要把自己注册为客户端。
现在我将创建Java文件:
package com.example.EmployeeEurekaServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EmployeeEurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EmployeeEurekaServerApplication.class, args);
}
}
通过@EnableEurekaServer注释,Spring Boot将该服务生成为Eureka服务器。如果我运行服务,并在浏览器中输入http://localhost:9091/,我们将看到以下页面:
创建员工搜索服务
现在,我们将创建一个微服务,它根据传递的ID实际返回Employee信息。此外,它还可以返回所有Employee信息。我将发布一个REST API,并在Eureka服务器上注册这个微服务,以便其他微服务可以发现它。
我们从start.spring.io中选择eurekclient。
让我们看看这个的pomm .xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>EmployeeSearchService</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>EmployeeSearchService</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</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-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
让我们看看这个的bootstrap.properties:
spring.application.name=EmployeeSearch
spring.cloud.config.uri=http://localhost:9090
eureka.client.serviceUrl.defaultZone:http://localhost:9091/eureka
server.port=8080
security.basic.enable: false
management.security.enabled: false
在这里,我们给服务的逻辑名称起名为EmployeeSearch,该服务的所有实例在Eureka服务器中都以这个名称注册。这是所有EmployeeSerach服务实例的通用逻辑名称。另外,我给出了配置服务器的URL(请注意,当我们在Docker中部署它时,我们应该更改localhost为配置服务器的Docker容器IP,以便找到配置服务器)。
另外也提到了Eureka服务器的URL(请注意,当我们在Docker中部署它时,我们应该将localhost更改为Eureka的Docker容器IP,以便找到Eureka服务器)。
现在我们来创建controller和service文件。
package com.example.EmployeeSearchService.service;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.stereotype.Service;
import com.example.EmployeeSearchService.domain.model.Employee;
@Service
public class EmployeeSearchService {
private static Map < Long, Employee > EmployeeRepsitory = null;
static {
Stream < String > employeeStream = Stream.of("1,Shamik Mitra,Java,Architect", "2,Samir Mitra,C++,Manager",
"3,Swastika Mitra,AI,Sr.Architect");
EmployeeRepsitory = employeeStream.map(employeeStr -> {
String[] info = employeeStr.split(",");
return createEmployee(new Long(info[0]), info[1], info[2], info[3]);
}).collect(Collectors.toMap(Employee::getEmployeeId, emp -> emp));
}
private static Employee createEmployee(Long id, String name, String practiceArea, String designation) {
Employee emp = new Employee();
emp.setEmployeeId(id);
emp.setName(name);
emp.setPracticeArea(practiceArea);
emp.setDesignation(designation);
emp.setCompanyInfo("Cognizant");
return emp;
}
public Employee findById(Long id) {
return EmployeeRepsitory.get(id);
}
public Collection < Employee > findAll() {
return EmployeeRepsitory.values();
}
}
Controller文件:
package com.example.EmployeeSearchService.controller;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.EmployeeSearchService.domain.model.Employee;
import com.example.EmployeeSearchService.service.EmployeeSearchService;
@RefreshScope
@RestController
public class EmployeeSearchController {
@Autowired
EmployeeSearchService employeeSearchService;
@RequestMapping("/employee/find/{id}")
public Employee findById(@PathVariable Long id) {
return employeeSearchService.findById(id);
}
@RequestMapping("/employee/findall")
public Collection < Employee > findAll() {
return employeeSearchService.findAll();
}
}
package com.example.EmployeeSearchService.domain.model;
public class Employee {
private Long employeeId;
private String name;
private String practiceArea;
private String designation;
private String companyInfo;
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPracticeArea() {
return practiceArea;
}
public void setPracticeArea(String practiceArea) {
this.practiceArea = practiceArea;
}
public String getDesignation() {
return designation;
}
public void setDesignation(String designation) {
this.designation = designation;
}
public String getCompanyInfo() {
return companyInfo;
}
public void setCompanyInfo(String companyInfo) {
this.companyInfo = companyInfo;
}
@Override
public String toString() {
return "Employee [employeeId=" + employeeId + ", name=" + name + ", practiceArea=" + practiceArea + ", designation=" + designation + ", companyInfo=" + companyInfo + "]";
}
}
这里没有什么特别的;我只创建了几个employee,并将它们映射到Rest URL。
现在让我们看看Spring Boot文件:
package com.example.EmployeeSearchService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableDiscoveryClient
@SpringBootApplication
public class EmployeeSearchServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EmployeeSearchServiceApplication.class, args);
}
}
在这里,我们使用@EnableDiscoveryClient注释将该服务注册为Eureka客户机。
现在,如果我们在浏览器中输入http://localhost:8080/employee/find/1,可以看到以下输出:
{
"employeeId":1,
"name":"Shamik Mitra",
"practiceArea":"Java",
"designation":"Architect",
"companyInfo":"Cognizant"
}
创建员工大屏服务
现在,我将创建另一个服务,它使用Employee Search 服务来获取Employee信息,以便与Employee Search服务通信。我会使用Feign客户端,也会使用Hystrix作为断路器,所以如果Employee搜索服务宕机,它可以返回默认数据。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>EmployeeDashBoardService</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>EmployeeDashBoardService</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</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-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
bootstrap.properties:
spring.application.name=EmployeeDashBoard
spring.cloud.config.uri=http://localhost:9090
eureka.client.serviceUrl.defaultZone:http://localhost:9091/eureka
server.port=8081
security.basic.enable: false
management.security.enabled: false
Feign client:
package com.example.EmployeeDashBoardService.controller;
import java.util.Collection;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.EmployeeDashBoardService.domain.model.EmployeeInfo;
@FeignClient(name = "EmployeeSearch")
@RibbonClient(name = "EmployeeSearch")
public interface EmployeeServiceProxy {
@RequestMapping("/employee/find/{id}")
public EmployeeInfo findById(@PathVariable(value = "id") Long id);
@RequestMapping("/employee/findall")
public Collection < EmployeeInfo > findAll();
}
Controller:
package com.example.EmployeeDashBoardService.controller;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.EmployeeDashBoardService.domain.model.EmployeeInfo;
@RefreshScope
@RestController
public class FeignEmployeeInfoController {
@Autowired
EmployeeServiceProxy proxyService;
@RequestMapping("/dashboard/feign/{myself}")
public EmployeeInfo findme(@PathVariable Long myself) {
return proxyService.findById(myself);
}
@RequestMapping("/dashboard/feign/peers")
public Collection < EmployeeInfo > findPeers() {
return proxyService.findAll();
}
}
Spring boot启动service:
package com.example.EmployeeDashBoardService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class EmployeeDashBoardService {
public static void main(String[] args) {
SpringApplication.run(EmployeeDashBoardService.class, args);
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
一切就绪!我们现在准备好了。如果我们在浏览器中输入url http://localhost:8081/dashboard/feign/1,将看到以下输出:
{
"employeeId":1,
"name":"Shamik Mitra",
"practiceArea":"Java",
"designation":"Architect",
"companyInfo":"Cognizant"
}
创建网关服务
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
<artifactId>spring-boot-starter-parent</artifactId>
<packaging>pom</packaging>
<name>Spring Boot Starter Parent</name>
<description>Parent pom providing dependency and plugin management for applications
built with Maven</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<java.version>1.6</java.version>
<resource.delimiter>@</resource.delimiter>
<!-- delimiter that doesn't clash with Spring ${} placeholders -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<!-- Turn on filtering by default for application properties -->
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
<excludes>
<exclude>**/application*.yml</exclude>
<exclude>**/application*.yaml</exclude>
<exclude>**/application*.properties</exclude>
</excludes>
</resource>
</resources>
<pluginManagement>
<plugins>
<!-- Apply more sensible defaults for user projects -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>${start-class}</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Tests.java</include>
<include>**/*Test.java</include>
</includes>
<excludes>
<exclude>**/Abstract*.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<archive>
<manifest>
<mainClass>${start-class}</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<configuration>
<delimiters>
<delimiter>${resource.delimiter}</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<verbose>true</verbose>
<dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
</configuration>
</plugin>
<!-- Support our own plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
<!-- Support shade packaging (if the user does not want to use our plugin) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.6.RELEASE</version>
</dependency>
</dependencies>
<configuration>
<keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
<resource>META-INF/spring.factories</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${start-class}</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
bootstrap.properties:
spring.application.name=EmployeeAPIGateway
eureka.client.serviceUrl.defaultZone:http://localhost:9091/eureka
server.port=8084
security.basic.enable: false
management.security.enabled: false
zuul.routes.employeeUI.serviceId=EmployeeDashBoard
zuul.host.socket-timeout-millis=30000
在这里,请注意属性zuul.routes.employeeUI.serviceId=EmployeeDashBoard。有了这个,我们指示Zuul,任何包含employeeUI的URL都会重定向到EmployeeDashboard服务。
Spring Boot file:
package com.example.EmployeeZuulService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class EmployeeZuulServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EmployeeZuulServiceApplication.class, args);
}
}
现在,如果我运行服务并在浏览器中输入
http://localhost:8084/employeeUI/dashboard/feign/1,将看到以下输出:
{
"employeeId":1,
"name":"Shamik Mitra",
"practiceArea":"Java",
"designation":"Architect",
"companyInfo":"Cognizant"
}
在Docker容器中部署
现在代码已写完。让我们看看应用程序是如何运行的。到目前为止,我们所有的服务都在本地机器上完美地运行。但是,我们不希望我们的代码最终只在本地环境中运行。相反,我们希望看到它在生产环境中出色地运行。(我们爱我们的代码就像爱我们的孩子,我们希望看到它一直成功)。但当我们送孩子上学或引导他们就是为了他们能走上正确的成功之路,同样我们也需要引导我们的应用走上成功之路。因此,让我们进入DevOps的世界,并尝试为我们的源代码在生产环境中提供正确的路径。
● 欢迎来到Docker世界
Docker就不用介绍了。如果你觉得你还需要一个指南,看看这里。。
接下来,我将假设你的电脑上安装了Docker CE。我们将在这里使用以下部署概念:
1、Dockerfile:这是一个文本文档,包含了构建Docker映像所需的所有指令。使用Dockerfile的指令集,我们可以编写复制文件、进行安装等步骤。如需更多参考,请访问此链接。
2、Docker Compose:这是一个可以创建和生成多个容器的工具。它有助于使用一个命令构建所需的环境。
3、如微服务架构图所示,我们将为每个微服务创建一个单独的容器。下面是我们示例中的容器列表:
- 配置服务器(Config Server)
- 员工微服务(EmployeeService)
- 员工搜索微服务(Employee Search Service)
- 员工大屏微服务(Employee Dashboard Service)
- 网关微服务(Gateway Service)
● 配置服务器的Docker配置
容器应该包含配置服务器jar文件,我们将从本地电脑中选择jar文件上传到容器中。在真实的场景中,我们应该将jar文件推送到一个Artifact Repository Manager系统,比如Nexus或Artifactory,容器应该从repo manager下载该文件。
根据bootstrap.properties,配置服务器应该在端口8888上可用。
如上所述,我们将让配置服务器从文件读取配置,因此我们将确保即使容器宕机,这些属性文件也能被检索到。
创建一个名为config-repo的文件夹,其中将包含所需的属性文件。我们将确保对Config Server容器执行以下操作。
# mkdir config-repo
# cd config-repo
# echo "service.employyesearch.serviceId=EmployeeSearch" > EmployeeDashBoard.properties
# echo "user.role=Dev" > EmployeeSearch.properties
回到上一级文件夹,创建一个名为Dockerfile的Docker文件。这个Dockerfile将创建我们的基础镜像,其中包含Java。
# cd ../
# vi Dockerfile
放置以下内容:
FROM alpine:edge
MAINTAINER javaonfly
RUN apk add --no-cache openjdk8
FROM:这个关键字告诉Docker使用给定的图像及其标签作为构建基础。
MAINTAINER:映像的作者。
RUN:该命令将在系统中安装openjdk8。
执行下面的命令来创建基本的Docker镜像:
docker build --tag=alpine-jdk:base --rm=true
在成功构建基础映像之后,就该为Config Server创建Docker映像了。
创建一个名为files的文件夹,并将配置服务器jar文件放在该目录中。然后,创建一个名为Dockerfile-configserver的文件,内容如下:
FROM alpine-jdk:base
MAINTAINER javaonfly
COPY files/MicroserviceConfigServer.jar /opt/lib/
RUN mkdir /var/lib/config-repo
COPY config-repo /var/lib/config-repo
ENTRYPOINT ["/usr/bin/java"]
CMD ["-jar", "/opt/lib/MicroserviceConfigServer.jar"]
VOLUME /var/lib/config-repo
EXPOSE 9090
在这里,我们提到了从前面创建的alpine-jdk映像构建映像。我们将把名为employeeconfigserver.jar的jar文件复制到/opt/lib位置,并将config-repo复制到/root目录。当容器启动时,我们希望配置服务器开始运行,因此将ENTRYPOINT和CMD设置为运行Java命令。我们需要挂载一个卷来从容器外部共享配置文件;VOLUME命令可以帮助我们实现这一点。外部应该可以通过端口9090访问配置服务器;这就是为什么我们有公开端口 9090。
现在让我们构建Docker镜像,并将其标记为config-server:
# docker build --file=Dockerfile-configserver --tag=config-server:latest --rm=true .
现在让我们创建一个Docker卷:
# docker volume create --name=config-repo
# docker run --name=config-server --publish=9090:9090 --volume=config-repo:/var/lib/config-repo config-server:latest
一旦我们运行上面的命令,我们应该能够看到一个Docker容器启动并运行。如果我们打开浏览器输入URL http://localhost:9090/config/default/,我们也应该能够访问这些属性。
● EurekaServer
类似地,我们需要为EurekaServer创建Docker文件,它将运行在9091端口上。Eureka服务器的Dockerfile应该如下所示:
FROM alpine-jdk:base
MAINTAINER javaonfly
COPY files/MicroserviceEurekaServer.jar /opt/lib/
ENTRYPOINT ["/usr/bin/java"]
CMD ["-jar", "/opt/lib/MicroserviceEurekaServer.jar"]
EXPOSE 9091
要构建映像,使用以下命令:
docker build --file=Dockerfile-EurekaServer --tag=eureka-server:latest --rm=true .
docker run --name=eureka-server --publish=9091:9091 eureka-server:latest
微服务
现在是时候部署我们真正的微服务了。步骤应该是类似的;我们唯一需要记住的是我们的微服务是依赖于ConfigServer和EurekaServer的,所以我们总是需要确保在我们启动微服务之前,上面两个服务是启动并运行的。容器之间存在依赖关系,所以是时候研究Docker Compose了。这是确保容器生成保持一定秩序的一种很好的方式。
为此,我们应该为其他容器编写一个Dockerfile。下面是Dockerfile的内容:
Dockerfile-EmployeeSearch.
================================
FROM alpine-jdk:base
MAINTAINER javaonfly
RUN apk --no-cache add netcat-openbsd
COPY files/EmployeeSearchService.jar /opt/lib/
COPY EmployeeSearch-entrypoint.sh /opt/bin/EmployeeSearch-entrypoint.sh
RUN chmod 755 /opt/bin/EmployeeSearch-entrypoint.sh
EXPOSE 8080
Dockerfile-EmployeeDashboard
====================================
FROM alpine-jdk:base
MAINTAINER javaonfly
RUN apk --no-cache add netcat-openbsd
COPY files/EmployeeDashBoardService.jar /opt/lib/
COPY EmployeeDashBoard-entrypoint.sh /opt/bin/EmployeeDashBoard-entrypoint.sh
RUN chmod 755 /opt/bin/EmployeeDashBoard-entrypoint.sh
EXPOSE 8080
Dockerfile-ZuulServer
=========================================
FROM alpine-jdk:base
MAINTAINER javaonfly
COPY files/EmployeeZuulService.jar /opt/lib/
ENTRYPOINT ["/usr/bin/java"]
CMD ["-jar", "/opt/lib/EmployeeZuulService.jar"]
EXPOSE 8084
这里需要注意的是,我为Employee和Employee Dashboard服务创建了两个shell脚本。它指示Dockercompose在Config服务器和Eureka服务器启动之前不要启动Employee和Employee Dashboard服务。
Employee dashBoard Script
==================================
while ! nc -z config-server 9090 ; do
echo "Waiting for the Config Server"
sleep 3
done
while ! nc -z eureka-server 9091 ; do
echo "Waiting for the Eureka Server"
sleep 3
done
java -jar /opt/lib/EmployeeDashBoardService.jar
==================================
Employee service Script
==================================
while ! nc -z config-server 9090 ; do
echo "Waiting for the Config Server"
sleep 3
done
while ! nc -z eureka-server 9091 ; do
echo "Waiting for the Eureka Server"
sleep 3
done
java -jar /opt/lib/EmployeeSearchService.jar
现在让我们创建一个名为docker-compose.yml的文件。它将使用所有这些Dockerfiles来生成我们所需的环境。它还将确保生成所需的容器保持正确的顺序,并且它们是相互连接的。
version: '2.2'
services:
config-server:
container_name: config-server
build:
context: .
dockerfile: Dockerfile-configserver
image: config-server:latest
expose:
- 9090
ports:
- 9090:9090
networks:
- emp-network
volumes:
- config-repo:/var/lib/config-repo
eureka-server:
container_name: eureka-server
build:
context: .
dockerfile: Dockerfile-EurekaServer
image: eureka-server:latest
expose:
- 9091
ports:
- 9091:9091
networks:
- emp-network
EmployeeSearchService:
container_name: EmployeeSearch
build:
context: .
dockerfile: Dockerfile-EmployeeSearch
image: employeesearch:latest
environment:
SPRING_APPLICATION_JSON: '{"spring": {"cloud": {"config": {"uri": "http://config-server:9090"}}}}'
entrypoint: /opt/bin/EmployeeSearch-entrypoint.sh
expose:
- 8080
ports:
- 8080:8080
networks:
- emp-network
links:
- config-server:config-server
- eureka-server:eureka-server
depends_on:
- config-server
- eureka-server
logging:
driver: json-file
EmployeeDashboardService:
container_name: EmployeeDashboard
build:
context: .
dockerfile: Dockerfile-EmployeeDashboard
image: employeedashboard:latest
environment:
SPRING_APPLICATION_JSON: '{"spring": {"cloud": {"config": {"uri": "http://config-server:9090"}}}}'
entrypoint: /opt/bin/EmployeeDashBoard-entrypoint.sh
expose:
- 8081
ports:
- 8081:8081
networks:
- emp-network
links:
- config-server:config-server
- eureka-server:eureka-server
depends_on:
- config-server
- eureka-server
logging:
driver: json-file
ZuulServer:
container_name: ZuulServer
build:
context: .
dockerfile: Dockerfile-ZuulServer
image: zuulserver:latest
expose:
- 8084
ports:
- 8084:8084
networks:
- emp-network
links:
- eureka-server:eureka-server
depends_on:
- eureka-server
logging:
driver: json-file
networks:
emp-network:
driver: bridge
volumes:
config-repo:
external: true
下面是Docker compose文件中的几个重要属性:
1、version:一个必填字段,我们需要在这里维护Docker Compose格式的版本。
2、服务:每个属性都定义了我们需要生成的容器。
● build:如果提到了,那么Docker Compose应该从给定的Dockerfile构建一个映像。
● image:将要创建的镜像的名称。
● network:使用的网络名称,这个名称应该出现在网络部分。
● links:这将在服务和上述服务之间创建一个内部链接。这里,EmployeeSearch服务需要访问Config和Eureka服务器。
● depends:这是维持顺序所必需的属性。EmployeeSearch容器依赖于Eureka和Config Server。因此,Docker确保在派生EmployeeSearch容器之前派生Eureka和Config Server容器。
在创建文件之后,我们构建镜像,创建所需的容器,并从下面这个命令开始:
docker-compose up --build
要停止整个环境,我们可以使用这个命令:
docker-compose down
在这个链接中可以找到Docker Compose的完整文档。
总之,编写Dockerfile和Docker Compose文件是一个一次性的活动,但它允许你在任何时候根据需要生成一个完整的环境。
结论
这是关于如何在微服务中构建不同组件并在Docker中部署它们的完整指南。在生产环境中,应该会涉及到CI/CD,所以你不需要知道所有用于构建映像的Docker命令,但作为一个全栈开发人员,学习如何在Docker中创建和构建映像是很重要的。
译者介绍
涂承烨,51CTO社区编辑,信息系统项目管理师、信息系统监理师、PMP,某省综合性评标专家,拥有15年的开发经验。对项目管理、前后端开发、微服务、架构设计、物联网、大数据等较为关注。
原文标题:Building Microservices Using Spring Boot and Docker,作者:Shamik Mitra