1、SpringBoot项目的pom.xml文件中标签说明:

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

2、微服务项目中的公共模块

  • 在微服务项目中,通常会将子模块中都用到的实体类提取出来,形成一个公共模块(api模块),在其他子模块需要使用公共模块中的实体类时,只需要通过<module> 标签导入公共模块即可。

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

  • 在 公共模块的 pom 文件中,只需要添加了少量的“工具类”依赖,比如 lombok、guava 和 validation-api 包等通用组件,这些工具类用来帮助我们自动生成代码并提供一些便捷的功能特性。

3、防御性编程

定义一个用来表示优惠券类型的 enum 对象, CouponType。

@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 的恶意请求。
  • 在实际的开发工作中,必须要认为,所有需要用户输入的信息都是不可靠的,并且需要对各种意外输入做拦截、防范,这就是“防御性编程”的思维。

4、lombok注解、表示金额的数据类型

创建两个用来定义优惠券模板规则的类,分别是 TemplateRule 和 Discount。

  • TemplateRule 包含了两个规则,一是领券规则,包括每个用户可领取的数量和券模板的过期时间(TemplateRule );二是券模板的计算规则(Discount )。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

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

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

    // 过期时间
    private Long deadline;

}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Discount {

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

    // 最低达到多少消费才能用
    private Long threshold;
}
  • 注意:应该Long 来表示“金额”,对于境内电商行业来说,金额往往是以分为单位的,这样我们可以直接使用 Long 类型参与金额的计算,比如 100 就代表 100 分,也就是一块钱。这比使用 Double 到处转换 BigDecimal 省了很多事儿。

5、SpringDataJPA的常用注解

@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 查询的复杂度。

6、接口名查询操作的命名规则

  • 定义 DAO 的地方,可以借助 Spring Data 的强大功能,我们只通过接口名称就可以声明一系列的 DB 层操作。
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 方法删除数据

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

  • 方法名的过程需要遵循 < 起手式 >By< 查询字段 >< 连接词 > 的结构。

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

  • 以接口名查询的方式在面对复杂查询时,容易导致接口名称过长,以及维护起来也挺吃力的。所以,对于复杂查询,我们可以使用自定义 SQL、或者 Example 对象查找的方式。

7、JPA的复杂查询

  • 关于自定义 SQL,你可以参考上面CouponTemplateDao 中的 makeCouponUnavailable 方法,我将 **SQL 语句定义在了 Query 注解中,**通过参数绑定的方式从接口入参处获取查询参数,这种方式是最接近 SQL 编码的 CRUD 方式。
  • Example 查询的方式也很简单,构造一个 CouponTemplate 的对象,将你想查询的字段值填入其中,做成一个查询模板,调用 Dao 层的 findAll 方法即可,这里留给你自己动手验证。
couponTemplate.setName("查询名称");
templateDao.findAll(Example.of(couponTemplate));

8、全局配置文件和启动类

  • application.yml
# 项目的启动端口
server:
  port: 20000
  error:
    include-message: always

spring:
  application:
    # 定义项目名称
    name: coupon-template-serv
  datasource:
    # mysql数据源
    username: root
    password: 123456
    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
    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
    properties:
      hibernate.format_sql: true
      hibernate.show_sql: true
    open-in-view: false

logging:
  level:
    com.broadview.coupon: debug
  • 启动类
@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 即可。
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