微服务构建: Spring Boot

在展开 Spring Cloud 的微服务架构部署之前, 我们先了解一下用于构建微服务的基础框架-Spring Boot。 由于 Spring Cloud 的构建基于 Spring Boot 实现, 在后续的示例中我 们将大量使用 Spring Boot 来构建微服务架构中的基础设施以及一些试验中使用的微服务。 为了能够辅助后续内容的介绍,确保读者有一定的Spring Boot基础,在这里先对Spring Boot 做一个简单的介绍, 以保证读者能够有一定的基础去理解后续介绍的内容并顺利完成后续 的一些示例试验。

       在这里介绍 Spring Boot 的目的除了它是 Spring Cloud 的基础之外, 也由于其自身的各 项优点, 如自动化配置、 快速开发、 轻松部署等, 非常适合用作微服务架构中各项具体微 服务的开发框架。所以我们强烈推荐使用 Spring Boot 来构建微服务, 它不仅可以帮助我们 快速地构建微服务, 还可以轻松简单地整合 Spring Cloud 实现系统服务化, 而如果使用了 传统的 Spring 构建方式的话, 在整合过程中我们还需要做更多的依赖管理工作才能让它们 完好地运行起来。

       在本文中我们将介绍下面这些与后续介绍有密切联系的内容: 

               • 如何构建 Spring Boot 项目 

               • 如何实现 RESTfulAPI 接口 

               • 如何实现多环境的 Spring Boot 应用配置 

               • 深入理解 Spring Boot 配置的启动机制

               • Spring Boot 应用的监控与管理

1、框架介绍

    对于很多Spring框架的初学者来说, 经常会因为其繁杂的配置文件而却步。 而对于很 多老手来说, 每次新构建项目总是会重复复制粘贴一 些差不多的配置文件这样枯燥乏味的事。

   Spring Boot的出现 可以有效改善这类问题,SpringBoot的宗旨并非要重写Spring或是 替代Spring, 而是希望通过设计大量的自动化配置等方式来简化Spring原有样板化的配置, 使得开发者可以快速构建应用。 

    除了解决配置问题之外, Spring Boot还通过一系列StaiterPOMs的定义, 让我们整合 各项功能的时候, 不需要在 Maven的pom.xml中维护那些错综复杂的依赖关系, 而是通 过类似模块化的Starter模块定义来引用, 使得依赖管理工作变得更为简单。

     在如今容器化大行其道的时代,Spring Boot除了可以很好融入Docker之外, 其自身就 支持嵌入式的 Tomcat、 Jetty 等容器。 所以, 通过Spring Boot 构建的应用不再需要安装 Tomcat, 将应用打包成war, 再部署到Tomcat 这样复杂的构建与部署动作, 只需将Spring Boot应用打成jar包, 并通过java -jar命令直接运行就能启动一个标准化的Web应用, 这使得Spring Boot应用变得非常轻便。

2、快速搭建SpringBoot项目

   在本文中, 我们将逐步指引读者创建一个Spring Boot的基础项目, 并且实现 一个简单 的RESTfulAPL 通过这个例子对Spring Boot有一个初步的了解, 并体验其结构简单、 开 发迅速的特性。

   项目构建与解析

         系统及工具版本要求 

                • Java 7及以上版本 

                • Spring Framework 4.2.7及以上版本 

                • Maven 3.2及以上版本/Gradle 1.12及以上版本 

        本文内容均采用Java 1.7、 Spring Boot 1.5.10调试通过。

https://img1.mukewang.com/5b28a12b0001ae2113220976.jpg

工程结构解析

• src/main/java: 主程序入口 DmsApplication, 可以通过直接运行该类来 启动Spring Boot应用。

• src/main/resources: 配置目录, 该目录用来存放应用的一些配置信息, 比如 应用名、服务端口、数据库链接等。由千我们引入了Web模块,因此产生了static 目录与templates目录, 前者用于存放静态资源, 如图片、 css、JavaScript等; 后者用千存放Web页面的模板文件, 这里我们主要演示提供RESTful APL所以这 两个目录并不会用到。 

