你好,我是姚秋辰。

今天我们来动手搭建优惠券平台的实战项目。为了让你体验从0到1的微服务改造过程,我们先使用Spring Boot搭建一个基础版的优惠券平台项目,等你学习到Spring Cloud的时候,我们就在这个项目之上做微服务化改造,将Spring Cloud的各个组件像添砖加瓦一样集成到项目里。

如果你没有太多Spring Boot的相关开发经验,通过今天的学习,你可以掌握如何通过Spring Boot组件快速落地一个项目。如果你之前了解过Spring Boot,那么今天的学习不仅可以起到温故知新的作用,你还可以从我分享的开发经验里得到一些启发。

在03讲中,我们介绍了优惠券平台的功能模块。我们说过,在用户领取优惠券的过程当中,优惠券是通过券模板来生成的,因此,优惠券模板服务是整个项目的底层基础服务。今天咱就直接上手搭建这个服务模块:coupon-template-serv。不过在此之前,我们先来看看整体的项目结构是怎样搭建的。

1.搭建项目结构

我把整个优惠券平台项目从Maven模块管理的角度划分为了多个模块。

优惠券秒杀系统架构 优惠券模块_优惠券秒杀系统架构

在顶层项目geekbang-coupon之下有四个子模块,我先来分别解释下它们的功能:

  • coupon-template-serv: 创建、查找、克隆、删除优惠券模板;
  • coupon-calculation-serv:计算优惠后的订单价格、试算每个优惠券的优惠幅度;
  • coupon-customer-serv:通过调用template和calculation服务,实现用户领取优惠券、模拟计算最优惠的券、删除优惠券、下订单等操作;
  • middleware:存放一些与业务无关的平台类组件。

在大型的微服务项目里,每一个子模块通常都存放在独立的Git仓库中,为了方便你下载代码,我把所有模块的代码都打包放到了这个代码仓库里,你可以在这里找到课程各阶段对应的源代码。

在每一个以“-serv”结尾的业务子模块中,我从内部分层的角度对其做了进一步拆分,以我们今天要搭建的coupon-template-serv为例,它内部包含了三个子模块:

  • coupon-template-api:存放公共POJO类或者对外接口的子模块;
  • coupon-template-dao:存放数据库实体类和Dao层的子模块;
  • coupon-template-impl:核心业务逻辑的实现层,对外提供REST API。

你会发现,我把coupon-template-api作为一个单独的模块,这样做的好处是:当某个上游服务需要获取coupon-template-serv的接口参数时,只要导入轻量级的coupon-template-api模块,就能够获取接口中定义的Request和Response的类模板,不需要引入多余的依赖项(比如Dao层或者Service层)。

这就是开闭原则的应用,它使各个模块间的职责和边界划分更加清晰,降低耦合的同时也更加利于依赖管理。

搭建好项目的结构之后,接下来我们借助Maven工具将需要的依赖包导入到项目中。

2.添加Maven依赖项

这里你要注意一下,添加Maven依赖项需要遵循“从上到下”的原则,也就是从顶层项目geekbang-coupon开始,顺藤摸瓜直到coupon-template-serv下的子模块。首先,我们来看看顶层geekbang-coupon依赖项的编写。

2.1.编写geekbang-coupon依赖项

geekbang-coupon是整个实战项目的顶层项目,它不用操心具体的业务逻辑,只用完成一个任务:管理子模块和定义Maven依赖项的版本。这就像一个公司的大boss一样,只用制定方向战略,琐碎的业务就交给下面人(子模块)来办就好了。

那么顶层战略在哪里制定?其实就在pom.xml文件里,我们看一下geekbang-coupon的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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
    </parent>

    <groupId>com.geekbang</groupId>
    <artifactId>geekbang-coupon</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>coupon-template-serv</module>
        <module>coupon-calculation-serv</module>
        <module>coupon-customer-serv</module>
        <module>middleware</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2020.0.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>


            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.0</version>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-collections4</artifactId>
                <version>4.0</version>
            </dependency>

            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>1.9</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.31</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.20</version>
            </dependency>

            <dependency>
                <groupId>jakarta.validation</groupId>
                <artifactId>jakarta.validation-api</artifactId>
                <version>2.0.2</version>
            </dependency>

            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>16.0</version>
            </dependency>
        </dependencies>
    </dependencyManagement>


