谷粒商城基础篇
1.环境搭建
1.1 环境配置:
CentOS 7.6
JDK1.8
mysql5.7
idea内的模块结构图
使用人人开源快速搭建后台管理系统
2.Spring Cloud Alibaba
1、简介
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用 微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布 式应用服务。 依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用 接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
2、为什么使用
SpringCloud Alibaba 的优势: 阿里使用过的组件经历了考验,性能强悍,设计合理,现在开源出来大家用 成套的产品搭配完善的可视化界面给开发运维带来极大的便利 搭建简单,学习曲线低。
结合 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,即分布式事务解决方
3、版本选择
由于 Spring Boot 1 和 Spring Boot 2 在 Actuator 模块的接口和注解有很大的变更,且 spring-cloud-commons 从 1.x.x 版本升级到 2.0.0 版本也有较大的变更,因此我们采取跟 SpringBoot 版本号一致的版本:
- 1.5.x 版本适用于 Spring Boot 1.5.x
- 2.0.x 版本适用于 Spring Boot 2.0.x
- 2.1.x 版本适用于 Spring Boot 2.1.
4、项目中的依赖
在 common 项目中引入如下。进行统一管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.1 SpringCloud Alibaba-Nacos[作为配置中心]
Nacos 是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理 平台。他是使用 java 编写。需要依赖 java 环境 Nacos 文档地址: https://nacos.io/zh-cn/docs/quick-start.
2.1.1下载Nacos
https://github.com/alibaba/nacos/releases
2.1.2启动Nacos-server
- 双击 bin 中的 startup.cmd 文件
- 访问 http://localhost:8848/nacos/
- 使用默认的 nacos/nacos 进行登录
2.1.3 将微服务注册到 nacos
1、首先,修改 pom.xml 文件,引入 Nacos Discovery Starter
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.1.1.在项目中引入stater
<!--引入nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
-----------------------------------------------------------------------------------
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.在yaml配置文件中设置IP地址和端口号,并且为每一个服务设置application name
3.Application类上标记@EnableDiscoveryClient注解开启服务发现功能
2.2 Feign 声明式远程调用
2.2.1 简介
Feign 是一个声明式的 HTTP 客户端,它的目的就是让远程调用更加简单。Feign 提供了 HTTP 请求的模板,通过编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地 址等信息。 Feign 整合了 Ribbon(负载均衡)和 Hystrix(服务熔断),可以让我们不再需要显式地使用这 两个组件。 SpringCloudFeign 在 NetflixFeign 的基础上扩展了对 SpringMVC 注解的支持,在其实现下,我 们只需创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。简化了 SpringCloudRibbon 自行封装服务调用客户端的开发量。
2.2.2 使用
1、引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId> </artifactId>
</dependency>
2、开启 feign 功能
@EnableFeignClients(basePackages = "com.atguigu.gulimall.pms.feign")
远程调用的一方要声明一个接口,并且标记@FeignClient注解,注解的参数要指定微服务名称。(前提是该微服务已经注册到注册中心)
3、声明远程接口
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/skus")
public Resp<List<SkuStockVo>> skuWareInfos(@RequestBody List<Long> skuIds);
}
//使用@FeignClient注解指定要调用哪个微服务(前提是这个这些微服务都必须注册到注册中心中)
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
//调用这个微服务的controller的方法
//注意@RequestBody注解的使用
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
@PostMapping("coupon/skufullreduction/saveinfo")
R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
//使用时直接注入这个远程接口 调用其方法
- 在主启动类上要加上注解@EnableFeignClients
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign") // basePackages(可选的)可以明确的指定远程调用的相关接口在那个位置下
2.2.3 原理
原理示意图:
3.SpringCloud 相关技术
3.1 Gateway组件
简介
网关作为流量的入口,常用功能包括路由转发、权限校验、限流控制等。而 springcloud gateway 作为 SpringCloud 官方推出的第二代网关框架,取代了 Zuul
nacos作为配置中心
1.引入依赖
<!--配置中心作配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
3.2Gateway网关配置及使用
3.2.1路由配置
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product
predicates :
- Path=/api/product/**
filters:
# 把前缀api去掉
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: admin_route
# 通过注册中心访问到renren-fast的服务器(负载均衡) 等于http://localhost:8080/
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
# 把前缀api替换成renren-fast
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
# 配置网关的路由规则 有先后顺序 所有要把小范围的断言写在前面 如上
路径匹配 :
如 前端发来的路径是 http://localhost:88(固定 所有的请求都要发到网关中 由网关进行路由映射)/api/..
只要匹配predicates断言成功 就映射到uri的地址及在注册中心的地址
- RewritePath=/api/(?<segment>.*),/$\{segment} 作用: 把api去掉 后面的照写
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment} 作用: 把api去掉替换成
renren-fast api后的路径照写
网关GateWay的组成:
网关的三大核心概念及其核心逻辑:
核心逻辑:
路由转发+执行过滤器链
三大核心:
Route(路由): 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
Predicate(断言):参考的是java8的java.util.function.Predicate开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
Filter(过滤): 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
我们的所有的请求都希望发送给88端口(Gateway服务的端口号)所以需要配置网关
配置
ES6语法
ES6语法
Vue
5.gulimall-product商品服务API
5.1catrgory 商品分类服务
5.1.1 菜单三级分类(递归)
流式编程及lambda表达式
- peek和map的区别
- peek返回值为空
- map必须有返回值
- filter后面如果是大括号表明是一个函数要有返回 小括号直接写
//直接过滤
filter(item -> item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() || item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode())
//返回的是真假
if(item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() || item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode() ){
return true;
}
return false;
过程:
表的结构信息:
表中的数据样例:
1、先清楚parentID=0的商品是一级菜单 所有的子菜单规则就是子菜单的parentID=它的父菜单的cat_id也就是说除了三级菜单没有子菜单外每一个菜单都有很多的子菜单 那么就可以给实体类加上一个属性,就是一个集合属性,我们查出来的子菜单可以设置到它的父菜单的这个属性中
@TableField(exist = false) //mybatis-plus注解 表示这个属性数据库中没有
private List<CategoryEntity> children; //查出来的所有子菜单都设置到这个属性集合中 那么在传给前端时就可使用这个集合 从而显示出树结构的菜单
实现代码:
map:
映射
map(非常常用)
1、传入一个函数型接口,接口的第一个参数就是当前集合的数据类型作为抽象方法的参数的数据类型 第二个参数的数据类型R即返回类型是一个不确定的,默认就是Object
2、相当于是SQL语句中的 将集合中的所有元素看做是一张表,我们只取表中的某一列的元素,对其进行一些操作,然后返回一列操作后的集合
select 函数(name/age....) from xxx
作用
- 返回一个当前集合的所有元素经过某种操作后的得到一个新的集合的一个流(数据类型可能发生变化)
- 通俗的讲 就是说 有一个集合里面有很多元素 你可以根据集合中的元素生成一个这个集合中的所有元素经过同一个操作后形成的新集合(新集合的数据类型可以发生改变)
过程概述:
通过递归调用方法(getChildrens)向CategoryEntity的childrens属性添加符合要求的元素。
进入方法后先根据父子菜单的规则进行过滤出传来的父菜单(一级菜单)的所有子菜单(二级菜单) 然后进入map进行递归查出子菜单(二级菜单)下的所有子菜单然后把子菜单作为一个集合设置到它的父菜单(二级菜单)的属性中 然后完成所有的子菜单(三级)集合注入到二级菜单的属性后,然后重新进入到上面的方法的map方法中把这些所有的一个一级菜单的所有子菜单(二级菜单)设置到一级菜单的属性中 这样 就完成了所有的子菜单集合都设置到了它的父菜单的集合属性中
实现代码:
/**
* 查出所有分类,并且以树形结构组装起来
*
* @return
*/
@Override
public List<CategoryEntity> listWithTree() {
//1、查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2、组装成为父子的属性结构
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 level1Menus;
}
/**
* 递归查找所有菜单的子菜单
*/
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream()
.filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
//1.找到子菜单
categoryEntity.setChildren(getChildrens(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
//2.菜单的排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return children;
}
5.1.2 逻辑删除
参考MyBatis-Plus官方文档
说明:
只对自动注入的sql起效:
- 插入: 不作限制
- 查找: 追加where条件过滤掉已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
- 更新: 追加where条件防止更新到已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
- 删除: 转变为 更新
例如:
- 删除:
update user set deleted=1 where id = 1 and deleted=0
- 查找:
select id,name,deleted from user where deleted=0
字段类型支持说明:
- 支持所有数据类型(推荐使用
Integer
,Boolean
,LocalDateTime
) - 如果数据库字段使用
datetime
,逻辑未删除值和已删除值支持配置为字符串null
,另一个值支持配置为函数来获取值如now()
附录:
- 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
- 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。
使用方法:
#步骤1: 配置com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig
- 例: application.yml
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
#步骤2: 实体类字段上加上@TableLogic
注解
@TableLogic
private Integer deleted;
商品业务要实现逻辑删除需要实现一下步骤:
1、配置全局的逻辑删除规则
2、如果是MyBatis3.1.1一下的版本需要配置逻辑删除的组件(高版本可以省略)
3、需要在要操作的实体类中的逻辑删除相关属性加上逻辑删除相关注解
5.2品牌管理Brand
5.2.1阿里云对象存储(OSS)
阿里云操作
-
开通oss存储功能
-
创建bucket
-
创建子账户 分配权限
操作示例:
使用云对象存储时我们的微服务工作流程:
普通上传是用户想连接应用服务器,再由应用服务器上传到OOS,但是会对应用服务器造成很大的压力。
另一种是用户直接上传到OOS,由js操作用户的密码等隐私控制,但是安全性就得不到保障。
所以要采用服务端签名后再上传的方式:
这样既可以保证用户的安全性也可以减轻服务器的压力。
使用步骤:
参考官方文档
1.引入相关依赖
引入阿里云oos依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.5.0</version>
</dependency>
2.代码实现
public void test() throws FileNotFoundException {
//填写创建的bucket的域名 上图
String endpoint = "oss-cn-beijing.aliyuncs.com";
String accessKeyId = "<accesskeyId>"; //创建的子账户的账号
String accessKeySecret = "<accesskeypwd>";//创建的子账户的密码
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("D:\\壁纸\\aaa.png"); //路径
//第一个参数就是创建的bucket名 第二个参数是自定义的文件名称
ossClient.putObject("<yourbucketname>", "aaa.png", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
}
---------------------------------------------------
使用spring自动注入的方式,但是要在配置文件中配置好相关的信息。
@Autowired
OSSClient ossClient;
public void test() throws FileNotFoundException {
// 上传文件流。
InputStream inputStream = new FileInputStream("D:\\壁纸\\aaa.png"); //路径
//第一个参数就是创建的bucket名 第二个参数是自定义的文件名称
ossClient.putObject("<yourbucketname>", "aaa.png", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
}
@Autowired相关问题
中途出现OssClient无法自动注入的问题,是因为OssClient创建相关的配置文件出现了问题,导致OssClient实例化失败从而无法自动注入。
第三方的依赖可以自动注入的原因:
查看第三方的API可以看到,API中编写了相关配置类,所以可以通过@Autowired自动注入
同时要注意返回值的类型,返回值的类型是OSS接口,如果自动注入的类型名是OssClient同样会导致自动注入失败。
例如:
@Configuration
@ConditionalOnClass(name = "com.alibaba.alicloud.oss.OssAutoConfiguration")
@ConditionalOnProperty(name = "spring.cloud.alicloud.oss.enabled", matchIfMissing = true)
@EnableConfigurationProperties(OssProperties.class)
@ImportAutoConfiguration(AliCloudContextAutoConfiguration.class)
public class OssContextAutoConfiguration {
@ConditionalOnMissingBean
@Bean
// 返回值得类型是OSS接口
public OSS ossClient(AliCloudProperties aliCloudProperties,
OssProperties ossProperties) {
if (ossProperties.getAuthorizationMode() == AliCloudAuthorizationMode.AK_SK) {
Assert.isTrue(!StringUtils.isEmpty(ossProperties.getEndpoint()),
"Oss endpoint can't be empty.");
Assert.isTrue(!StringUtils.isEmpty(aliCloudProperties.getAccessKey()),
"${spring.cloud.alicloud.access-key} can't be empty.");
Assert.isTrue(!StringUtils.isEmpty(aliCloudProperties.getSecretKey()),
"${spring.cloud.alicloud.secret-key} can't be empty.");
return new OSSClientBuilder().build(ossProperties.getEndpoint(),
aliCloudProperties.getAccessKey(), aliCloudProperties.getSecretKey(),
ossProperties.getConfig());
}
else if (ossProperties.getAuthorizationMode() == AliCloudAuthorizationMode.STS) {
Assert.isTrue(!StringUtils.isEmpty(ossProperties.getEndpoint()),
"Oss endpoint can't be empty.");
Assert.isTrue(!StringUtils.isEmpty(ossProperties.getSts().getAccessKey()),
"Access key can't be empty.");
Assert.isTrue(!StringUtils.isEmpty(ossProperties.getSts().getSecretKey()),
"Secret key can't be empty.");
Assert.isTrue(!StringUtils.isEmpty(ossProperties.getSts().getSecurityToken()),
"Security Token can't be empty.");
return new OSSClientBuilder().build(ossProperties.getEndpoint(),
ossProperties.getSts().getAccessKey(),
ossProperties.getSts().getSecretKey(),
ossProperties.getSts().getSecurityToken(), ossProperties.getConfig());
}
else {
throw new IllegalArgumentException("Unknown auth mode.");
}
}
}
能**@Autowired**注入的实例都必须要有配置文件或者配置类,将响应的实例注入到IOC容器中。
5.2.2 JSR303数据校验
在使用新增功能时前端会对填入表单的数据进行校验,但是为了安全起见,后端也要进行校验。
引入依赖:
<!-- JSR303校验-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
- 给实体类加上各种注解
@Pattern 自定义校验 可以写正则表达式
@NotBlank 字符串用
@NotNull 数字类型使用
@NotEmpty 字符串用
参考文章:
如
(1) 给Bean添加校验注解: javax.validation.constraints,并定义自己的message提示
(2) 开启校验功能:使用注解@Valid
效果: 校验错误后会有默认的响应
(3) 给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
代码:
BrandEntity实体类:
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
// @Pattern()
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups={AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups={AddGroup.class})
@Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
private Integer sort;
}
controller中的方法:
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
// 获取校验的结果
result.getFieldErrors().forEach((item) -> {
// 获取错误提示
String message = item.getDefaultMessage();
// 获取校验出错的属性名
String field = item.getField();
// 将获取到的属性名和响应的提示信息封装到map集合中
map.put(field, message);
});
return R.error(400,"提交的数据不合法").put("data", map);
} else {
brandService.save(brand);
}
return R.ok();
}
这样就可以将错误信息传给前端进行展示。
但是如果每一个方法都进行以上的方式进行异常处理会使controller层代码变得臃肿。
所以我们要使用springboot提供统一异常处理方案解决。
5.2.3 统一异常处理
因为异常状态码需要进行规定并且需要解耦合,所以我们在gulimall-common模块中定义一个枚举类用来记录异常状态码和异常信息。
异常枚举类编写:
package com.atguigu.common.exception;
/**
* 异常信息枚举类
*/
public enum BizCodeEnume {
UNKNOWN_EXCEPTION(10000, "系统未知异常"),
VAILD_EXCEPTION(10001, "参数格式校验失败");
private int code;
private String msg;
private BizCodeEnume(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
编写统一异常处理类:
核心注解: @ControllerAdvice
package com.atguigu.gulimall.product.exception;
@Slf4j
// basePackages表明指定包下的全部异常都会被处理
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
//因为需要将异常信息返回给前台,所以返回值类型定义为R(通一信息返回类),要返回JSON类型的数据就要加上@ResponseBody注解,
//但是@RestControllerAdvice继承了@ControllerAdvice和@ResponseBody所以最后使用@RestControllerAdvice
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
/**
* 处理数据校验异常
*
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{},异常类型{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach(item -> {
String field = item.getField();
String message = item.getDefaultMessage();
map.put(field, message);
});
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(), BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data", map);
}
/**
* 处理全部异常
*/
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable e) {
return R.error(BizCodeEnume.UNKNOWN_EXCEPTION.getCode(),BizCodeEnume.UNKNOWN_EXCEPTION.getMsg());
}
}
最终,实现了解耦合,使得controller层专心处理业务,异常由全局异常处理器进行处理。
注意: 每一个异常处理方法都要标记 @ExceptionHandler
注解, 类声明的上方也要标记相应的注解: @ControllerAdvice
、@RestControllerAdvice
5.2.4 JSR303分组校验
分组校验用于完成多场景的复杂校验。
@Valid注解改为使用@Validated注解
@NotBlank
、@NotBlank
等注解中有一个groups属性
Class<?>[] groups() default { };
groups属性需要传递接口作为参数。
所以我们需要定义一些接口来作为groups的值。
此时的接口起到了标记的作用
package com.atguigu.common.valid;
public interface AddGroup {
}
----------------------------------------------------------------
package com.atguigu.common.valid;
public interface UpdateGroup {
}
然后controller接口上要将@Valid
换为@Validated
如:
@RequestMapping("/save")
public R save(@Validated @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}
5.2.5 自定义校验
应用场景 : 可能数据在不同业务下所校验的规则不一样 这时候就要添加分组 来进行不同业务下的不同校验规则
1.编写一个自定义的校验注解
2.编写一个自定义校验器
3.关联自定义的校验器和自定义的校验注解
编写自定义的校验注解:
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })//指定了用什么进行校验
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue
// 导入以下三个重要的核心方法
String message() default "{com.atguigu.common.valid.ListValue}";//会在配置文件中取出对应的message
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
// 自定义属性值
int[] vals() default { };
}
编写一个自定义校验器:
//ConstraintValidator<ListValue,Integer>泛型中第一个值为我们的注解,第二个值是要校验的属性的类型
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
// 初始化方法,会将注解获取的详细信息给我们
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
// 将通过注解传入的值封装到set集合中
set.add(val);
}
}
/**
* 判读是否校验成功
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// 判断待校验值是否存在我们指定的校验规则的数中,如果符合,则返回true校验通过,反之校验失败
return set.contains(value);
}
}
5.3 领域对象模型
5.3.1 VO
Vo的全称是: View Object
Vo用来接受前端传递过来的数据进行封装。
可以根据前端传来的参数编写VO。
例如:
public class AttrVo {
/**
* 属性id
*/
private Long attrId;
/**
* 属性名
*/
private String attrName;
/**
* 是否需要检索[0-不需要,1-需要]
*/
private Integer searchType;
/**
* 属性图标
*/
private String icon;
/**
* 可选值列表[用逗号分隔]
*/
private String valueSelect;
/**
* 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
*/
private Integer attrType;
/**
* 启用状态[0 - 禁用,1 - 启用]
*/
private Long enable;
/**
* 所属分类
*/
private Long catelogId;
/**
* 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
*/
private Integer showDesc;
private Long attrGroupId;
}
Vo中携带的数据最终要对拷到相应的Entity对象中。所以Vo中的属性尽量要与Entity中的属性一致,同时一个Vo对象也可以携带多个Entity所需要的数据。
在Vo中我们也可以使用参数校验的注解对前端传入的数据进行校验。
Vo To Entity的区别
Entity:字段与数据库完全一致
Vo:是指前端页面所需要后端接口的数据不仅仅是某个数据库中的所有字段 还可能牵扯其他的字段 使用vo来进行返回数据
5.3.2 TO
POJO
全称为:Plain Ordinary Java Object,即简单普通的java对象。一般用在数据层映射到数据库表的类,类的属性与表字段一一对应。
PO
全称为:Persistant Object,即持久化对象。可以理解为数据库中的一条数据即一个BO对象,也可以理解为POJO经过持久化后的对象。
DTO
全称为:Data Transfer Object,即数据传输对象。一般用于向数据层外围提供仅需的数据,如查询一个表有50个字段,界面或服务只需要用到其中的某些字段,DTO就包装出去的对象。可用于隐藏数据层字段定义,也可以提高系统性能,减少不必要字段的传输损耗。
DAO
全称为:Data Access Object,即数据访问对象。就是一般所说的DAO层,用于连接数据库与外层之间的桥梁,并且持久化数据层对象。
BO
全称为:Business Object,即业务对象。一般用在业务层,当业务比较复杂,用到比较多的业务对象时,可用BO类组合封装所有的对象一并传递。
VO
全称为:Value Object,有的也称为View Object,即值对象或页面对象。一般用于web层向view层封装并提供需要展现的数据。
5.4 事务的使用
调用service层处理Vo中的数据的时候,常常涉及到多张表的操作。所以在进行更新,保存,删除的操作时要注意使用事务—>在方法上标记@Transactional注解声明这个方法是事务的。
事务的概念
先来回顾一下事务的基本概念和特性。数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。事务,就必须具备ACID特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
@Transactional方法的可见度
@Transactional 可以作用于接口、接口方法、类以及类方法上。但是 Spring 小组建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的(从上面的Spring AOP 事务增强可以看出,就是针对方法的)。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
可参考: https://www.cnblogs.com/hjwublog/p/5626465.html#_label0