【读书笔记】Spring Cloud 微服务实战 第二章

2018/11/30

第二章 微服务构建:Spring Boot

1-框架简介
2-快速入门
 2.1-项目构建与解析
 2.2-实现RESTful API
3-配置详解
 3.1-配置文件
 3.2-自定义参数
 3.3-参数引用
 3.4-使用随机数
 3.5-命令行参数
 3.6-多环境配置
 3.7-加载顺序
4-监控与管理
 4.1-初识actuator
 4.2-原生端点
5-小结

Spring Boot 是 Spring Cloud 的基础框架,如果对 Spring Boot 已经了解,可以跳过本章。 Spring Boot 具有 自动化配置、快速开发、轻松部署等优点,后续将围绕以下内容展开:

  • 如何构建 Spring Boot 项目
  • 如何实现 RESTful API 接口
  • 如何实现多环境的 Spring Boot 应用配置
  • 深入理解 Spring Boot 配置的启动机制
  • Spring Boot 应用的监控与管理

1-框架简介

使用过 Spring 的开发人员都会认识到 Spring 的配置繁琐复杂。即使我们可以通过 Maven 创建脚手架来减轻重复配置,但是这些配置依然存在并常常映入眼帘耽误视线。Spring Boot 很好的弥补了这些困扰:

  • Spring Boot 在 Spring 的基础上,提出了 约定优于配置 的理念,将大量配置自动化模板化,开发人员只需要做少量修改
  • 为了解决 maven 依赖复杂的问题,通过类似模块化的 Starter 模块来定义引用
  • 通过内嵌容器使得 Spring Boot 应用更方便启动部署,更为方便的融入 Docker 等容器中

2-快速入门

下面将逐步创建一个 Spring Boot 项目实现一个简单的 RESTful API

2.1-项目构建与解析

系统及工具版本要求

  • Java 7 及以上版本
  • Spring Framework 4.2.7 及以上版本
  • Maven 3.2 及以上版本 / Gradle 1.12 及以上版本 本内容均采用 Java 1.8.0_91、Spring Boot 1.5.18.RELEASE

构建 Maven 项目

  1. 通过官方 Spring Initializer 工具创建项目
  2. 访问 https://start.spring.io/,如下图所示,该页面提供了以 Maven 或 Gradle 构建 Spring Boot 项目的功能。
  3. 选择构建工具 Maven Project、Spring Boot 版本现在 1.5.18,填写 Group 和 Artifact 信息,在 Search for dependencies 中可以搜索需要的依赖,我们选择 Web 依赖,因为要实现 RESTful API。
  4. 点击 Generate Project 下载项目包、
  5. 解压包,使用 IDE 以 Maven 项目导入,IntelliJ IDA 2018.1.6 为例。
  6. 从菜单中选择 File->New->Project from Existing Sources…。
  7. 选择解压后的项目项目文件,单机 OK 按钮。
  8. 点击 Import project from external model 选择 Maven,一直点击 Next 。
  9. 如环境中已经有多个版本 JDK 选择 Java 7 以上。

工程解析 项目导入后,如下图 项目结构如果所示,分为以下几个部分:

  • src/main/java:项目源码目录,DemoApplication 为程序入口类,可以使用它启动应用。
  • src/main/resources:资源目录,包括配置文件、页面模板、静态文件等。application.properties 项目的配置信息,可以修改为 yml 格式,static 静态文件,templates 模板文件。
  • src/test:单元测试目录,会自动生成 DemoApplicationTests 类用于测试 Spring Boot。

Maven 配置分析 项目初始化后 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.mylater</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>demo</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.18.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>
	</properties>

	<dependencies>
		<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>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
	
</project>

大家可以看到,默认打包方式为 jar ,web 模块会依赖内嵌 Tomcat,使得项目可以直接启动。另外父项目为 spring-boot-starter-parent,它提供了基础依赖和一些默认配置。 dependencies 中包含的一下两部分:

  • spring-boot-starter-web:Web 模块,内嵌了 Tomcat 和 SpringMVC
  • spring-boot-starter-test:测试模块,包含 JUnit、Hamcrest、Mockito

这里的 web 和 test 模块称作 Starter POMs,它是一系列依赖包的集合。这样做的好处是当我们需要项目具备某个功能时,只需要引入这个集合即可,不必像 Spring 一样映入大量包还需要考虑其间的冲突。便于理解和使用。

在 build 中使用了插件 spring-boot-maven-plugin ,可以让我们使用 mvn spring-boot:run 命令即可快速启动和停止服务,不必每次都去打包。

2.2-实现 RESTful API

用法和之前使用 Spring MVC 方式相差不大,不过由于使用了 spring-boot-starter-web 之后大量的配置我们可以不去关心。

  1. 创建 Controller 存放包 web
  2. 新建 HelloController,内容如下
     @RestController
     public class HelloController {
    	
         @RequestMapping("/hello")
         public String index() {
             return "Hello World!";
         }
     }
    
  3. 启动应用后,通过 http://lodalhost:8080/hello 返回了 Hello World

