1.5 土豆微服务案例快速上手

Todo List是使用得最多的一种时间管理方法,即把要做的事情写下来,标注上优先级、计划开始时间、估计完成时间、地点,如表1-3所示。

表1-3 Todo List

①优先级可分为四级:A—重要并紧急,B—重要不紧急,C—紧急不重要,D—不紧急不重要

“拖延症”患者最大的问题在于Todo List越来越长,想做的事情越来越多,可是没有几件大事是能够坚持完成的,小事也丢三落四,延误了很多。很多事情计划了,可并没有及时完成,有的压根儿没能开始,就已经超过了截止时间,久而久之,这些事的进展仍然只是停留在计划阶段。

Todo List的关键在于提醒我们专注和坚持。在此我们就来构建一个“土豆”(Todo的中文谐音)服务来维护我们的待办事项,提醒我们坚持完成任务。接下来就从如何构建和部署两个方面介绍,让大家能快速上手微服务开发。

1.5.1 土豆微服务构建计划

按照以上需求,可以大体设计一下土豆微服务的架构,绘制用例图,如图1-3所示。

图1-3 “土豆”微服务用例图

对于待办事项,有6个基本用例:

1)Create Potato(创建待办事项):其扩展用例包括创建提醒器Create Reminder(提醒用户及时开始和完成任务)。

2)Retrieve Potato(获取待办事项):根据事项的唯一标识符来获取其详细信息。

3)Update Potato(更新待办事项):其扩展用例有修改提醒器Update Reminder(不用提醒开始,只要提醒按时完成任务)。

4)Start Potato(启动待办事项)。其扩展用例也是修改提醒器Update Reminder(不用提醒开始,只要提醒按时完成任务)。

5)Stop Potato(停止待办事项):停止正在进行的事项,或者是完成事项,或者是中止事项。

6)Delete Potato(删除待办事项)。其扩展用例有删除提醒器Remove Reminder。

在初始版本中,可以先创建如下3个微服务。

❑ Potato-Web:提供一个前端服务,主要功能是渲染前端页面,并为前端页面的Ajax调用提供API。

❑ Potato-Service:核心服务,主要功能是对待办事项(Potato)进行增删改查,以及开始和结束待办事项。

❑ Potato-Reminder:提醒功能,主要用来在待办事项(Potato)预定开始时间及截止时间之前发送提醒邮件、即时消息或播放音乐。

3个微服之间的关系如图1-4所示。

图1-4 “土豆”系列微服务之间的关系

微服务可以用任何一种语言实现,笔者比较喜欢用Java和Python。这里用Java实现,后端基于Spring Boot框架,前端使用Vue框架。

下面我们一一介绍这3个微服务。先从核心的Potato-Service说起。

1.5.2 微服务构建一:土豆管理微服务

土豆管理微服务Potato-Service的内部结构采用标准的3层架构,如图1-5所示。

图1-5 土豆管理微服务内部结构

❑ Controller模块:用户界面层,也就是API交互层。

❑ Service模块:逻辑应用层。

❑ Repository模块:数据访问层。

1.具体构建与结构

我们采用Spring Boot来实现这个微服务。Spring Boot有一个用来快速生成应用骨架的页面https://start.spring.io,选中所需的依赖库,会生成一个压缩文件,解压缩后就生成一个简单的Spring Boot项目。也可以用命令行工具来生成。例如,在Macbook上可以用brew来安装springboot命令行工具。

brew tap pivotal/tap
brew install springboot

在其他Linux系统上可通过sdkman来安装,在Windows上建议通过vagrant安装一个ubuntu虚拟机。

构建工具可以选择Gradle或传统的Maven,也可以使用https://start.spring.io,从中选取所需要的子模块,生成一个项目骨架。假设项目命名为potato(“土豆”),保存后即为potato.zip。这里用Spring Cli命令工具来生成项目骨架。

spring init --java-version=1.8--dependencies=web, actuator, cloud-eureka, devtools -packaging=jar --groupId=com.github.walterfan.potato --artifactId=server unzip server.zip -d potato-server

使用上面生成的“骨架”文件完成业务功能的编写。最终生成的代码结构如图1-6所示。

