谷粒商城项目学-分布式基础

项目框架图

谷粒商城项目学-分布式基础_第1张图片

分布式基础概念

• 微服务、注册中心、配置中心、远程调用、Feign、网关
• 2、基础开发
• SpringBoot2.0、SpringCloud、Mybatis-Plus、Vue组件化、阿里云对象存储
• 3、环境
• Vagrant、Linux、Docker、MySQL、Redis、逆向工程&人人开源
• 4、开发规范
• 数据校验JSR303、全局异常处理、全局统一返回、全局跨域处理
• 枚举状态、业务状态码、VO与TO与PO划分、逻辑删除
• Lombok:@Data、@Slf4j

前情提要

本项目全部都采用了最新配置,和视频版本不一样。报错改了好久。

技巧

将一个实体的值复制到另一个实体,不需要单独一个个get,set了
BeanUtils.copyProperties(attr,attrEntity);

大数据情况下不建议联表查询,笛卡尔积,应分开查询

日期格式化 统一设置
spring
jackson:
date-format: yyyy-MM-dd HH:mm:ss

ge >=
gt >
le <=

如果传过来的数据可能为空,用包装类

对于不重要查询失败错误,不要回滚事务,直接trycatch不进行处理

对于部分数据可能有取消原有信息的处理:直接先全部删除所有数据,再依次添加新数据,不用依次判断是否删除数据。

parentPath.toArray(new Long[0]);
区别(new Long[])parentPath.toArray()

循环查库
循环依赖

复杂的json网上可以直接生成java实体类 bejson.com

stream.map和ForEach区别

BigDecimal的比较
skuReductionTo.getFullPrice().compareTo(new BigDecimal(“0”)) == 1

  • 排除数据源相关的配置
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

  • 熟悉流式编程
    过滤、排序、统计分组
    多线程高效

Lambda 表达式中访问的局部变量不能为变量

        Long purchaseId = mergeVo.getPurchaseId();
        if(purchaseId == null){
            //...
            purchaseId = purchaseEntity.getId();
        }
        Long finalPurchaseId = purchaseId;
  • 这里finalPurchaseId和purchaseId没区别,final是因为lambda表达式的机制要求使用的局部变量不能为变量
  • Java 8 之后,在匿名类或 Lambda 表达式中访问的局部变量,如果不是 final 类型的话,编译器自动加上 final 修饰符,即Java8新特性:effectively final

配置

  • 设置容器自动启动
    docker update redis --restart=always

  • linux的mysql和docker中的mysql端口碰撞
    3306: bind: address already in use
    netstat -tanlp # 查看所有已被占用端口和所在进程ID
    kill 101427 # 杀死当前进程

  • ctrl+alt+shift+u 查看Pom依赖冲突

整合MyBatis-Plus

需要看文档熟悉

1)、导入依赖

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.2.0version>
dependency>

2)、配置
1、配置数据源;
1)、导入数据库的驱动。https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-versions.html
2)、在application.yml配置数据源相关信息
2、配置MyBatis-Plus;
1)、使用@MapperScan
2)、告诉MyBatis-Plus,sql映射文件位置

service中不需要手动注入dao

public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity>

//继承的ServiceImpl已经自动注入了baseMapper就是CategoryDao
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
    protected Log log = LogFactory.getLog(this.getClass());
    @Autowired
    protected M baseMapper;

自定义属性

@TableField(exist = false)
private List childrean;

只有当该字段不为空时,才返回

@JsonInclude(JsonInclude.Include.NON_EMPTY)
应用场景:三级分类最后一个分类的子分类children字段不需要置为空,而是直接不需要

逻辑删除

1)、配置全局的逻辑删除规则(省略)

 mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

2)、配置逻辑删除的组件Bean(低版本)
3)、给Bean加上逻辑删除注解@TableLogic

批量删除