</project>

在pom文件里有以下三个重点标签。

  • < parent >标签
    在parent标签中我们指定了geekbang-coupon项目的“父级依赖”为spring-boot-starter-parent,这样一来,spring-boot-starter-parent里定义的Spring Boot组件版本信息就会被自动带到子模块中。这种做法也是大多数Spring Boot项目的通用做法,不仅降低了依赖项管理的成本,也不需要担心各个组件间的兼容性问题。
  • < packaging >标签
    maven的打包类型有三种:jar、war和pom。当我们指定packaging类型为pom时,意味着当前模块是一个“boss”,它只用关注顶层战略,即定义依赖项版本和整合子模块,不包含具体的业务实现。
  • < dependencymanagement >标签
    这个标签的作用和< parent >标签类似,两者都是将版本信息向下传递。dependencymanagement是boss们定义顶层战略的地方,我们可以在这里定义各个依赖项的版本,当子项目需要引入这些依赖项的时候,只用指定groupId和artifactId即可,不用管version里该写哪个版本。

完成了geekbang-coupon依赖项的编写,接下来我们看看coupon-template-serv依赖项的编写。

2.2.编写coupon-template-serv依赖项

coupon-template-serv是大boss下面的一个小头目,和geekbang-coupon一样,它的packaging类型也是pom。我们说过boss只用管顶层战略,因此coupon-template-serv的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">
    <parent>
        <artifactId>geekbang-coupon</artifactId>
        <groupId>com.geekbang</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-template-serv</artifactId>
    <packaging>pom</packaging>
    <modules>
        <module>coupon-template-api</module>
        <module>coupon-template-dao</module>
        <module>coupon-template-impl</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

我们已经把geekbang-coupon和coupon-template-serv两个父级项目的依赖项添加完毕,接下来就去搭建coupon-template-serv下面的三个子模块。

coupon-template-api模块存放了接口Request和Response的类模板,是另两个子模块需要依赖的公共类库,所以我就先从coupon-template-api开始项目构建。

2.3.搭建coupon-template-api模块

coupon-template-api模块是专门用来存放公共类的仓库,我把REST API接口的服务请求和服务返回对象的POJO类放到了里面。在微服务领域,将外部依赖的POJO类或者API接口层单独打包是一种通用做法,这样就可以给外部依赖方提供一个“干净”(不包含非必要依赖)的接口包,为远程服务调用(RPC)提供支持。

在coupon-template-api项目的pom文件中,我只添加了少量的“工具类”依赖,比如lombok、guava和validation-api包等通用组件,这些工具类用来帮助我们自动生成代码并提供一些便捷的功能特性,具体的依赖项你可以参考项目源码。

首先,我们需要定义一个用来表示优惠券类型的enum对象,在com.geekbang.coupon.template.api.enum包下创建一个名为CouponType的枚举

package com.geekbang.coupon.template.api.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.stream.Stream;

@Getter
@AllArgsConstructor
public enum CouponType {

    UNKNOWN("unknown", "0"),
    MONEY_OFF("满减券", "1"),
    DISCOUNT("打折", "2"),
    RANDOM_DISCOUNT("随机减", "3"),
    LONELY_NIGHT_MONEY_OFF("寂寞午夜double券", "4"),
    ANTI_PUA("PUA加倍奉还券", "5");

    private String description;

    // 存在数据库里的最终code
    private String code;

    public static CouponType convert(String code) {
        return Stream.of(values())
                .filter(couponType -> couponType.code.equalsIgnoreCase(code))
                .findFirst()
                .orElse(UNKNOWN);
    }
}

CouponType类定义了多个不同类型的优惠券,convert方法可以根据优惠券的编码返回对应的枚举对象。这里还有一个“Unknown”类型的券,它专门用来对付故意输错code的恶意请求。

作为一个骨灰级程序员,我会认为所有需要用户输入的信息都是不可靠的,并且需要对各种意外输入做拦截、防范,这就是“防御性编程”的思维。工作的时间越久,人往往会变得越怂(都是被各种故障吓大的)。

接下来,我们创建两个用来定义优惠券模板规则的类,分别是TemplateRule和Discount。我把它们放在com.geekbang.coupon.template.api.beans.rules包路径下。

TemplateRule包含了两个规则,一是领券规则,包括每个用户可领取的数量和券模板的过期时间;二是券模板的计算规则。