图1-6 土豆管理微服务代码结构

2.主要 代码类

构建标准的Java微服务,首先要创建一个POM文件,这里不列举其内容,请参考前言中提到的网站所附实例中的源码。其核心要点是设置spring-boot-starter-parent,并添加相关依赖。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/> <! -- lookup parent from repository -->
</parent>

<dependency>
<! -- 省略大部分内容 -->
<dependencies>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

</dependencies>

主要的数据传输对象如图1-7所示。

图1-7 土豆管理微服务中数据传输对象“土豆”的定义

主要的启动类PotatoApplication用于启动这个微服务,代码如下:

package com.github.walterfan.potato.server;
//省略import语句

@EnableEurekaClient
@SpringBootApplication
public class PotatoApplication {

    public static void main(String[] args) {
        SpringApplication.run(PotatoApplication.class, args);
    }

}

控制类PotatoController用于指定API的URL映射,并调用下层的服务:

package com.github.walterfan.potato.server;

//省略import语句

@RestController
@RequestMapping("/potato/api/v1")
@Slf4j
public class PotatoController {

    @Value("${spring.application.name}")
    private String serviceName;

    @Value("${server.port}")
    private Integer serverPort;

    @Autowired
    private PotatoService potatoService;

    @RequestMapping(value = "/potatoes", method = RequestMethod.POST)
    @ApiCallMetricAnnotation(name = "CreatePotato")
    public PotatoDTO create(@RequestBody PotatoDTO potatoRequest) {
        log.info("create {}", potatoRequest);
        return potatoService.create(potatoRequest);
    }

    @LogDetail
    @RequestMapping(value = "/potatoes/{id}/start", method = RequestMethod.POST)
    @ApiCallMetricAnnotation(name = "StartPotato")
    public void start(@PathVariable UUID id) {
        potatoService.startPotato(id);
    }

    @LogDetail
    @RequestMapping(value = "/potatoes/{id}/stop", method = RequestMethod.POST)
    @ApiCallMetricAnnotation(name = "StopPotato")
    public void stop(@PathVariable UUID id) {
        potatoService.stopPotato(id);
    }

    @LogDetail
    @RequestMapping(value = "/potatoes/{id}", method = RequestMethod.DELETE)
    @ApiCallMetricAnnotation(name = "DeletePotato")
    public void delete(@PathVariable UUID id) {
        potatoService.delete(id);
    }
//省略其他管理操作,如删除、查询等

服务类PotatoService用来处理核心的业务逻辑,调用下层的数据存取方法来访问数据库:

package com.github.walterfan.potato.server;

//省略import语句

@Service
@Slf4j
public class PotatoServiceImpl implements PotatoService {

  //省略属性代码

    @Override
    public PotatoDTO create(PotatoDTO potatoRequest) {
        PotatoEntity potato = potatoDto2Entity(potatoRequest, null);
        PotatoEntity savedPotato = potatoRepository.save(potato);
        scheduleRemindEmails(potatoRequest);

        return this.potatoEntity2Dto(savedPotato);
    }

    private void scheduleRemindEmails(PotatoDTO potatoRequest) {
        String emailContent = potatoRequest.getDescription();
        //schedule remind
        RemindEmailRequest remindEmailRequest = RemindEmailRequest.builder()
                .email(this.remindEmail)
                .subject("To start:" + potatoRequest.getName())
                .body(emailContent)
                .dateTime(potatoRequest.getScheduleTime())
                .build();
        ResponseEntity<RemindEmailResponse> respEntity1 = potatoSchedulerClient. scheduleRemindEmail(remindEmailRequest);
        log.info("respEntity1:{}", respEntity1.getStatusCode());

        RemindEmailRequest remindEmailRequest2 = RemindEmailRequest.builder()
                .email(this.remindEmail)
                .subject("To finish:" + potatoRequest.getName())
                .body(emailContent)
                .dateTime(potatoRequest.getDeadline())
                .build();
        ResponseEntity<RemindEmailResponse>  respEntity2 = potatoSchedulerClient. scheduleRemindEmail(remindEmailRequest2);
        log.info("respEntity2:{}", respEntity2.getStatusCode());
    }

//省略其他维护“土豆”的操作

数据仓库类PotatoRespository用来读写数据库:

package com.github.walterfan.potato.server.repository;

//省略import语句
@Repository
public interface PotatoRepository extends PagingAndSortingRepository<PotatoEntity, UUID>, JpaSpecificationExecutor<PotatoEntity> {
    Page<PotatoEntity> findByUserId(UUID userId, Pageable pageable);

