你好,我是姚秋辰。

上一节课我们搭建了coupon-template-serv模块,实现了优惠券模板的创建和批量查询等功能,相信你已经对如何使用Spring Boot搭建应用驾轻就熟了。今天我们就来搭建优惠券平台项目的另外两个模块,coupon-calculation-serv(优惠计算服务)和coupon-customer-serv(用户服务),组建一个完整的实战项目应用(middleware模块将在Spring Cloud环节进行搭建)。

通过今天的课程,你可以巩固并加深Spring Boot的实操能力,为接下来Spring Cloud微服务化改造打好前置知识的基础,在这节课里我也会分享一些关于设计模式和数据冗余的经验之谈。

另外,这节课的源码都可以在Gitee代码库中找到。你可不要只读爽文不动手敲代码,我建议你把代码下载到本地,对照着源码动手练习一遍,才能学为己用。

闲话少叙,我们根据优惠券项目的依赖关系,先从上游服务coupon-calculation-serv开始动手搭建吧。

搭建coupon-calculation-serv

coupon-calculation-serv提供了用于计算订单的优惠信息的接口,它是一个典型的“计算密集型”服务。所谓计算密集型服务一般具备下面的两个特征:

  1. 不吃网络IO和磁盘空间
  2. 运行期主要占用CPU、内存等计算资源

在做大型应用架构的时候,我们通常会把计算密集型服务与IO/存储密集型服务分割开来,这样做的一个主要原因是提高资源利用率。

比如说,我们有一个计算密集型的微服务A和一个IO密集型微服务B,大促峰值流量到来的时候,如果微服务A面临的压力比较大,我可以专门调配高性能CPU和内存等“计算类”的资源去定向扩容A集群;如果微服务B压力吃紧了,我可以定向调拨云上的存储资源分配给B集群,这样就实现了一种“按需分配”。

假如微服务A和微服务B合二为一变成了一个服务,那么在分配资源的时候就无法做到定向调拨,全链路压测环节也难以精准定位各项性能指标,这难免出现资源浪费的情况。这也是为什么,我要把优惠计算这个服务单独拿出来的原因。

现在,我们开始着手搭建coupon-calculation-serv下的子模块。和coupon-template-serv结构类似,coupon-calculation-serv下面也分了若干个子模块,包括API层和业务逻辑层。API层定义了公共的POJO类,业务逻辑层主要实现优惠价格计算业务。因为calculation服务并不需要访问数据库,所以没有DAO模块。

根据子模块间的依赖关系,我们就先从coupon-calculation-api这个接口层子模块开始搭建吧。

搭建coupon-calculation-api

如果coupon-calculation-serv需要计算订单的优惠价格,那就得知道当前订单用了什么优惠券。封装了优惠券信息的Java类CouponInfo位于coupon-template-api包下,因此我们需要把coupon-template-api的依赖项加入到coupon-calculation-api中。

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>coupon-template-api</artifactId>
    <version>${project.version}</version>
</dependency>

添加好了依赖项之后,接下来我们定义用于封装订单信息的ShoppingCart类。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ShoppingCart {
    // 订单的商品列表 - 
    @NotEmpty
    private List<Product> products;
     
    // 封装了优惠券信息,目前计算服务只支持单张优惠券
    // 为了考虑到以后多券的扩展性,所以定义成了List
    private Long couponId;   
    private List<CouponInfo> couponInfos;

    // 订单的最终价格
    private long cost;
    // 用户ID
    @NotNull
    private Long userId;
}

在上面的源码中,我们看到ShoppingCart订单类中使用了Product对象,来封装当前订单的商品列表。在Product类中包含了商品的单价、商品数量,以及当前商品的门店ID。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    // 商品的价格
    private long price;
    // 商品在购物车里的数量
    private Integer count;
    // 商品销售的门店
    private Long shopId;
}

在电商领域中,商品的数量通常不能以Integer整数来表示,这是因为只有标品才能以整数计件。对于像蔬菜、肉类等非标品来说,它们的计件单位并不是“个”。所以在实际项目中,尤其是零售行业的业务系统里,计件单位要允许小数位的存在。而我们的实战项目为了简化业务,就假定所有商品都是“标品”了。

在下单的时候,你可能有多张优惠券可供选择,你需要通过“价格试算”来模拟计算每张优惠券可以扣减的金额,进而选择最优惠的券来使用。SimulationOrder和SimulationResponse分别代表了“价格试算”的订单类,以及返回的计算结果Response。我们来看一下这两个类的源码。

