全站课-阶段二(Java SpringBoot API打造高级电商业务后端)

项目目录

missyou
├─github
│  └─wxpay
│      └─sdk	# 微信sdk   
└─lin
    └─missyou
        │  MissyouApplication.java	# 启动类
        ├─api
        │  ├─v1     #  小程序v1版本api,存放Controller
        │  └─v2	
        ├─bo   # 代表BusinessObject业务对象,通常用于ServiceController传递数据 
        ├─core
        │  ├─config	# 配置文件   
        │  ├─enumeration	# 枚举
        │  ├─interceptors	# 拦截器
        │  └─money	# 金额计算      
        ├─dto # 接收前端传过来的Object
        │  └─validators	# 校验注解          
        ├─exception	# 异常类       
        ├─hack    
        ├─lib    
        ├─logic	# 逻辑层   
        ├─manager	# 对接一些通用的第三方服务
        ├─model  # 数据表映射模型
        ├─repository # 数据库操作层
        ├─service	# 服务层     
        ├─util   # 工具类
        └─vo	# 返回给前端的数据模型ViewObject
                
                


SpringBoot

OCP -> IOC

IOC实现:容器 加入容器 注入
灵活性 场景

目的:
抽象意义:控制权交给用户
灵活的OCP

把对象注入到容器

XML

注解

模式注解(stereotype annotations)

@Component 组件/类/bean 注入到IOC容器 类的实例化 new
用的最多也是最为基础的模式注解,springboot中在一个类上打上@Component就会被springboot扫描之后加入到容器中,然后在我们需要的时候就可以将它注入到代码里了。

@Controller
@Service
@Repository
这3个注解目前在功能上和@Component没有任何区别,通常用来标明类的不同的作用。分别代表控制器,服务层,模型层。从基本功能上来讲都是@Component所赋予他们的功能

@Configuration
在使用方式上和以上4个有所不同,是一个更灵活的,可以把一组bean加入到容器的方式

桥节点 IOC

IOC 对象 实例化 注入 时机

默认立即实例化
我们可以更改成延迟实例化@Lazy

几种注入方式

字段注入 / 成员变量注入
setter注入
构造注入

一个接口多个实现类的处理

@Autowired被动注入方式

bytype
byname

bytype默认的注入方式
寻找实现ISkill的Bean 报错
1.找不到任何一个bean
2.找到一个 直接注入
3.找到多个 并不一定会报错 按照字段名字推断选择哪个bean

@Autowired主动注入方式

@Qualifier(" ")指定bean的名字

面向对象中变化的应对方案

1.制定一个interface,然后用多个类实现同一个interface——策略模式
2.一个类,通过更改类的属性 解决变化

MySQL Class

@Configuration

@Configuration里面也包含@Component,也可以实现以上几个注解相同的效果,但是在使用方法与以上几个注解不太一样。

public class Camille implements ISkill {

    private String name;
    private Integer age;

    public Camille(String name,Integer age) {
        this.name = name;
        this.age = age;
    }

    public Camille() {
        System.out.println("Hello Camille");
    }

    public void r(){
        System.out.println("Camille R");
    }
}
-----------------------------------------------------------------------
@Configuration
public class HeroConfiguration {
    @Bean
    public ISkill camille(){
        return new Camille();
    }
}

使用配置类的方式和在类上加@Component可以起到相同的效果

使用@Configuration的意义

@Component实际应用中使用的比较多,但是单纯的使用@Component对于我们处理变化意义不是很大,还需要和其它的注解进一步去搭配组合才能真正解决变化的问题。
对于面向对象编程来说,一个类除了行为之外还有特征。类中的方法体现了类的行为,类中的字段体现了类的特征。而单纯的@Component不能给成员变量赋值。
@Configuration和@Bean的组合就可以实现。除此之外,一个@Configuration配置类下面可以有多个@Bean

@Configuration
public class HeroConfiguration {
    @Bean
    public ISkill camille(){
        return new Camille("Camille",18);
    }

    @Bean
    public ISkill irelia(){
        return new Irelia();
    }
}

@Configuration相当于以前xml配置文件中的< beans > ,@Bean就相当于原来的< bean > 。上面的代码就相当于:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<bean id="camille" class="com.lin.sample.hero.camille">
		<property name="name" value="Camille"></property>
		<property name="age" value="18"></property>
	</bean>
</beans>

为什么Spring偏爱配置

OCP 变化 隔离/反映 配置文件

为什么隔离到配置文件

1.配置文件集中性
2.清晰(没有业务逻辑)


第10周· 3-5 @Configuation和@Bean的真实作用

配置分类

1.常规配置 key:value
2.xml 配置 类/对象

Configuration 的意义

public class MySQL implements IConnected {

    private String ip = "localhost";
    private Integer port = 3306;
    
    public MySQL() {}

    public MySQL(String ip, Integer port) {
        this.ip = ip;
        this.port = port;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    @Override
    public void connect(){
        System.out.println(this.ip+":"+this.port);
    }
}

我们可能会觉得这种写法比较麻烦,是否可以把配置文件中的这两个值直接绑定到MySQL的两个属性,而不是像现在这样在配置类中读取?可以是可以的。但是这样DatabaseConfiguration的意义就不大了。配置类实质上起到了两个作用:读取配置文件和把Bean加入到IOC容器。如果我们直接把配置文件的值绑定到MySQL,然后在MySQL上加@Component,这样看似配置类就没有什么用了。我们还是要回到之前提到的两个变化,一个变化是通过更改类的属性,那直接绑定配置文件是可以解决第二种变化的,但是在某些情况下既存在第二种变化又存在第一种变化。上面的例子更改ip和port实质上是属于第二种变化。那第一种变化呢?比如以后不想用MySQL想要换成Oracle。通过配置类我们可以有选择的注入某一个Bean,比如条件注解

1.使用配置类的形式更加灵活,可以通过一些处理有选择的把一些什么样的类注入到容器里。
2.通过一个配置类可以把所需要的一组Bean批量导入到容器中,方便统一管理某一方向的所有配置类
这也就是为什么在有了@Component的基础上还需要有配置类,Springboot其中很多的内置类都是采用的这种形式。比如org.springframework.boot:spring-boot-autoconfigure下面的mongo -> MongoProperties读取配置文件

@ConfigurationProperties(prefix = "spring.data.mongodb")
public class MongoProperties {

MongoAutoConfiguration配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class MongoAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(type = { "com.mongodb.MongoClient", "com.mongodb.client.MongoClient" })
	public MongoClient mongo(MongoProperties properties, ObjectProvider<MongoClientOptions> options,Environment environment) {
		return new MongoClientFactory(properties, environment).createMongoClient(options.getIfAvailable());
	}
}

@Configuration是一种编程模式

我们在学习其他框架时都能看到清晰的调用关系,但是在学习Spring的时候却发现很多时候调用关系并不是非常明确。为什么很多时候我们找不到明确的调用关系?是因为很多东西都是从容器中给我们注入进来的。
这里的@Configuration@Bean只是自动配置最基本的原理,真实情况下还会组合其他注解去应对更加复杂的场景。
大家也不要误认为在一个类上面打上了一个@Configuration这个类下面就必须要有@Bean。例如框架中的MailSenderAutoConfiguration中并没有@Bean,但是它又导入了另一个类MailSenderJndiConfiguration,在MailSenderJndiConfiguration中是有@Bean的

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ MimeMessage.class, MimeType.class, MailSender.class })
@ConditionalOnMissingBean(MailSender.class)
@Conditional(MailSenderCondition.class)
@EnableConfigurationProperties(MailProperties.class)
@Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class })
public class MailSenderAutoConfiguration {

	/**
	 * Condition to trigger the creation of a {@link MailSender}. This kicks in if either
	 * the host or jndi name property is set.
	 */
	static class MailSenderCondition extends AnyNestedCondition {

		MailSenderCondition() {
			super(ConfigurationPhase.PARSE_CONFIGURATION);
		}

		@ConditionalOnProperty(prefix = "spring.mail", name = "host")
		static class HostProperty {

		}

		@ConditionalOnProperty(prefix = "spring.mail", name = "jndi-name")
		static class JndiNameProperty {

		}
	}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Session.class)
@ConditionalOnProperty(prefix = "spring.mail", name = "jndi-name")
@ConditionalOnJndi
class MailSenderJndiConfiguration {

	private final MailProperties properties;

	MailSenderJndiConfiguration(MailProperties properties) {
		this.properties = properties;
	}

	@Bean
	JavaMailSenderImpl mailSender(Session session) {
		JavaMailSenderImpl sender = new JavaMailSenderImpl();
		sender.setDefaultEncoding(this.properties.getDefaultEncoding().name());
		sender.setSession(session);
		return sender;
	}

	@Bean
	@ConditionalOnMissingBean
	Session session() {
		String jndiName = this.properties.getJndiName();
		try {
			return JndiLocatorDelegate.createDefaultResourceRefLocator().lookup(jndiName, Session.class);
		}
		catch (NamingException ex) {
			throw new IllegalStateException(String.format("Unable to find Session in JNDI location %s", jndiName), ex);
		}
	}
}

Spring通过@Configuration和@Bean的组合给了我们一个通用的编程模式,我们只要采用@Configuration写出的代码都是符合OCP的代码。

第8周 初识SpringBoot

第2章 环境与常见问题踩坑

2-4 maven的作用与安装

maven官网下载地址
下载安装包并解压,把目录加入到环境变量中。安装完成执行mvn -v查看版本号

构建工具
项目引入jar包有可能引用关系非常复杂,比如A引用B,B引用C,C引用D。
没有maven时,A项目需要手动引入B这个jar包,再引入依赖的C,再引入依赖的D,如果D修改了还要相应的修改C修改B。有了maven只需要关心我们直接引入的依赖B,B的依赖由maven自己管理。

第11周 sprinboot的条件注解与配置

@ComponentScan

策略模式的变化方案 (第11周 1-3)

1.byname 切换bean的name
2.@Qualifier指定bean
3.有选择的只注入一个bean——注释掉某个bean上的@Component注解
4.使用@Primary

条件注解 @Conditional

自定义条件注解
@Conditional + Condition


2-1 SpringBoot自动配置原理导学

自动配置/装配

1.原理是什么
2.为什么要有自动装配

LInUI
1.npm 安装/拷贝
2.引用组件/函数/类

讲Spring一直围绕着如何更灵活的把Bean加入到IOC容器

SpringBoot SDK

1.@Component @Configuration
2.加载第三方SDK

##@EnableXXXX
模块装配

MongoDB @Configuration组合
Redis @Configuration
业务 @Configuration

自己的业务

SPI机制/思想
SPI的全名为Service Provider Interface
应对变化 模块 实现方案

										方案A

调用方 标准服务接口 方案B
方案C
基于interface + 策略模式 + 配置文件
解决 @Primary @条件注解
类 对象 具体/粒度小
整体解决方案的变化
2-8 深度理论课总结
SpringBoot为什么要有自动装配
SpringBoot比其他框架多出了一个IOC容器。如何去发现一些配置的Bean,然后把他们加入到IOC容器。


第12周 Java异常深度剖析

核心机制

框架机制

不会直接使用框架
二次封装/开发

增强型SpringBoot

异常反馈
资源部存在/参数错误/内部错误
反馈给客户端/前端

异常分类

Throwable

Error 错误
Exception 异常 1/0

CheckedException 编译阶段进行处理
RuntimeException 运行时异常

HttpException extends RuntimeException
APIException extends Exception

如果我们定义了全局异常处理继承那都一样

异常 我们可以处理 CheckedException Bug
如: B.C() 方法找不到
读取文件
无能为力 RuntimeException unchecked
如:客户端发送请求查询数据库中的一条记录,有可能找到也有可能找不到

已知异常、未知异常

未知异常:对于前端开发者和用户,都是无意义的。服务端开发者代码逻辑有问题。

第1章 Java异常分类剖析与自定义异常

SpringBoot全局异常处理

第2章 自动配置Url前缀

SpringBoot根据目录结构自动配置Url前缀

第13周 参数校验机制与LomBok工具集的使用

重点讲SpringBoot参数校验机制。参数校验选择使用JSR-303BeanValidation,使用注解的方式来进行参数校验。还将学习LomBok工具集常见注解及Builder构造模式

第1章 LomBok工具集的使用

1-1 修改Properties文件的编码解决乱码问题

IntelliJ Idea设置text file encoding UTF-8;换行符为 Unix 格式

1-2 参数校验机制导学

参数校验对于我们Web开发是非常重要的。第一,作为服务端的开发者,如果你的参数校验写的足够规范是可以大大提高前后端协同开发的开发效率的,从而为公司及自己大大节约时间成本以及经济成本。第二,参数校验对于保护Web里面的机密数据和机要信息也是非常重要的。校验的代码不能直接写在控制器里。控制器主要是用于承接视图层与服务层之间的桥梁,不是用来编写主要的业务逻辑的,也不是用来写大量的校验的代码的。
我们要去做参数校验首先要学习的是如何在控制器里能够方便的接收到参数。参数主要分两大类,一类是通过url传递过来的参数,另一类是通过post的body里传递过来的参数。

1-3 获取URL路径中的参数和查询参数