    Page<PotatoEntity> findByUserId(UUID userId, Specification<PotatoEntity>spec, Pageable pageable);

    PotatoEntity findByUserIdAndName(UUID userId, String name);
}

3.配置文件

配置文件application.properties中指定了profiles为dev,如下所示:

spring.profiles.active=dev

# ==============================================================
# app
# ==============================================================
potato.remind.email=fanyamin@hotmail.com
potato.guest.userId=53a3093e-6436-4663-9125-ac93d2af91f9

配置文件application-dev.properties用来设置所需的若干属性:

# ===============================
# = General
# ===============================
app.id=potato
debug=false

server.port=9003

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet. error.ErrorMvcAutoConfiguration

spring.application.name=potato-service
spring.application.version=1.0
spring.application.component=potato-app
spring.application.env=production

spring.messages.encoding=UTF-8
server.tomcat.uri-encoding=UTF-8

logging.level.root=INFO
logging.level.org.hibernate=DEBUG
logging.level.org.springframework.web.servlet.DispatcherServlet=DEBUG
# ===============================
# = DATA SOURCE
# ===============================
spring.datasource.url=jdbc:sqlite:/var/lib/sqlite/potato.db
spring.datasource.username=walter
spring.datasource.password=pass1234
spring.datasource.testWhileIdle=true
spring.datasource.validationQuery=SELECT 1
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.datasource.sql-script-encoding=UTF-8

# ===============================
# = JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.database-platform=org.hibernate.dialect.SQLiteDialect
# ==============================================================
# = Initialize the database using data.sql script
# ==============================================================
spring.datasource.initialization-mode=always
# ===============================
# = Thymeleaf configurations
# ===============================
#spring.thymeleaf.mode=LEGACYHTML5
spring.thymeleaf.cache=false
# ==============================================================
# InfluxDB
# ==============================================================

spring.influxdb.url=${INFLUXDB_URL}
spring.influxdb.username=admin
spring.influxdb.password=admin
spring.influxdb.database=potato

# ==============================================================
# = Actuator
# ==============================================================
spring.jmx.default-domain=potato
management_endpoints_jmx.exposure.include=*
management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=*

management.endpont.shutdown.enabled=true
management.endpont.health.show-details=when_authorized

info.app.name=Potato Task Application
info.app.description=This is Potato Application based on spring boot
info.app.version=1.0.0
# ==============================================================
# spring cloud
# ==============================================================
spring.cloud.bus.enabled:false
spring.cloud.bootstrap.enabled:false
spring.cloud.discovery.enabled:false
spring.cloud.consul.enabled:false
spring.cloud.consul.config.enabled:false
spring.cloud.config.discovery.enabled:false

eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.serviceUrl.defaultZone: http://registry:8761/eureka

#http://zipkin:9411
spring.zipkin.url=${ZIPKIN_URL}
spring.sleuth.sampler.percentage=1.0

4.数据库表结构

由上面的配置可知,我们使用了sqlite这个迷你的文件型数据库。可以安装sqlite3这个命令行工具查看由spring-data-jpa自动生成的表结构,其ORM对象关系映射如图1-8和图1-9所示。

图1-8 土豆管理微服务数据实体(Entity)

图1-9 土豆管理微服务数据表设计

5.测试

我们在pom文件中加入了springfox-swagger-ui的支持,并且加入了相关的配置,这样可以生成一份API文档,并可以用它做一些简单的测试。示例如下:

package com.github.walterfan.potato.server.config;

//省略import

@Configuration
@EnableAutoConfiguration
@EnableSwagger2
@EnableJpaRepositories(basePackages = {"com.github.walterfan.potato.server"})
@ComponentScan(basePackages = {"com.github.walterfan.potato.server"})
@PropertySource("classpath:application.properties")
public class WebConfig {