// 优惠券价格试算
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SimulationOrder {

    @NotEmpty
    private List<Product> products;

    @NotEmpty
    private List<Long> couponIDs;

    private List<CouponInfo> couponInfos;

    @NotNull
    private Long userId;
}

// 订单试算结果,可以看出哪个优惠券的优惠力度最大
@Data
@NoArgsConstructor
public class SimulationResponse {
    // 最省钱的coupon
    private Long bestCouponId;
    // 每一个coupon对应的order价格
    private Map<Long, Long> couponToOrderPrice = Maps.newHashMap();
}

到这里,coupon-calculation-api模块就搭建好了。因为calculation服务不需要访问数据库,所以我们就不用搭建dao模块了,直接来实现coupon-calculation-impl业务层的代码逻辑。

搭建coupon-calculation-impl

首先,我们在coupon-calculation-impl的pom.xml文件中添加下面的三个依赖项。

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>coupon-template-api</artifactId>
    <version>${project.version}</version>
</dependency>

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>coupon-calculation-api</artifactId>
    <version>${project.version}</version>
</dependency>

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

从coupon-template-api和coupon-calculation-api两个依赖项中,你可以拿到订单优惠计算过程用到的POJO对象。接下来,我们可以动手实现优惠计算逻辑了。

在搭建优惠计算业务逻辑的过程中,我运用了模板设计模式来封装计算逻辑。模板模式是一种基于抽象类的设计模式,它的思想很简单,就是将共性的算法骨架部分上升到抽象层,将个性部分延迟到子类中去实现

优惠券类型有很多种,比如满减券、打折券、随机立减等等,这些券的计算流程(共性部分)是相同的,但具体的计算规则(个性部分)是不同的。我将共性的部分抽象成了AbstractRuleTemplate抽象类,将各个券的差异性计算方式做成了抽象类的子类。

让我们看一下计算逻辑的类结构图。

在这张图里,顶层接口RuleTemplate定义了calculate方法,抽象模板类AbstractRuleTemplate将通用的模板计算逻辑在calculate方法中实现,同时它还定义了一个抽象方法calculateNewPrice作为子类的扩展点。各个具体的优惠计算类通过继承AbstractRuleTemplate,并实现calculateNewPrice来编写自己的优惠计算方式。

我们先来看一下AbstractRuleTemplate抽象类的代码,走读calculate模板方法中的计算逻辑实现。

public ShoppingCart calculate(ShoppingCart order) {
    // 获取订单总价
    Long orderTotalAmount = getTotalPrice(order.getProducts());
    // 获取以shopId为维度的总价统计
    Map<Long, Long> sumAmount = getTotalPriceGroupByShop(order.getProducts());
    CouponTemplateInfo template = order.getCouponInfos().get(0).getTemplate();
    // 最低消费限制
    Long threshold = template.getRule().getDiscount().getThreshold();
    // 优惠金额或者打折比例
    Long quota = template.getRule().getDiscount().getQuota();
    // 如果优惠券未指定shopId,则shopTotalAmount=orderTotalAmount
    // 如果指定了shopId,则shopTotalAmount=对应门店下商品总价
    Long shopId = template.getShopId();
    Long shopTotalAmount = (shopId == null) ? orderTotalAmount : sumAmount.get(shopId);
    
    // 如果不符合优惠券使用标准, 则直接按原价走,不使用优惠券
    if (shopTotalAmount == null || shopTotalAmount < threshold) {
        log.debug("Totals of amount not meet");
        order.setCost(orderTotalAmount);
        order.setCouponInfos(Collections.emptyList());
        return order;
    }
    // 子类中实现calculateNewPrice计算新的价格
    Long newCost = calculateNewPrice(orderTotalAmount, shopTotalAmount, quota);
    if (newCost < minCost()) {
        newCost = minCost();
    }
    order.setCost(newCost);
    log.debug("original price={}, new price={}", orderTotalAmount, newCost);
    return order;
}

在上面的源码中,我们看到大部分计算逻辑都在抽象类中做了实现,子类只要实现calculateNewPrice方法完成属于自己的订单价格计算就好。我们以满减规则类为例来看一下它的实现。

@Slf4j
@Component
public class MoneyOffTemplate extends AbstractRuleTemplate implements RuleTemplate {
    @Override
    protected Long calculateNewPrice(Long totalAmount, Long shopAmount, Long quota) {
        // benefitAmount是扣减的价格
        // 如果当前门店的商品总价<quota,那么最多只能扣减shopAmount的钱数
        Long benefitAmount = shopAmount < quota ? shopAmount : quota;
        return totalAmount - benefitAmount;
    }    
}