<delete id="deleteBatchRelation">
        DELETE FROM `pms_attr_attrgroup_relation` WHERE
        <foreach collection="entities" item="item" separator=" OR ">
            (attr_id=#{item.attrId} AND attr_group_id=#{item.attrGroupId})
        foreach>
    delete>

非自增主键

public enum IdType {
     //数据库ID自增
    AUTO(0),
    // 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
    NONE(1),
    //用户输入ID:该类型可以通过自己注册自动填充插件进行填充
    INPUT(2),
    
@TableId(type = IdType.INPUT)
private Long spuId;

springcloud

结合 SpringCloud Alibaba 我们最终的技术搭配方案:
SpringCloud Alibaba - Nacos:注册中心(服务发现/注册)
SpringCloud Alibaba - Nacos:配置中心(动态配置管理)
SpringCloud - Ribbon:负载均衡
SpringCloud - Feign:声明式 HTTP 客户端(调用远程服务)
SpringCloud Alibaba - Sentinel:服务容错(限流、降级、熔断)
SpringCloud - Gateway:API 网关(webflux 编程模式)
SpringCloud - Sleuth:调用链监控
SpringCloud Alibaba - Seata:原 Fescar,即分布式事务解决方案

feign

1、想要远程调用别的服务
1)、引入open-feign

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        //一定要加
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-loadbalancerartifactId>
        dependency>

2)、编写一个接口,告诉SpringCloud这个接口需要调用远程服务
1、声明接口的每一个方法都是调用哪个远程服务的那个请求

@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    @GetMapping("/coupon/coupon/member/list") //地址要写全
    public R memberCoupons();
}

3)、开启远程调用功能
@EnableFeignClients(basePackages = “com.vinson.gulimall.member.feign”)

两种请求模式

1)、让所有请求过网关;
1、@FeignClient(“gulimall-gateway”):给gulimall-gateway所在的机器发请求
2、/api/product/skuinfo/info/{skuId}
2)、直接让后台指定服务处理
1、@FeignClient(“gulimall-product”)
2、/product/skuinfo/info/{skuId}

实体类无需一致

1、CouponFeignService.saveSpuBounds(spuBoundTo);
1)、@RequestBody将这个对象转为json。
2)、找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。
将上一步转的json放在请求体位置,发送请求;
3)、对方服务收到请求。请求体里有json数据。
(@RequestBody SpuBoundsEntity spuBounds);将请求体的json转为SpuBoundsEntity;
只要json数据模型是兼容的。双方服务无需使用同一个to

Nacos

使用

1)、引入依赖,

 <dependency>
 <groupId>com.alibaba.cloudgroupId>
 <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
 dependency>
 //一定要加
 <dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-bootstrapartifactId>
 dependency>

2)、创建一个bootstrap.properties。
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
3)、需要给配置中心默认添加一个叫 数据集(Data Id)gulimall-coupon.properties。默认规则,应用名.properties
4)、给 应用名.properties 添加任何配置
5)、动态获取配置。
@RefreshScope:动态获取并刷新配置
@Value(“${配置项的名}”):获取到配置。
如果配置中心和当前应用的配置文件中都配置了相同的项,优先使用配置中心的配置。

细节

1)、命名空间:配置隔离;
默认:public(保留空间);默认新增的所有配置都在public空间。
1、开发,测试,生产:利用命名空间来做环境隔离。
注意:在bootstrap.properties;配置上,需要使用哪个命名空间下的配置,
spring.cloud.nacos.config.namespace=9de62e44-cd2a-4a82-bf5c-95878bd5e871
2、每一个微服务之间互相隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置

2)、配置集:所有的配置的集合

3)、配置集ID:类似文件名。
Data ID:类似文件名

4)、配置分组:
默认所有的配置集都属于:DEFAULT_GROUP;
1111,618,1212
spring.cloud.nacos.config.group=1111

项目中的使用:每个微服务创建自己的命名空间,使用配置分组区分环境,dev,test,prod