url传递的参数也分两种,一种是在路径里的参数,如下面代码中的id1。一种是查询参数,也就是?后面的参数,如下面代码中的name2。路径里的参数通过注解@PathVariable来接收,查询参数可以不用注解,也可以通过加@RequestParam来接收。当参数名与方法中接收参数的参数名不一致时,可以通过在注解后面加name参数来映射。

    @GetMapping(value = "/test/{id1}")
    public String test(@PathVariable(name="id1") Integer id,@RequestParam(name="name2") String name){

发送请求,请求url为 http://localhost:8080/v1/banner/test/2?name2=雪,查看接收到的参数。
在这里插入图片描述
在这里插入图片描述

1-4 数据传输对象DTO的接收

当我们要传递大量数据的时候,通常采用POST请求在httpbody中传JSON格式的数据
在这里插入图片描述
接收这种JSON格式的数据用注解@RequestBody 。接收的数据类型可以定义成一个Map<String,Object>,但是这种方式接收到的参数Object在使用时还需要转型,频繁的拆箱装箱对性能是有一定影响的。我们通常定义一个类来接收参数。

    @PostMapping(value = "/test/{id1}")
    public PersonDTO test(@PathVariable(name="id1") Integer id,
                          @RequestParam(name="name2") String name,
                          @RequestBody Map<String,Object> personDTO){

定义类接收对象

@Getter
@Setter
public class PersonDTO {
    private String name;
    private Integer age;
}

@PostMapping(value = "/test/{id1}")
public PersonDTO test(@PathVariable(name="id1") Integer id,
                       @RequestParam(name="name2") String name,
                       @RequestBody PersonDTO personDTO){

1-5lombok的基本使用方式

lombok是一个可以帮助我们大幅度简化代码的Java代码工具。
比如JavaBean中private的成员变量需要getter和setter方法,大量的getter和setter方法在一个类中会让我们的类显得非常长。使用lombok工具就可以省去这些代码。
使用方法
在pom文件中添加配置,此处未指定版本,你也可以指定版本

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

idea安装lombok插件
此步骤也可省略,不安装插件只是在idea的Structrue窗口看不到lombok自动生成的方法,安装插件之后可以看到由lombok自动生成的方法。
在这里插入图片描述
安装步骤
打开设置 =》Plugins ,搜索lombok,点击install,重启idea。
在这里插入图片描述

1-6 lombok中关于构造函数的几个注解

常用注解
@Setter 注解在类或字段,注解在类时为所有字段生成setter方法,注解在字段上时只为该字段生成setter方法。
@Getter 注解在类或字段,注解在类时为所有字段生成getter方法,注解在字段上时只为该字段生成getter方法。
@EqualsAndHashCode 注解在类,生成hashCode和equals方法。
@Data 注解在类,生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
@AllArgsConstructor 注解在类,生成包含类中所有字段的构造方法。
@RequiredArgsConstructor 注解在类,为类中需要特殊处理的字段生成构造方法,比如final和被@NonNull注解的字段。
@NoArgsConstructor 注解在类,生成无参的构造方法。
@NonNull 注解在类,定义字段不能为空
@Builder 声明实体,表示可以进行Builder方式初始化

@NoArgsConstructor

public class PersonDTO {
    @NonNull
    private String name;
    private Integer age;

    public PersonDTO() {}
}

@RequiredArgsConstructor

public class PersonDTO {
    @NonNull
    private String name;
    private Integer age;
    
    public PersonDTO(String name) {
        this.name = name;
    }
}

@AllArgsConstructor

public class PersonDTO {
    @NonNull
    private String name;
    private Integer age;

    public PersonDTO(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

1-7 @Builder构造器模式的使用

1-8 JSR-269与Builder模式的序列化

@Builder
传统实例化对象的方式

PersonDTO dto = new PersonDTO();
dto.setName("雪");
dto.setAge(18);

使用@Builder模式,这种方式更优雅。但是@Builder会将类的无参构造方法私有化,如果再想使用new PersonDTO();需要自己手动在类中定义一个无参构造方法。如果这个类需要能被当作一个Bean返回到前端还需要能够被序列化,我们需要给类再加一个@Getter。
一般地,我们可以这样设计实体

@Builder
public class PersonDTO {
    private String name;
    private Integer age;
}

PersonDTO dto = PersonDTO.builder()
                .name("雪")
                .age(18)
                .build();

第2章 参数校验机制以及自定义校验

2-1 使用@Validated注解进行基础参数校验

常见注解

Bean validation中内置的constraint
@Null  被注释的元素必须为null
@NotNull  被注释的元素必须不为null
@AssertTrue   被注释的元素必须为true
@AssertFalse  被注释的元素必须为false
@Min(value=)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value=)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value=,inclusive=)  被注释的元素必须是一个数字,其值必须大于等于value,inclusive=true,是大于等于
@DecimalMax(value=,inclusive=)  被注释的元素必须是一个数字,其值必须小于等于value,inclusive=true,是小于等于
@Size(min=, max=)  字符串,集合,map 被注释的元素的大小必须在指定的范围内
@Digits(integer,fraction)  被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past  被注释的元素必须是一个过去的日期
@Future  被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式
Hibernate validator附加的constraint
@NotBlank(message=)  验证字符串非null,且长度必须大于0
@Email  被注释的元素必须是电子邮箱地址
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内
@NotEmpty  被注释的字符串必须非空
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

基本使用方式
要想开启参数校验,需要在类上标注@Validated注解

@Validated
public class BannerController {

    @PostMapping(value = "/test/{id1}")
    public PersonDTO test(@PathVariable(name="id1") @Range(min = 1,max = 10,message = "不能超过10噢") Integer id){

2-2 验证HTTP Body中的参数与级联校验

2-3 补充:@Validated和@Valid注解的关系

如果要开启Http Body中的参数校验,那么在参数列表该字段前加上@Validated即可

@Validated
public class BannerController {

    @PostMapping(value = "/test")
    public PersonDTO test(@RequestBody @Validated PersonDTO personDTO){

如果一个类中包含了另外一个实体类,那么在上面加上@Valid即可

public class PersonDTO {
    @Valid
    private SchoolDTO schoolDTO;
}

2-4 自定义校验注解

@Documented 注解标记的元素,Javadoc工具会将此注解标记元素的注解信息包含在javadoc中。默认,注解信息不会包含在Javadoc中。
@Retention({RetentionPolicy.Runtime}) RetentionPolicy这个枚举类型的常量描述保留注释的各种策略,它们与元注释(@Retention)一起指定注释要保留多长时间
@Target({ElementType.TYPE}) 用于描述注解可以用在什么地方
@Constraint(validatedBy = PasswordValidator.class)将自定义校验注解与关联类关联在一起。validatedBy接收的参数可以是一个数组,也就是可以指定多个关联类来修饰这个自定义注解

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Constraint(validatedBy = PasswordValidator.class)
public @interface PasswordEqual {

    int min() default 2;
    int max() default 10;

    String message() default "passwords are not equal";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

2-5 自定义校验注解的关联类

自定义校验注解还需要一个关联类,校验的业务逻辑应该写在关联类中。关联类要实现ConstraintValidator这个接口,这个接口是一个泛型类,第一个参数是注解的类型,第二个参数是这个自定义注解所修饰的目标的类型。重写initialize()方法,在initialize()方法中获取注解的参数。重写isValid()方法校验参数是否通过校验

import com.lin.missyou.dto.PersonDTO;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PasswordValidator implements ConstraintValidator<PasswordEqual, PersonDTO> {

	private int min;
    private int max;

	/**获取注解的参数*/
    @Override
    public void initialize(PasswordEqual constraintAnnotation) {
        this.min = constraintAnnotation.min();
        this.max = constraintAnnotation.max();
    }
    
    @Override
    public boolean isValid(PersonDTO personDTO, ConstraintValidatorContext constraintValidatorContext) {
        String password1 = personDTO.getPassword1();
        String password2 = personDTO.getPassword2();
        boolean match = password1.equals(password2);
        return match;
    }
}

2-6 获取自定义校验注解的参数

注解中定义变量只能是基本类型。

	private int min;
    private int max;

    @Override
    public void initialize(PasswordEqual constraintAnnotation) {
        this.min = constraintAnnotation.min();
        this.max = constraintAnnotation.max();
    }

2-7 捕获DTO类的参数校验异常信息

    //参数校验
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public UnifyResponse handleBeanValidation(HttpServletRequest req, MethodArgumentNotValidException e){
        String method = req.getMethod();
        String requestUrl = req.getRequestURI();

        List<ObjectError> errors = e.getBindingResult().getAllErrors();
        String message = formatAllErrorMessages(errors);

        return new UnifyResponse(10001,message,method+" "+requestUrl);
    }

	/**将多个异常信息拼成一个字符串*/
    private String formatAllErrorMessages(List<ObjectError> errors){
        StringBuffer errorMsg = new StringBuffer();
        errors.forEach(error ->
                errorMsg.append(error.getDefaultMessage()).append(";")
        );
        return errorMsg.toString();
    }

2-8 捕获Url和查询参数的异常

@ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public UnifyResponse handleConstrainException(HttpServletRequest req, ConstraintViolationException e){
        String method = req.getMethod();
        String requestUrl = req.getRequestURI();
        String message = e.getMessage();
        return new UnifyResponse(10001,message,method+" "+requestUrl);
//        for(ConstraintViolation error : e.getConstraintViolations()){
//            ConstraintViolation a = error;
//        }
//        return null;
    }

第14周 项目分层设计与JPA技术

第1章 项目分层原则与层与层的松耦合原则

1-1 JPA导学

分层的目的 微服务
大型项目 每一层 都是由不同的开发者或者团队负责
水平分隔
垂直分隔

OCP 类与类
更大的方面 OCP的实践 中台

在微服务出现以前,大型项目每一层都是由不同的开发者或者团队负责开发。分层可以让每一层的开发者或团队各司其职互不打扰,让自己的问题集中在自己这层,而不影响其他人的相关代码。这种分层我们通常叫他水平分隔。微服务实际上是垂直分隔
他们归根接地要解决的问题都是OCP,只不过我们之前谈的OCP是类与类之间。分层和微服务是从一个更大的方面在进行OCP的实践。
JPA

1-2 Service业务逻辑层(服务层)的建立

1-3 层与层之间是否一定要用interface建立关联

Java规范中要求层与层之间要用interface建立关联。这种方式写起来过于繁琐,绝大多数项目下意义不大。根据个人开发的观点层与层之间没有特殊的情况就直接调用实现不需要interface,除非确定Service会有变化,一个接口会有多个实现类,就应该用interface关联。

为什么规范与我们的实际情况不太相符?
因为我们的代码粒度不够小,一个类承担的职责过多。OCP要求我们替换一个类代替修改,如果一个类承担的职责过多代码过多,想要替换这个类的难度就很大。

1-4 创建数据表的3种主要方式

如何创建数据表

1.可视化管理工具(navicat, mysql workbench, php admin)
2.手写SQL语句
3.Model模型类

1-7 多环境配置文件(profiles)以及启用方式

我们的项目在开发环境、生产环境,不同的环境下会有不同的配置。比如访问服务的端口,或者数据库的配置。如果每次切换环境都去直接修改这些配置的值,不仅麻烦而且很容易出错。我们如何才能根据环境动态的选择配置文件呢?

如图是我项目中的3个配置文件。其中,记录在application.yml中的配置项在任何环境下都会生效,记录在application-dev.yml中的配置项只在dev(开发)环境下生效,记录在application-prod.yml中的配置项只在prod(生产)环境下生效。所有的环境配置文件前面的application-是固定规范,后面的部分是自定义的。多环境的配置文件可以有多个,不只这两个。
在这里插入图片描述
那么如何启动某一环境下的配置文件?

方法一 修改配置文件

修改application.yml中的配置项,具体内容如下图
在这里插入图片描述
这样,spring.profiles.active: dev在启动服务时就会调用application-dev.yml这个配置文件。同理,若spring.profiles.active: prod,那么在启动服务时,服务器就会调用application-prod.yml这个配置文件。
此外,我们不需要去关心这些环境配置文件是位于哪一个目录下面的,只要是在resources下面的都可以。只要遵守application-开头的这个约定,springboot就会自动寻找这些环境的配置文件。

方法二 命令启动服务,命令中带参数指定配置文件

此方式不需要application.yml文件中的spring.profiles.active配置项
第一步: idea中在maven窗口点击 m 图标,在弹出的命令行窗口输入命令mvn clean package,控制台出现BUILD SUCCESS说明打包成功。(也可采用其他的maven打包方式,本文就不一一介绍了)。打包完成后会在target目录下生成项目的jar包。
在这里插入图片描述
打包好的这个jar包我们通常称它为uber jar——超级jar包,这个jar包里面内置了tomcat服务器。不同于过去的java项目打成war包,war包需要依赖于外部tomcat服务器。

第二步: 生产环境下,通常我们就不会再使用开发工具来启动项目,而是通过java命令来启动jar包。
进入target目录,执行命令:java -jar 项目的jar包 --spring.profiles.active=dev
项目就会调用application-dev.yml配置文件,即以开发环境的配置要求启动服务。同理,若是生产环境,只需将dev改为prod即可。
在这里插入图片描述

第2章 数据库设计、实体关系与查询方案探讨

2-1 mysql数据库连接配置

要想使用orm方式生成数据表首先要确保能够成功的连接数据库

1.pom.xml中加入依赖
<!--         JPA的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
<!--         MySQL驱动的依赖-->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
2.配置文件中添加配置项

此处修改的是开发环境的配置文件application-dev.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sleeve?characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: 123456

'root’是访问数据库的用户名,'123456’是密码。此处根据自己数据库设置。
url中,jdbc指我们使用jdbc的驱动方式连接数据库;mysql表示连接的数据库类型是mysql;localhost是数据库的具体地址;3306是数据库的端口号;sleeve是数据库名;characterEncoding设置编码格式;serverTimezone设置时区,GMT%2B8表示的是东8时区也就是北京时间。

2-2 Maven依赖无法安装的几个解决方案

idea设置中Maven的离线模式取消。开启离线模式是不会从maven的远程仓库下载依赖的。
在这里插入图片描述

修改本项目的数据源
<repositories>
        <repository>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
    </repositories>
修改maven配置的镜像仓库

C:\Users\XFX.m2\setting.xml

  <servers>
	     <server>
	 		<id>nexus-aliyun</id>
    	</server>
  </servers>
  <mirrors>
		<mirror>
            <id>nexus-aliyun</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <mirrorOf>*</mirrorOf>
        </mirror>
   <mirrors>

2-3 使用ORM的方式生成数据表

2-4 常用实体类注解与ORM生成表的优劣势浅谈

create – 启动时删数据库中的表,然后创建,退出时不删除数据表
create-drop – 启动时删数据库中的表,然后创建,退出时删除数据表 如果表不存在报错
update – 如果启动时表格式不一致则更新表,原有数据保留
validate – 项目启动表结构进行校验 如果不一致则报错

实体类示例代码:

package com.lin.missyou.model;

import javax.persistence.*;

@Entity
@Table(name = "banner1")	//默认情况下数据表名与实体类名相同,@Table设置数据表名
public class Banner {
    @Id		//设置该字段为主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)	//设置为自增长
    private long id;

    @Column(length = 16)		//限制字段长度
    private String name;

    @Transient		//该字段不会映射到数据表
    private String description;
    private String img;
    private String title;
}

2-5 多对多关系的第三张表探讨(重点)

ORM
数据库表与表之间的关系

1对1 人 身份证
1对多 班级 学生
多对多 老师 学生

1对1 表是否拆分2方面考虑
1.查询效率 1表太多字段
2.业务的角度

多对多

2-6 数据库设计步骤及数据库优化原则

表 模型(实体) 面向对象
业务 业务对象
Coupon 表
Order 表
Banner 表

第二步:对象与对象之间的关系
外键

第三步:细化
字段 限制 长度、小数点、唯一索引

把表当作面向对象中的模型(实体)来思考。我们在做项目的时候会面临很多业务,首先要找到很多业务对象。比如这个项目中优惠券有优惠券(Coupon)这个对象,对应就会有Coupon这个表。所以在做数据库设计的时候首先要找到业务中的各个业务对象,把它当成一个个的模型实体来对待。第二步,就是来思考对象与对象之间的关系。具体到数据库他们是通过外键建立联系的。第三步,就是一些细化的工作,比如限制字段的长度、小数点、唯一索引。

海量数据的时候才需要考虑数据库性能方面的优化,这才是数据库的重点和难点:
1.一个数据表中的记录是不能太多的,mysql承载的上限是5000万条,但是我们不能按照上限来设计,尽可能保证表中的数据记录是不太多的(没有一个确切的数据,是根据七月老师的经验)
如何优化数据太多的问题:(1)建立索引。建立索引就可以更加有效地进行数据库的查询。
(2)水平分表。把原来一张表中的很多记录拆分成多张表,数据记录少查询效率会比较高。
(3)垂直分表。如果表中字段过多,把表垂直分隔,也会提高查询效率。

数据库的设计在一定程度上确实是可以提高查询效率的,但是数据库的设计很多时候是不能从根本上解决查询效率的问题的。数据库的优化更多的不是体现在数据库设计上的,而是体现在查询方式上。

上面提到的数据库优化方式是在软件的早期阶段。随着技术的不断革新,对于数据库效率优化有了更简单粗暴的方式——缓存。利用各种各样的缓存尽可能的少去查询数据库。缓存是对于数据库优化最为有效的方式。也就是当你一次把这些数据从数据库中查询出来之后,把数据存储在像redis这种key-value这种键值对的内存型的数据库。以后再查询优先去缓存里查询,而不是直接去数据库里查询。所以说,对于数据库优化最好的方式就是尽量少去查询数据库。再好的优化也是有上限的。

2-7 实体与实体之间一对多关系配置@oneToMany

2-8 指定外键字段与是否需要物理外键

实体Banner与BannerItem之间是一对多的关系,Banner是一方BannerItem是多方。一个Banner中包含一组BannerItem,也就是Banner中需要有一个成员变量标识出多方的BannerItem。
使用JPA的注解@OneToMany标识Banner中的成员变量items

	@OneToMany
    private List<BannerItem> items;

启动应用,数据库中自动生成3张表,多出了一张banner_items表。只有多对多关系才需要第三张表去维护两张表之间的关系,但是Banner和BannerItem是一对多关系为什么也出现了第三张表?这是因为JPA是不知道如何维护这两张表之间的关系的,只能新增第三张表来维护两张表之间的关系。我们要想办法消除第三张表,就需要指明Banner与BannerItem之间的外键。在Banner的items上标注@JoinColumn,在BannerItem中加成员变量bannerId

	@OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name="bannerId")
    private List<BannerItem> items;

在互联网的项目中数据量较大,变动比较频繁,是不推荐使用物理外键的,有外键约束数据库查询的效率是比较低的。使用物理外键可以强制保证数据一致性。但是如果既不想使用物理外键又想保证数据的一致性,就需要在代码中做比较强的校验保证数据的一致性。大多数的互联网项目是倾向于不使用物理外键。

@GeneratedValue设置主键自增长

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

很多时候我们认为JPA不好用就是因为这里面有很多自动化的规则。 可以肯定的是JPA是有一定的学习成本的,但是JPA用熟练之后在查询时可以给我们提供非常多的便利。其实数据库的设计并不是一个重点和难点,也不是很繁琐。我们所有的这些繁琐的操作都是为了后边查询方便,数据库的查询才是最消耗成本的。

第3章 jpa的关联关系与规则查询

3-1 JPA的Repository定义

设置外键是为了使查询更加方便。Banner和BannerItem是两张表,前端访问接口时Banner和BannerItem的数据一起返回。传统的使用sql语句查询方式一种是2条sql语句查询2次数据库,另一种是一条sql通过join查询1次数据库。
用JPA可以不用写sql语句直接查询数据。
新建接口BannerRepository 继承JPA内置的接口JpaRepository

package com.lin.missyou.repository;

import com.lin.missyou.model.Banner;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BannerRepository extends JpaRepository<Banner,Long> {
     Banner findOneById(Long id);

     Banner findOneByName(String name);
}

第一层,api下面放controller用于接收请求;第二层,service下面属于业务层;第三层,repository用于操作数据库。而model下面是一些对于业务对象的定义,严格意义上来讲我不认为它是一层。之前说过层与层之间要用接口去衔接。之前定义了一个BannerService的接口就要给BannerService写一个实现,那么现在还要给BannerRepository写一个实现吗?不需要。JPA最强大的地方就在于我们定义了BannerRepository这个接口JPA就能自动生成sql语句帮我们查询出Banner来。

3-2 执行Repository查询语句

repository的调用

package com.lin.missyou.service;

import com.lin.missyou.model.Banner;
import com.lin.missyou.repository.BannerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BannerServiceImpl implements BannerService{

    @Autowired
    private BannerRepository bannerRepository;

    @Override
    public Banner getByName(String name) {
        return bannerRepository.findOneByName(name);
    }
}
package com.lin.missyou.service;

import com.lin.missyou.model.Banner;

public interface BannerService {
    public Banner getByName(String name);
}
@RestController
@RequestMapping("/banner")
@Validated
public class BannerController {

    @Autowired
    private BannerService bannerService;

    @GetMapping("/name/{name}")
    public Banner getByName(@PathVariable String name){
        Banner banner = bannerService.getByName(name);
        return banner;
    }
}  

3-3 懒加载和急加载

开发环境打印JPA生成的sql语句
application-dev.yml中添加配置项。默认情况下是惰性的,只打印出一条sql语句。当展开banner下面的items打印第二条sql语句。

spring:
  jpa:
    properties:
      hibernate:
        show_sql: true
        format_sql: true

懒加载对应的是急加载,在 @OneToMany注解加参数(fetch = FetchType.EAGER)就能一次性执行两条sql语句。通常情况下不建议使用急加载,尽量节约数据库的性能。

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name="bannerId")
    private List<BannerItem> items;

3-4 双向一对多配置

为什么要有双向一对多?在上节课中我们看到了在Banner中配置了导航属性之后查询变得简单了。我们只需要查询出banner来再通过访问它的导航属性就可以拿到这个banner所关联的一组bannerItem,这就是导航属性的作用。但是我们再反过来想想这个问题,有时候我们查询出来的是bannerItem,那这bannerItem是属于哪一个banner的呢?所以在BannerItem中也可以增加一个导航属性。这样由于两方都存在相关联的导航属性,就有了双向一对多关系。

双向一对多
关系维护端和关系被维护端
多方、一方
在双向一对多关系中Banner叫做一方,BannerItem叫做多方。多方也叫关系维护端,一方也叫做关系被维护端。在双向一对多中**@JoinColumn**是要打在关系维护端的,也就是多端。

总结:

双向一对多关系中
1.在一方打上@OneToMany,在多方打上@ManyToOne
2.需要指明关联的外键@JoinColumn打在多方也就是关系维护方上
3.在关系的被维护方也就是一方的@OneToMany增加一个参数mappedBy,值是多方中的导航属性的名字

@Entity
@Table(name = "banner")
public class Banner {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(length = 16)
    private String name;

    @Transient
    private String description;
    private String img;
    private String title;

    @OneToMany(mappedBy = "banner",fetch = FetchType.EAGER)
//    @JoinColumn(name="bannerId")
    private List<BannerItem> items;
}



@Entity
public class BannerItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String img;
    private String keyword;
    private Short type;
    private String name;
//    private Long bannerId;

    @ManyToOne
    @JoinColumn(name="bannerId")
    private Banner banner;

}

3-5 双向一对多的外键配置问题

我们原来在banner中手动添加的banner就没有实际的业务意义了,banner只是表明两个表的关系。在双向关系中bannerId是会自动生成的。这里不能显示的声明bannerId,自动创建表的时候会自动生成banner_id。如果一定想让bannerId显示的表达出来要么就使用单向一对多,或者使用双向一对多但是要在@JoinColumn中添加两个参数

    @ManyToOne
    @JoinColumn(insertable = false,updatable = false,name="bannerId")
    private Banner banner;

有些同学可能会有疑问,实体不去创建表那这个实体还有用吗?实体的主要作用并不是用来创建表的,实体的主要作用是用来表达数据结构的或者说是用来表示数据库的数据的。千万不要狭隘的认为ORM就是用实体来创建表的。ORM其实是用实体操作数据库的对象,用实体去表达数据库的数据

3-7 单向多对多关系配置与常见问题

在有些业务中我们是不需要双向多对多的。
默认规则为我们生成的第三张表的命名存在一些问题,我们需要去指定第三张表的名称和第三张表中外键的名称。
使用@JoinTable,参数name指定第三表的标名;joinColumns 和inverseJoinColumns 用于定义外键的名称。

package com.lin.missyou.model;

import javax.persistence.*;
import java.util.List;

@Entity
public class Theme {
    @Id
    private Long id;
    private String title;
    private String name;

    @ManyToMany
    @JoinTable(name="theme_spu",
            joinColumns = @JoinColumn(name="theme_id"),
    inverseJoinColumns = @JoinColumn(name="spu_id"))
    private List<Spu> spuList;
}

参数joinColumns 是由于有一些复杂的情况是允许有多个外键的。但是多外键很少用到,现在也不推荐设置多外键。还有联合主键,联合外键也不推荐使用。更推荐通过业务拆分成单外键,但主键。我们这里认为数据库设计越轻量越好。
这种方式生成的第三张表是没有主键的,但是这个theme_spu没有实际业务意义,所以没有主键没有影响。如果想用这种方式生成有主键的第三张表需要自定义模型来生成第三张表。

3-8 双向多对多配置

用@ManyToMany(mappedBy = “spuList”)声明关系的被维护端。此处双向多对多与双向一对多就有所区别,双向多对多维护端与被维护端是可以调换的,而双向一对多的维护端与被维护端是不可以调换的。

@Entity
public class Theme {
    @Id
    private Long id;
    private String title;
    private String name;

    @ManyToMany
    @JoinTable(name="theme_spu",
            joinColumns = @JoinColumn(name="theme_id"),
    inverseJoinColumns = @JoinColumn(name="spu_id"))
    private List<Spu> spuList;
}


@Entity
public class Spu {
    @Id
    private Long id;
    private String title;
    private String subtitle;

    @ManyToMany(mappedBy = "spuList")
    private List<Theme> themeList;

}

3-9 如何禁止JPA生成物理外键

方法一:

    @ManyToOne
    @org.hibernate.annotations.ForeignKey(name="null")
    private Banner banner;

方法二:

    @ManyToOne
    @JoinColumn(foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT), insertable = false,updatable = false,name="bannerId")
    private Banner banner;

但是方法二现在存在bug设置无效。

第15周 ORM的概念与思维

第1章 ORM的概念与思维

1-1 谈谈ORM的概念、意义与常见误区问题

ORM 技术 思想 存储数据 数据库 数据表 字段
面向对象的方式 数据库 Object Relation Mapping

存储数据 关系
面向对象 是对现实事物的抽象

数据表、记录、字段

类、对象、属性/成员变量

ORM(Object Relation Mapping,对象关系映射)不是一种技术,而是一种思想。这种思想探讨的是如何去看待数据库,用传统的观念去看待数据库,数据库是用于存储数据的。数据库的理论中,我们知道最基本的有数据库、数据表、字段。
ORM其实是换了个角度,让我们用面向对象的方式去看待数据库。
那么如何理解ORM呢?还是从数据库的基础理论谈起。我们都熟知的数据库的特性是存储数据,除此之外数据库还有一个重要的特点就是表示关系。这两点刚好是可以与我们的面向对象对应起来的,面向对象其实讲究的也是存储和关系这两个特征。
所以之前就有人提出这样一个观点,我们可不可以不用数据库、数据表、字段这种方式来理解数据库,而是用面向对象的方式来理解数据库。那这样有什么好处呢?第一点,面向对象这种思维方式是我们程序员必须要掌握的方式;第二点,面向对象其实就是对我们人类,我们世界里各种各样的事物的一种抽象。越是离我们生活中接近的事物越容易被我们理解。所以说用面向对象的思维去理解计算机方面的问题会更加容易理解。
那么面向对象是如何与数据库中的相关理论进行对于的呢?
在数据库中有4个概念是最需要大家去理解的。数据库、数据表、记录、字段。对应到面向对象里就是类、对象、属性/成员变量。这样我们就可以把数据库中的概念和面向对象的知识体系进行相互的映射和对照。我们操作数据库、理解数据库、查询数据库各种各样的操作其实更希望不是直接去操作数据表和字段,而是需要我们操作类、对象、属性以及成员变量。这就是ORM的思想。此外,之所以我们可以用面向对象的思维去思考数据库,还有一个非常重要的原因是因为面向对象本身也是一种
关系的表达方式

我们之前课程中提到的依赖依赖依赖,依赖其实很多时候就是面向对象表达它的依赖关系的一种方式。

1-3 项目开发顺序

优先开发CMS C端 Customer
运营
CMS 麻烦 1,2
SKU 线上 报表
4个阶段 2个项目 ToC ToO
对客户 对运营

中大型项目中,理论上比较合适的流程是先开发CMS,然后开发C端的应用,就是给用户使用的程序。
前后端分离如果有条件的话最好先开发后端,开发前端需要数据的支撑。

1-4 导入项目的SQL文件

在这里插入图片描述
DDL(Data Definition Language)数据定义语言,用来描述和操作数据库的一种语言。

第2章 Banner等相关业务

2-2 @MappedSuperClass的作用

在基类BaseEntity上标注@MappedSuperClass注解,表明这个类是一个映射的基类。

  jackson:
    property-naming-strategy: SNAKE_CASE		//用snake方式命名
    serialization:
      WRITE_DATES_AS_TIMESTAMPS: true			//返回时间格式设置为时间戳

字段上标注 @JsonIgnore序列化实体时不返回该字段

@Getter
@Setter
@MappedSuperclass
public abstract class BaseEntity {
    @JsonIgnore
    private Timestamp createTime;
    @JsonIgnore
    private Timestamp updateTime;
    @JsonIgnore
    private Timestamp deleteTime;
}

2-6 表的列行转换思维

扩展数据库
面临最多的情况就是
数据表的数据字段不够用
比如Theme表中需要有Color1 Color2 Color3 ,在设计数据库的时候就要考虑这是不是一个一对多的关系,是不是应该新增一个Color表。这种思维方式可以最大程度上解决数据库表结构不稳定的问题。在这里插入代码片

表 字段 不具备扩展性
表 记录 具备扩展性

列 不具备扩展性
行 随意新增

Key Config

id name value table_name
1 color1 green theme
2 color2 red
3
4 name xue banner

2-7 SPU表设计分析

spu表中的price用varchar类型是因为spu的价格不是一个确切的价格,一般只是在前端做一个展示,也不会直接去参与计算。sku的价格就一定要用数字类型了。
文本类型其实是对于我们数据库字段查询和处理最为灵活的一个类型,如果没有特殊理由的话可以尽可能的把字段设置成文本类型。文本类型用来表示数字问题也不大,从数据库查询出来最终可以在JAVA代码里转型成数字,但是如果把一个字段设置成数字而又不确定这是不是一直是数字,在这种情况下未来数据库的变更就有可能会发生的。

tags其实是可以设计成一对多单独提取成一张表的。但是这里用的是一个字段中间用一些分隔符变成一个字符串。用分隔符分隔开变成一个字符串优点是查询方便提高数据库性能,避免连表查询;缺点是更新这个字段比较麻烦。数据库设计标准的设计规范确实是把tag单独作为一个表让spu和tag之间是一对多的关系。但是有时候数据库设计是不能按照标准的范式去设计的,有时候我们为了提高数据库查询性能是需要有一些冗余字段的,也称反范式设计。

2-8 静态资源托管的几种方式

静态资源:
图片、html、css、js、Vue

最标准的托管方式:
单独构建一个服务或者使用nginx
开放的云端服务:OSS、七牛、码云

做一个项目,首先购买云端ECS,我们通常是把我们的代码放到ECS上,静态资源不推荐放到ECS上,业务ESC带宽不会太大。大型企业可能考虑到成本会选择自己构建一个nginx集群,但是这样运维成本是比较高的。小公司通常选择直接使用第三方服务的。
我们在开发阶段可以自己构建静态服务器,或者用SpringBoot另外写一个项目仅仅存放图片,另外一种选择是放到当前项目下。

2-9 SpringBoot访问静态资源

添加依赖

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

在resources/static下新建目录存放图片。访问图片的路径为新建的目录名/图片名

第3章 再谈数据库设计技巧与VO层对象的技巧

3-1 为什么需要提供多个SPU数据的接口?

我们服务端人员在开发接口时的主要顺序应该以实体作为基本的参考对象。
在获取列表数据的时候应该返回的是概要数据,如果列表直接返回详情数据会让数据量变得非常大。但是这种方式也是存在问题的,加入之后前端需求变更需要添加一些其他信息,那接口就需要更改。
建议:不能只看设计图返回指定数据,应该通过思考选择适中的字段进行返回。

3-2 Spu实体映射(增补)

3-4 查询列表类数据

规律:一般在实现才需要加入到容器,也就是说在Class上标注@Repository、@Service,不会在interface上标注。

Repository下面的很多默认方法不需要定义就可以直接调用。

3-5 延迟思考,逐层深入

Spu

DetailImage 多
SpuImage 多
Sku 多
现在分析到这步能确定的是这个地方需要4张表,不能确定的是Sku还需要什么表。在这个时候我们应当把Sku的复杂性延迟到后面再去考虑。我们首先是要把这3张表给建立好,然后把它们之间的关系也建立好。至于Sku要如何去跟其他的表进行关联我们应该放到后面再去逐层分析sku。

3-6 数据库多层关系设计思考与冗余字段设计原则

category -> spu -> sku -> xx -> xx -> xxx

通过这个关系链可以看出sku是不需要记录categoryId和categoryRootId的。这个地方在Sku中加了两个冗余的字段,为了提高数据库查询的性能。在某种程度上我们是可以选择去做一些冗余字段的,某些接口如果查询频率很高那能省去一次sql的join查询就省去一次。

3-7 导航关系的配置原则(需要时才配置)

如何配置导航关系?
导航关系本身并不是属于数据库里的数据,任何实体其实都可以不要导航关系。那么我们为什么要配置这个导航关系?因为我们在操作model的时候去获取它的关系数据非常的方便。
所以说,你也可以完全不去配置导航关系。比如,你既需要Banner又需要BannerItem,如果两方都没有配置导航关系,那也可以先查Banner再去查BannerItem。所有数据库里的复杂查询都可以通过多次的单表查询来完成。
所以说什么时候配置导航关系,这个就要根据我们的业务需求来配置。比如,SpuDetailImg中如果配置一个spu的导航关系,就可以根据spuDetailImg很方便的得到spu的信息,但是在我们的业务中没有这种业务需求。但是我们有通过一个spu获取下面所有spuDetailImg的需求,所以说我们选择在spu下面增加一个导航关系。不可能所有两两实体之间都需要双向导航关系。

3-9 VO视图层对象的概念和意义

直接给前端返回spu有大量不需要的字段,新建VO简化返回数据。

第16周 JPA的多种查询规则

第1章 DozerBeanMapper的使用

1-1 DozerBeanMapper拷贝属性

        <dependency>
            <groupId>com.github.dozermapper</groupId>
            <artifactId>dozer-core</artifactId>
            <version>6.5.0</version>
        </dependency>
    @GetMapping("/latest")
    public List<SpuSimplifyVO> getLatestSpuList(){
        Mapper mapper = DozerBeanMapperBuilder.buildDefault();
        List<Spu> spuList = this.spuService.getLatestPagingSpu();
        List<SpuSimplifyVO> voList = new ArrayList<>();
        spuList.forEach(s->{
            SpuSimplifyVO vo = mapper.map(s, SpuSimplifyVO.class);
            voList.add(vo);
        });
        return voList;
    }

DozerBeanMapper的真正优势是可以做生拷贝,比如当SpuSimplifyVO中有一个成员变量是Sku,复制的时候不仅要拷贝spu中的属性同时要拷贝sku中的属性,Dozer就可以完成这样的功能。

1-2 PageRequest.Of方法进行分页参数构建

PageRequest.of()中传递参数Sort进行排序 Sort.by()指定按照哪个字段排序 .descending()倒序
PageRequest.of()只是构建了一个Pageable的查询对象。

    public Page<Spu> getLatestPagingSpu(Integer pageNum, Integer size){
        Pageable pageable = PageRequest.of(pageNum,size, Sort.by("createTime").descending());
        return  this.spuRepository.findAll(pageable);
    }

1-3 BO对象与分页参数转换

VO DTO BO

VO:返回到前端的ViewObject
DTO:代表前端传过来的Object (Data Transfer Object)
BO:代表BusinessObject业务对象,通常用于Service向Controller传递数据
DAO:Data Access Object数据层向业务层返回的数据
因为我们使用的JPA,JPA已经给大家定义了一组Entity,可以把Entity或者Model理解成DAO

public class CommonUtil {

    public static PageCounter convertToPageParameter(Integer start, Integer count){
        int pageNum = start / count;
        PageCounter pageCounter = PageCounter.builder()
                .page(pageNum)
                .count(count)
                .build();
        return pageCounter;
    }
}

1-4 Paging分页对象的构建

@Getter
@Setter
@NoArgsConstructor
public class Paging<T> {

    private Long total;
    private Integer count;
    private Integer page;
    private Integer totalPage;
    private List<T> items;

    public Paging(Page<T> pageT) {
        this.initPageParameters(pageT);
        this.items = pageT.getContent();
    }

    void initPageParameters(Page<T> pageT){
        this.total = pageT.getTotalElements();
        this.count = pageT.getSize();
        this.page = pageT.getNumber();
        this.totalPage = pageT.getTotalPages();
    }

}

1-5 PagingDozer对象的封装

重点是用泛型抽象类型的思想。PagingDozer大幅度精简化我们的代码,以后再有既要返回分页的数据,同时又需要做属性精简的时候就可以使用PagingDozer这个对象。

public class PagingDozer<T,K> extends Paging{

    @SuppressWarnings("unchecked")
    public PagingDozer(Page<T> pageT,Class<K> classK){
    	this.initPageParameters(pageT);
    	
        List<T> tList = pageT.getContent();
        Mapper mapper = DozerBeanMapperBuilder.buildDefault();
        List<K> voList = new ArrayList<>();
        tList.forEach(t -> {
            K vo = mapper.map(t,classK);
            voList.add(vo);
        });
        this.setItems(voList);
    }
}

1-6 Paging对象测试与Java的伪泛型缺陷

使用PagingDozer

    @GetMapping("/latest")
    public PagingDozer<Spu,SpuSimplifyVO> getLatestSpuList(@RequestParam(defaultValue = "0") Integer start,
                                                @RequestParam(defaultValue = "10") Integer count){
        PageCounter pageCounter = CommonUtil.convertToPageParameter(start,count);
        Page<Spu> spuPage = spuService.getLatestPagingSpu(pageCounter.getPage(),pageCounter.getCount());
        return new PagingDozer(spuPage,SpuSimplifyVO.class);
    }

1-7 分类表的常见结构设计

这个项目中我们只做了二级分类,理论上我们是可以做无限极分类的。但是无限极分类的数据库虽然不难但是查询效率是比较低的,会大大增加代码的复杂度。无限极分类在电商业务中用处也不大。如果不确定具体有多少级,建议是4级分类,4级对于中小型电商已经够了,普通电商项目二级就够了,二级可以大大降低开发成本。

无限级分类其实相对于树状结构,由一级一级的节点组成,只要让每一个节点记住上级节点的id号,用一个二维表就可以表示树状结构。
在数据表中加level字段,表示层级,可以大大简化数据库查询。

1-8 无限级分类的数据表达方式(自定义协议路径)

路径表示法
但是这种方式比较占用数据库的空间资源,数据库如果占用的空间资源太大查询效率也是会受影响的,但是这个影响的程度是远小于大量高频次的去查询数据库的。如果是二级或者三级分类用parentId的形式也是可以的,影响不大,但如果是五级六级分类还是路径表示法更好一些。

1-9 分类冗余字段设计

spu中设置两个categoryId,一个跟级别的id一个二级的categoryid,设置冗余字段方便查询。

1-10 JPA接口命名规则

JPA会通过接口方法的命名规则自动生成sql,不过一些复杂的查询还是需要我们自己去写sql的。但也不是写原生sql,原生sql要防止SQL注入,而且查询出来的也不是实体对象不好操作,返回的是数据库的记录集,更多的时候是以数组的方式返回给我们的,这些数组里都是零散的字段。原生的sql的优势就在于灵活,可控性强,性能好。但是写原生sql维护是一件比较麻烦的事情。

    Page<Spu> findByRootCategoryIdOrderByCreateTimeDesc(Long cid,Pageable pageable);

    Page<Spu> findByCategoryIdOrderByCreateTimeDesc(Long cid, Pageable pageable);

spring data jpa方法命名规则

第2章 详解SKU的规格设计

2-1 Java Bean中可不可以写业务逻辑

JavaBean中可以写一些简单的业务逻辑,但是不适合写大量的业务逻辑。有些项目严格要求不能在model中写业务逻辑,但是有些简单的业务逻辑如果不写在model中代码写的会非常繁琐,所有的业务逻辑都要外置。把部分的业务逻辑内置到Entity中会让代码变得更简单。

2-2 jSR303校验的message模板配置

在resources下新建ValidationMessages.properties文件,这个文件名是固定的。
文件中的键值可以自定义,模板可以带参数

id.positive = {min}{max}测试错误

代码中引用模板用 {键值} 的方式来引用

    @RequestMapping("/by/category/{id}")
    public PagingDozer<Spu,SpuSimplifyVO> getByCategoryId(@PathVariable @Positive(message = "{id.positive}") Long id,

2-3 规格、规格名与规格值的设计(上)

库存量stock 在很多电商中还有专门针对库存量单位的数据表 (张、个、件、斤)
categoryId rootCategoryId specs冗余字段为了查询方便

sku的规格设计(服务端)

Spec 很多电商中也叫Attribute

Spec 颜色 图案 尺码

Spec_Key 颜色 尺码
Spec_Value 蓝色 橙色 S M XL
spec还有两个衍生实体Spec_key和Spec_Value
规格是一种比较主观的概念

2-4 规格、规格名和规格值的设计(中)

spec_key和spec_value是一对多的关系,所有在spec_value表中有spec_id把value和key关联在一起,
spec_key与spu是多对多的关系,是不是多对多其实取决于表中的记录能否被复用,也可以不设置standard字段,设计成一对多的关系

spu - spec_key 多对多
spu - sku 一对多
sku - spec_value 多对多

2-5 规格、规格名和规格值的设计(下)

数据库设计技巧

实体 模型 完整的数据库
好的数据库设计
设计数据库的时候不要去考虑表和字段,应该从实体模型的角度考虑,这样可以帮助我们将数据库的表设计完整的做完,但是完整的数据库设计并不代表是好的数据库设计。数据库的职责并不是完整的去表达数据与数据之间的关系,还需要考虑到前端的业务的相关操作。数据库的设计要考虑到返回给前端的数据好不好用,查询的效率高不高。

序列化

最初就是为了对象的传输和存储才有了序列化这样的概念。
mysql不会跟java做对应,提供一个统一的类型去存储java中的类,但是我们可以把类序列化后以文本的方式存储到数据库里。有可能一个对象在java中产生要传输到C#或C++、python去,语法各不相同。但是大家都认字符串这种数据类型,所以可以把对象序列化成字符串再传到另外一种语言中去。另外一种语言再通过反序列化用它自己语言所描述的对象,这样就解决了对象传输的问题。
如果我们现在要存储一个对象最好的方式就是MongoDB这种文档型的数据库。Mysql以前的版本是没有json这种数据类型的,但是我们可以使用varchar或者text。

“code”:“2$1-45#3-9#4-14”, $前边的表示spuId #分隔的是多个规格,而每个规格key-value

第3章 通用泛型Converter

3-1 通用泛型映射方案(1)

3-2 通用泛型类映射方案(2)

数据库中存储的是json对象,Entity中的属性是String,如果直接返回到前端的就是字符串
在这里插入图片描述
在这里插入图片描述

3-3 单体JSON对象的映射处理(1)

强类型的语言虽然比弱类型的语言要麻烦但是好维护。我们用某种方式尝试对他进行封装就可以变得和动态语言一样的好用。如果不封装就用原生的java去写肯定是越写越麻烦的。

新建类MapAndJson封装单体序列化和反序列化的方法,Sku中在需要序列化的字段上标注@Convert

public class MapAndJson implements AttributeConverter<Map<String,Object>,String> {

    @Autowired
    private ObjectMapper mapper;

    @Override
    public String convertToDatabaseColumn(Map<String, Object> stringObjectMap) {
        try {
            return mapper.writeValueAsString(stringObjectMap);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new ServerErrorException(9999);
        }
    }

    @Override
    public Map<String, Object> convertToEntityAttribute(String s) {
        try {
            return mapper.readValue(s, HashMap.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new ServerErrorException(9999);
        }
    }
}

    @Convert(converter = MapAndJson.class)
    private Map<String,Object> test;

3-6 数组类型JSON与List的映射(4)

新建类MapAndJson封装List序列化和反序列化的方法,

@Converter
public class ListAndJson implements AttributeConverter<List<Object>,String> {

    @Autowired
    private ObjectMapper mapper;

    @Override
    public String convertToDatabaseColumn(List<Object> objects) {
        try {
            return mapper.writeValueAsString(objects);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new ServerErrorException(9999);
        }
    }

    @Override
    public List<Object> convertToEntityAttribute(String s) {
        try {
            if(null == s){
                return null;
            }
            List<Object> list = mapper.readValue(s,List.class);
            return list;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new ServerErrorException(9999);
        }
    }
}
    @Convert(converter = ListAndJson.class)
    private List<Object> specs;

3-7 谈Java类的内聚性、方法外置的问题

之前课中做了两个MapAndJson和ListAndJson的工具类,确实可以解决我们的问题,但是这样做失去了我们面向对象中最重要的类的优势。类和对象是千变万化的,很多时候代表了我们的业务逻辑的处理,但是List和Map让我们千变万化的类失去了它的业务意义。一个类如果没有了自己的业务方法就需要把方法外置,调用就变得麻烦了。所以说一个类就可以去拥有方法也应该去拥有方法,能内置的方法就尽量不要外置,这是面向对象最基本的原则。

第17周 令牌与权限

第1章 通用泛型类与java泛型的思考

1-1 Java的泛型是编译期间的而非运行期间

1-5 方案调整与优化

debug访问接口发现下面这个方法并不能在运行时推断出类型,而是转化成了LinkedHashMap

public static <T> List<T> jsonToList(String s){
        if(null == s){
            return null;
        }
        try{
            List<T> list = GenericAndJson.mapper.readValue(s, new TypeReference<List<T>>() {
            });
            return list;
        }catch (Exception e){
            e.printStackTrace();
            throw new ServerErrorException(9999);
        }
    }

在这里插入图片描述
最后只留下objectToJson和JsonToObject就可以了

@Component
public class GenericAndJson {

    private static ObjectMapper mapper;

    @Autowired
    public void setMapper(ObjectMapper mapper){
        GenericAndJson.mapper = mapper;
    }

    public static <T> String objectToJson(T o){
        try{
            return GenericAndJson.mapper.writeValueAsString(o);
        }catch (Exception e){
            e.printStackTrace();
            throw new ServerErrorException(9999);
        }
    }

    public static <T> T jsonToObject(String s,TypeReference<T> tr) {
        if(null == s){
            return null;
        }
        try{
            T o = GenericAndJson.mapper.readValue(s,tr);
            return o;
        }catch (Exception e){
            e.printStackTrace();
            throw new ServerErrorException(9999);
        }
    }
    
}

new TypeReference<List>() 将List当作一个整体的泛型传入

    private String specs;

    public List<Spec> getSpecs() {
        if(null == this.specs){
            return Collections.emptyList();
        }
        return GenericAndJson.jsonToObject(this.specs,new TypeReference<List<Spec>>(){});
    }

    public void setSpecs(List<Spec> specs) {
        if(specs.isEmpty()){
            return;
        }
        this.specs = GenericAndJson.objectToJson(specs);
    }

1-6 @Where条件查询

在Entity上标注@Where注解限定查询条件

@Where(clause = "delete_time is null and online = 1 ")
public class Spu extends BaseEntity{

第2章 Category、Theme接口

2-1 Category分类业务分析

分类在调用API接口时把数据返回给前端,如何返回大致可分为两种情况。
第一种,如果数据量不大从效率和体验上来讲直接把所有的一级分类和二级分类一次全部返回到前端就可以了。
第二种,前端需要什么数据就实时访问服务器取一次数据,用户点击一次就加载一次,这种方案的弊端就是如果用户频繁点击就需要频繁向服务器发送请求。
选择哪种方案主要在于有多少级的分类,如果级别多一次返回是不好的,如果只有两级分类可以直接采用方案一。
本项目中的分类数据的改变不会太频繁,所以不需要实时更新数据让服务器和数据库承受比较大的压力。
如果确实需要每次切换都去更新,就一定要在服务器做缓存了,像这种频繁的切换每次都去服务器加载,服务器的压力特别大,去服务器加载也是没有问题的,但是绝对不能每次都去查询数据库。最直接的解决方案还是使用redis把数据缓存起来,但是缓存数据最麻烦的是更新缓存,如何把真实的数据和redis进行合理的同步才是最麻烦的。缓存的时机和体量。

2-2 Category接口数据结构分析与探讨

2-3 循环序列化的解决方案

Category模型中配置了Coupon导航属性,跟category是多对多关系。直接返回给前端存在两个问题,第一,返回给前端时如果返回Coupon会造成数据冗余,Coupon中还有可能有其他很多属性;第二,会存在循环序列化的问题,造成内存泄漏。可以在coupon上标注**@JsonIgnore**不序列化这个字段,但是这种方式不灵活,任何时候都不会序列化个字段。序列化时应该再另外定义一个VO,不序列化这些导航属性。

2-4 Java的Stream与Method Reference应用

package com.lin.missyou.vo;

import com.lin.missyou.model.Category;
import lombok.Getter;
import lombok.Setter;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Getter
@Setter
public class CategoriesAllVO {
    private List<CategoryPureVO> roots;
    private List<CategoryPureVO> subs;

    public CategoriesAllVO(Map<Integer,List<Category>> map) {
        this.roots = map.get(1).stream()
                .map(r-> {
                    return new CategoryPureVO(r);
                })
                .collect(Collectors.toList());
        this.subs = map.get(2).stream()
                .map(CategoryPureVO::new)
                .collect(Collectors.toList());
    }
}

map.get(1).stream()把map转换成stream流,然后就有很多函数式编程的方法,此处我们就可以调用它的.map()这个方法,map()可以接收一个lambada表达式, .collect(Collectors.toList())把map()方法返回的流转换成List,
我们还可以直接使用idea的Replace lambada with method reference
.map(r-> {
return new CategoryPureVO®;
})
简化成:
.map(CategoryPureVO::new)
这两个方法是等同的,也是JAVA8的新特性。方法的引用有4种形式:(1)类上静态方法的引用 (2)构造函数的引用(3)实例方法的引用(4) 这里其实是构造方法的一种引用方式
Stream不要把它理解成像map这样的数据结构,其实stream很多时候只是提供了一种视图或者是一种管道,stream很多时候是不会修改源的,源在我们这个地方其实就是map.get(1),map.get(1)的实质就是一个list,很多时候流其实都是用来操作一个集合的。流不是必须的,但是现在函数式编程比较流行,使用JAVA就新增了这样一种方式,主要作用是可以帮我们简化代码,这个地方如果不用stream用for循环也是可以实现的。
在这里插入图片描述
本节知识点: lambada表达式 stream流 函数式编程 Method Reference 方法的引用

2-6 Theme业务及模板概念分析

theme主题与spu是多对多关系。本项目中通过不同的主题点击进入spu列表的样式是不同的,但是展示的数据是一样的,我们把这些页面提取成为不同的模板。theme表中tplName字段指定使用哪个模板,从而在服务端控制前端样式。

2-7 自定义JPQL替代JPA命名方法

使用@Query
在方法上标注@Query来指定本地查询
参数nativeQuery默认为false,nativeQuery=false时,value参数写的是JPQL,JPQL是用来操作model对象的

    @Query("select t from Theme t where t.name in (:names)")      
    List<Theme> findByNames(@Param("names") List<String> names);  

nativeQuery=true时,value参数写的是原生sql,JPQL比原生sql支持的关键字少,JPQL中的名字要优先使用model的名字。

使用@Param命名化参数,如果接收的参数名与JPQL中的参数名一样,@Param可以省略

第3章 Optional精讲(必看)

3-1 Optional的意义与价值

Optional可以帮助我们简化代码,给我们的判空操作提供了一种标准的写法,强制我们考虑空值的情况。

3-2 SpringBoot中的单元测试

Optional的使用,repository中的方法返回值可以直接放到Optional容器中,
optional.orElseThrow() 如果optional不为空,则将Theme这个结果返回,如果为空则抛出异常。orElseThrow()接收的参数为Supplier<?>
@GetMapping(“/name/{name}/with_spu”)
public Theme getThemeByNameWithSpu(@PathVariable String name){
Optional themeOptional = themeService.findByname(name);
return themeOptional.orElseThrow(()-> new NotFoundException(30003));
}

单元测试
pom.xml中添加依赖

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

3-3 Optional的精髓

3-4 Optional的经典使用场景、Consumer与Supplier

package com.lin.missyou.optional;


import org.junit.Test;

import java.util.Optional;

public class OptionalTest {
    @Test
    public void testOptional(){
        //构建Optional
        Optional<String> empty = Optional.empty();
//        Optional<String> t1 = Optional.of(null);
        Optional<String> t2 = Optional.ofNullable(null);

//        empty.get();
//        String s = t2.get();
//        t2.ifPresent(t-> System.out.println(t));
        t2.ifPresent(System.out::println);

//        String s = "默认值";
//        if(null != t2){
//            s = t2.get();
//        }else{
//
//        }
        String s = t2.orElse("默认值");
        System.out.println(s);

		t2.map(t->t+"b").ifPresent(System.out::println);

    }
}

Optional的get()方法,如果Optional的值是空值在get()取值的时候就会直接报错,这就是Optional的意义所在。
不用Optional的时候会直接把空值赋值给s,此处不会报错,空指针异常令我们是否头疼。如果在赋值时不产生异常,空指针异常会成为隐藏的错误,随着我们函数调用栈变深会越来越难以调试。如果空指针的问题贯穿了整个函数调用栈一直到最后一个函数才被发现,在溯源的时候就需要跟踪调试栈上的所有函数。

Consumer与Supplier是Java中比较重要的两个概念,Consumer是消费者,t2.ifPresent(t-> System.out.println(t)); 把值提供给你让你去消费掉;Supplier是提供者,要返回一个值,不会在再提供给你值了。Consumer与Supplier都可以是lambada表达式。

3-5 Optional的链式操作与Function、Predicate、Filter

区分consumer supplier runnable function predicate
consumer:接收参数无返回值
supplier:不接收参数有返回值
runnable:不接收参数也没有返回值
function::接收参数也有返回值
predicate:返回的是Boolean值

第4章 权限、分组与用户

4-1 权限、分组与用户的关系探讨

4-2 @ScopeLevel注解的机制

通过给接口设置@ScopeLevel来控制权限

4-3 令牌与微信登录机制

微信登录,静默登录,微信已经验证了账号密码,由微信母体已经验证过了。
code -> API -> 微信服务器
在小程序中可以获取到一个code码,我们需要把这个code码发送到我们的API,我们的API再调用微信的服务器,从而完成验证的操作。

用户票据
在传统的网页中用户登录后会生成一个票据然后存储在Cookie中,一段时间之内可以免登陆。
现在前后端分离的项目Vue、App、小程序都使用JWT令牌的机制。JWT 实质上就是字符串,用户的userId可以直接写在JWT这个字符串里,除此之外我们还可以写入其他很多额外的信息。

4-4 无感知二次登陆问题探讨

JWT是有时效性的。令牌有效期设置太长不安全,一般120min(2小时),较短的有效期只能提高安全性,但是令牌仍然有可能会丢失。
JWT可以写入很多信息,我们可以把用户的IP也写到JWT里去。
我们在验证JWT时,首先要验证JWT的合法性,第二要验证JWT是否过期。当两个验证都通过后就可以反序列化这个JWT,然后把其中的userId提取出来,从而确定用户的身份。
验证IP可以建立一个用户常用IP表,用户登录去常用IP表中查找验证。或者 在颁布JWT令牌时记录当前的IP,如果IP改变了就要求重新登录。

网站要想实现无感知二次登录,要使用双令牌机制access_token refresh_token

4-5 getToken接口

作业:自己编写ScopeLevel方式的权限校验
1.自定义注解@ScopeLevel
2.JWT令牌生成与颁布
3.拦截用户的JWT令牌,全局拦截机制,验证JWT令牌的合法性

新建TokenController用户登录接口,接收body参数TokenGetDTO,TokenGetDTO支持多种登录方式,LoginType用于区分登录方式。

4-6 TokenPassword校验注解

4-7 错误消息的模板参数

4-8 微信验证服务

登录时只有在校验是否合法时有区别,验证通过后都是生成JWT令牌。
使用微信登录,是去服务器校验code码;
使用账号密码登录是去数据库中查找比对。
命名时通常用Authentication表示登录、验证 Authorize 表示授权

4-9 Sleeve-Mini小程序测试工具准备

4-10 对Enumeration的理解与扩展

Enum:在JAVA中enum实质上也是一个类,只不过是受限制的类,不具备类所有的功能。enum也具备类的一些特性,可以定义一个构造函数,枚举中定义构造函数访问修饰符定义成私有的,可以自己决定构造函数参数的数量和类型。构造函数访问修饰符private可加可不加,但是不能定义成public。枚举中的一项我们就可以当做是一个枚举类,比如实例中的USER_WX。在枚举中定义一个public的方法,所有的枚举都会有这个方法。

package com.lin.missyou.core.enumeration;

public enum LoginType {

    USER_WX(0,"微信登录"),
    USER_Email(1,"邮箱登录");

    private Integer value;

    private LoginType(Integer value,String description){
        this.value = value;
    }

    public void test(){

    }
}

在这里插入图片描述
对于枚举我们可以这样理解,下边的部分是对枚举的定义,成员变量和方法,上边是对枚举的实例化。

第5章 Auth0生成JWT令牌与微信身份认证

5-1 获取用户OpenId

将微信小程序中获取的token传到微信服务器获取OpenId
配置文件中带入参数 Spring发送Http请求
获取微信的openid,openid代表一个用户相对于你当前的这个小程序的唯一标识,同一个用户登录到不同的小程序的openid是不同的。要想同一个用户登录到你不同的小程序、公众有同一个标识,应该获取unionid,但是unionid的获取是比较麻烦的。
application中添加配置项

wx:
  appid: wxf4d9a22063d92aa6
  appsecret: 65badd566e4360f043f25857e79d3c00
  code2session: https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code
    @Value("${wx.code2Session}")
    private String code2SessionUrl;
    @Value("${wx.appid}")
    private String appid;
    @Value("${wx.appsecret}")
    private String appsecret;
public String code2Session(String code){
		//传入模板字符串和要带入的参数,拼接成完整的url
        String url = MessageFormat.format(code2SessionUrl,this.appid,this.appsecret,code);
        //发送Http请求
        RestTemplate rest = new RestTemplate();
        Map<String,String> session = new HashMap<>();
        String sessionText = rest.getForObject(url,String.class);
        try {
            session = mapper.readValue(sessionText,HashMap.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return session.get("");
    }

5-2 JWT令牌全流程解析

1.用code码换取用户的openid
2.我们自己数据库中的user表 的id -> uid
3.我们自己的系统注册 -> openid写入user表 / 查询user uid
4.最终都是要返回uid
5.uid写入到JWT令牌 发送到小程序

5-3 User实体分析与常见用户系统设计

最好在User表中添加应该group字段对用户进行分组,再给用户的分组设置权限,但是我们这个项目不做会员系统,暂时不加这个字段。
UnifyUid存储微信的unionid

5-4 User对象的写入

根据从微信服务器获取的openid到User表中查询该用户是否已经注册过了。
if else 两端都有业务逻辑就不适合用Optional 。 Java9中有optional.ifPresentOrElse(Consummer, Runable) 是与否的情况下都可以传入业务逻辑,但是Java8中不支持,所以对于这种是与否都有相应业务逻辑的情况就不适合使用Optional

5-5 Auth0的JWT

将uid和scope写入到JWT令牌中,scope用于判断用户是否有访问某一个接口的权限。
生成JWT的令牌需要引入第三方库,Java中有两个比较主流的第三方库,jjwtauth0, 此处使用auth0,Java的JWT库比其他语言的要复杂。JWT库用于生成和验证JWT令牌。可以在JWT官网查找JWT库
引入auth0库,在pom.xml中添加配置

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.1</version>
        </dependency>

5-6 Auth0生成JWT令牌

JWT .withClaim()用于写入自定义数据,withExpiresAt()和withIssuedAt()是属于JWT标准里面的参数。

/**
 * @作者 7七月
 * @微信公号 林间有风
 * @开源项目 $ http://7yue.pro
 * @免费专栏 $ http://course.7yue.pro
 * @我的课程 $ http://imooc.com/t/4294850
 * @创建时间 2020-03-14 04:06
 */
package com.lin.missyou.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class JwtToken {

    private static String jwtKey;
    private static Integer expiredTimeIn;
    private static Integer defaultScope = 8;

    @Value("${missyou.security.jwt-key}")
    public void setJwtKey(String jwtKey) {
        JwtToken.jwtKey = jwtKey;
    }

    @Value("${missyou.security.token-expired-in}")
    public void setExpiredTimeIn(Integer expiredTimeIn) {
        JwtToken.expiredTimeIn = expiredTimeIn;
    }

    public static Optional<Map<String, Claim>> getClaims(String token) {
        DecodedJWT decodedJWT;
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            decodedJWT = jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
            return Optional.empty();
        }
        return Optional.of(decodedJWT.getClaims());
    }

    public static Boolean verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(token);
        } catch (JWTVerificationException e) {
            return false;
        }
        return true;
    }


    public static String makeToken(Long uid, Integer scope) {
        return JwtToken.getToken(uid, scope);
    }

    public static String makeToken(Long uid) {
        return JwtToken.getToken(uid, JwtToken.defaultScope);
    }

    private static String getToken(Long uid, Integer scope) {
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        Map<String,Date> map = JwtToken.calculateExpiredIssues();


        return JWT.create()
                .withClaim("uid", uid)
                .withClaim("scope", scope)
                .withExpiresAt(map.get("expiredTime"))
                .withIssuedAt(map.get("now"))
                .sign(algorithm);
    }

    private static Map<String, Date> calculateExpiredIssues() {
        Map<String, Date> map = new HashMap<>();
        Calendar calendar = Calendar.getInstance();
        Date now = calendar.getTime();
        calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn);
        map.put("now", now);
        map.put("expiredTime", calendar.getTime());
        return map;
    }
}

5-7 令牌生成测试

6-1 JWT令牌的校验

客户端会把JWT令牌放到http header中,服务端做全局拦截获取header中的JWT令牌。
读取令牌中的参数

    public Optional<Map<String,Claim>> getClaims(String token){
        DecodedJWT decodedJWT ;
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try{
            decodedJWT = jwtVerifier.verify(token);
        }catch (JWTVerificationException e){
            return Optional.empty();
        }
        return Optional.of(decodedJWT.getClaims());
    }

6-2 Filter、Interceptor、AOP机制的区别于联系

在应用程序中写一个机制,拦截所有http请求中附加的JWT令牌
拦截 http请求:所有的http请求都会被我们的某一个类或方法所拦截,我们的这个方法里可以去获取到http请求里一系列的参数,获取参数后我们可以决定个http请求是否能够进入到我们的Controller里面去。
SpringBoot中有三种方式可以实现拦截,Filter、Interceptor、AOP。其实这三种方式都是AOP思想的运用。filter是基于servlet的,而Interceptor和AOP是基于Spring的。严格意义上来讲filter是可以不依赖于spring框架的。servlet是一种标准的接口和规范。这三种方式的区别:假如我们同时做了这三种机制,当http请求发送到服务器,拦截的顺序是先进入filter ——> Interceptor ——>AOP ——>Controler,一个http是有request和response一进一出两个过程的。
这三种方式的选择:filter没有特殊原因就不用,因为使用spring的机制接口和功能会更丰富一些。Interceptor和aop相比,AOP粒度更小一些,我们可以对类或类中的方法做aop的。如果Interceptor和aop都可以选择的话,建议选择Interceptor,因为Interceptor的实现比较简单。aop写起来比较麻烦,AOP的概念比较复杂,但是在一些情况下不可以互换,只能使用AOP。对于令牌的拦截更适合使用Interceptor,因为是无差别的校验。

filter ——> Interceptor ——>AOP ——>Controler		——>AOP——> Interceptor  ——> filter
|--------------------------request--------------------|	|----------------------response------------------------|

6-3 PermissionInterceptor的逻辑分析

package com.lin.missyou.core.interceptors;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class PermissionInterceptor extends HandlerInterceptorAdapter {
    public PermissionInterceptor() {
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

preHandle() 是在请求进入Controller之前的时候的回调函数;postHandler() 中给出了一个ModelAndView,是指SpringBoot在渲染页面之前给了一个修改ModelAndView 的机会;afterCompletion() 主要用于给我们去清理一些资源的,后面我们会在和里面去清楚local变量里的一些资源的。

1.获取到请求token
2.验证token
3.读取token中的scope
4.读取API@ScopeLevel level
5.比较scope 和 level

6-4 获取Bearer Token

6-5 hasPermission()权限核查

package com.lin.missyou.core.interceptors;

import com.auth0.jwt.interfaces.Claim;
import com.lin.missyou.exception.ForbiddenException;
import com.lin.missyou.exception.UnAuthenticatedException;
import com.lin.missyou.util.JwtToken;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Optional;

public class PermissionInterceptor extends HandlerInterceptorAdapter {
    public PermissionInterceptor() {
        super();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler);
        if(!scopeLevel.isPresent()){
            return true;
        }
        String bearerToken = request.getHeader("Authorization");
        if(StringUtils.isEmpty(bearerToken)){
            throw new UnAuthenticatedException(10004);
        }
        if(!bearerToken.startsWith("Bearer")){
            throw new UnAuthenticatedException(10004);
        }
        String token[] = bearerToken.split(" ");
        if(token.length !=2){
            throw new UnAuthenticatedException(10004);
        }
        Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token[1]);
        Map<String,Claim> map = optionalMap.orElseThrow(
                ()-> new UnAuthenticatedException(10004));
        Boolean valid = this.hasPermission(scopeLevel.get(),map);
        return valid;
    }

    private Boolean hasPermission(ScopeLevel scopeLevel,Map<String,Claim> map){
        Integer level = scopeLevel.value();
        Integer scope = map.get("scope").asInt();
        if(level > scope){
            throw new ForbiddenException(10005);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

    private Optional<ScopeLevel> getScopeLevel(Object handler){
        if(handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            ScopeLevel scopeLevel = handlerMethod.getMethod().getAnnotation(ScopeLevel.class);
            if(null==scopeLevel){
                return Optional.empty();
            }
            return Optional.of(scopeLevel);
        }
        return Optional.empty();
    }
}


6-6 注册Interceptor

package com.lin.missyou.core.config;

import com.lin.missyou.core.interceptors.PermissionInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Component
public class InterceptorConfiguration implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PermissionInterceptor());
    }
}

6-8 VerifyToken接口

    @PostMapping("/verify")
    public Map<String,Boolean> verify(@RequestBody TokenDTO token){
        Map<String,Boolean> map = new HashMap<>();
        Boolean valid = JwtToken.verifyToken(token.getToken());
        map.put("is_valid",valid);
        return map;
    }


JwtToken
    public static Boolean verifyToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try{
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
        }catch (JWTVerificationException e){
            return false;
        }
        return true;
    }

第18周 优惠券、订单与支付系统

第1章 优惠券系统设计

1-1 优惠券系统设计分析

1.创建优惠券 一般在CMS中创建
2.选择类型 参数
比如:满减券 满多少 减多少 全场折扣券 折扣
3.审核
4.投放
5.领取 定时 抢券
6.使用优惠券 金额的计算
7.优惠券的核销 前端计算,服务端还要校验

退款设计到的优惠券问题。 平摊、分布
优惠券模板 活动
品类 优惠券适用的品类
商品分类: 一级分类、二级分类、N级分类
一件商品、某几件商品
店铺
品牌

1-2 Coupon优惠券实体设计

优惠券涉及的实体 Activity、ActivityCover、Coupon、UserCoupon

package com.lin.missyou.model;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Where;

import javax.persistence.*;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.List;
import java.util.Objects;

@Getter
@Setter
@Entity
@Where(clause = "delete_time is null")
public class Coupon extends BaseEntity{
    @Id
    private Long id;
    private Long activityId;
    private String title;
    private Timestamp startTime;    //优惠券开始时间
    private Timestamp endTime;      //优惠券结束时间
    private String description;
    private BigDecimal fullMoney;   //满减券   满的金额
    private BigDecimal minus;       //满减券   减的金额
    private BigDecimal rate;        //折扣券   折扣
    private String remark;
    private Boolean wholeStore;     //是否为全场券
    private Integer type;           //优惠券类型 1. 满减券 2.折扣券 3.无门槛券 4.满金额折扣券

    @ManyToMany(fetch = FetchType.LAZY,mappedBy = "couponList")
    private List<Category> categoryList;
}

1-3 带有实际业务意义的多对多的第三张表

优惠券是通过活动发放的,活动与优惠券是多对多的关系。
Activity和Coupo的第三张表是没有实际的业务意义的,不需要直接去访问这张表所以没有建立实体。
User和Coupon之间有实际的业务意义,需要直接去操作这张表,所以给这个第三张表建立了UserCoupon这个实体。

1-4 优惠券的适用范围限定

coupon和category也是多对多的关系。优惠券适用于哪些分类。

活动、优惠券和分类之间的关系

优惠券一定是和活动有关系
1.优惠券 ——> 分类
2.活动 ——> 分类

案例:某个商品它能够适用哪些优惠券
SPU -> Category -> 优惠券
SPU -> Category -> Activity -> 优惠券

1-5 有效期优惠券的设计思路

有些优惠券没有固定的开始时间,比如新人折扣券,以用户的领取时间为开始时间。user_coupon表中create_time作为领取时间,在coupon表中设置有效期字段。

1-6 多对多向一对多的转化思维

领取优惠券方式:通过Spu进入商品详情页领取商品适用的优惠券,或者进入活动页面领取。
多对多的查询比较麻烦,数据库的设计能转化成一对多的就尽量转化成一对多的处理。此处引入优惠券模板的概念,优惠券模板与优惠券是一对多的关系,活动与优惠券也是一对多的关系,优惠券模板可复用。
任何多对多的关系都可以拆分成两个一对多的关系。

1-7 多级属性精简技巧

接口返回数据属性精简不建议非常精确,业务那样接口的通用性会大大降低。在不牺牲带宽的情况下应尽量保持接口的通用性。精简的时候主要去掉复杂的关联数据。
ActivityCoupon需要做二级属性的精简,activity中带有coupon的信息,coupon中带有category的信息
ActivityCouponVO

@Getter
@Setter
public class ActivityCouponVO extends ActivityPureVO{

    private List<CouponPureVO> couponList;

    public ActivityCouponVO(Activity activity){
        super(activity);
        couponList = activity.getCouponList()
                .stream().map(c ->{
                    return new CouponPureVO(c);
                }).collect(Collectors.toList());
    }
}

CouponPureVO

@Getter
@Setter
@NoArgsConstructor
public class CouponPureVO {
    private Long id;
    private String title;
    private Date startTime;
    private Date endTime;
    private String description;
    private BigDecimal fullMoney;
    private BigDecimal minus;
    private BigDecimal rate;
    private Integer type;
    private String remark;
    private Boolean wholeStore;

    public CouponPureVO(Coupon coupon){
        BeanUtils.copyProperties(coupon,this);
    }

第2章 优惠券的实现、JPQL技巧

2-1 根据分类查询所属优惠券接口分析

在本项目中约定,优惠券都是属于二级分类的,不和一级分类直接产生关系。所以我们默这个接口中接收的是二级分类的id,查询的也是二级分类的categoryId

2-2 原生SQL的多对多查询

2-3 JPQL完成复杂的多表查询

2-4 原生SQL语句剖析与JPQL的对比

 public interface CouponRepository extends JpaRepository<Coupon,Long> {

    @Query("SELECT c from Coupon c\n" +
            "join c.categoryList ca\n" +
            "join Activity a on a.id = c.activityId\n" +
            "WHERE ca.id = :cid\n" +
            "AND a.startTime < : now\n" +
            "AND a.endTime > :now")
    List<Coupon> findByCatory(Long cid, Date now);
}

上面的JPQL语句中,Activity与Coupon的join使用ON,而CategoryList与Coupon的join没有使用ON,是因为Coupon中没有配置Activity的导航属性就需要按照原生SQl去写ON的条件连接,而配置了CategoryList的导航属性。
上面的JPQL相对于原生sql的

    select
        coupon0_.id as id1_5_,
        coupon0_.create_time as create_t2_5_,
        coupon0_.delete_time as delete_t3_5_,
        coupon0_.update_time as update_t4_5_,
        coupon0_.activity_id as activity5_5_,
        coupon0_.description as descript6_5_,
        coupon0_.end_time as end_time7_5_,
        coupon0_.full_money as full_mon8_5_,
        coupon0_.minus as minus9_5_,
        coupon0_.rate as rate10_5_,
        coupon0_.remark as remark11_5_,
        coupon0_.start_time as start_t12_5_,
        coupon0_.title as title13_5_,
        coupon0_.type as type14_5_,
        coupon0_.whole_store as whole_s15_5_ 
    from
        coupon coupon0_ 
    inner join
        coupon_category categoryli1_ 
            on coupon0_.id=categoryli1_.coupon_id 
    inner join
        category category2_ 
            on categoryli1_.category_id=category2_.id 
    inner join
        activity activity3_ 
            on (
                activity3_.id=coupon0_.activity_id
            ) 
    where
        (
            coupon0_.delete_time is null
        ) 
        and category2_.id=? 
        and activity3_.start_time<? 
        and activity3_.end_time>?

2-5 单表查询、Join与JPA的优势_1

2-6 查询全场券_1

第3章 ThreadLocal 与线程安全

3-1 超权问题

用户id不能通过参数显示传递,需要从令牌中获取

3-2 LocalUser类的设计

在每个接口中都写一段代码,从JWT令牌中读取用户id,这样显然太麻烦了。
可能还会想到通过依赖注入的方式将当前的Http请求注入到我们的方法里面,从http的header中读取JWT令牌,从而读取用户id。这也是我们最容易想到的方法,但是这种方法也很麻烦。但是如果要在其他的非Controller的类中使用uid就需要从controller中将httpRequest一层一层的传递。
我们希望有一个更加灵活的方式,在任意一个代码位置都可以获取到uid值。我们希望的接口形式是有一个类,其中有一个静态方法, 在项目的任意一个地方都可以调用获取到uid的值。
返回user对象而不是只返回uid,避免以后写具体业务时频繁查询数据库。但是只返回uid占用的内存会比较小。
LocalUser的SetUser方法也需要接收User对象,这个更适合在拦截器PermissionInterceptor中调用。每个请求从前端发送到服务器都会经过PermissionInterceptor,在PermissionInterceptor中获取到token然后查询数据库得到User,这样可以保证在此次请求里我们最多只查询一次数据库去获取User。

3-3 写入LocalUser

我们希望LocalUser保持静态方法,这样调用比较方便,但是这样静态的方法和静态的成员变量存在的问题是,静态变量只能保存数据保存不了状态。静态变量约等于是全局共享的,web的环境下是可以同时接受多个API请求的,一个静态变量保存的到底是哪一个用户的User对象我们是不确定的。

package com.lin.missyou.core;

import com.lin.missyou.model.User;

public class LocalUser {
    private static User user;

    public static void setUser(User user,Integer scope){
        LocalUser.user = user;
    }

    public static User getUser(){
        return LocalUser.user;
    }
}

这种方式是非线程安全的。

3-4 ThreadLocal与线程安全

每一个Http请求都是一个不同的线程。为什么是多个线程?举个简单的例子,如果只有一个线程很有可能同一时间就只有一个用户可以访问我们的API,如果这次访问API耗时特别长其他用户就卡住了。我们需要一个类似Map 的数据结构,Map的key相当于线程的标识,Value相当于User对象。Java提供了ThreadLocal 这种数据结构帮我们处理一些多线程的数据存储的问题。
很多同学在解决多线程的时候往往会用** 锁** ,但很多多线程的问题引用内置的ThreadLocal就可以很方便的去解决了。用锁需要自己去考虑加锁等等一些操作。ThreadLocal给每一个线程都准备了一份数据的副本,在不同的线程里读取的都是数据的副本,它们都是互不干扰的。

3-5 ThreadLocal资源释放时机

package com.lin.missyou.core;

import com.lin.missyou.model.User;

import java.util.HashMap;
import java.util.Map;

public class LocalUser {
    private static ThreadLocal<Map<String,Object>> threadLocal = new ThreadLocal<>();

    public static void setUser(User user,Integer scope){
        Map<String,Object> map = new HashMap<>();
        map.put("user",user);
        map.put("scope",scope);
        LocalUser.threadLocal.set(map);
    }

    public static void clear(){
        LocalUser.threadLocal.remove();
    }

    public static User getUser(){
        Map<String,Object> map = LocalUser.threadLocal.get();
        User user = (User) map.get("user");
        return user;
    }

    public static Integer getScope(){
        Map<String,Object> map = LocalUser.threadLocal.get();
        Integer scope = (Integer) map.get("scope");
        return scope;
    }
}

threadLocal.remove();释放当前线程中对应的资源,在PermissionInterceptor中的afterCompletion()中调用

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LocalUser.clear();
        super.afterCompletion(request, response, handler, ex);
    }

第4章 优惠券领取与异步状态的不可靠性探讨

4-1 用户领取优惠券的细节考虑

用JPA的命名方法表达join查询

Optional<Activity> findByCouponListId(Long couponId);

相对于SQL



4-2 用户领取优惠券代码编写

createTime用服务器生成的时间,status使用枚举类型

        UserCoupon userCoupon = UserCoupon.builder()
                .userId(uid)
                .couponId(couponId)
                .status(CouponStatus.AVAILABLE.getValue())
                .createTime(now)
                .build();

4-3 更新成功、删除成功、创建成功的HttpStatusCode值

				查询成功		创建成功		删除成功		更新成功

建议状态码 200 201 200 200
状态码返回204的时候body中会是空值。

操作成功返回信息封装

public class CreateSuccess extends HttpException {
    public CreateSuccess(int code) {
        this.httpStatusCode = 201;
        this.code = code;
    }
}
    @PostMapping("/collect/{id}")
    public void collectCoupon(@PathVariable Long id){
        Long uid = LocalUser.getUser().getId();
        couponService.collectOneCoupon(uid,id);
        throw new CreateSuccess(0);
    }

这种方式让我们的编码变得非常简洁,这种用抛出异常的方式返回信息是不需要我们自己去获取url的。
但是这样的写法读起来有点怪,明明是一个正确的操作却要抛出一个异常。这个缺点也是可以回避的,我们也可以写一个类,在这个类的静态方法中抛出异常。

4-4 注意注入到容器里的对象是否是同一个对象

InterceptorConfiguration 中以这种方式注入到容器的是在PermissionInterceptor 上标注@Component注入的对象,而不是此处new PermissionInterceptor 。所以PermissionInterceptor中注入的service是null

@Component
public class InterceptorConfiguration implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PermissionInterceptor());
    }
}

