极客时间SpringCloud微服务项目实战课程的学习总结1
本文是我学习 极客时间SpringCloud微服务项目实战课程的学习总结。
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 即可。
更多推荐
所有评论(0)