谷粒商城—分布式基础第四部分
# 16. 菜单拖动
| 同一个菜单内拖动 | 正常 |
| 拖动到父菜单的前面或后面 | 正常 |
| 拖动到父菜单同级的另外一个菜单中 | 正常 |
关注的焦点在于,拖动到目标节点中,使得目标节点的catlevel+deep小于3即可。拖动到目标节点前后的条件是,使得
# 拖动菜单时需要修改顺序和级别
需要考虑两种类型节点的catLevel
一种关系是:如果是同一个节点下的子节点的前后移动,则不需要修改其catLevel
如果是拖动到另外一个节点内或父节点中,则要考虑修改其catLevel
如果拖动到与父节点平级的节点关系中,则要将该拖动的节点的catLevel,设置为兄弟节点的Level,
先考虑parentCid还是先考虑catLevel?
两种关系在耦合
另外还有一种是前后拖动的情况
哪个范围最大?
肯定是拖动类型关系最大,
如果是前后拖动,则拖动后需要看待拖动节点的层级和设置待拖动节点的parentId,
如果待拖动节点和目标节点的层级相同,则认为是同级拖动,只需要修改节点的先后顺序即可;
否则认为是跨级拖动,则需要修改层级和重新设置parentID
如果
以拖动类型来分,并不合适,比较合适的是跨级拖动和同级拖动
如何判断是跨级拖动还是同级拖动,根据拖动的层级来看,如果是同一级的拖动,只需要修改先后顺序即可,但是这样也会存在一个问题,就是当拖动到另外一个分组下的同级目录中,显然也需要修改parentID,究竟什么样的模型最好呢?
另外也可以判断在跨级移动时,跨级后的parentID是否相同,如果不相同,则认为是在不同目录下的跨级移动需要修改parentID。
顺序、catLevel和parentID
同级移动:
(1)首先判断待移动节点和目标节点的catLevel是否相同,
(2)相同则认为是同级移动,
如果此时移动后目标节点的parentID和待移动节点的相同,但是移动类型是前后移动,只需要调整顺序即可,此时移动类型是inner,则需要修改catLevel和parentId和顺序
如果此时移动后目标节点的parentID和待移动节点的不相同,但是移动类型是前后移动,则需要调整顺序和parentId,此时移动类型是inner,则需要修改catLevel和parentId和顺序
通过这两步的操作能看到一些共性,如果抽取移动类型作为大的分类,则在这种分类下,
如果是前后移动,则分为下面几种情况:
同级别下的前后移动:界定标准为catLevel相同,但是又可以分为parentID相同和parentID不同,parent相同时,只需要修改顺序即可;parentID不同时,需要修改parentID和顺序
不同级别下的前后移动:界定标准为catLevel不同,此时无论如何都要修改parentID,顺序和catLevel
如果是inner类型移动,则分为一下的几种情况。
此时不论是同级inner,还是跨级innner,都需要修改parentID,顺序和catLevel
哪种情况需要更新子节点呢?
那就要看要拖拽的节点是否含有子节点,如果有子节点,则需要更新子节点的catLevel,不需要更新它之间的顺序和parentId,只需要更新catLevel即可。这种更新子节点的Level应该归类,目前的目标是只要有子节点就更新它的catLevel,
(2)如果待移动节点和目标节点的catLevel不同,则认为是跨级移动。如果是移动到父节点中,则需要设置catLevel,parentID和顺序。此时需要分两种情况来考虑,如果是移动到父节点中,则需要设置catLevel,parentID和顺序,如果是移动到兄弟节点中,则需要设置
包含移动到父节点同级目录,兄弟节点中。
# 设置菜单拖动开关
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>
但是现在存在的一个问题是每次拖拽的时候,都会发送请求,更新数据库这样频繁的与数据库交互,现在想要实现一个拖拽过程中不更新数据库,拖拽完成后,统一提交拖拽后的数据。
现在还存在一个问题,如果是将一个菜单连续的拖拽,最终还放到了原来的位置,但是updateNode中却出现了很多节点更新信息,这样显然也是一个问题。
# 批量删除
<el-button type="danger" plain size="small" @click="batchDelete">批量删除</el-button>
//批量删除
batchDelete() {
let checkNodes = this.$refs.menuTree.getCheckedNodes();
// console.log("被选中的节点:",checkNodes);
let catIds = [];
for (let i = 0; i < checkNodes.length; i++) {
catIds.push(checkNodes[i].catId);
}
this.$confirm(`确定要删除?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false)
}).then(({ data }) => {
this.$message({
message: "菜单批量删除成功",
type: "success"
});
//重新刷新页面
this.getMeus();
});
})
.catch(() => {
//取消删除
});
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 17. 品牌管理菜单

(2)将“”逆向工程得到的resources\src\views\modules\product文件拷贝到gulimall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件
brand.vue brand-add-or-update.vue
但是显示的页面没有新增和删除功能,这是因为权限控制的原因,

<el-button v-if="isAuth('product:brand:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:brand:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
2
3
查看“isAuth”的定义位置:

它是在“index.js”中定义,现在将它设置为返回值为true,即可显示添加和删除功能。
再次刷新页面能够看到,按钮已经出现了:

# 添加“显示状态按钮”
brand.vue
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
@change="updateBrandStatus(scope.row)"
:active-value = "1"
:inactive-value = "0"
></el-switch>
</template>
2
3
4
5
6
7
8
9
10
brand-add-or-update.vue
<el-form-item label="显示状态" prop="showStatus">
<el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</el-form-item>
2
3
//更新开关的状态
updateBrandStatus(data) {
console.log("最新状态", data);
let {brandId,showStatus} = data;
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: "post",
data: this.$http.adornData({brandId,showStatus}, false)
}).then(({ data }) => {
this.$message({
message: "状态更新成功",
type: "success"
});
});
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 添加上传
和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。
这里我们选择将图片放置到阿里云上,使用对象存储。
阿里云上使使用对象存储方式:

创建Bucket

上传文件:

上传成功后,取得图片的URL

这种方式是手动上传图片,实际上我们可以在程序中设置自动上传图片到阿里云对象存储。
上传模型:

查看阿里云关于文件上传的帮助: https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ
# 1)添加依赖包
在Maven项目中加入依赖项(推荐方式)
在 Maven 工程中使用 OSS Java SDK,只需在 pom.xml 中加入相应依赖即可。以 3.8.0 版本为例,在 <dependencies> 内加入如下内容:
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.0</version>
</dependency>
2
3
4
5
# 2)上传文件流
以下代码用于上传文件流:
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "<yourAccessKeyId>";
String accessKeySecret = "<yourAccessKeySecret>";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("<yourlocalFile>");
ossClient.putObject("<yourBucketName>", "<yourObjectName>", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
endpoint的取值:

accessKeyId和accessKeySecret需要创建一个RAM账号:

创建用户完毕后,会得到一个“AccessKey ID”和“AccessKeySecret”,然后复制这两个值到代码的“AccessKey ID”和“AccessKeySecret”。
另外还需要添加访问控制权限:

@Test
public void testUpload() throws FileNotFoundException {
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-shanghai.aliyuncs.com";
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "LTAI4G4W1RA4JXz2QhoDwHhi";
String accessKeySecret = "R99lmDOJumF2x43ZBKT259Qpe70Oxw";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Pictures\\timg.jpg");
ossClient.putObject("gulimall-images", "time.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功.");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
更为简单的使用方式,是使用SpringCloud Alibaba

详细使用方法,见: https://help.aliyun.com/knowledge_detail/108650.html
(1)添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
2
3
4
5
(2)创建“AccessKey ID”和“AccessKeySecret”
(3)配置key,secret和endpoint相关信息
access-key: LTAI4G4W1RA4JXz2QhoDwHhi
secret-key: R99lmDOJumF2x43ZBKT259Qpe70Oxw
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
2
3
4
(4)注入OSSClient并进行文件上传下载等操作

但是这样来做还是比较麻烦,如果以后的上传任务都交给gulimall-product来完成,显然耦合度高。最好单独新建一个Module来完成文件上传任务。
# 其他方式
# 1)新建gulimall-third-party
# 2)添加依赖,将原来gulimall-common中的“spring-cloud-starter-alicloud-oss”依赖移动到该项目中
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.bigdata.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
另外也需要在“pom.xml”文件中,添加如下的依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3)在主启动类中开启服务的注册和发现
@EnableDiscoveryClient
# 4)在nacos中注册
(1)创建命名空间“ gulimall-third-party ”

(2)在“ gulimall-third-party”命名空间中,创建“ gulimall-third-party.yml”文件
spring:
cloud:
alicloud:
access-key: LTAI4G4W1RA4JXz2QhoDwHhi
secret-key: R99lmDOJumF2x43ZBKT259Qpe70Oxw
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
2
3
4
5
6
7
# 5)编写配置文件
application.yml
server:
port: 30000
spring:
application:
name: gulimall-third-party
cloud:
nacos:
discovery:
server-addr: 192.168.137.14:8848
logging:
level:
com.bigdata.gulimall.product: debug
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bootstrap.properties
spring.cloud.nacos.config.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=192.168.137.14:8848
spring.cloud.nacos.config.namespace=f995d8ee-c53a-4d29-8316-a1ef54775e00
spring.cloud.nacos.config.extension-configs[0].data-id=gulimall-third-party.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
2
3
4
5
6
# 6) 编写测试类
package com.bigdata.gulimall.thirdparty;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
@SpringBootTest
class GulimallThirdPartyApplicationTests {
@Autowired
OSSClient ossClient;
@Test
public void testUpload() throws FileNotFoundException {
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-shanghai.aliyuncs.com";
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "LTAI4G4W1RA4JXz2QhoDwHhi";
String accessKeySecret = "R99lmDOJumF2x43ZBKT259Qpe70Oxw";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
//上传文件流。
InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Pictures\\timg.jpg");
ossClient.putObject("gulimall-images", "time3.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功.");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
https://help.aliyun.com/document_detail/31926.html?spm=a2c4g.11186623.6.1527.228d74b8V6IZuT
背景
采用JavaScript客户端直接签名(参见JavaScript客户端签名直传 (opens new window))时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。
原理介绍
服务端签名后直传的原理如下:
- 用户发送上传Policy请求到应用服务器。
- 应用服务器返回上传Policy和签名给用户。
- 用户直接上传数据到OSS。
编写“com.bigdata.gulimall.thirdparty.controller.OssController”类:
package com.bigdata.gulimall.thirdparty.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class OssController {
@Autowired
OSS ossClient;
@Value ("${spring.cloud.alicloud.oss.endpoint}")
String endpoint ;
@Value("${spring.cloud.alicloud.oss.bucket}")
String bucket ;
@Value("${spring.cloud.alicloud.access-key}")
String accessId ;
@Value("${spring.cloud.alicloud.secret-key}")
String accessKey ;
@RequestMapping("/oss/policy")
public Map<String, String> policy(){
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format; // 用户上传文件时指定的前缀。
Map<String, String> respMap=null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap= new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return respMap;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
测试: http://localhost:30000/oss/policy
{"accessid":"LTAI4G4W1RA4JXz2QhoDwHhi","policy":"eyJleHBpcmF0aW9uIjoiMjAyMC0wNC0yOVQwMjo1ODowNy41NzhaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIwLTA0LTI5LyJdXX0=","signature":"s42iRxtxGFmHyG40StM3d9vOfFk=","dir":"2020-04-29/","host":"https://gulimall-images.oss-cn-shanghai.aliyuncs.com","expire":"1588129087"}
以后在上传文件时的访问路径为“ http://localhost:88/api/thirdparty/oss/policy”,
在“gulimall-gateway”中配置路由规则:
- id: third_party_route
uri: lb://gulimall-gateway
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>/?.*),/$\{segment}
2
3
4
5
6
测试是否能够正常跳转: http://localhost:88/api/thirdparty/oss/policy

# 上传组件
放置项目提供的upload文件夹到components目录下,一个是单文件上传,另外一个是多文件上传
PS D:\Project\gulimall\renren-fast-vue\src\components\upload> ls
目录: D:\Project\gulimall\renren-fast-vue\src\components\upload
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2020/4/29 星期三 12:0 3122 multiUpload.vue
2
-a---- 2019/11/11 星期一 21: 343 policy.js
20
-a---- 2020/4/29 星期三 12:0 3053 singleUpload.vue
1
PS D:\Project\gulimall\renren-fast-vue\src\components\upload>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
修改这两个文件的配置后
开始执行上传,但是在上传过程中,出现了如下的问题:

Access to XMLHttpRequest at 'http://gulimall-images.oss-cn-shanghai.aliyuncs.com/' from origin 'http://localhost:8001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问:

再次执行文件上传。
# 18. JSR303校验
# 步骤1:使用校验注解
在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。
在非空处理方式上提供了@NotNull,@Blank和@
(1)@NotNull
The annotated element must not be null. Accepts any type. 注解元素禁止为null,能够接收任何类型
(2)@NotEmpty
the annotated element must not be null nor empty.
该注解修饰的字段不能为null或""
Supported types are:
支持以下几种类型
CharSequence (length of character sequence is evaluated)
字符序列(字符序列长度的计算)
Collection (collection size is evaluated) 集合长度的计算
Map (map size is evaluated) map长度的计算
Array (array length is evaluated) 数组长度的计算
(3)@NotBlank
The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence. 该注解不能为null,并且至少包含一个非空白字符。接收字符序列。
# 步骤2:在请求方法种,使用校验注解@Valid,开启校验,
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
2
3
4
5
6
测试: http://localhost:88/api/product/brand/save
在postman种发送上面的请求
{
"timestamp": "2020-04-29T09:20:46.383+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能为空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
能够看到"defaultMessage": "不能为空",这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:
javax.validation.constraints.AssertFalse.message = 只能为false
javax.validation.constraints.AssertTrue.message = 只能为true
javax.validation.constraints.DecimalMax.message = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message = 必须大于或等于{value}
javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message = 最大不能超过{value}
javax.validation.constraints.Min.message = 最小不能小于{value}
javax.validation.constraints.Negative.message = 必须是负数
javax.validation.constraints.NegativeOrZero.message = 必须是负数或零
javax.validation.constraints.NotBlank.message = 不能为空
javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.NotNull.message = 不能为null
javax.validation.constraints.Null.message = 必须为null
javax.validation.constraints.Past.message = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message = 必须是正数
javax.validation.constraints.PositiveOrZero.message = 必须是正数或零
javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间
org.hibernate.validator.constraints.CreditCardNumber.message = 不合法的信用卡号码
org.hibernate.validator.constraints.Currency.message = 不合法的货币 (必须是{value}其中之一)
org.hibernate.validator.constraints.EAN.message = 不合法的{type}条形码
org.hibernate.validator.constraints.Email.message = 不是一个合法的电子邮件地址
org.hibernate.validator.constraints.Length.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.CodePointLength.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配
org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue}的校验码不合法, 模10校验和不匹配
org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue}的校验码不合法, 模11校验和不匹配
org.hibernate.validator.constraints.ModCheck.message = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
org.hibernate.validator.constraints.NotBlank.message = 不能为空
org.hibernate.validator.constraints.NotEmpty.message = 不能为空
org.hibernate.validator.constraints.ParametersScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.Range.message = 需要在{min}和{max}之间
org.hibernate.validator.constraints.SafeHtml.message = 可能有不安全的HTML内容
org.hibernate.validator.constraints.ScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.URL.message = 需要是一个合法的URL
org.hibernate.validator.constraints.time.DurationMax.message = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
org.hibernate.validator.constraints.time.DurationMin.message = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotBlank.message}";
2
3
可以在添加注解的时候,修改message:
@NotBlank(message = "品牌名必须非空")
private String name;
2
当再次发送请求时,得到的错误提示信息:
{
"timestamp": "2020-04-29T09:36:04.125+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "品牌名必须非空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
但是这种返回的错误结果并不符合我们的业务需要。
# 步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if( result.hasErrors()){
Map<String,String> map=new HashMap<>();
//1.获取错误的校验结果
result.getFieldErrors().forEach((item)->{
//获取发生错误时的message
String message = item.getDefaultMessage();
//获取发生错误的字段
String field = item.getField();
map.put(field,message);
});
return R.error(400,"提交的数据不合法").put("data",map);
}else {
}
brandService.save(brand);
return R.ok();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。
# 步骤4:统一异常处理
可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
(1)抽取一个异常处理类
package com.bigdata.gulimall.product.exception;
import com.bigdata.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 集中处理所有异常
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.bigdata.gulimall.product.controller")
public class GulimallExceptionAdvice {
@ExceptionHandler(value = Exception.class)
public R handleValidException(MethodArgumentNotValidException exception){
Map<String,String> map=new HashMap<>();
BindingResult bindingResult = exception.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> {
String message = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field,message);
});
log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
return R.error(400,"数据校验出现问题").put("data",map);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(2)测试: http://localhost:88/api/product/brand/save

(3)默认异常处理
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),BizCodeEnum.UNKNOW_EXEPTION.getMsg());
}
2
3
4
5
(4)错误状态码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码
package com.bigdata.common.exception;
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
public enum BizCodeEnum {
UNKNOW_EXEPTION(10000,"系统未知异常"),
VALID_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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(5)测试: http://localhost:88/api/product/brand/save