改用注入方式

@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {

    @Bean
    public HandlerInterceptor getPermissionIntercettor(){
        return new PermissionInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.getPermissionIntercettor());
    }

4-5 异步机制下的数据一致性不一定能100%保证

小程序 我的—>优惠券中优惠券
在我们的这个系统中优惠券的状态并不一定能非常准确的表示优惠券的状态。根本原因是我们的这个系统中引入延迟订单支付。用户下单有可能不支付,订单失效后是否退回用户的优惠券。redis或者消息队列异步机制很难做到百分百一定能执行成功。

第5章 优惠券过期问题的处理方案

5-1 定时器的解决方案:主动轮询与被动消息触发

优惠券过期问题
status 未使用 已使用 已过期
触发机制 寻找一种触发机制
1.主动触发 轮训 线程 1天 扫描数据库(优惠券)Coupon Start-EndTime
数据库更新 Coupon Status ->Status
主动触发机制事件间隔设置的太小是很耗费数据库性能的,

2.被动触发 需要引入第三方 过期时间点 发消息
Redis / RocketMQ

5-2 枚举值向枚举类型转换

优惠券
1.一定要标识出Status=3吗?
2.标识出来就一定可信吗?
将枚举值转换为枚举类

    public static CouponStatus toType(int value){
        return Stream.of(CouponStatus.values())
                .filter(c->{c.value == value})
                .findAny()
                .orElse(null);
    }