• src/test/: 单元测试目录, 生成的DmsApplicationTests通过JUnit 4实 现, 可以直接用运行Spring Boot应用的测试。 后文中, 我们会演示如何在该类中测 试RESTfulAPI。

    Maven配置分析 打开当前工程下的pom.xml文件, 看看生成的项目都引入了哪些依赖来构建Spring Boot工程, 内容大致如下所示。

<?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.baihe.dms</groupId>
    <artifactId>dms</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <name>dms</name>
    <description>Deduct Money System</description>

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

    <dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.7</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.14</version>
        </dependency>
        <dependency>
            <groupId>commons-httpclient</groupId>
            <artifactId>commons-httpclient</artifactId>
            <version>3.1</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.5</version>
        </dependency>
         <dependency>
            <groupId>net.minidev</groupId>
               <artifactId>json-smart</artifactId>
                <version>1.2</version>
         </dependency>
    </dependencies>

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


</project>

 

   在基础信息部分, groupid和 artifactId 对应生成项目时页面上输入的内容。 另 外, 我们还可以注意到, 打包形式为 jar, 正如我们 之前所介绍的,Spring Boot默认将该Web应用打包为jar 的形式, 而非war 的形式, 因为 默认 的Web模块依赖 会包含嵌入式的Tomcat , 这样使得我们的应用jar自身就具备了提供 Web服务的能力, 后续我们会演示如何启动它。 

    父项目parent配置指定为 spring-boot-starter-parent的1. 5.10 版本, 该父项 目中定义了Spring Boot版本的基础依赖以及 一 些默认配置内容 , 比如,配置文件application.properties的位置等。 在项目依赖 dependencies配置中, 包含了下面两项。 

          • spring-boot-starter-web: 全栈Web开发模块, 包含嵌入式Tomcat、 Spring MVC。 

          • spring-boot-starter-test: 通用测试模块, 包含JUnit、 Hamcrest、 Mockito 。 

    这里所引用的web和test 模块,在SpringBoot 生态中被称为Starter POMs。Starter POMs 是一系列轻便的依赖 包, 是一套一站式的Spring相关技术的解决方案。 开发者在使用和整 合模块时, 不必再去搜寻样例代码中的依赖配置来复制使用, 只需要引入对应的模块包即 可 。 

    比如, 开发Web应用的时候, 就引入spring-boot-starter-web, 希望应用具备 访问数据库能力的时候, 那就再引入 spring-boot-starter-jdbc 或是更好用的 spring-boot-starter-data-jpa。 在使用SpringBoot构建应用的时候, 各项功能模 块的整合不再像传统Spring应用的开发方式那样,需要在 pom.xml中做大量的依赖配置, 而是通过使用StarterPOMs定义的依赖包,使得功能模块整合变得非常轻巧, 易于理解与使用。 

3、实现RESTfulAPI

   在Spring Boot中创建一个RESTfulAPI的实现代码同SpringMVC应用一样, 只是不 需要像SpringMVC那样先做很多配置, 而是像下面这样直接开始编写Controller内容:

• 新建package, 命名为com.baihe.dms.controller.CmsController, 可根据实际的构建情况修改成自 己的路径。

 • 新建CmsController类,内容如下所示。

package com.baihe.dms.controller;

import com.baihe.dms.entity.common.CmsException;
import com.baihe.dms.entity.common.ResponseData;
import com.baihe.dms.service.WithholdService;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller("cmsController")
@RequestMapping(value = "/")
public class CmsController {

    private Logger logger = Logger.getLogger(this.getClass());

    private WithholdService withholdService;

    CmsController(@Autowired  WithholdService withholdService) {
        this.withholdService = withholdService;
    }

    @RequestMapping(value = "/withhold" , method =  RequestMethod.POST)
    @ResponseBody
    public ResponseData withhold(String contractNo, Long requestNo, Long userId, Long projectId,
    		Integer planStep, String bankCode, Double totalAmount, Integer overdueFlag, Integer incomeFree) {
        try {
            return withholdService.withhold(contractNo, requestNo, userId, projectId, planStep, bankCode, totalAmount, overdueFlag, incomeFree);
        } catch (CmsException e) {
            return ResponseData.no(e.getErrCode());
        } catch (Exception e) {
            logger.debug("withhold", e);
            return ResponseData.no(ResponseData.INTERNAL_ERROR);
        }
    }
    