    private boolean enableSwagger = true;
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER 2)
                .forCodeGeneration(Boolean.TRUE)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.github.walterfan. potato.server"))
                //.paths(regex("/potato/api/v1/*"))
                .paths(PathSelectors.any())
                .paths(Predicates.and(PathSelectors.regex("/potato/api.*")))
                .build()
                .enable(enableSwagger)
                .apiInfo(apiInfo());
    }

    private ApiInfo apiInfo() {
        return new ApiInfo(
                "REST API",
                "REST description of API.",
                "API TOS",
                "Terms of service",
                new Contact("Walter Fan", "http://www.fanyamin.com", "walter. fan@gmail.com"),
                "License of API", "API license URL", Collections.emptyList());
    }
}

这样就得到了一个开箱即用的API文档和测试工具。运行如下命令:

cd potato-server
java -jar target/task-0.0.1-SNAPSHOT.jar

打开http://localhost:9005/v2/api-docs,可以看到详细的API文档说明,如图1-10所示。

图1-10 API文档说明

打开http://localhost:9005/swagger-ui.html,可以看到各个API端点,如图1-11所示。

图1-11 土豆管理微服务API

直接单击一个API端点,可以在Example Value输入框中直接输入json格式的请求内容,提交一个请求,如图1-12所示。

图1-12 土豆管理微服务API测试之请求

得到的响应如图1-13所示。

图1-13 土豆管理微服务API测试之响应

1.5.3 微服务构建二:土豆提醒微服务

多个微服务需要共享并交换信息,它们之间通过网络通信协议进行通信,彼此之间存在依赖与被依赖的关系。我们在前面设计的PotatoService也需要和其他服务一起工作。

1.功能说明

当创建一个待办事项,指定了预定开始时间和结束时间后,就会调用potato-service的API来创建一个potato对象,potato-service又会调用potato-reminder的API创建两个RemindTask来发送提醒。所以提醒微服务的主要功能就是提醒,和其他微服务的互动时序图如图1-14所示。

图1-14 土豆微服务之间的互动时序图

2.主要代码

此处创建的还是典型的Spring Boot项目,在pom.xml中加入相关依赖项,最主要的是spring-boot-starter-quartz。这里使用著名的作业安排开源库Quartz,数据库选用MySQL。代码结构类似于土豆管理微服务,所以不做过多解释。下面展示核心实现代码。

数据传输对象为RemindEmailRequest。

package com.github.walterfan.potato.common.dto;

//省略若干import语句

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RemindEmailRequest extends AbstractDTO {
    @Email
    @NotEmpty
    private String email;

    @NotEmpty
    private String subject;

    @NotEmpty
    private String body;
    @NotNull
    private Instant dateTime;

    @NotNull
    private ZoneId timeZone;

}

Controller层实现如下:

package com.github.walterfan.potato.scheduler;

//省略import

@RestController
@RequestMapping("/scheduler/api/v1")
@Slf4j
public class ScheduleController {

    @Value("${spring.application.name}")
    private String serviceName;

    @Value("${server.port}")
    private Integer serverPort;

    @Autowired
    private ScheduleService scheduleService;

    @PostMapping("/reminders")
    public ResponseEntity<RemindEmailResponse> scheduleEmail(@Valid @RequestBody RemindEmailRequest scheduleEmailRequest) {
        log.info("Receive {}", scheduleEmailRequest);
        return ResponseEntity.of(Optional.ofNullable(scheduleService.scheduleEmail (scheduleEmailRequest)));
    }

  //省略其他类型提醒

Service层实现如下:

@Service
@Slf4j
public class ScheduleServiceImpl implements ScheduleService {
    public static final String EMAIL_JOB_GROUP = "emailReminder";
    @Autowired
    private Scheduler scheduler;