5-3 不能以状态枚举来作为优惠券过期的唯一判断标准

购物车下边显示的满减券不是金额够了就可以使用的,而是优惠券对应的分类的商品达标才可以使用。所以购物车下方显示的可用优惠券需要返回分类的数据。

5-4 获取我可用的优惠券(带分类数据)

第6章 订单中的优惠券合法性校验

6-1 Order订单表的数据库设计

Order主要记录我们的订单数据,会关联一系列sku的信息,还有用户的收货地址。order表中4个snap_的字段,表示快照的状态。比如snap_address,用户下完订单之后地址有可能会改变,用户下单时的地址不应该根据用户收货地址的改变而跟着改变,他永远表示当前这一时刻所下订单的数据状态。
Order是mysql中的保留关键字,用@Table标注时在双引号中要加反引号

@Table(name="`Order`")
public class Order extends BaseEntity {

6-2 前端提交的Price是不可信的

OrderDTO中无论接收到的totalPrice和finalTotalPrice是多少服务端都要进行一次价格的校验。前端是要给用户展示这些价格的肯定要计算一次,如果服务端计算的结果与前端传过来的不相等是要给用户提示的。

6-3 订单校验所需要考虑的若干种校验项

订单校验参数有哪些
1.商品 是否无货
2.商品最大购买数量 总数量限制
3.单品数量限制
4.totalPrice
5.finalTotalPrice
6.优惠券 是否拥有这张优惠券
7.优惠券 优惠券是否过期
……

6-5 订单校验(2)构建CouponChecker

业务分层:当感觉Service层不够细致时可以将业务层再细分为service和logic

6-7 订单校验(4)满减券计算

前端 计算订单的价格
计算订单的折扣价(最终价)

服务端 计算订单的价格
计算订单的折扣价

6-9 订单校验(6)银行家舍去算法

银行家算法 是IEEE里规定的小数取整的基本方案
四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一。

js中的toFix()在有些浏览器实现的是银行家算法,有些浏览器实现的是四舍五入。

RoundingMode.HALF_UP  =>	BigDecimal.ROUND_HALF_UP		//四舍五入
RoundingMode.HALF_DOWN  =>	BigDecimal.ROUND_HALF_DOWN		//五舍六入
RoundingMode.HALF_EVEN  =>	BigDecimal.ROUND_HALF_EVEN		//银行家算法

BigDecimal.ROUND_UP		//始终进一
BigDecimal.ROUND_DOWN		//始终舍去

6-11 订单校验(8)SpringBoot默认注入单例模式所带来的问题

SringBoot中如果一个类要注入另外一个组件那么这个类本身就应该在Spring中,也就是也是一个组件。
当把CouponChecker当作一个组件后和原本不把它当成是组件产生的变化:原来的CouponChecker不是单例的, 而在上面标注@Service之后CouponChecker变成了单例的。单例模式带来的最直接的问题就是,单例的类中的成员变量也应该是单例的。在单例模式中写一个成员变量实际上就和定义了一个静态变量是一样的行为。

但是想让Java对象脱离SpringBoot托管有些类是非常非常麻烦的。

6-13 订单校验(10)prototype多例模式的注入方案(ObjectFactory与动态代理)

public class TestController {