启动 Spring Boot 应用 有以下方式:

  • 通过 main 函数
  • 通过 spring-boot:run
  • 通过 java -jar xxx.jar

编写单元测试 作为一个开发人员,大家一定要养成写单元测试的习惯,很多时候能为我节省很多不必要的时间开支。在微服务架构中更是如此,很多组件相互依赖,许多问题不能在开始就暴露出来,一旦出现问题将会付出更多时间去解决。

Spring Boot 实现单元测试比较简单,下面我们就通过 test 目录下自动生成的 DemoApplicationTests 来测试前面我们编写的 hello 接口。

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {

    private MockMvc mvc;

    @Before
    public void setUp() {
        mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
    }

    @Test
    public void hello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/hello")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(Matchers.equalTo("Hello World!")));
    }
}

小结 大家可以看到,我们并没有写多少代码,就实现了上面的功能。到这里,我们已经了解了 Spring Boot 项目的基本开发流程,下面我们会针对配置进行讲解。

3-配置详解

使用默认配置,可以快速搭建一个 Spring Boot 环境。当我们需要修改这些默认配置时,比如自定义启动端口,增加数据库配置,修改日志级别等等,该如何实现呢?

3.1-配置文件

前面我们提到,resources 是存放 web 资源和配置文件的地方,打开此目录,会发现 application.properties 文件,这就是 Spring Boot 的配置文件。我们修改为 yml 格式。增加以下内容:

server:
  port: 8888
spring:
  application:
    name: demo

这里使用 yml 格式,它有许多优点,对格式也有严格的要求,和 properties 达到的效果一样,有时候甚至优于后者,不清楚的话,可以搜索了解一下,如下:

server:
  port: 8001
---
spring:
  profiles: test
server:
  port: 8002
---
spring:
  profiles: pro
server:
  port: 8003

在使用 test 环境时端口为 8002,pro 环境端口为 8003,如果不指定环境则使用 8001,是不是很方便。


注意 yml 文件不能通过 @PropertySource 注解载入


3.2-自定义参数

除了项目本身的配置,我们也可以在配置文件中添加自定义配置内容:

book:
 name: bookname
 author: bookauthor

在spring组建中通过如下方式注入

@Component
public class Book {
    @Value("$(book,name)")
    private String name;
    @Value("$(book.author)")
    private String author;
    ...
}

@Value 注解支持两种方式

  • ${PlaceHolder:default_value}
  • #{SpEL?:default_value}

3.3-参数引用

参数间可以通过 PlaceHolder 方式引用,如

book.name=bookname
book.author=bookautho
book.desc=${book.author} is writing 《${book.name}》

3.4-使用随机数

有的时候,配置信息需要随机生成的 int,long,String 值,以前的方式是代码编写,现在可以直接在配置文件中实现就可以了

# 随机字符串
book.isbn=${random.value}
# 随机 int
book.num=${random.int}
# 随机 long
book.size=${random.long}
# 10 以内的随机数 
book.num1=${random.int(10)}
# 10 - 20 以内的随机数
book.num2=${random.[10,20]}

3.5-命令行参数

前面的案例,我们启动 Spring Boot 项目时使用了命令行java -jar xxx.jar --server.port=888 其中--就是对项目参数进行赋值的操作。 这个特性有很多好处,同一个 jar 包就能分别运行于测试和生产环境中,但是这样配置显得有一些繁琐。

3.6-多环境配置

对于不同环境的配置,我们可以使用分组的方式,Spring Boot 很好的支持了这一点。步骤:

  1. 创建主配置文件 application.yml
  2. 创建开发环境配置文件 application-dev.yml
  3. 创建测试环境配置文件 application-test.yml
  4. 支持的环境没有限制,可以如上方式增加更多配置,把对应的差异内容配置到具体的文件中。
  5. 在主配置文件中,通过 spring.profiles.active 属性指定所需要采用的环境配置
  6. 在启动是,可以使用 –spring.profiles.active 参数覆盖住配置文件值,做到轻易变换环境

3.7-加载顺序

到这里,似乎很完美的解决了我们在单体应用中遇到的配置繁琐问题,但是,我们还需要考虑更多。那现在还存在哪些问题呢?

  1. 配置都保存在项目中,对开发人员没有限制,存在安全隐患。
  2. 当使用微服务框架时,每个子模块依然要重复上述操作,对于运维来说依然复杂难于维护。