    @GetMapping("/withholdCancelOrRecover")
    @ResponseBody
    public ResponseData withholdCancelOrRecover(Long requestNo, int status) {
        try {
            return withholdService.withholdCancelOrRecover(requestNo, status);
        } catch (CmsException e) {
            return ResponseData.no(e.getErrCode());
        } catch (Exception e) {
            logger.debug("withholdCancelOrRecover", e);
            return ResponseData.no(ResponseData.INTERNAL_ERROR);
        }
    }
    /*
     * 由于豁免流程的存在
     * 会出现立即还款情况
     * 参数 requestNo 还款计划流水号
     * channelType  扣款渠道  宝付还是易宝
     * */
    @RequestMapping("/atOnceWithhold")
    @ResponseBody
    public ResponseData atOnceWithhold(String requestNo, String channelType) {
        try {
            return withholdService.atOnceWithhold(Long.valueOf(requestNo), channelType);
        } catch (CmsException e) {
            return ResponseData.no(e.getErrCode());
        } catch (Exception e) {
            logger.debug("atOnceWithhold", e);
            return ResponseData.no(ResponseData.INTERNAL_ERROR);
        }
    }

}

  

ResponseData类:

package com.baihe.dms.entity.common;

import java.io.Serializable;

/**
 * 接口返回的数据
 */
public class ResponseData implements Serializable {

    private static final long serialVersionUID = 2047667816784695690L;

    public static final int OK = 200; // 非 OK 的都是失败
    public static final int NO = -1;

    public static final int INTERNAL_ERROR = -99;
    public static final int INVALID_PARAMETER = -100;
    public static final int NULL_PARAMETER = -101;
    public static final int INVALID_AMOUNT = -102;
    public static final int INVALID_BANKCODE = -103;
    public static final int HAS_UNFINISHED_REQUEST = -104;
    public static final int INVALID_SPLIT_CONFIG = -105;
    public static final int SPLIT_TOO_MANY = -106;
    public static final int NOT_NEED_SPLIT = -107;

    private Integer code = NO;
    private String message = "";
    private Object data;


    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    private void generateMessage(int errCode) {
        String message = "未知错误";

        switch (errCode) {
            case INTERNAL_ERROR:
                message = "内部错误";
                break;

            case INVALID_PARAMETER:
                message = "无效的参数";
                break;

            case NULL_PARAMETER:
                message = "参数不能为空";
                break;

            case INVALID_AMOUNT:
                message = "无效的金额";
                break;

            case INVALID_BANKCODE:
                message = "无效的银行编码";
                break;

            case HAS_UNFINISHED_REQUEST:
                message = "用户尚有未完成的交易";
                break;

            case INVALID_SPLIT_CONFIG:
                message = "拆分配置无效";
                break;

            case SPLIT_TOO_MANY:
                message = "拆分数目过多";
                break;
                
            case NOT_NEED_SPLIT:
                message = "金额没有变化,无需拆分";
                break;

            case NO:
                message = "内部错误";
                break;

            case OK:
                message = "成功";
                break;
        }

        this.message = message;
    }

    public static ResponseData create(int code, Object data) {
        ResponseData responseData = new ResponseData();
        responseData.code = code;
        responseData.generateMessage(code);
        responseData.data = data;

        return responseData;
    }

    public static ResponseData create(int code) {
        return create(code, null);
    }

    public static ResponseData ok(Object data) {
        return ResponseData.create(ResponseData.OK, data);
    }

    public static ResponseData ok() {
        return ResponseData.create(ResponseData.OK, null);
    }

    public static ResponseData no(int code, Object data) {
        return ResponseData.create(code, data);
    }

    public static ResponseData no(int code) {
        return ResponseData.create(code);
    }

}

  

启动Spring Boot应用的方式有很多种:

• 作为一个 Java 应用程序, 可以直接通过运行拥有 main 函数的类来启动。

• 在 Maven 配置中, 之前提到了 spring-boot 插件, 可以使用它来启动, 比如执行 mvn spring-boot: run 命令。

• 在服务器上部署运行时, 通常先使用 mvn install 将应用打包成 jar 包, 再通过 java -jar xxx. jar 来启动应用。

配置详解

   在面我们轻松地实现了一个简单的RESTfulAPI应用, 体验了SpringBoot的 诸多优点。我们用非常少的代码就成功实现了一个Web应用, 这是传统Spring应用无法办到的。虽然在实现Controller时用到的代码是一样的,但是在配置方面,相信大家也注意到了, 在上面的例子中, 除了Maven的配置之外, 没有引入任何其他配置。 

    这就是之前我们提到的,SpringBoot针对常用的开发场景提供了一系列自动化配置来 减少原本复杂而又几乎很少改动的模板化配置内容。但是,我们还是需要 了解如何在Spring Boot中修改这些自动化的配置内容, 以应对一些 特殊的场景需求, 比如, 我们在同一台主 机上需要启动多个基千Spring Boot的Web应用, 若不为每个应用指定特别的端口号, 那 么默认的8080 端口必将导致冲突。 后续我们在使用SpringCloud的各个组件的时候, 其实有大量的工作都 会是针对配置 文件的。所以我们有必要深入了解一些关于SpringBoot中的配置文件的知识, 比如配置方 式、 如何实现多环境配置、 配置信息的加载顺序等。

  • 配置文件

     

    在快速入门示例中, 我们介绍Spring Boot 的工程结构时, 提到过 src/rnain/ resources 目录是Spring Boot的配置目录, 所以当要为应用创建个性化配置时, 应在该 目录下进行。

    Spring Boot 的默认配置文件位置为 src/main/resources/application. properties 。关于SpringBoot应用的配置内容都可以集中在该文件中, 根据我们引入的 不 同Starter模块,可以在这里定义容器端口号、 数据库连接信息、 日志级别等各种配置信 息。比如, 我们需要自定义Web模块的服务端口号,可以在application.properties  中添加 server.port=8888 来指定服务端口为 8888 , 也可 以通过 spring.appliction.name =hello 来指定应用名(该名字在后续SpringCloud中会被 注册为服务名)。 

    Spring Boot的配置文件除了可以使用传统的 properties文件之外,还支持现在被广泛推 荐使用的YAML文件。

    YAML 采用的配置格式不像 properties 的配置那样以单纯的键值对形式来表示,而是以 类似大纲的缩进形式来表示。 下面是一段 YAML 配置信息:

server:
    port: 8081

spring:
    profiles:
        active: prod