3、同时加载多个配置集
1)、微服务任何配置信息,任何配置文件都可以放在配置中心中
2)、只需要在bootstrap.properties说明加载配置中心中哪些配置文件即可
3)、@Value,@ConfigurationProperties。。。
以前SpringBoot任何方法从配置文件中获取值,都能使用。
配置中心有的优先使用配置中心中的,

spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

spring.cloud.nacos.config.namespace=d5c78392-5fd3-4737-ab6e-3d3038f7bd82
# 默认的配置分组
spring.cloud.nacos.config.group=prod

spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true

spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true

spring.cloud.nacos.config.extension-configs[2].data-id=other.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true

网关

谷粒商城项目学-分布式基础_第2张图片

将renren-fast加入到注册中心,需要加cloud配置,一定要和原Boot版本对应!!!
http://localhost:88/api/captcha.jpg
http://localhost:8080/renren-fast/captcha.jpg
http://localhost:88/api/product/category/list/tree
http://localhost:10000/product/category/list/tree
通过过滤方法中的路径转换功能

  • 需要注意的是路由的顺序是由上至下,精确的应该放上面,粗略的放下面
  • 新版的路径需要加个?
  • 并且加上Loadbalancer依赖,版本一定要和cloud,boot对应!!!

    org.springframework.cloud
    spring-cloud-starter-loadbalancer
    3.1.1
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/?(?>.*),/$\{segment}
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/?(?>.*),/renren-fast/$\{segment}

跨域

跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是
浏览器对javascript施加的安全限制。
• 同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;
谷粒商城项目学-分布式基础_第3张图片
谷粒商城项目学-分布式基础_第4张图片

使用nginx部署为同一域

谷粒商城项目学-分布式基础_第5张图片

不适合开发阶段

配置当次请求允许跨域

1、添加响应头
Access-Control-Allow-Origin:支持哪些来源的请求跨域
Access-Control-Allow-Methods:支持哪些方法跨域
Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含
cookie
Access-Control-Expose-Headers:跨域请求暴露的字段
CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

@Configuration
public class GuliCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOriginPattern("*");
        corsConfiguration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);
    }
}

@RequestBody:获取请求体,必须发送POST请求
SpringMVC自动将请求体的数据(json),转为对应的对象

文件存储

阿里云的对象存储
谷粒商城项目学-分布式基础_第6张图片

普通上传方式

用户经服务器上传,会影响带宽,加大压力

服务端签名后直传

谷粒商城项目学-分布式基础_第7张图片

看官网
1、引入oss-starter
2、配置key,endpoint相关信息即可
3、使用OSSClient 进行相关操作

JSR303数据校验

1)、给Bean添加校验注解:javax.validation.constraints,并定义自己的message提示
@NotBlank(message = “品牌名必须提交”)
2)、开启校验功能@Valid
public R save(@Valid @RequestBody BrandEntity brand){
效果:校验错误以后会有默认的响应;
3)、给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
4)、分组校验(多场景的复杂校验)
1)、 @NotBlank(message = “品牌名必须提交”,groups = {AddGroup.class,UpdateGroup.class})
给校验注解标注什么情况需要进行校验
2)、@Validated({AddGroup.class})
3)、默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效;

5)、自定义校验
1)、编写一个自定义的校验注解
2)、编写一个自定义的校验器 ConstraintValidator
3)、关联自定义的校验器和自定义的校验注解
看源码实现自定义,找格式搬

@Documented
//可以指定多个不同的校验器,适配不同类型的校验
@Constraint(
        validatedBy = {ListValueConstraintValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
    String message() default "{com.vinson.common.validator.group.ListValue.message}";

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

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

    int[] vals() default {};
}
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> set=new HashSet<>();
    //初始化
    @Override
    public void initialize(ListValue listValue) {
        int[] vals = listValue.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    //校验
    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(integer);
    }
}