    @Override
    public RemindEmailResponse scheduleEmail(RemindEmailRequest scheduleEmailRequest) {
        log.info("schedule {}", scheduleEmailRequest);
        try {
            ZonedDateTime dateTime = getZonedDateTime(scheduleEmailRequest.
getDateTime(), scheduleEmailRequest.getTimeZone());

            JobDetail jobDetail = buildJobDetail(scheduleEmailRequest);
            Trigger trigger = buildJobTrigger(jobDetail.getKey(), dateTime);

            Date scheduledDate = scheduler.scheduleJob(jobDetail, trigger);

            RemindEmailResponse scheduleEmailResponse = new RemindEmailResponse(true,
                    jobDetail.getKey().getName(), jobDetail.getKey().getGroup(),"Email Scheduled Successfully at " + scheduledDate);
            log.info("Send {}", scheduleEmailResponse);
            return scheduleEmailResponse;
        } catch (SchedulerException ex) {
            log.error("Error scheduling email", ex);

            throw new ResponseStatusException(
                    HttpStatus.INTERNAL_SERVER_ERROR, "dateTime must be after current time");
        }
    }

    //省略其他类型提醒

    private JobDetail buildJobDetail(RemindEmailRequest scheduleEmailRequest) {
        JobDataMap jobDataMap = new JobDataMap();

        jobDataMap.put("email", scheduleEmailRequest.getEmail());
        jobDataMap.put("subject", scheduleEmailRequest.getSubject());
        jobDataMap.put("body", scheduleEmailRequest.getBody());

        return JobBuilder.newJob(EmailJob.class)
                .withIdentity(UUID.randomUUID().toString(), EMAIL_JOB_GROUP)
                .withDescription("Send Email Job")
                .usingJobData(jobDataMap)
                .storeDurably()
                .build();
    }

    private Trigger buildJobTrigger(JobKey jobKey, ZonedDateTime startAt) {
        return TriggerBuilder.newTrigger()
                .withIdentity(jobKey.getName(), jobKey.getGroup())
                .withDescription("Send Email Trigger")
                .startAt(Date.from(startAt.toInstant()))
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfire-HandlingInstructionFireNow())
                .build();
    }
}

最后,启动土豆提醒微服务:

package com.github.walterfan.potato.scheduler;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableEurekaClient
@SpringBootApplication
public class SchedulerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SchedulerApplication.class, args);
    }
}

3.配置文件

配置文件中的相关属性可参考src/main/resources/application-dev.properties。

spring.application.name=potato-scheduler

server.port=9002

# ==============================================================
# Eureka client
# ==============================================================
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.serviceUrl.defaultZone: http://localhost:8761/eureka/

# ==============================================================
# data source
# ==============================================================
## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url=jdbc:mysql://mysqldb/scheduler? useUnicode=true&character-Encoding=utf8
spring.datasource.username=${MYSQL_USER}
spring.datasource.password=${MYSQL_PWD}

# ==============================================================
## QuartzProperties
# ==============================================================

spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.threadPool.threadCount=5

# ==============================================================
## MailProperties
# ==============================================================
spring.mail.host=${EMAIL_SMTP_SERVER}
spring.mail.port=587
spring.mail.username=${EMAIL_USER}
spring.mail.password=${EMAIL_PWD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
## https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-
Migration-Guide#spring-boot-actuator

# ==============================================================
# = Actuator
# ==============================================================
spring.jmx.default-domain=potato
management_endpoints_jmx.exposure.include=*
management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=*

management.endpont.shutdown.enabled=true
management.endpont.health.show-details=when_authorized
management.endpoints.web.base-path=/

management.endpoints.enabled-by-default=true
management.endpoint.info.enabled=true
management.endpoint.info.sensitive=false

# actuator info

info.app.name=Potato Schedule Application
info.app.description=This is Potato Scheduler Application based on spring boot
info.app.version=1.0.0

4.数据库表结构

这里选用Quartz的MySQL实现数据库表结构,直接用SQL脚本来创建数据库表,脚本参见源码。其中共有11张数据库表,如图1-15所示。

图1-15 土豆提醒微服务数据库设计

5.测试

打开http://localhost:9002/swagger-ui.html,可以看到我们提供的API,如图1-16所示。

图1-16 土豆提醒微服务API

可以通过swagger的测试界面来发起一个请求,在规定时间到达时,用户就会收到提醒的邮件,如图1-17所示。

图1-17 土豆提醒微服务“提醒”效果

1.5.4 微服务构建三:土豆网页微服务

提供一个前端页面由VUE框架简单实现,后端页面由Thymeleaf提供模板,再由Spring Boot框架提供Rest API。

1.主页界面

主页界面上有Potato待办事项的增删改查,以及启动、停止等功能。其中显示所有待办事项的网页如图1-18所示。

图1-18 土豆网页微服务之待办事项

创建Potato的界面如图1-19所示。

图1-19 土豆网页微服务之Potato创建界面

2.代码结构

这里创建的还是标准的Spring Boot Web应用程序,增加了thymeleaf模板文件(potato. html)和JavaScript代码(potato.js),代码结构如图1-20所示。

图1-20 土豆网页微服务之代码结构

3.主要代码

主要的Java代码就是两个Controller。

1)PotatoWebController将URL指向potatoes.html。