在上面的源码中,我们看到子类业务的逻辑非常简单清爽。通过模板设计模式,我在抽象类中封装了共性逻辑,在子类中扩展了可变逻辑,每个子类只用关注自己的特定实现即可,使得代码逻辑变得更加清晰,大大降低了代码冗余。

随着业务发展,你的优惠券模板类型可能会进一步增加,比如赠品券、随机立减券等等,如果当前的抽象类无法满足新的需求,你可以通过建立多级抽象类的方式进一步增加抽象层次,不断将共性不变的部分抽取为抽象层。

创建完优惠计算逻辑,我们接下来看一下Service层的代码实现逻辑。Service层的calculateOrderPrice代码非常简单,通过CouponTemplateFactory工厂类获取到具体的计算规则,然后调用calculate计算订单价格就好了。simulate方法实现了订单价格试算,帮助用户在下单之前了解每个优惠券可以扣减的金额,从而选出最省钱的那个券。

@Slf4j
@Service
public class CouponCalculationServiceImpl implements CouponCalculationService {
    
    // 优惠券结算
    // 这里通过Factory类决定使用哪个底层Rule,底层规则对上层透明
    @Override
    public ShoppingCart calculateOrderPrice(@RequestBody ShoppingCart cart) {
        log.info("calculate order price: {}", JSON.toJSONString(cart));
        RuleTemplate ruleTemplate = couponTemplateFactory.getTemplate(cart);
        return ruleTemplate.calculate(cart);
    }
    
    // 试计算每个优惠券在使用后订单的价格
    // 页面上给用户提示最省钱的优惠券
    @Override
    public SimulationResponse simulate(@RequestBody SimulationOrder order) {
        SimulationResponse response = new SimulationResponse();
        Long minOrderPrice = Long.MIN_VALUE;
        // 计算每一个优惠券的订单价格
        for (CouponInfo coupon : order.getCouponInfos()) {
            ShoppingCart cart = new ShoppingCart();
            cart.setProducts(order.getProducts());
            cart.setCouponInfos(Lists.newArrayList(coupon));
            cart = couponProcessorFactory.getTemplate(cart).calculate(cart);
            Long couponId = coupon.getId();
            Long orderPrice = cart.getCost();
            // 设置当前优惠券对应的订单价格
            response.getCouponToOrderPrice().put(couponId, orderPrice);
            // 比较订单价格,设置当前最优优惠券的ID
            if (minOrderPrice > orderPrice) {
                response.setBestCouponId(coupon.getId());
                minOrderPrice = orderPrice;
            }
        }
        return response;
    }
    // 其它方法未列出,请至源码仓库查看完整代码 
}

在上面的源码中,我们看到,优惠券结算方法不用关心订单上使用的优惠券是满减券还是打折券,因为工厂方法会将子类转为顶层接口RuleTemplate返回。在写代码的过程中,我们也要有这样一种意识,就是尽可能对上层业务屏蔽其底层业务复杂度,底层具体业务逻辑的修改对上层是无感知的,这其实也是开闭原则的思想。

完成Service层后,我们接下来新建一个CouponCalculationController类,对外暴露2个POST接口,第一个接口完成订单优惠价格计算,第二个接口完成优惠券价格试算。

@Slf4j
@RestController
@RequestMapping("calculator")
public class CouponCalculationController {
    @Autowired
    private CouponCalculationService couponCalculationService;
    
    // 优惠券结算
    @PostMapping("/checkout")
    @ResponseBody
    public ShoppingCart calculateOrderPrice(@RequestBody ShoppingCart settlement) {
        log.info("do calculation: {}", JSON.toJSONString(settlement));
        return couponCalculationService.calculateOrderPrice(settlement);
    }
    
    // 优惠券列表挨个试算
    // 给客户提示每个可用券的优惠额度,帮助挑选
    @PostMapping("/simulate")
    @ResponseBody
    public SimulationResponse simulate(@RequestBody SimulationOrder order) {
        log.info("do simulation: {}", JSON.toJSONString(order));
        return couponCalculationService.simulateOrder(order);
    }
    
    // 其它方法未列出,请至源码仓库查看完整代码 
}

好了,现在你已经完成了所有业务逻辑的源码。最后一步画龙点睛,你还需要为coupon-calculation-impl应用创建一个Application启动类并添加application.yml配置项。因为它并不需要访问数据库,所以你不需要在配置文件或者启动类注解上添加spring-data的相关内容。