@NotBlank(message = “品牌名必须提交”) 至少一个字符
@NotEmpty可以为空串
@Min(value = 0)

统一的异常处理

@ControllerAdvice
1)、编写异常处理类,使用@ControllerAdvice。
2)、使用@ExceptionHandler标注方法可以处理的异常。

@Slf4j
@RestControllerAdvice(basePackages = "com.vinson.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e) {
        log.info(e.getMessage() + e.getClass());
        BindingResult bindingResult = e.getBindingResult();
        HashMap<String, String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach((item) -> {
            map.put(item.getField(), item.getDefaultMessage());
        });
        return R.error(400, "提交数据不合法").put("data", map);
    }

    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        log.error("错误:",throwable);//需要输出log,不然报错啥也看不到
        return R.error();
    }
}

系统错误码

错误码和错误信息定义类

  1. 错误码定义规则为 5 为数字
  2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
  3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
    错误码列表:
    10: 通用
    001:参数格式校验
    11: 商品
    12: 订单
    13: 购物车
    14: 物流
public enum BizCodeEnum {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败");

    private int code;
    private String msg;
    BizCodeEnum(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());

SPU 与 SKU

SPU:Standard Product Unit(标准化产品单元)
是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
SKU:Stock Keeping Unit(库存量单位)
即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU 这是对于大型连锁超市DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的 SKU 号。

基本属性【规格参数】与销售属性
每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的
属性;
 属性是以三级分类组织起来的
 规格参数中有些是可以提供检索的
 规格参数也是基本属性,他们具有自己的分组
 属性的分组也是以三级分类组织起来的
 属性名确定的,但是值是每一个商品不同来决定的

spu举例
谷粒商城项目学-分布式基础_第8张图片

sku举例
谷粒商城项目学-分布式基础_第9张图片

数据库设计
谷粒商城项目学-分布式基础_第10张图片

谷粒商城项目学-分布式基础_第11张图片

Object 划分

  • PO(persistant object) 持久对象
  • DO(Domain Object)领域对象
  • TO(Transfer Object) ,数据传输对象
    A传给B,通过Json的形式,两边都是object
  • DTO(Data Transfer Object)数据传输对象
  • VO(value object) 值对象
    View Object:视图对象
    不是数据库的数据
    接受页面传递来的数据,封装对象
    将业务处理完成的对象,封装成页面要用的数据
  • BO(business object) 业务对象
    我们可以把教育经历对应一个 PO ,工作经历对应一个 PO ,社会关系对应一个 PO 。 建立一个对应简历的 BO 对象处理简历
  • POJO(plain ordinary java object) 简单无规则 java 对象
  • DAO(data access object) 数据访问对象

实体类的构造

优惠生效情况[1111(四个状态位,从右到左);0 - 无优惠,成长积分是否赠送;1 - 无优惠,购物积分是否赠送;2 - 有优惠,成长积分是否赠送;3 - 有优惠,购物积分是否赠送【状态位0:不赠送,1:赠送】]
private Integer work;

采购流程

定时任务
谷粒商城项目学-分布式基础_第12张图片

待完善:

  • 失败理由
  • 部分失败,应采购10,实采购8

三级分类

@Override
    public List<CategoryEntity> listWithTree() {
        //1查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2组装父子结构
        //2.1 找到所有一级分类
        List<CategoryEntity> level1Menus = entities.stream().filter((categoryEntity ->
                categoryEntity.getParentCid() == 0)
        ).map((menu)->{
            menu.setChildren(getChildrens(menu,entities));
            return menu;
        }).sorted((menu1,menu2)->{
            return (menu1.getSort()==null?0:menu1.getSort())-(menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());
        return entities;
    }

    //递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
        List<CategoryEntity> children = all.stream().filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId()
        ).map(categoryEntity -> {
            categoryEntity.setChildren(getChildrens(categoryEntity, all));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());
        return children;
    }

你可能感兴趣的:(Java项目,分布式,mybatis,java)