package com.geekbang.coupon.template.api.beans.rules;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 优惠券计算规则
 * @author 姚半仙
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TemplateRule {

    /** 可以享受的折扣 */
    private Discount discount;

    // 每个人最多可以领券数量
    private Integer limitation;

    // 过期时间
    private Long deadline;

}

这里我强烈推荐你使用一键三连的lombok注解自动生成基础代码,它们分别是Data、NoArgsConstructor和AllArgsConstructor。其中,Data注解自动生成getter、setter、toString等方法,后两个注解分别生成无参构造器和全参构造器,省时省力省地盘。

TemplateRule中的Discount成员变量定义了使用优惠券的规则,代码如下。

package com.geekbang.coupon.template.api.beans.rules;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Discount {

    // 满减 - 减掉的钱数
    // 折扣 - 90 = 9折,  95=95折
    private Long quota;

    // 最低达到多少消费才能用
    private Long threshold;
}

从上面代码中可以看出,我使用Long来表示“金额”。对于境内电商行业来说,金额往往是以分为单位的,这样我们可以直接使用Long类型参与金额的计算,比如100就代表100分,也就是一块钱。这比使用Double到处转换BigDecimal省了很多事儿。

最后,我们在com.geekbang.coupon.template.api.beans包下创建一个名为CouponTemplateInfo的类,用来创建优惠券模板,代码如下:

package com.geekbang.coupon.template.api.beans;

import com.geekbang.coupon.template.api.beans.rules.TemplateRule;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;

//import javax.validation.constraints.NotNull;

/**
 * 创建优惠券模板
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CouponTemplateInfo {

    private Long id;

    @NotNull
    private String name;

    // 优惠券描述
    @NotNull
    private String desc;

    // 优惠券类型
    @NotNull
    private String type;

    // 适用门店 - 若无则为全店通用券
    private Long shopId;

    /** 优惠券规则 */
    @NotNull
    private TemplateRule rule;

    private Boolean available;

}

在上面的代码中,我们应用了jakarta.validate-api组件的注解@NotNull,对参数是否为Null进行了校验。如果请求参数为空,那么接口会自动返回Bad Request异常。当然,jakarta组件还有很多可以用来做判定验证的注解,合理使用可以节省大量编码工作,提高代码可读性。

此外,你还会发现,CouponTemplateInfo内封装了优惠券模板的基本信息,我们可以把优惠券模板当做一个“模具”,每一张优惠券都经由模具来制造,被制造出来的优惠券则使用CouponInfo对象来封装。

CouponInfo对象包含了优惠券的模板信息、领券用户ID、适用门店ID等属性。除此之外,我还在源码中定义了用来实现分页查找的对象,如果你特别感兴趣,可以到项目源码中查看完整的类定义。

到这里我们就完成了coupon-template-api项目的搭建,下面我们开始搭建Dao层模块:coupon-template-dao。它主要负责和数据库的对接、读取。

2.4.搭建coupon-template-dao模块

首先,我们把必要的依赖项添加到coupon-template-dao项目中,比较关键的maven依赖项有以下几个。

  • coupon-template-api: 引入api包下的公共类;
  • spring-boot-starter-data-jpa: 添加spring-data-jpa的功能进行CRUD操作;
  • mysql-connector-java: 引入mysql驱动包,驱动版本尽量与mysql版本保持一致。

接下来,我们在com.geekbang.coupon.template.dao.entity目录下创建了一个数据库实体对象的Java类:CouponTemplate。

package com.geekbang.coupon.template.dao.entity;

import com.geekbang.coupon.template.api.beans.rules.TemplateRule;
import com.geekbang.coupon.template.api.enums.CouponType;
import com.geekbang.coupon.template.dao.converter.CouponTypeConverter;
import com.geekbang.coupon.template.dao.converter.RuleConverter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