    @Autowired
    private Test test;

这样的注入方式只有在TestController实例化的时候会执行一次注入Test。

    @GetMapping("")
    public void getDetail(@Autowired Test test){
        System.out.println(this.test);
    }

这种注入方式可以在每次调用getDetail()的时候都注入Test,但是这种方式不推荐,比较麻烦
推荐两种方式,
方式一:

@Getter
@Setter
@Component
@Scope("prototype")
public class Test {
    private String name = "7";
}

    @Autowired
    private ObjectFactory<Test> test;

    @GetMapping("")
    public void getDetail(){
        System.out.println(this.test.getObject());
    }

方式二:
动态代理的方式

@Getter
@Setter
@Component
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Test {
    private String name = "7";
}

public class TestController {

    @Autowired
    private Test test;

    @GetMapping("")
    public void getDetail(){
        System.out.println(this.test);
    }
}

在这里插入图片描述

第18+周 订单、延迟支付与库存归还

第1章 订单参数校验(下)

1-1 订单校验(12)BO的概念与意义

带有使用分类限制的优惠券校验
canBeUsed()方法需要接受的参数有:
sku price <= sku_model
sku count <= order
sku categoryId <= sku_model
coupon category
此方法接受的参数我们封装成一个BO

通常如果说我们的业务非常复杂的情况下,我们在业务与业务之间要去交换数据的话,我们为这些数据定义的对象可以把它称作是BO
是否需要建立BO视具体业务的复杂度而定。

1-3 订单校验(14)reduce函数的巧妙应用

