• 微服务、注册中心、配置中心、远程调用、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})
熟悉流式编程
过滤、排序、统计分组
多线程高效
Long purchaseId = mergeVo.getPurchaseId();
if(purchaseId == null){
//...
purchaseId = purchaseEntity.getId();
}
Long finalPurchaseId = purchaseId;
设置容器自动启动
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依赖冲突
需要看文档熟悉
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映射文件位置
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 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,即分布式事务解决方案
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
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
将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
通过过滤方法中的路径转换功能
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施加的安全限制。
• 同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;
不适合开发阶段
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),转为对应的对象
用户经服务器上传,会影响带宽,加大压力
看官网
1、引入oss-starter
2、配置key,endpoint相关信息即可
3、使用OSSClient 进行相关操作
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();
}
}
错误码和错误信息定义类
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:Standard Product Unit(标准化产品单元)
是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
SKU:Stock Keeping Unit(库存量单位)
即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU 这是对于大型连锁超市DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的 SKU 号。
基本属性【规格参数】与销售属性
每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的
属性;
属性是以三级分类组织起来的
规格参数中有些是可以提供检索的
规格参数也是基本属性,他们具有自己的分组
属性的分组也是以三级分类组织起来的
属性名确定的,但是值是每一个商品不同来决定的
优惠生效情况[1111(四个状态位,从右到左);0 - 无优惠,成长积分是否赠送;1 - 无优惠,购物积分是否赠送;2 - 有优惠,成长积分是否赠送;3 - 有优惠,购物积分是否赠送【状态位0:不赠送,1:赠送】]
private Integer work;
待完善:
@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;
}