mybatis:
    mapper-locations: classpath:mapping/*.xml
    type-aliases-package: com.baihe.dms.entity.common
    configLocation: classpath:mybatis-config.xml

logging:
    level:
        com.baihe.dms.mapper: debug

endpoints:
    shutdown:
        enabled: true
    sensitive:
      false

 

  • 自定义参数

    除了可以在 Spring Boot 的配置文件中设置各个 Starter 模块中预定义的配置属性, 也可 以在配置文件中定义一些我们需要的自定义属性。 比如在 application.properties 中 添加: book.name=SpringCloudinAction

    book.author=ZhaiYongchao

    然后, 在应用中可以通过@Value 注解来加载这些自定义的参数,

    比如:

    @Component

    public class Book {

    @Value(“${book.name}”)

    private String name;

    @Value(“${book.author}”)

    private String author;

    //省略getter和setter @Value 注解加载属性值的时候可以支持两种表达式来进行配置,

    如下所示。

    • 一种是上面介绍的 PlaceHolder 方式, 格式为${...}, 大括号内为 PlaceHolder。

    • 另一种是使用SpEL 表达式 (Spring Expression Language), 格式为#{...}, 大括号 内为 SpEL 表达式。

  • 使用随机数

    在 一些特殊情况下, 我们希望有些参数每次被加载的时候不是 一个固定的值, 比如密 钥、 服务端口等。 在 SpringBoot的属性配置文件中, 可以 通过 使用${random}配置来产 生随机的int值、long值或者string字符串,这样我们就可以容易地通过 配置随机生成属性, 而不是在程序中通过编码来实现这些逻辑。

    #随机字符串 com.didispace.blog.value=${random.value}

    #随机int com.didispace.blog.number=${random.int}

    #随机long com.didispace.blog.bignumber=${random.long}

    # 10以内的随机数 com.didispace.blog.test1=${random.int(l0)}

    # 10-20的随机数 com.didispace.blog.test2=${random.int[l0,20]}

  • 命令行参数

    在用命令行方式 启 动 Spring Boot 应用时, 连续的两个减号–就 是对 application.properties 中的属性值进行赋值 的标识。 所以 , java -jar xxx.jar–server.port=8888命令, 等价千在 application.properties 中添加 属性server.port= 8888。 通过命令行来修改属性值是 SpringBoot非常重要的一个特性。 通过此特性, 理论上已经使得应用的属性在启动前是可变的, 所以其中的端口号也好、 数据库连接也好, 都是可 以在应用启动时发生改变的, 而不同于以往的Spring应用通过Maven的Profile在编译器 中进行不同环境的构建。 SpringBoot的这种方式, 可以让应用程序的打包内容贯穿开发、 测试以及线上部署, 而Maven不同Profile的方案为每个环境所构建的包,其内容本质上是 不同的。 但是, 如果 每个参数都需要通过命令行来指定, 这显然也不是 一个好的方案, 所 以下面我们看看如何在SpringBoot中实现多环境的配置。

  • 多环境配置

    我们在开发应用的时候, 通常同一套程序会被应用和安装到几个不同的环境中, 比如 开发 、 测试、 生产等。 其中 每个环境的数据库地址、 服务器端口等配置都不同, 如果在为 不同环境打包时都要频繁修改配置文件的话, 那必将是个非常烦琐且容易发生错误的事。 对于多环境的配置,各种项目构建工具或是框架的基本思路是 一致的, 通过配置多份 不同环境的配置文件,再通过打包命令指定需要打包的内容之后进行区分打包,SpringBoot 也不 例外, 或者说实现起来更加简单。

    在 Spring Boot 中, 多环境配置的文件名需要满足 application-{profile}. proper巨es的格式, 其中{profile}对应你的环境标识,

    如下所示。

    • applicaction-dev.properties: 开发环境。

    • applicaction-test.properties: 测试环境。

    • application-prod.properties: 生产环境。

    至于具体哪个配置文件会被加载, 需要在 application.properties 文件中通过 spring.profiles.active 属性来设置, 其 值 对应配置文件中的{profile}值。 如 spring.profiles.active= test就会加载 application-test.properties配置 文件内容。

  • 加载顺序

    为了能够更合理地重写各属性的值,SpringBoot使用了下面这种较为特别的属性加载 顺序:

    1 在命令行中传入的参数。

    2. SPRING APPLICATION JSON中的属性。 SPRING_APPLICATION—JSON是以 JSON格式配置在系统环境变量中的内容。

    3. java:comp/env中的JNDI 属性。

    4. Java的系统属性, 可以通过System.getProperties()获得的内容。

    5 操作系统的环境变量 。

    6 通过random.*配置的随机属性。

    7 位于当前应用 jar 包之外,针对不同{profile}环境的配置文件内容,例如 application-{profile}.properties或是YAML定义的配置文件。

    8 位于当前应用 jar 包之内 ,针对不同{profile}环境的配置文件内容,例如 application-{profile}.properties或是YAML定义的配置文件。

    9 位于当前应用jar包之外的application.properties和YAML配置内容。

    10位于当前应用jar包之内的application.properties和YAML配置内容。

    11在@Configura巨on注解修改的类中,通过@PropertySource注解定义的属性。

    12应用默认属性,使用SpringApplication.setDefaultProperties 定义的 内容。

    优先级按上面的顺序由高到低,数字越小优先级越高。