    private BigDecimal getSumByCategory(List<SkuOrderBO> skuOrderBOList,Long cid){
        BigDecimal sum = skuOrderBOList.stream()
                .filter(sku -> sku.getCategoryId().equals(cid))
                .map(bo -> bo.getTotalPrice())
                .reduce(BigDecimal::add)
                .orElse(BigDecimal.ZERO);
        return sum;
    }

干货!如何用.map() .filter() 和.reduce()给代码做“减法”

1-5 订单校验(16)OrderChecker对象

调用关系可以分为两种
OrderChecker CouponChecker XXXChecker //平行关系

OrderService -> OrderChecker -> CouponChecker //外观模式 facade

OrderChecker要校验参数并且返回数据

1-6 订单校验(17)OrderChecker校验项分析

  • orderTotalPrice serverTotalPrice
  • 下架
  • 售罄
  • 购买数量是否超出库存
  • 给sku设置一个整体的上限 在数据库中给每个sku再单独设置上限
  • 优惠券校验 CouponChecker

1-7 订单校验(18)最大购买数量、售罄、超卖检验

由于OrderChecker上标注了@Where(clause = "delete_time is null and online = 1 "),此处判断订单中是否有下架商品可以直接通过比较前端传过来的订单数据中sku的数量和服务器查出来的sku的数量,如果不相等就说明订单中有已下架的商品。

1-8 订单校验(19)如何让Spring调用Bean的有参构造函数?

应用@Configuration @Bean的方式注入

第2章 创建订单

2-1 下单逻辑(1)谈订单号的特点与注意事项

订单号既要考虑可读性也要考虑随机性,还要考虑订单号的长度,一般建议在10-16位之间,最长不要超过20位。
订单号最好不要数字字母混合,开头字母没关系。

为什么订单已经有一个id了还需要orderNo?
用自增的数字id做主键,从性能的角度来讲自增的数字是最好的,查询也最快,但是这种主键不太适合做分库分表,也不适合做分布式的数据库。假如,把order这个数据表分布到几个分布式的数据库里面去,如果标识一个订单的是依靠自增id,多个数据库的order中的id会出现重复的数字。
用orderNo去标识一个订单,orderNo本身具有很强的随机性,无论分几个数据库几个数据表,可以保证orderNo不重复。不管分几个库几个表都不会影响到order
用UUID或GUID这种很长的字符串做主键,查询起来效率是比较低的。

如果我们要做分布式是需要有一个全局的ID生成器。对于单体的应用主键或唯一标识最好的选择就是自增id,查询起来比较快也不需要自己去考虑id的生成。
即使我们建立了全局ID生成器,很多时候我们也不需要所有数据库里的数据表的主键全部都从全局的ID生成器里去取。数据量规模非常大的数据表才需要去做分库分表。

2-3 下单逻辑(3)订单的五种状态

为了避免超卖,在用户下单时预扣除库存。但是如果用户没有支付要把库存归还。
订单在时间范围内没有支付,自动变为已取消。

3-1 库存扣减(1)乱序思维

下订单操作逻辑:

  • reduceStock扣减库存
  • 核销优惠券
  • 加入到延迟消息队列

扣减库存时要避免超卖的情况,避免库存出现负数。虽然在订单校验的时候已经校验了库存,但是在下订单时仍然要对库存进行校验。虽然从订单校验isOk()到下订单placeOrder时间非常短,但是库存仍然有可能被其他线程的操作消耗掉了。web编程是在一个在多线程的环境下进行代码编写,任何时候都有可能有多个线程在操作统一资源,这也就是之前说的线程安全的问题,哪个线程的操作先执行不一定。

3-2 库存扣减(2)不能SQL1查询SQL2减除库存

isOk()的检测只是一个预检测,可以防止绝大多数情况下的下单失败,但是不能作为用户最终是否能下单成功的依据。

库存扣减
1.正数
2.负数
3.报错(库存不足)

4.加锁 排队 给库存量加上一把锁,拿到锁的线程先去判断库存够不够,其他线程排队等待
数据库加行锁 Java加锁

不能用两条sql,一条先查询先查询再判断,然后再执行update操作,A、B两个时间点之间还有一小段时间间隙,两条sql不是一个原子的操作。加事务并不能解决这个问题。

3-3 库存扣减(3)Java锁、悲观锁的解决方案探讨

要避免库存出现负数大致有3个方案:
1.Java加锁
2.数据库 加锁 —— 悲观锁
3.乐观锁 没有实际锁 :乐观锁更多时候是我们在处理并发竞态时候的一种思想
悲观锁和乐观锁只是一种思想,redis、mongoDB等还有Java代码里都可以应用这种思想,并不是关系型数据库中特有的。
从性能角度来讲使用悲观锁性能比较低。
在Java代码中加锁,不是直接加在数据库上的,在分布式的环境下,由于我们没有锁住最终操作的目标资源,多个进程不在同一个应用程序中,只能保证每一处应用程序中的进程是有序的,不能保多个应用程序中的线程是有序访问的。通常如果是单体应用可以考虑在Java中加锁,但是没有必要,因为使用乐观锁的成本也非常低。

悲观锁 大概的步骤
1.开启事务
2.select……sku.stock for update
3.校验
4.update set stock - count
5.commit
事务和锁没有直接的联系,此处我们是在事务中自己加锁。悲观锁与乐观锁只是相对的概念,并不是一个特定的语法。
不建议使用悲观锁。在高并发的环境下频发的加锁和解锁对数据库性能消耗是比较大的。其次,悲观锁我们期望的是行级锁,但是如果索引设置不正确的话是有可能造成锁住整张表的

3-5 优惠券核销(1)

方法上标注@Transactional加事务。当要连续的更新或新增的操作不同数据表时是必须要加上事务的。

4-1 订单状态查询(1)

tab栏最好不要超过5个,这里我们将已取消的订单合并到代付款。
订单超时未支付失效,用延迟消息队列的方式触发。

4-2 订单状态查询(2)订单支付过期状态的不可信

订单延迟支付,已取消的订单触发方式有3种:
1.用户主动取消
2.客服在CMS中取消
3.支付时间到期,主动触发更改状态(此处使用延迟消息队列)
此处的难点不在于延迟消息队列,而是在于我们要考虑订单状态没有成功改变。跟据createTime和pay-time-limit自己计算订单是否过期。两种方式:1.expiredTime(过期时间)写入order表 2.

第19周 微信支付、Redis与RocketMQ

第1章 微信支付

1-1 微信支付流程分析

微信支付业务流程
在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200419124649575.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hmeF8xOTk0,size_16,color_FFFFFF,t_70
在这里插入图片描述

1-2 订单过期时间的再次校验

微信支付时需要对订单信息再次校验,订单的有效时间等。

1-4 微信支付配置类

微信支付商户平台
点击开发文档(V2板) ——> JSAPI支付 ——> SDK与DEMO下载
将demo中的WxPayAPI_JAVA\java_sdk_v3.0.9\src\main\java\com中的github目录粘贴到我们项目中的com目录下

1-5 统一下单(预订单)接口参数解析

1-6 统一下单的常用参数组装

微信支付服务后台生成预支付交易单

WxPaymentService.java

1-7 配置微信支付回调API地址

外网IP解决方案:

  • 上线 租用 云端服务器 ECS 自带外网IP
    调试不方便
  • 内网穿透 Ngrok、花生壳

1-8 更新订单的PrepayId

调用微信“统一下单”接口——unifiedOrder后会返回微信的预付订单id——PrepayId,用户有可能不支付,我们要将PrepayId保存,当客户端再次调用wx.requestPayment拉起微信支付时还可以使用之前的PrepayId。

1-9 微信支付签名计算

发起微信支付
wx.requestPayment(Object object)

第2章 微信回调通知处理

2-1 微信支付回调结果WxNotify接收与处理

第3章 redis与redis的键空间通知

3-1 延迟消息队列是什么?能解决什么问题?

延迟消息队列 用于取代定时器
优惠券过期和订单过期。
Coupon没有明确的标识出过期标识Status,而是查询时用时间限制。因为过期不好触发。
Order如果只是查询也没有必要明确的标识Status,因为有expiredDate。但是订单有一个非常重要的问题——库存。
下单延迟支付是预扣除库存,订单过期就要归还库存Stock
1.怎么归还库存
2.什么时候
3.谁来触发归还库存的操作
修改订单状态并不是主要目的,主要目的是归还库存,顺带修改订单状态。

触发方式:
1.主动轮询/其他应用 时间间隔长不够精确,时间间隔短比如1s扫描一次,浪费性能。定时器频繁扫描数据库浪费性能。
定时器:Timer 定时器 linux下的Cortab定时任务 定时任务框架Quartz 等等
这些本质都是主动轮询
2.被动触发 用户触发
3.队列 数据结构:先进先出
RocketMQ,RabbitMQ,Kafka
4.延迟消息队列
队列主动返回给我们
Redis 、 RocketMQ比较适合。 RabbitMQ不是原生的支持延迟消息队列,需要我们自己装插件,比较麻烦。
如果项目是轻量级的适合用redis的key-space,但是精度和可靠性不如RocketMQ高;RocketMQ精度和可靠性比Redis高,但是性能消耗比较大。

3-2 Redis的安装与测试

redis的难点不在于命令和数据结构在于如何与业务结合去解决问题。
官网下载的只支持linux,windows的版本需要到github上下载
启动命令 redis-server
redis-cli 连接客户端
在这里插入图片描述
修改redis密码
windows中安装,修改配置文件redis.windows.confredis.windows-service.conf中的requirepass(line:386),重启redis
在这里插入图片描述

3-3 Redis键空间通知(KeySpaceNotifyfication)

当redis受到某种事件的影响,比如del、expired,就会发布一个通知。
严格意义上来讲,redis的键空间通知并不是我们所说的延迟消息队列,因为实现机制不是以队列的形式来实现的。其实就是常规的pub/sub,发布/订阅机制。我们首先订阅一个事件的通知,当这个事件触发了之后,redis就会发布这个事件对应的通知。
redis的键空间通知有两种类型:
比如有键 name : xue 执行del事件
key-space 返回事件的名称 del
key-event 返回删除的键的名字 name
我们想接收哪种类型的通知就订阅哪种类型的通知。

事件的类型: 详细的命令代号可以在redis的配置文件中看到

#  K     Keyspace events, published with __keyspace@<db>__ prefix.
#  E     Keyevent events, published with __keyevent@<db>__ prefix.
#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
#  $     String commands
#  l     List commands
#  s     Set commands
#  h     Hash commands
#  z     Sorted set commands
#  x     Expired events (events generated every time a key expires)
#  e     Evicted events (events generated when a key is evicted for maxmemory)
#  A     Alias for g$lshzxe, so that the "AKE" string means all the events.

3-4 key-event通知订阅机制

开启事件通知

在redis的配置文件redis.conf(windows中为redis.windows.conf)中添加配置项

notify-keyspace-events Ex

在这里插入图片描述
修改配置文件,再重启redis服务,然后重启项目,接收到了键空间通知
windows中通过命令行修改配置参数,直接修改配置文件不好使

CONFIG GET *	//查看所有配置项
CONFIG SET notify-keyspace-events Ex	//设置配置项notify-keyspace-events为Ex
CONFIG GET notify-keyspace-events

E—— 开启Keyevent events事件,x——开启 Expired events(过期)事件

消息订阅

cmd中输入命令:

redis-cli
psubscribe __keyevent@0__:expired

setex name 10 xue

psubscribe keyevent@0:expired 表示我们在redis的客户端cli中订阅了keyevent在第0号数据库中的expired事件的通知,
在这里插入图片描述
再打开一个cmd,输入命令setex name 10 xue setex可以设置一个key 指定过期时间 value值

SUBSCRIBE redischat	订阅消息			
PUBLISH redischat "learn redis"	发布消息

3-5 SpringBoot中的Redis配置

redis的键空间通知可以用到很多地方,但是最适合用到的地方就是用来处理订单的库存归还。订单失效时,归还订单对应的库存和用户的优惠券。
当程序执行placeOrder()下订单时,redis写一个key设置一个过期时间,过期时间和我们的延迟支付的时间相同。当redis的键过期的时候就可以给我们的代码一个通知。

redis:
  localhost: localhost	//ip
  port: 6379			//端口号
  database: 7			//指定当前应用程序用来操作7号数据库,redis总共有0~15号 16个数据库
  password:
  listen-pattern: __keyevent@7__:expired

我们现在用redis做键空间通知专门用来归还库存和优惠券,建议我们单独给这个业务指定一个数据库,也就是7号数据库专门用来存放归还库存和优惠券的数据,不要往7号数据库里写其他的redis的数据。

3-6 将订单、优惠券信息写入Redis

引入redis依赖

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

向redis发送请求stringRedisTemplate.opsForValue().set()有很多重载方法,此处传入4个参数,键、值、过期时间、时间单位

    private void sendToRedis(Long oid,Long uid,Long couponId){
        String key = oid.toString()+","+uid.toString()+","+couponId.toString();
        try{
            stringRedisTemplate.opsForValue().set(key,"1",payTimeLimit, TimeUnit.SECONDS);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

3-7 MessageListener

新建manager包,主要用于对接一些通用的第三方服务的。在阿里的分层指引规范里也是有这一层的。

设置redis的监听器

package com.lin.missyou.manager.redis;

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;

public class TopicMessageListener implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] bytes) {
        byte[] body = message.getBody();
        byte[] channel =message.getChannel();
        String expiredKey = new String(body);
        String topic = new String(channel);
        System.out.println(expiredKey);
        System.out.println(topic);
    }
}

3-8 设置Listener和Topic

将监听器加入到IOC容器

package com.lin.missyou.manager.redis;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;

@Configuration
public class MessageListenerConfiguration {

    @Value("${spring.redis.listen-pattern}")
    public String pattern;

    @Bean
    public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory redisConnection){
    	//container负责连接redis服务器 并且将之前写的listener绑定到监听里
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnection);	//redisConnection取到redis的连接信息
        Topic topic = new PatternTopic(pattern);	//监听主题初始化
        container.addMessageListener(new TopicMessageListener(),topic);	
        return container;
    }
}
总结 springboot中监听和接收redis的键空间通知的过程
1.设置监听器listener 用于完成当redis回调给我们数据后的业务处理,写在message()中
2.configuration配置文件 用于返回container,负责我们springboot代码与redis的联系的

3-9 测试Listener

记录使用rabbit mq处理订单超时业务

第5章 RocketMQ的延迟消息队列应用

5-1 常见消息队列

RabbitMQ RocketMQ Kafka ActiveMQ
最常见的是前3种,选择RocketMQ是因为我们要用的是延迟消息队列

服务器 -> 消息队列 普通的消息队列是服务器主动到消息队列里去取
消息队列 -> 服务器 我们需要的是被动触发,消息队列按照我们设定的时间主动通知服务器从而触发某些逻辑的执行

定时器 轮询 不够精确,使用延迟消息队列相对更精确

RocketMQ是自带有延迟消息队列的,RabbitMQ和Kafka原生是没有延迟消息队列,通过安装插件让RabbitMQ和Kafka支持延迟消息队列。

5-2 订单过期状态的3种解决方案总结

1.create_time + expiredTime = expiredTimePoint Now
2.expiredTimePoint 跟第一种差不多,只是在订单创建的时候就把过期时间记录到数据库了
3.Redis Canceled / RocketMQ 更改订单的status为已取消,这种方案不同于上面两种,可以直接通过状态筛选出订单
第三种相比于1,2种status是相对准确的
为了提高订单status的准确率,我们还可以做一个扫描机制。做一个离线脚本,存在于另一个应用程序,定期扫描数据库改变订单状态。这种情况下扫描的周期通常是非常长的,或者选择服务器负载非常低的时候。

5-3 谈消息队列的应用场景

消息队列是非常重要也是非常好的提高系统并发能力的机制。但是引用的东西越多,系统的维护成本就会越高。

消息队列给我们应用程序带来的好处和帮助:
1.解耦 消息队列是一个很好的中间件

从代码的模式上降低系统的复杂性,便于维护
比如:有A、B两个应用,A只负责生产订单,后续的逻辑交给B应用处理。A生产订单后把订单消息加入到消息队列,然后B再去消息队列里取,这样消息队列就成了中间件桥接起了A、B两个应用。

2.异步 下单 -> 提示成功 验证 发送短信、邮件、仓储调度

从异步的角度提高系统的性能,提高系统性能,削峰
比如:下单后服务端返回下单成功的消息,服务端验证,发送短信、邮件、仓储调度以一个一个的消息加入到消息队列里去,我们可以控制业务执行的频度,可以减少服务器在某一时间点的压力,减少系统宕机的概率。消息队列原本被发明出来就是为了解决这个问题的。这也是消息队列主要的应用场景。

也可以不用消息队列,用开启线程的方式也可以实现,但是缺点是没有解耦的作用,代码复杂的时候不好维护。在当前的应用中开启一个一个的线程,也没有办法很好的去分摊性能,开启线程也还是运行在本机上面,并发量大的时候是没有办法把发送短信,邮件的功能分摊到其他机器上的。我们应用消息队列最主要的是可以把消息队列做成一种集群,消息队列可以充当中间件,把复杂的业务分摊到其他机器上去。

5-4 启动RocketMQ的NameSrv和Broker

Windows下安装,启动RocketMQ步骤:(课程中使用Linux)
官网下载压缩包,解压。
分别在runserver.cmd和runbroker.cmd中添加JAVA环境配置

set "JAVA_HOME=C:\Program Files\Java\jdk1.8.0_191"

cmd中输入命令 start mqnamesrv.cmd启动生产者,start mqbroker.cmd启动消费者

RocketMQ安装For Windows 10

5-5 生产者和消费者的简单测试

5-6 RocketMQ延迟消息队列原理与延迟精度

定时精度
RocketMQ不支持任意精度的设置,RocketMQ内置了一组时间点,我们可以在一组时间点里选择一个符合自己的时间点。
在这里插入图片描述

pom.xml中引入rocketmq依赖

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.7.0</version>
</dependency>

5-7 生产者实例化(上)

application-dev.yml中添加配置

rocketmq:
  producer:
    producer-group: SleeveProducerGroup
  namesrv-addr: 127.0.0.1:9876

5-8 生产者实例化(2)

5-9 生产者实例化(3)发送消息

package com.lin.missyou.manager.rocketmq;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class ProducerSchedule {

    private DefaultMQProducer producer; //默认生产者

    @Value("${rocketmq.producer.producer-group}")
    private String producerGroup;

    @Value("${rocketmq.namesrv-addr}")
    private String namesrvAddr;

    public ProducerSchedule() {

    }

    @PostConstruct
    public void defaultMQProducer(){
        if(this.producer == null){
            this.producer = new DefaultMQProducer(this.producerGroup);
            this.producer.setNamesrvAddr(this.namesrvAddr);
        }
        try{
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }

    public String send(String topic, String messageText) throws Exception {
        Message message = new Message(topic, messageText.getBytes());
//      messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
        message.setDelayTimeLevel(2);   //延时级别9,即5min

        SendResult result = this.producer.send(message);
        System.out.println(result.getMsgId());
        System.out.println(result.getSendStatus());
        return result.getMsgId();
    }
}

5-10 消费者类与匿名类写法

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三毛村滴雪鱼粉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值