@Controller
public class PotatoWebController {
    @RequestMapping(path = {"/potatoes"})
    public String potatoes(Model model) {

        return "potatoes";
    }

    @RequestMapping(path = {"/admin"})
    public String admin(Model model) {
        return "welcome";
    }

}

2)PotatoApiController提供待办事项的增删改查功能,以及启动和停止的API供前端调用。

@RestController
@RequestMapping("/api/v1")
@Slf4j
public class PotatoApiController {

    @Value("${spring.application.name}")
    private String serviceName;

    @Value("${server.port}")
    private Integer serverPort;

    @Value("${potato.guest.userId}")
    private String guestUserId;

    @Autowired
    private PotatoClient potatoClient;

    @RequestMapping(value = "/potatoes", method = RequestMethod.POST)
    @ApiCallMetricAnnotation(name = "CreatePotato")
    public PotatoDTO create(@RequestBody PotatoDTO potatoRequest) {
        log.info("create {}", potatoRequest);
        return potatoClient.createPotato(potatoRequest);
    }

    @LogDetail
    @RequestMapping(value = "/potatoes/{id}", method = RequestMethod.GET)
    @ApiCallMetricAnnotation(name = "RetrievePotato")
    public PotatoDTO retrieve(@PathVariable UUID id) {
        return potatoClient.retrievePotato(id);
    }

    //省略其他维护操作

HTML模板文件不在此详述,请参见源代码。potato.js文件提供模板内容的填充和与后台REST API的交互,使用了VUE框架。这里给出部分源码,仅供演示。

<! --  potato list -->

function pad(number, length) {
    var str = "" + number
    while (str.length < length) {
        str = '0' + str
    }
    return str
}

Date.prototype.plusHours = function (hours) {
    var mm = this.getMonth() + 1; // getMonth() is zero-based
    var dd = this.getDate();
    var hh = this.getHours() + hours;
    var mi = this.getMinutes();
    var ss = this.getSeconds();

    var offset = this.getTimezoneOffset();
    offset = (offset < 0 ? '+' :'-') + pad(parseInt(Math.abs(offset / 60)), 2)+ ":" + pad(Math.abs(offset % 60), 2);

    return [this.getFullYear(), "-",
        (mm > 9 ? '' :'0') + mm, "-",
        (dd > 9 ? '' :'0') + dd, "T",
        (hh > 9 ? '' :'0') + hh, ":",
        (mi > 9 ? '' :'0') + mi, ":",
        (ss > 9 ? '' :'0') + ss, offset
    ].join('');
};

var Potato = Vue.extend({
    template: '#potato',
    data:function () {
        return {
            'potato': {}
        };
    },
    mounted() {
        axios
            .get('/api/v1/potatoes/' + this.$route.params.potato_id)
            .then(response => {
            this.potato = response.data;
    })
    .
        catch(e => {
            this.errors.push(e);
    })
        ;
    }
});

var AddPotato = Vue.extend({
    template: '#add-potato',
    data:function () {

        var rightNow = new Date();
        var later1 = rightNow.plusHours(1);
        var later2 = rightNow.plusHours(2);
        return {
            potato: {
                name: '',
                description: '',
                tags: '',
                priority: 1,
                duration: 1,
                timeUnit: "HOURS",
                scheduleTime: later1,
                deadline: later2
            },
            errors: []
        }
    },
    methods: {
        createPotato:function () {
            console.log("--- createPotato:" + this.potato.name + ", " + this.
potato.scheduleTime + ", " + this.potato.deadline);
            axios.post('/api/v1/potatoes', this.potato)
                .then(response => {}
        )
        .
            catch(e => {
                this.errors.push(e)
        })
            router.push('/');
        }
    }
});

//省略其他土豆维护操作

var router = new VueRouter({
    routes: [
        {path:'/', component:PotatoList},
        {path:'/potatoes/:potato_id', component:Potato, title:'Toast Potato'},
        {path:'/add-potato', component:AddPotato},

    ]
});

var app = new Vue({
    router: router
}).$mount('#app')

1.5.5 部署土豆微服务

通过前面3节,我们完成了3个微服务的构建。接下来用Docker把每个微服务构建为一个docker image,其中potato-web的Docker文件如下,其他两个类似,请参见源码。

FROM java:8

MAINTAINER Walter Fan

VOLUME /tmp
RUN mkdir -p /opt

ADD ./target/web-0.0.1-SNAPSHOT.jar /opt/potato-web.jar

EXPOSE 9005

ENTRYPOINT ["java", "-jar", "/opt/potato-web.jar"]

用docker compose将这3个微服务部署在一台计算机上启动。docker-compose.yml的内容如下:

version: '2'
services:
    mysqldb:
        image: mysql
        container_name: local-mysql
        environment:
            - MYSQL_DATABASE=test
            - MYSQL_ROOT_PASSWORD=pass1234
            - MYSQL_USER=walter
            - MYSQL_PASSWORD=pass1234
        ports:
            -3306:3306
        volumes:
            - ./data/db/mysql:/var/lib/mysql