/**
 * 优惠券模板
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
@EntityListeners(AuditingEntityListener.class)
@Table(name = "coupon_template")
public class CouponTemplate implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    // 状态是否可用
    @Column(name = "available", nullable = false)
    private Boolean available;

    @Column(name = "name", nullable = false)
    private String name;

    // 适用门店-如果为空,则为全店满减券
    @Column(name = "shop_id")
    private Long shopId;

    @Column(name = "description", nullable = false)
    private String description;

    // 优惠券类型
    @Column(name = "type", nullable = false)
    @Convert(converter = CouponTypeConverter.class)
    private CouponType category;

    // 创建时间,通过@CreateDate注解自动填值(需要配合@JpaAuditing注解在启动类上生效)
    @CreatedDate
    @Column(name = "created_time", nullable = false)
    private Date createdTime;

    // 优惠券核算规则,平铺成JSON字段
    @Column(name = "rule", nullable = false)
    @Convert(converter = RuleConverter.class)
    private TemplateRule rule;

}

在CouponTemplate上,我们运用了javax.persistence包和Spring JPA包的标准注解,对数据库字段进行了映射,我挑几个关键注解说道一下。

  • Entity:声明了“数据库实体”对象,它是数据库Table在程序中的映射对象;
  • Table:指定了CouponTemplate对应的数据库表的名称;
  • ID/GeneratedValue:ID注解将某个字段定义为唯一主键,GeneratedValue注解指定了主键生成策略;
  • Column:指定了每个类属性和数据库字段的对应关系,该注解还支持非空检测、对update和create语句进行限制等功能;
  • CreatedDate:自动填充当前数据的创建时间;
  • Convert:如果数据库中存放的是code、string、数字等等标记化对象,可以使用Convert注解指定一个继承自AttributeConverter的类,将DB里存的内容转化成一个Java对象。

这里我要补充一点,其实JPA也支持一对多、多对多的级联关系(ManyToOne、OneToOne等注解),但是你发现我并没有在项目中使用,原因是这些注解背后有很多隐患。过深的级联层级所带来的DB层压力可能会在洪峰流量下被急剧放大,而DB恰恰是最不抗压的一环。所以,我们很少在一些一二线大厂的超高并发项目中看到级联配置的身影。

我的经验是尽可能减少级联配置,用单表查询取而代之,如果一个查询需要join好几张表,最好的做法就通过重构业务逻辑来简化DB查询的复杂度。

最后,我们来到定义DAO的地方,借助Spring Data的强大功能,我们只通过接口名称就可以声明一系列的DB层操作。我们先来看一下CouponTemplateDao这个类的代码。

package com.geekbang.coupon.template.dao;

import com.geekbang.coupon.template.dao.entity.CouponTemplate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface CouponTemplateDao
        extends JpaRepository<CouponTemplate, Long> {

    // 根据Shop ID查询出所有券模板
    List<CouponTemplate> findAllByShopId(Long shopId);

    // IN查询 + 分页支持的语法
    Page<CouponTemplate> findAllByIdIn(List<Long> Id, Pageable page);

    // 根据shop ID + 可用状态查询店铺有多少券模板
    Integer countByShopIdAndAvailable(Long shopId, Boolean available);

    // 将优惠券设置为不可用
    @Modifying
    @Query("update CouponTemplate c set c.available = 0 where c.id = :id")
    int makeCouponUnavailable(@Param("id") Long id);
}

看了这段代码,你一定在想这里都是查询数据的场景,那么“增删改”的方法在哪里?

其实,这些方法都在CouponTemplateDao所继承的JpaRepository类中。这个父类就像一个百宝箱,内置了各种各样的数据操作方法。我们可以通过内置的save方法完成对象的创建和更新,也可以使用内置的delete方法删除数据。

此外,它还提供了对“查询场景”的丰富支持,除了通过ID查询以外,我们还可以使用三种不同的方式查询数据。

  • 通过接口名查询:将查询语句写到接口的名称中;
  • 通过Example对象查询:构造一个模板对象,使用findAll方法来查询;
  • 自定义查询:通过Query注解自定义复杂查询语句。

在CouponTemplateDao中,第一个方法findAllByShopId就是通过接口名查询的例子,jpa使用了一种约定大于配置的思想,你只需要把要查询的字段定义在接口的方法名中,在你发起调用时后台就会自动转化成可执行的SQL语句。构造方法名的过程需要遵循<起手式>By<查询字段><连接词>的结构。

  • 起手式:以find开头表示查询,以count开头表示计数;
  • 查询字段:字段名要保持和Entity类中定义的字段名称一致;
  • 连接词:每个字段之间可以用And、Or、Before、After等一些列丰富的连词串成一个查询语句。

以接口名查询的方式虽然很省事儿,但它面对复杂查询却力不从心,一来容易导致接口名称过长,二来维护起来也挺吃力的。所以,对于复杂查询,我们可以使用自定义SQL、或者Example对象查找的方式。

关于自定义SQL,你可以参考CouponTemplateDao中的makeCouponUnavailable方法,我将SQL语句定义在了Query注解中,通过参数绑定的方式从接口入参处获取查询参数,这种方式是最接近SQL编码的CRUD方式。

Example查询的方式也很简单,构造一个CouponTemplate的对象,将你想查询的字段值填入其中,做成一个查询模板,调用Dao层的findAll方法即可,这里留给你自己动手验证。

couponTemplate.setName("查询名称");
templateDao.findAll(Example.of(couponTemplate));

现在,API和Dao层都已经准备就绪,万事俱备只差最后的业务逻辑层了,接下来我们去搭建coupon-template-impl模块。

2.5.搭建coupon-template-impl模块

coupon-template-impl是coupon-template-serv下的一个子模块,也是实现业务逻辑的地方。从依赖管理的角度,它引入了coupon-template-api和coupon-template-dao两个内部依赖项到pom.xml。

当然,我们也需要加入几个外部依赖项,你可以参考项目的pom.xml源代码获取完整的依赖项列表。

首先,我们先来定义Service层的接口类:CouponTemplateService。在这个接口中,我们定义了优惠券创建、查找优惠券和修改优惠券可用状态的方法。

package com.geekbang.coupon.template.service.intf;


import com.geekbang.coupon.template.api.beans.CouponTemplateInfo;
import com.geekbang.coupon.template.api.beans.PagedCouponTemplateInfo;
import com.geekbang.coupon.template.api.beans.TemplateSearchParams;

import java.util.Collection;
import java.util.Map;

public interface CouponTemplateService {

    // 创建优惠券模板
    CouponTemplateInfo createTemplate(CouponTemplateInfo request);

    CouponTemplateInfo cloneTemplate(Long templateId);

    // 模板查询(分页)
    PagedCouponTemplateInfo search(TemplateSearchParams request);

    // 通过模板ID查询优惠券模板
    CouponTemplateInfo loadTemplateInfo(Long id);

    // 让优惠券模板无效
    void deleteTemplate(Long id);

    // 批量查询
    // Map是模板ID,key是模板详情
    Map<Long, CouponTemplateInfo> getTemplateInfoMap(Collection<Long> ids);
}

由于这部分比较简单,就是通过CouponTemplateDao层来实现优惠券模板的增删改查,这里我就不展开介绍实现层代码了,你可以参考源码中的CouponTemplateServiceImpl类。

不过,我建议你不要直接copy源码,先尝试自己实现这几个Service方法,写完之后再和我的源码做比较,看一看有哪些可以改进的地方。

接下来,我们创建CouponTemplateController类对外暴露REST API,可以借助spring-web注解来完成,具体代码如下。

@Slf4j
@RestController
@RequestMapping("/template")
public class CouponTemplateController {


    @Autowired
    private CouponTemplateService couponTemplateService;


    // 创建优惠券
    @PostMapping("/addTemplate")
    public CouponTemplateInfo addTemplate(@Valid @RequestBody CouponTemplateInfo request) {
        log.info("Create coupon template: data={}", request);
        return couponTemplateService.createTemplate(request);
    }
    
    // 克隆券模板
    @PostMapping("/cloneTemplate")
    public CouponTemplateInfo cloneTemplate(@RequestParam("id") Long templateId) {
        log.info("Clone coupon template: data={}", templateId);
        return couponTemplateService.cloneTemplate(templateId);
    }


    // 读取优惠券
    @GetMapping("/getTemplate")
    public CouponTemplateInfo getTemplate(@RequestParam("id") Long id){
        log.info("Load template, id={}", id);
        return couponTemplateService.loadTemplateInfo(id);
    }
    
    // 搜索模板(支持分页查询)
    @PostMapping("/search")
    public PagedCouponTemplateInfo search(@Valid @RequestBody TemplateSearchParams request) {
        log.info("search templates, payload={}", request);
        return couponTemplateService.search(request);
    }
    
    // ... 完整代码请至源码查看
}

在这里,Controller类中的注解来自spring-boot-starter-web依赖项,通过这些注解将服务以RESTful接口的方式对外暴露。现在,我们来了解下上述代码里,服务寻址过程中的三个重要注解:

  • RestController:用来声明一个Controller类,加载到Spring Boot上下文;
  • RequestMapping:指定当前类中所有方法在URL中的访问路径的前缀;
  • Post/Get/PutMapping:定义当前方法的HTTP Method和访问路径。

项目启动类是最后的代码部分,我们在com.geekbang.coupon.template下创建一个Application类作为启动程序的入口,并在这个类的头上安上SpringBoot的启动注解。

@SpringBootApplication
@EnableJpaAuditing
@ComponentScan(basePackages = {"com.geekbang"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

SpringBootApplication注解会自动开启包路径扫描,并启动一系列的自动装配流程(AutoConfig)。在默认情况下,Spring Boot框架会扫描启动类所在package下的所有类,并在上下文中创建受托管的Bean对象,如果我们想加载额外的扫包路径,只用添加ComponentScan注解并指定path即可。

所有代码环节全部完工后,我们还剩最后的画龙点睛之笔:创建配置文件application.yml,它位于src/main/resources文件夹下。Spring Boot支持多种格式的配置文件,这里我们顺应主流,使用yml格式。

# 项目的启动端口
server:
  port: 20000
spring:
  application:
    # 定义项目名称
    name: coupon-template-serv
  datasource:
    # mysql数据源
    username: root
#    password: 这里写上你自己的密码
    url: jdbc:mysql://127.0.0.1:3306/geekbang_coupon_db?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
    # 指定数据源DataSource类型
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 数据库连接池参数配置,比如池子大小、超时时间、是否自动提交等等
    hikari:
      pool-name: GeekbangCouponHikari
      connection-timeout: 5000
      idle-timeout: 30000
      maximum-pool-size: 10
      minimum-idle: 5
      max-lifetime: 60000
      auto-commit: true
  jpa:
    show-sql: true
    hibernate:
      # 在生产环境全部为none,防止ddl结构被自动执行,破坏生产数据
      ddl-auto: none
    # 在日志中打印经过格式化的SQL语句
    properties:
      hibernate.format_sql: true
      hibernate.show_sql: true
    open-in-view: false

在配置文件中,有一个地方需要你多加注意,那就是jdbc连接串(spring.datasource.url)。不同版本的MySQL对连接串中的参数有不同的要求。

如果你发现项目启动过程中抛出了MySQL连接报错,一定记得检查自己的MySQL版本,检查是否缺失了某些参数(比如MySQL 8.x版本下要求传入serverTimezone参数)。如果你本地安装的MySQL版本早于8.x系列,我推荐你重新安装和我一样的MySQL 8.0.27版本,这样就不会碰到兼容性问题了。

好,到这里,我们优惠券平台项目的第一个模块coupon-template-serv就搭建完成了,你可以在本地启动项目并通过Postman发起调用。我已经将Postman API集合上传到了这个Gitee源码库中的“资源文件”目录下,文件名为“Spring Boot阶段.postman_collection.json”,你可以导入到自己本地的Postman中使用。

现在,我们来回顾一下这节课的重点内容。

3.总结

今天我带你搭建了整个优惠券服务的整体项目结构,并且用Spring Boot快速落地了优惠券模板服务。如果你在自己的项目中还在使用繁琐的sql资源文件来操作数据库,不妨升级成coupon-template-dao中使用的spring-data-jpa来简化DB操作。spring-data-jpa的功能特性也折射出Spring框架的发展趋势:约定大于配置,且越来越轻量级。

在学习这节课的时候,我希望你不要只满足于把项目跑起来就万事大吉了,你还要做一些思考和总结沉淀,想一想如何能把课程中的一些技术点应用在自己的项目中。我在这节课分享了很多开发小技巧,比如防御性编程、代码自动生成、金额计算、如何简化数据校验、级联关系的误区等,这些都可以作为你的开发素材。

希望你能够动起手来,顺着这节课程的内容动手搭建整个服务,不要直接照搬源码本地执行一下就完事儿了,只有上手实际搭建项目我们才能了解技术细节、积累排查问题的经验。要知道,纸上得来终觉浅,绝知此事要躬行。

在下一节课中,我会带你搭建coupon-calculation-ser和coupon-customer-serv,构建一个完整的优惠券平台Spring Boot项目。