到这里,我们就完成了优惠计算服务的搭建工作,你可以到我的代码仓库中查看完整的coupon-calculation-serv源码实现。

下面,我们去搭建优惠券项目的最后一个服务:coupon-customer-serv。

搭建coupon-customer-serv

coupon-customer-serv是一个服务于用户的子模块,它的结构和coupon-template-serv一样,包含了API层、DAO层和业务逻辑层。它实现了用户领券、用户优惠券查找和订单结算功能。

为了简化业务逻辑,我在源码里省略了“用户注册”等业务功能,使用userId来表示一个已注册的用户。

按照惯例,我们先从API层开始搭建,搭建coupon-customer-api的过程非常简单。

搭建coupon-customer-api

首先,我们需要把coupon-template-api和coupon-calculation-api这两个服务的依赖项添加到coupon-customer-api的pom依赖中,这样一来customer服务就可以引用到这两个服务的Request和Response对象了。

接下来,我们在API子模块中创建一个RequestCoupon类,作为用户领取优惠券的请求参数,通过传入用户ID和优惠券模板ID,用户可以领取一张由指定模板打造的优惠券。另一个类是SearchCoupon,用来封装优惠券查询的请求参数。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class RequestCoupon {
    // 用户领券
    @NotNull
    private Long userId;

    // 券模板ID
    @NotNull
    private Long couponTemplateId;

}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchCoupon {
    @NotNull
    private Long userId;
    private Long shopId;
    private Integer couponStatus;
}

到这里,coupon-customer-api就搭建完了。接下里我们去搭建coupon-customer-dao层,从数据层实现用户优惠券的增删改查。

搭建coupon-customer-dao

我在DAO子模块中创建了一个Coupon数据库实体对象用于保存用户领到的优惠券,并按照spring-data-jpa规范创建了一个CouponDAO接口用来提供CRUD操作。

我们先来看一下Coupon实体对象的内容。

// 使用了lomkob注解自动生成建造者代码和getter、setter
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "coupon")
public class Coupon {

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

    // 对应的模板ID - 不使用one to one映射
    @Column(name = "template_id", nullable = false)
    private Long templateId;

    // 拥有这张优惠券的用户的ID
    @Column(name = "user_id", nullable = false)
    private Long userId;

    // 冗余一个shop id方便查找
    @Column(name = "shop_id")
    private Long shopId;

    // 自动生成时间戳
    @CreatedDate
    @Column(name = "created_time", nullable = false)
    private Date createdTime;

    // CouponStatusConverter实现了AttributeConverter接口
    // 将数据库value转化为CouponStatus类
    @Column(name = "status", nullable = false)
    @Convert(converter = CouponStatusConverter.class)
    private CouponStatus status;

    @Transient
    private CouponTemplateInfo templateInfo;
}

在上面的源码中,我在class级别使用了Lombok注解自动生成代码,如果你对Lomkob比较感兴趣,可以从Lomkob官网上获取更多的使用方法。

从这段代码引申一下,我想和你分享一个关于“数据冗余”的小知识点。我们看到Coupon实体对象中冗余保存了一个Shop ID,之所以说它是冗余字段,是因为Shop ID可以从CouponTemplate表中获取,顺着Coupon对象的templateID字段可以关联到CouponTemplate表,进而获取到ShopID对象。

那我们为什么需要在Coupon表中再保存一次shop ID呢?如果严格遵循数据库的范式,那确实不应该保存一个冗余的shop ID字段,但我们也不要忘了,所谓范式和规则就是留给后人打破的。

数据库的标准范式是上一个时代的产物,以那个时代的眼光来看,“存储”是一项很宝贵的资源,在做程序设计的时候应该尽可能节省磁盘空间、内存空间,反倒“性能”和“高并发”并不是需要担心的事情。

当我们用现在的眼光来审视程序设计,你会发现“存储资源”已经不再是制约生产力的瓶颈,为了应对高并发的场景,你必须尽可能提高系统的吞吐量和性能

因此,你经常可以看到一二线大厂的高并发系统大量使用了“数据冗余”和“数据异构”方案。这是一个“以空间换时间”的路子,通过将一份数据冗余或异构到多处,提升业务的查询和处理效率。

了解了数据冗余的扩展知识后,我们来看下DAO层的接口类的内容:

public interface CouponDao extends JpaRepository<Coupon, Long> {
    // 根据用户ID和Template ID,统计用户从当前优惠券模板中领了多少张券
    long countByUserIdAndTemplateId(Long userId, Long templateId);
}

在上面的源码中,我们只创建了一个接口用于count计算,至于其他增删改查功能则统一由父类JpaRepository一手包办了。spring-data-jpa沿袭了spring框架的简约风,大道至简解放双手,整个Spring框架从诞生至今,也一直都在朝着不断简化的方向发展。

到这里,coupon-customer-dao层的代码就写完了,接下来我们去搞定最后一个子模块coupon-customer-impl业务逻辑层。

搭建coupon-customer-impl

既然coupon-customer-impl需要调用template和calculation两个服务,在没有进入微服务化改造之前,我们只能先暂时委屈一下template和calculation,将它俩作为customer服务的一部分,做成一个三合一的单体应用。等你学到微服务课程的时候,这个单体应用会被拆分成独立的微服务模块。

首先,你需要将template、calculation的依赖项添加到coupon-customer-impl的配置文件中,注意这里我们添加的可不是API接口层的依赖,而是Impl接口实现层的依赖。

    <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>coupon-customer-dao</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>coupon-calculation-impl</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>coupon-template-impl</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

添加完依赖项之后,我们就可以去动手实现业务逻辑层了。

CouponCustomerService是业务逻辑层的接口抽象,我添加了几个方法,用来实现用户领券、查询优惠券、下单核销优惠券、优惠券试算等功能。

// 用户对接服务
public interface CouponCustomerService {
    // 领券接口
    Coupon requestCoupon(RequestCoupon request);
    // 核销优惠券
    ShoppingCart placeOrder( info);
    // 优惠券金额试算
    SimulationResponse simulateOrderPrice(SimulationOrder order);
    // 用户删除优惠券
    void deleteCoupon(Long userId, Long couponId);
    // 查询用户优惠券
    List<CouponInfo> findCoupon(SearchCoupon request);
    // xxx其它方法请参考源码
}

这里,我以placeOrder方法为例,带你走读一下它的源码。如果你对其它方法的源码感兴趣,可以到Gitee源码库中找到Spring Boot急速落地篇的CouponCustomerServiceImpl类,查看源代码。

placeOrder方法实现了用户下单+优惠券核销的功能,我们来看一下它的实现逻辑。

@Override
@Transactional
public ShppingCart placeOrder(ShppingCart order) {
    // 购物车为空,丢出异常
    if (CollectionUtils.isEmpty(order.getProducts())) {
        log.error("invalid check out request, order={}", order);
        throw new IllegalArgumentException("cart is empty");
    }

    Coupon coupon = null;
    if (order.getCouponId() != null) {
        // 如果有优惠券就把它查出来,看是不是属于当前用户并且可用
        Coupon example = Coupon.builder().userId(order.getUserId())
                .id(order.getCouponId())
                .status(CouponStatus.AVAILABLE)
                .build();
        coupon = couponDao.findAll(Example.of(example)).stream()
                .findFirst()
                // 如果当前用户查不到可用优惠券,就抛出错误
                .orElseThrow(() -> new RuntimeException("Coupon not found"));        
        // 优惠券有了,再把它的券模板信息查出
        // 券模板里的Discount规则会在稍后用于订单价格计算
        CouponInfo couponInfo = CouponConverter.convertToCoupon(coupon);
        couponInfo.setTemplate(templateService.loadTemplateInfo(coupon.getTemplateId()));
        order.setCouponInfos(Lists.newArrayList(couponInfo));
    }

    // 调用calculation服务使用优惠后的订单价格
    ShppingCart checkoutInfo = calculationService.calculateOrderPrice(order);

    if (coupon != null) {
        // 如果优惠券没有被结算掉,而用户传递了优惠券,报错提示该订单满足不了优惠条件
        if (CollectionUtils.isEmpty(checkoutInfo.getCouponInfos())) {
            log.error("cannot apply coupon to order, couponId={}", coupon.getId());
            throw new IllegalArgumentException("coupon is not applicable to this order");
        }
        log.info("update coupon status to used, couponId={}", coupon.getId());
        coupon.setStatus(CouponStatus.USED);
        couponDao.save(coupon);
    }
    return checkoutInfo;
}

在上面的源码中,我们看到Coupon对象的构造使用了Builder链式编程的风格,这是得益于在Coupon类上面声明的Lombok的Builder注解,只用一个Builder注解就能享受链式构造的体验。