于是,Spring Cloud Config 应运而生,他就是解决这些问题的。为了后续能更好的理解 Spring Cloud Config,我们现在先来看看 Spring Boot 对配置文件的加载顺序。

  1. 应用默认配置,实用 SpringApplication.setDefaultProperties 定义的内容。
  2. 在 @Configuration 注解的修改的类中,通过 。@PropertySource 注解定义的属性。
  3. 位于当前 jar 包之内的 application.yml 配置内容。
  4. 位于当前 jar 包之外的 application.yml 配置内容。
  5. 位于当前 jar 包之内,真对不同{profile}环境的配置文件内容。
  6. 位于当前 jar 包之外,真对不同{profile}环境的配置文件内容。
  7. 通过 random.* 配置的随机属性。
  8. 操作系统的环境变量。
  9. Java 的系统属性,可以通过 System.getProperties() 获得的内容。
  10. java:comp/env 中的 JNDI 属性。
  11. SPRING_APPLICATION_JSON 中的属性,SPRING_APPLICATION_JSON 是以json 格式配置在系统环境变量中的内容。
  12. 在命令行中传入的参数。 上述内容,相同配置,后面的会覆盖前面的配置内容。第 4 和 6 项都是从jar包之外加载配置内容,实现外部化配置的原理就是通过这里实现。

4-监控与管理

微服务框架使得部署应用的数量成倍增长,维护难度也加大,出现故障的概览也增大。虽然在高可用机制可以保护,但是出现问题还是需要及时发现和处理的。为此,我们需要新的方式来解决这个问题,这就是自动化监控运维机制,它的实现方式是不间断的收集各个服务的各项指标,并根据这些信息来监控预警。

Spring Boot 提供了 spring-boot-starter-actuator 模块,能够自动为应用提供一些列用于监控的端点。Spring cloud各个组件还对它进行了加强。在需要时,我们也可以对它进行扩展。

4.1-初识actuator

引入这个组件很简单,只需要添加一个依赖即可:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

此时,启动项目会看到如下信息:

2018-12-07 16:07:23.218  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/mappings || /mappings.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.219  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/metrics/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)
2018-12-07 16:07:23.219  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/metrics || /metrics.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.220  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/beans || /beans.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.221  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/loggers/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.get(java.lang.String)
2018-12-07 16:07:23.222  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/loggers/{name:.*}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v1+json || application/json],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.set(java.lang.String,java.util.Map<java.lang.String, java.lang.String>)
2018-12-07 16:07:23.222  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/loggers || /loggers.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.223  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/auditevents || /auditevents.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public org.springframework.http.ResponseEntity<?> org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint.findByPrincipalAndAfterAndType(java.lang.String,java.util.Date,java.lang.String)
2018-12-07 16:07:23.225  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/trace || /trace.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.225  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/dump || /dump.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.226  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/health || /health.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(javax.servlet.http.HttpServletRequest,java.security.Principal)
2018-12-07 16:07:23.227  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/env/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)
2018-12-07 16:07:23.227  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/env || /env.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.228  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/configprops || /configprops.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.228  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/autoconfig || /autoconfig.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2018-12-07 16:07:23.229  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/heapdump || /heapdump.json],methods=[GET],produces=[application/octet-stream]}" onto public void org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint.invoke(boolean,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) throws java.io.IOException,javax.servlet.ServletException
2018-12-07 16:07:23.230  INFO 36848 --- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped "{[/info || /info.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()

可以看到新暴露了一批端点,我们看看这些端点都有写什么内容: /health

{
  "status": "UP",
  "diskSpace": {
    "status": "UP",
    "total": 871226142720,
    "free": 341470404608,
    "threshold": 10485760
  }
}

由于项目简单,输出的内容不多

4.2-原生端点

spring-boot-starter-actuator 包含的原生端点,分为以下三类:

  • 应用配置类:获取应用配置、自动配置、环境变量等信息
  • 度量指标类:获取内存、线程池、网络请求等度量指标
  • 操作控制类:关闭应用等操作的端点

下面我来详细这三类端点以及如何做一些简单的扩展

应用配置类

  • /autoconfig:应用自动化配置报告,包括所有自动化配置后选项,以及每个后选项是否满足自动话配置的各个先决条件,可以查看为什么配置没生效原因。
    • /positiveMatches 条件匹配成功的自动化配置
    • /negativeMatches 条件匹配不成功的自动化配置
  • /beans:获取上下文创建的所有bean,包含
    • bean:Bean的名称
    • scope:作用域
    • type:Java类型
    • resource:Class文件的路径
    • dependencies:依赖的 Bean 名称
  • /configprops:获取配置的属性信息
  • /env:获取应用所有可用的环境属性报告,包括 环境变量、JVM属性、应用的配置属性、命令行中的参数…
  • /mappings:返回说有 Spring MVC 的控制器映射关系报告。
  • /info:返回应用自定义信息

度量指标类 度量指标对于微服务监控有非常大的帮助

  • /metrics:返回如内存、线程、垃圾收集等重要指标
  • /health:获取应用各类健康信息
  • /dump:暴露程序中的线程信息
  • /trace:返回基本的http跟踪信息

操作控制类 原生提供了关闭应用的端点 /shutdown,但是需要配置开启 endpoints.shutdown.enabled=true

5-小结

到此,我们对 Spring Boot 有了一个初步的了解,这有助于我们深入理解 Spring Cloud。

(转载本站文章请注明作者和出处 mylater

Show Disqus Comments

Post Directory