    scheduler:
        image: walterfan/potato-scheduler
        container_name: potato-scheduler
        ports:
            -9002:9002
        environment:
            - MYSQL_URL=jdbc:mysql://mysqldb/scheduler? useUnicode=true&charac-terEncoding=utf8
            - MYSQL_USER=${MYSQL_USER}
            - MYSQL_PWD=${MYSQL_PWD}
            - EMAIL_SMTP_SERVER=${EMAIL_SMTP_SERVER}
            - EMAIL_USER=${EMAIL_USER}
            - EMAIL_PWD=${EMAIL_PWD}
            - INFLUXDB_URL=http://graflux:8086
            - ZIPKIN_URL=http://zipkin:9411
        volumes:
            - ./data/db/sqlite:/var/lib/sqlite
            - ./data/logs:/opt/logs
        depends_on:
            - "mysqldb"
            - "graflux"
        links:
            - "mysqldb"
            - "graflux"
            - "zipkin"

    potato:
        image: walterfan/potato-app
        container_name: potato-app
        ports:
            -9003:9003
        environment:
            - INFLUXDB_URL=http://graflux:8086
            - ZIPKIN_URL=http://zipkin:9411
            - potato_scheduler_url=http://scheduler:9002/scheduler/api/v1
        volumes:
            - ./data/db/sqlite:/var/lib/sqlite
            - ./data/logs:/opt/logs
        depends_on:
            - "mysqldb"

        links:
            - "graflux"
            - "mysqldb"
            - "scheduler"
            - "zipkin"
    web:
        image: walterfan/potato-web
        container_name: potato-web
        ports:
            -9005:9005
        environment:
            - INFLUXDB_URL=http://graflux:8086
            - ZIPKIN_URL=http://zipkin:9411
            - potato_server_url=http://potato:9003/potato/api/v1
        volumes:
              - ./data/logs:/opt/logs
        depends_on:
            - "potato"

        links:
            - "graflux"
            - "potato"
            - "zipkin"

这样,通过如下命令就可以启动并运行这3个微服务和依赖的数据库了。

docker-compose up -d

当添加了一个待办事项时,在它计划的开始和截止时间之前我们可以收到提醒邮件。在之后的运行过程中,或许会遇到一些问题,例如收不到提醒邮件,我们不知道是什么原因,同时这3个微服务的运行状况、资源使用情况、用量、性能我们都不清楚。在后续章节中,将通过度量驱动的方法来逐一解决这些问题。