搞定了业务逻辑层后,接下来轮到Controller部分了,我在CouponCustomerController中对外暴露了几个服务,这些服务调用CouponCustomerServiceImpl中的方法实现各自的业务逻辑。

@Slf4j
@RestController
@RequestMapping("coupon-customer")
public class CouponCustomerController {
    @Autowired
    private CouponCustomerService customerService;
  
    // ....省略部分方法,完整方法列表请参考源码    

    // 用户模拟计算每个优惠券的优惠价格
    @PostMapping("simulateOrder")
    public SimulationResponse simulate(@Valid @RequestBody SimulationOrder order) {
        return customerService.simulateOrderPrice(order);
    }
    
    // 用户删除优惠券 - 非物理删除
    @DeleteMapping("deleteCoupon")
    public void deleteCoupon(@RequestParam("userId") Long userId,
                         @RequestParam("couponId") Long couponId) {
        customerService.deleteCoupon(userId, couponId);
    }
    
    // 下单核销优惠券
    @PostMapping("checkout")
    public ShppingCart checkout(@Valid @RequestBody ShppingCart info) {
        return customerService.placeOrder(info);
    }
}

以上,就是所有的业务逻辑代码部分了。接下来你只需要完成启动类和配置文件,就可以启动项目做测试了。我先来带你看一下启动类的部分:

@SpringBootApplication
@EnableJpaAuditing
@ComponentScan(basePackages = {"com.geekbang"})
@EnableTransactionManagement
//用于扫描Dao @Repository
@EnableJpaRepositories(basePackages = {"com.geekbang"})
//用于扫描JPA实体类 @Entity,默认扫本包当下路径
@EntityScan(basePackages = {"com.geekbang"})
public class Application {

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

在上面的源码中,我们看到很多注解上都注明了com.geekbang作为包路径。之所以这么做,是因为Spring Boot的潜规则是将当前启动类类所在package作为扫包路径。

如果你的Application在com.geekbang.customer下,而你在项目中又需要加载来自com.geekbang.template下的类资源,就必须额外声明扫包路径,否则只有在com.geekbang.customer和其子路径之下的资源才会被加载。

关于配置项的部分,你可以直接把coupon-template-impl的配置文件application.yml照搬过来,不过,要记得把里面配置的spring.application.name改成coupon-customer-serv

好,到这里,我们优惠券平台项目的Spring Boot版本就搭建完成了。现在,coupon-customer-serv已经成了一个三合一的单体应用,你只要在本地启动这一个应用,就可以调用customer、template和calculation三个服务的功能。

总结

现在,我们来回顾一下这两节Spring Boot实战课的重点内容。通过这两节课,我带你搭建了完整的Spring Boot版优惠券平台的三个子模块。为了让项目结构更加清晰,我用分层设计的思想将每个模块拆分成API层、DAO层和业务层。在搭建过程中,我们使用spring-data-jpa搞定了数据层,短短几行代码就能实现复杂的CRUD操作;使用spring-web搭建了Controller层,对外暴露了RESTFul风格的接口。

我们学习技术也分为外功修为和内功修行,讲究的是内外兼修。技术框架总会不断推陈出新,学会怎么使用一门技术,这修习的是外功。你掌握了一个功能强大的新框架,外功招式自然凌厉几分。但是能决定你武力值的上限有多高,还要靠你在工作学习中不断提高内功修为。

外功见效快而内功需要长期磨炼,就像我这节课分享的设计模式一样,设计模式就是典型的内功心法,学会一两种设计模式不会让你的技术水平产生突飞猛进的提高,但是当你逐渐融会贯通把各种设计模式活学活用到代码中,境界层次就变得不一样了。

从下一节课开始,我们将进入Spring Cloud基础篇的学习,通过基础篇的学习,你将熟练使用Nacos、Loadbalancer和OpenFeign组件来搭建基于微服务架构的跨服务调用。

思考题

如果我们分别把coupon-customer-serv、coupon-template-serv和coupon-calculation-serv分别部署在集群A、B和C上,你能想到几种方式,使得这几个应用可以在集群环境中互相发起调用呢?

我给你一个小提示,在思考这个问题的时候,你要想到一点,服务有可能会发生上下线而且集群也可能会扩容,要尽可能让调用请求发到正常工作的机器上,提高请求成功率。欢迎你在留言区分享你的想法和收获,我在留言区等你。

好啦,这节课就结束啦。也欢迎你把这节课分享给更多对Spring Cloud感兴趣的朋友。我是姚秋辰,我们下节课再见!

评论