一、前端新键品牌管理

1.1 引入&修改逆向生成的代码

image.png
将逆向生成的品牌展示以及修改添加前端模块放到我们的当前路径下(renren-fast-vue\src\views\modules\product)
image.png
brand.vue ——品牌列表
brand-add-or-update.vue —— 品牌增加修改
现在我们重启项目,发现我们并不能管理品牌,这是因为brand.vue中的新增删除按钮有一个认证,我们需要修改这个认证函数使其永远返回true
image.png

1.2 修改品牌显示状态

使用开关的方式来开启/关闭品牌显示。这里仅修改前端代码,不需要修改后端代码。
image.png

踩坑

前端改变显示状态时,需要更新数据库中的show_status字段,我使用的是mybatis-plus 3.4.2,结果一致报SQL语法错误。最终解决办法,因为mybatis-plus 3.4.2 在配置逻辑删除时,配置文件中可以设置全局逻辑删除的实体字段名。关键就在于mybatis-plus如果某个字段开启了逻辑删除,就无法使用update来更新这个字段(自己试验出来的,具体原因还没有弄明白,如果有大佬知道还请解释一下,谢谢)
image.png
而视频里面用的是mybatis-plus 3.2.0 其没有全局逻辑删除,而是通过@TableLogic注解来开启特定entity字段的逻辑删除,所以这里将全局逻辑删除注释即可。(后面删除品牌的时候可能会与更新产生冲突)

1.3 文件上传——管理品牌

需求:在管理品牌时,logo上传图片。
image.png

1.3.1 阿里云OSS

和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。这里我们选择将图片放置到阿里云上,使用阿里云提供的对象存储服务(OSS,Object Storage Service)。

对象存储服务是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。

image.pngimage.png(第二张图是用的别人的,名字自己取,地区就近选择)
进入bucket列表->文件管理->点击上传文件即可上传
image.png
上传完成后,点击详情->复制文件URL即可远程获取到文件
image.png
实际上我们可以在程序中设置自动上传图片到阿里云对象存储。

1.3.2 上传模型

image.pngimage.png
这里为了安全,我们通过第二种方式上传:浏览器向服务器请求上传policy->服务器利用阿里云账号密码生成一个防伪policy->前端携带防伪policy上传到OSS

1.3.3 OSS SDK快速开发

我们可以参考SDK(Java)文档一定要看)。

引入依赖

添加oss-sdk依赖

  1. <dependency>
  2. <groupId>com.aliyun.oss</groupId>
  3. <artifactId>aliyun-sdk-oss</artifactId>
  4. <version>3.10.2</version>
  5. </dependency>

文件流上传代码:注意导入的package

  1. // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
  2. String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
  3. // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
  4. String accessKeyId = "yourAccessKeyId";
  5. String accessKeySecret = "yourAccessKeySecret";
  6. // 填写Bucket名称,例如examplebucket。
  7. String bucketName = "examplebucket";
  8. // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
  9. // 如果没有文件夹就不需要写
  10. String objectName = "exampledir/exampleobject.txt";
  11. // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
  12. // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
  13. String filePath= "D:\\localpath\\examplefile.txt";
  14. // 创建OSSClient实例。
  15. OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
  16. try {
  17. InputStream inputStream = new FileInputStream(filePath);
  18. // 创建PutObject请求。
  19. ossClient.putObject(bucketName, objectName, inputStream);
  20. } catch (OSSException oe) {
  21. System.out.println("Caught an OSSException, which means your request made it to OSS, "
  22. + "but was rejected with an error response for some reason.");
  23. System.out.println("Error Message:" + oe.getErrorMessage());
  24. System.out.println("Error Code:" + oe.getErrorCode());
  25. System.out.println("Request ID:" + oe.getRequestId());
  26. System.out.println("Host ID:" + oe.getHostId());
  27. } catch (ClientException ce) {
  28. System.out.println("Caught an ClientException, which means the client encountered "
  29. + "a serious internal problem while trying to communicate with OSS, "
  30. + "such as not being able to access the network.");
  31. System.out.println("Error Message:" + ce.getMessage());
  32. } finally {
  33. if (ossClient != null) {
  34. ossClient.shutdown();
  35. }
  36. }

配置阿里云

endpoint需要进入创建的bucket中查看,点击概览查看:
image.png
创建AccessKeyId和AccessKeySecret:
进入AccessKey管理->选择开始使用子用户->创建用户->得到key跟密码 记得保存key跟密码!
image.pngimage.png
image.png
image.png
在使用之前,还需要给子账户添加权限:
image.png

测试

  1. package com.atguigu.gilumall.gulimallproduct;
  2. import com.aliyun.oss.ClientException;
  3. import com.aliyun.oss.OSS;
  4. import com.aliyun.oss.OSSClientBuilder;
  5. import com.aliyun.oss.OSSException;
  6. import com.atguigu.gulimall.gulimallproduct.GulimallProductApplication;
  7. import com.atguigu.gulimall.gulimallproduct.entity.BrandEntity;
  8. import com.atguigu.gulimall.gulimallproduct.service.BrandService;
  9. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  10. import org.junit.jupiter.api.Test;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.boot.test.context.SpringBootTest;
  13. import java.io.FileInputStream;
  14. import java.io.FileNotFoundException;
  15. import java.io.InputStream;
  16. import java.util.List;
  17. @SpringBootTest(classes = GulimallProductApplication.class)
  18. class GulimallProductApplicationTests {
  19. @Autowired
  20. BrandService brandService;
  21. @Test
  22. public void testUpload() {
  23. // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
  24. String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
  25. // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
  26. String accessKeyId = "xxxxx";
  27. String accessKeySecret = "xxxxxx";
  28. // 填写Bucket名称,例如examplebucket。
  29. String bucketName = "gulimall-mrlinxi";
  30. // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
  31. // 这里我没有文件夹,所以直接写文件名称就行,这个名称就是你上传到bucket后的文件名
  32. String objectName = "huawei.png";
  33. // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
  34. // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
  35. String filePath= "E:\\A尚硅谷课件\\项目&面试\\Guli Mall\\课件和文档(老版)\\基础篇\\资料\\pics\\huawei.png";
  36. // 创建OSSClient实例。
  37. OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
  38. try {
  39. InputStream inputStream = new FileInputStream(filePath);
  40. // 创建PutObject请求。
  41. ossClient.putObject(bucketName, objectName, inputStream);
  42. } catch (OSSException oe) {
  43. System.out.println("Caught an OSSException, which means your request made it to OSS, "
  44. + "but was rejected with an error response for some reason.");
  45. System.out.println("Error Message:" + oe.getErrorMessage());
  46. System.out.println("Error Code:" + oe.getErrorCode());
  47. System.out.println("Request ID:" + oe.getRequestId());
  48. System.out.println("Host ID:" + oe.getHostId());
  49. } catch (ClientException ce) {
  50. System.out.println("Caught an ClientException, which means the client encountered "
  51. + "a serious internal problem while trying to communicate with OSS, "
  52. + "such as not being able to access the network.");
  53. System.out.println("Error Message:" + ce.getMessage());
  54. } catch (FileNotFoundException e) {
  55. e.printStackTrace();
  56. } finally {
  57. if (ossClient != null) {
  58. ossClient.shutdown();
  59. System.out.println("上传完成");
  60. }
  61. }
  62. }
  63. }

image.png
上传成功!

1.3.4 SpringCloud Alibaba-OSS

更为简单的方式是直接使用SpringCloud Alibaba提供的OSS Github README-zh 阿里云帮助文档

  1. 修改 pom.xml 文件,引入 aliyun-oss-spring-boot-starter。(我们直接放到common下面)

aliyun-oss-spring-boot-starter搜索不到依赖,我去maven仓库找了,只有spring-cloud-starter-alicloud-oss可以用,记得指定版本

  1. 这个有问题,加了版本号也导不进去
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>aliyun-oss-spring-boot-starter</artifactId>
  5. </dependency>
  6. 用下面这个
  7. <!--OSS 对象存储服务-->
  8. <dependency>
  9. <groupId>com.alibaba.cloud</groupId>
  10. <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
  11. <version>2.2.0.RELEASE</version>
  12. </dependency>
  1. 在配置文件中配置 OSS 服务对应的 accessKey、secretKey 和 endpoint。

    1. # 配置阿里云OSS
    2. alicloud:
    3. access-key: LTAI5t6fKvdMW3JwHHKb5vF9
    4. secret-key: 2PVwPqcLf1fQw5dHmhHShuSPmPAgS6
    5. oss:
    6. endpoint: oss-cn-hangzhou.aliyuncs.com
  2. 注入 OSSClient 并进行文件上传下载等操作(这一步只是个示例)。

    1. @Service
    2. public class YourService {
    3. @Autowired
    4. private OSSClient ossClient;
    5. public void saveFile() {
    6. // download file to local
    7. ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File("pathOfYourLocalFile"));
    8. }
    9. }

    测试: ```java @Autowired OSSClient ossClient;

@Test public void testUpload() { // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 // String endpoint = “https://oss-cn-hangzhou.aliyuncs.com“; // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。 // String accessKeyId = “LTAI5t6fKvdMW3JwHHKb5vF9”; // String accessKeySecret = “2PVwPqcLf1fQw5dHmhHShuSPmPAgS6”; // 填写Bucket名称,例如examplebucket。 String bucketName = “gulimall-mrlinxi”; // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。 // 这里我没有文件夹,所以直接写文件名称就行,这个名称就是你上传到bucket后的文件名 String objectName = “xiaomi.png”; // 填写本地文件的完整路径,例如D:\localpath\examplefile.txt。 // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。 String filePath= “E:\A尚硅谷课件\项目&面试\Guli Mall\课件和文档(老版)\基础篇\资料\pics\xiaomi.png”;

  1. // 创建OSSClient实例。

// OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

  1. try {
  2. InputStream inputStream = new FileInputStream(filePath);
  3. // 创建PutObject请求。
  4. ossClient.putObject(bucketName, objectName, inputStream);
  5. } catch (OSSException oe) {
  6. System.out.println("Caught an OSSException, which means your request made it to OSS, "
  7. + "but was rejected with an error response for some reason.");
  8. System.out.println("Error Message:" + oe.getErrorMessage());
  9. System.out.println("Error Code:" + oe.getErrorCode());
  10. System.out.println("Request ID:" + oe.getRequestId());
  11. System.out.println("Host ID:" + oe.getHostId());
  12. } catch (ClientException ce) {
  13. System.out.println("Caught an ClientException, which means the client encountered "
  14. + "a serious internal problem while trying to communicate with OSS, "
  15. + "such as not being able to access the network.");
  16. System.out.println("Error Message:" + ce.getMessage());
  17. } catch (FileNotFoundException e) {
  18. e.printStackTrace();
  19. } finally {
  20. if (ossClient != null) {
  21. ossClient.shutdown();
  22. System.out.println("上传完成");
  23. }
  24. }

}

  1. 但是这样来做还是比较麻烦,我们可以单独创建一个微服务用于文件上传
  2. <a name="Mxhgr"></a>
  3. ### 1.3.5 gulimai-third-part
  4. <a name="k3aXV"></a>
  5. #### 新建第三方微服务gulimall-third-part
  6. 视频使用的是Spring initialnizr初始化,选中了spring-webopenfeign,其他nacos的注册和配置直接继承common;因为我之前是直接把spring-webopenfeign都放在了common中,所以我直接通过maven初始化微服务继承common
  7. <a name="hiTZM"></a>
  8. #### pom
  9. 引入common(将mybatis排除),和oss;将common中的oss删除
  10. ```xml
  11. <?xml version="1.0" encoding="UTF-8"?>
  12. <project xmlns="http://maven.apache.org/POM/4.0.0"
  13. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  14. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  15. <parent>
  16. <artifactId>gulimall</artifactId>
  17. <groupId>com.atguigu.gulimall</groupId>
  18. <version>0.0.1-SNAPSHOT</version>
  19. </parent>
  20. <modelVersion>4.0.0</modelVersion>
  21. <artifactId>gulimall-third-part</artifactId>
  22. <properties>
  23. <maven.compiler.source>8</maven.compiler.source>
  24. <maven.compiler.target>8</maven.compiler.target>
  25. </properties>
  26. <dependencies>
  27. <dependency>
  28. <groupId>com.atguigu.gulimall</groupId>
  29. <artifactId>gulimall-common</artifactId>
  30. <version>0.0.1-SNAPSHOT</version>
  31. </dependency>
  32. <!--OSS 对象存储服务-->
  33. <dependency>
  34. <groupId>com.alibaba.cloud</groupId>
  35. <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
  36. <version>2.2.0.RELEASE</version>
  37. </dependency>
  38. </dependencies>
  39. </project>

配置文件

在nacos中创建gulimall-third-part的命名空间;在该命名空间下创建如下配置问价oss.yaml

  1. spring:
  2. cloud:
  3. alicloud:
  4. access-key: LTAI5t6fKvdMW3JwHHKb5vF9
  5. secret-key: 2PVwPqcLf1fQw5dHmhHShuSPmPAgS6
  6. oss:
  7. endpoint: https://oss-cn-hangzhou.aliyuncs.com
  1. spring:
  2. application:
  3. name: gulimall-third-part
  4. cloud:
  5. nacos:
  6. config:
  7. server-addr: localhost:8848
  8. namespace: 84cba3ed-1257-4f80-9bc7-b4267ac0e9d5
  9. file-extension: yaml

使用服务端签名直传

帮助文档

  1. package com.atguigu.gulimall.gulimallthirdpart.controller;
  2. import com.aliyun.oss.OSS;
  3. import com.aliyun.oss.common.utils.BinaryUtil;
  4. import com.aliyun.oss.model.MatchMode;
  5. import com.aliyun.oss.model.PolicyConditions;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.beans.factory.annotation.Value;
  8. import org.springframework.web.bind.annotation.RequestMapping;
  9. import org.springframework.web.bind.annotation.RestController;
  10. import java.text.SimpleDateFormat;
  11. import java.time.LocalDate;
  12. import java.util.Date;
  13. import java.util.LinkedHashMap;
  14. import java.util.Map;
  15. /**
  16. * 对象存储专用controller
  17. * @author mrlinxi
  18. * @create 2022-03-06 16:51
  19. */
  20. @RestController
  21. public class OSSController {
  22. @Autowired
  23. OSS ossClient; // 自动注入OSSClient
  24. @Value("${spring.cloud.alicloud.oss.endpoint}")
  25. private String endpoint;
  26. @Value("${spring.cloud.alicloud.oss.bucket}")
  27. private String bucket;
  28. @Value("${spring.cloud.alicloud.access-key}")
  29. private String accessId;
  30. /**
  31. * 获取policy
  32. */
  33. @RequestMapping("/oss/policy")
  34. public Map<String, String> policy() {
  35. // https://gulimall-mrlinxi.oss-cn-hangzhou.aliyuncs.com/xiaomi.png
  36. // String bucket = "gulimall-mrlinxi"; // 请填写您的 bucketname 。
  37. String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
  38. // callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
  39. // String callbackUrl = "http://88.88.88.88:8888";
  40. // 我们将前缀设置为系统当前年月日
  41. String simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
  42. // String localDate = LocalDate.now().toString();
  43. String dir = simpleDateFormat; // 用户上传文件时指定的前缀。
  44. Map<String, String> respMap = null;
  45. try {
  46. long expireTime = 30;
  47. long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
  48. Date expiration = new Date(expireEndTime);
  49. // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
  50. PolicyConditions policyConds = new PolicyConditions();
  51. policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
  52. policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
  53. String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
  54. byte[] binaryData = postPolicy.getBytes("utf-8");
  55. String encodedPolicy = BinaryUtil.toBase64String(binaryData);
  56. String postSignature = ossClient.calculatePostSignature(postPolicy);
  57. respMap = new LinkedHashMap<String, String>();
  58. respMap.put("accessid", accessId);
  59. respMap.put("policy", encodedPolicy);
  60. respMap.put("signature", postSignature);
  61. respMap.put("dir", dir);
  62. respMap.put("host", host);
  63. respMap.put("expire", String.valueOf(expireEndTime / 1000));
  64. // respMap.put("expire", formatISO8601Date(expiration));
  65. } catch (Exception e) {
  66. // Assert.fail(e.getMessage());
  67. System.out.println(e.getMessage());
  68. } finally {
  69. ossClient.shutdown();
  70. }
  71. return respMap;
  72. }
  73. }

测试: http://localhost:30000/oss/policy

修改网关路由规则

规定通过:localhost:88/api/thirdpart/oss/policy 访问第三方服务获取policy,我们修改一下网关的路由规则,添加如下规则:

  1. - id: third_part_route
  2. uri: lb://gulimall-third-part # lb://服务名 进行负载均衡转发
  3. predicates: # 对包含有/api/gulimallproduct的url请求进行路径重写
  4. - Path=/api/thirdpart/**
  5. filters:
  6. - RewritePath=/api/thirdpart/(?<segment>.*), /$\{segment}

测试:localhost:88/api/thirdpart/oss/policy
image.png

1.3.6 前端后端联调—完成文件上传功能

1)图片上传

放置项目提供的upload文件夹到components目录下。
image.png
然后将两个vue文件中el-upload组件中action的地址替换成自己阿里云的bucket地址
image.png
image.png
在brand-add-or-update.vue中导入singleUplaod组件,在品牌logo地址处使用
image.png
在上传过程中,发现出现了跨域问题:
image.png

Access to XMLHttpRequest at ‘http://gulimall-mrlinxi.oss-cn-hangzhou.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.

所以我们需要将OSS的bucket设置成可以跨域访问:帮助文档中也说了需要设置跨域
image.pngimage.png再测试,发现上传成功。
现在我们添加一个品牌,发现品牌logo地址显示的是图片在阿里云OSS服务器上的地址:
image.png,现在需求是显示图片。
我们修改brand.vue中”品牌logo地址”的el-tale-colum,如下:

  1. <el-table-column
  2. prop="logo"
  3. header-align="center"
  4. align="center"
  5. label="品牌logo地址"
  6. >
  7. <!-- scope 是当前列对象, scope.row获取当前列的信息
  8. 那么scope.row.logo可以获取到logo的地址
  9. 再通过el-image组件即可显示图片
  10. -->
  11. <template slot-scope="scope">
  12. <!-- <el-image
  13. style="width: 100px; height: 80px"
  14. :src="scope.row.logo"
  15. fit="contain"
  16. ></el-image> -->
  17. <!-- 使用原生的image组件 el显示不了太大的图片-->
  18. <img :src="scope.row.logo" style="width: 100px; height: 80px">
  19. </template>
  20. </el-table-column>

二、JRS303数据校验

我们发现前端填写检索首字母字段的时候,不写会提示必填字段,但是却可以填写一段字符串,这显然不满足要求。所以需要使用到el的form验证功能。
但是前端的校验是很容易规避的,比如使用postman,因此我们还需要在后端进行数据校验!
使用方法:

  1. 直接给需要校验的数据加上校验注解(不同注解表示不同规则,可以自定义message)
  2. 告诉SpringMVC进行校验(@Valid注解,加在controller的入参处)
  3. 给校验的bean后紧跟一个BindingResult就可以获取校验结果

    注:spring-boot2.3及以上版本将不再内部依赖hibernate-validator做参数验证,需要手动添加依赖

这里我用的是2.3.12版本,所以我在common中添加了spring-boot-starter-validation依赖

  1. <!--validation-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-validation</artifactId>
  5. </dependency>

2.1 单独处理异常

步骤一:使用校验注解规定校验规则

  1. package com.atguigu.gulimall.gulimallproduct.entity;
  2. import com.baomidou.mybatisplus.annotation.TableId;
  3. import com.baomidou.mybatisplus.annotation.TableLogic;
  4. import com.baomidou.mybatisplus.annotation.TableName;
  5. import java.io.Serializable;
  6. import lombok.Data;
  7. import org.hibernate.validator.constraints.URL;
  8. import javax.validation.constraints.*;
  9. /**
  10. * 品牌
  11. *
  12. * @author mrlinxi
  13. * @email mrzheme@vip.qq.com
  14. * @date 2021-12-07 19:17:01
  15. */
  16. @Data
  17. @TableName("pms_brand")
  18. public class BrandEntity implements Serializable {
  19. private static final long serialVersionUID = 1L;
  20. /**
  21. * 品牌id
  22. */
  23. @TableId
  24. private Long brandId;
  25. /**
  26. * 品牌名
  27. */
  28. @NotBlank(message = "品牌名不能为空")
  29. private String name;
  30. /**
  31. * 品牌logo地址
  32. */
  33. @NotEmpty
  34. @URL(message = "logo必须是一个合法的url地址")
  35. private String logo;
  36. /**
  37. * 介绍
  38. */
  39. private String descript;
  40. /**
  41. * 显示状态[0-不显示;1-显示]
  42. */
  43. // @TableLogic(value = "1", delval = "0")
  44. private Integer showStatus;
  45. /**
  46. * 检索首字母
  47. */
  48. // @Pattern可以自定义规则 这里正则表达式需要去掉/ /^[a-zA-Z]$/
  49. @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
  50. @NotEmpty
  51. private String firstLetter;
  52. /**
  53. * 排序
  54. */
  55. @Min(value = 0, message = "排序必须大于等于0")
  56. @NotNull
  57. private Integer sort;
  58. }

步骤二:在请求方法中,使用校验注解@Valid开启校验;
步骤三:如果需要获取到校验结果,在校验的Bean后紧跟一个BindingResult即可。之后就可以自定义封装

  1. @RequestMapping("/save")
  2. //@RequiresPermissions("gulimallproduct:brand:save")
  3. public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
  4. if (result.hasErrors()) {
  5. Map<String, String> map = new HashMap<>();
  6. // 1.获取校验的错误结果
  7. result.getFieldErrors().forEach((item) -> {
  8. //FieldError 获取到错误提示
  9. String message = item.getDefaultMessage();
  10. // 获取错误的属性的名字
  11. String field = item.getField();
  12. map.put(field, message);
  13. });
  14. return R.error(400, "提交的数据不合法").put("data", map);
  15. }
  16. else {
  17. brandService.save(brand);
  18. }
  19. return R.ok();
  20. }

测试:
image.png

2.2 统一处理异常

显然,单独处理异常非常不方便,同时代码的复用不友好。可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
@RestControllerAdvice=ControllerAdvice+RestBody

2.2.1 抽取一个异常处理类

  1. package com.atguigu.gulimall.gulimallproduct.exception;
  2. import com.atguigu.common.utils.R;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.validation.BindingResult;
  5. import org.springframework.web.bind.MethodArgumentNotValidException;
  6. import org.springframework.web.bind.annotation.ControllerAdvice;
  7. import org.springframework.web.bind.annotation.ExceptionHandler;
  8. import org.springframework.web.bind.annotation.ResponseBody;
  9. import org.springframework.web.bind.annotation.RestControllerAdvice;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. /**
  13. * @author MrLinxi
  14. * @Description 集中处理所有异常
  15. * @create 2022-03-07-15:52
  16. */
  17. @Slf4j
  18. //@ResponseBody
  19. //@ControllerAdvice(basePackages = "com.atguigu.gulimall.gulimallproduct.controller")
  20. @RestControllerAdvice(basePackages = "com.atguigu.gulimall.gulimallproduct.controller")
  21. public class GulimallExceptionControllerAdvice {
  22. // ExceptionHandler注解可以指定可以处理什么异常
  23. @ExceptionHandler(value = MethodArgumentNotValidException.class)
  24. public R handleValidException(MethodArgumentNotValidException e) {
  25. Map<String, String> map = new HashMap<>();
  26. BindingResult bindingResult = e.getBindingResult();
  27. bindingResult.getFieldErrors().forEach(fieldError -> {
  28. String message = fieldError.getDefaultMessage();
  29. String field = fieldError.getField();
  30. map.put(field, message);
  31. });
  32. log.error("数据校验出现问题{},异常类型{}", e.getMessage(), e.getClass());
  33. return R.error(400,"数据校验出现问题").put("data",map);
  34. }
  35. }

测试一下:
image.png

2.2.2 错误代码规范

上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着定义规范,如该在项目中我们的错误状态码定义如下:
image.png

异常代码枚举类

我们可以在common中定义一个枚举类,用来存储这些错误代码;
枚举在类上加两个注解,可以不用自己手写get方法和构造方法(注意要有lombok依赖):@Getter @AllArgsConstructor

  1. package com.atguigu.common.exception;
  2. /***
  3. * 错误码和错误信息定义类
  4. * 1. 错误码定义规则为5为数字
  5. * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
  6. * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
  7. * 错误码列表:
  8. * 10: 通用
  9. * 001:参数格式校验
  10. * 11: 商品
  11. * 12: 订单
  12. * 13: 购物车
  13. * 14: 物流
  14. */
  15. public enum BizCodeEnum {
  16. UNKNOW_EXEPTION(10000, "系统未知异常"),
  17. VALIAD_EXCEPTION(10001, "参数格式校验失败");
  18. private int code;
  19. private String msg;
  20. BizCodeEnum(int code, String msg) {
  21. this.code = code;
  22. this.msg = msg;
  23. }
  24. public int getCode() {
  25. return code;
  26. }
  27. public String getMsg() {
  28. return msg;
  29. }
  30. }

处理其他异常

在GulimallExceptionControllerAdvice定义一个默认异常处理,使用上面定义的异常枚举类获得code和message

  1. /**
  2. * 处理其他任意异常
  3. * @return
  4. */
  5. @ExceptionHandler(value = Throwable.class)
  6. public R handleException() {
  7. return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(), BizCodeEnum.UNKNOW_EXEPTION.getMsg());
  8. }

2.2.3 测试

image.png

2.3 分组校验(多场景复杂校验)

假设如下场景:在新增或者修改品牌的时候,我们想要校验的字段、规则不同,应该如何进行校验?

  1. 给校验注解,标注上groups,指定什么情况下才需要进行校验

    1. @NotNull(message = "修改必须指定品牌ID", groups = {UpdateGroup.class})
    2. @Null(message = "新增不能指定ID", groups = {AddGroup.class})
    3. @TableId
    4. private Long brandId;

    UpdateGroup与AddGroup只需要定义一个接口就行,我们在common的valid包下定义这两个接口。

  2. 在Controller的方法上标注@Validated注解

之前我们使用的是@Valid注解,其提供了一种规范,@Validated注解是spring框架提供的。Validated可以指定一个或者多个校验分组,以表明该方法在什么校验分组下以什么规则进行校验

  1. /**
  2. * 保存
  3. */
  4. @RequestMapping("/save")
  5. //@RequiresPermissions("gulimallproduct:brand:save")
  6. // public R save(@Valid @RequestBody BrandEntity brand/*, BindingResult result*/){
  7. public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand/*, BindingResult result*/){
  8. brandService.save(brand);
  9. return R.ok();
  10. }

我们测试一下:
image.png,发现没有指定分组的校验规则失效了。
在分组校验情况下,没有指定分组的校验规则注解将不会生效,其只会在不分组的情况下生效。

2.4 自定义校验

  1. 编写一个自定义的校验注解
  2. 编写一个自定义的校验器
  3. 关联自定义校验器和自定义校验注解

    2.4.1 编写一个自定义的校验注解

    common的valid包下:

    1. @Documented
    2. @Constraint(validatedBy = { ListValueConstraintValidator.class }) // 这里绑定校验器
    3. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    4. @Retention(RUNTIME)
    5. public @interface ListValue {
    6. String message() default "{com.atguigu.common.valid.ListValue.message}";
    7. Class<?>[] groups() default { };
    8. Class<? extends Payload>[] payload() default { };
    9. int[] values() default { };
    10. }

    这里message的默认值可以通过配置文件来配置:

    1. com.atguigu.common.valid.ListValue.message=必须提交指定的值

    2.4.2 编写一个自定义的校验器

    校验器是通过指定@Constraint注解中的validatedBy设定的,validatedBy是一个数组,其中存放校验器,每个校验器均是实现了ConstraintValidator接口的类。ConstraintValidator中的两个泛型,分别代表校验器需要绑定的注解和需要校验的数据类型。
    image.png ```java package com.atguigu.common.valid;

import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.HashSet; import java.util.Set;

public class ListValueConstraintValidator implements ConstraintValidator {

  1. private Set<Integer> set = new HashSet<>();
  2. // 初始化方法
  3. @Override
  4. public void initialize(ListValue constraintAnnotation) {
  5. ConstraintValidator.super.initialize(constraintAnnotation);
  6. int[] values = constraintAnnotation.values();
  7. for (int value : values) {
  8. set.add(value);
  9. }
  10. }
  11. /**
  12. * 判断是否校验成功
  13. * @param value 这个就是浏览器提交过来需要校验的值
  14. * @param context
  15. * @return
  16. */
  17. @Override
  18. public boolean isValid(Integer value, ConstraintValidatorContext context) {
  19. return set.contains(value);
  20. }

}

  1. <a name="H42tm"></a>
  2. ### 2.4.3 关联自定义校验器和自定义校验注解
  3. 在validateBy中指定校验器:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22423156/1646654682106-2f33adcb-87d3-49c2-bb35-a3ce68ae41e0.png#clientId=u7f96f0dd-163a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=278&id=ub6384a11&name=image.png&originHeight=278&originWidth=678&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23283&status=done&style=none&taskId=ue56ea5ab-2e7c-4387-a893-9f0f80fc1f3&title=&width=678)<br />**注**:一个校验注解可以适配多个校验器,会自动匹配最佳校验器进行校验。可以查看ConstraintValidator的实现类来查看其他校验器或者自定义多个校验器与注解关联
  4. <a name="FMeB4"></a>
  5. ### 2.4.4 测试
  6. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22423156/1646655146557-23746739-c7f2-467f-982e-cf6701cfbd84.png#clientId=u7f96f0dd-163a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=241&id=u818efaa8&name=image.png&originHeight=321&originWidth=435&originalType=binary&ratio=1&rotation=0&showTitle=false&size=20530&status=done&style=none&taskId=uef960937-37c6-45a7-b5b3-b5c78341eaa&title=&width=326)
  7. 在测试修改显示状态的时候,发现报错:![image.png](https://cdn.nlark.com/yuque/0/2022/png/22423156/1646656582493-c05a4052-a310-469e-85f9-0b75b250851d.png#clientId=u7f96f0dd-163a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=17&id=u6429658b&name=image.png&originHeight=17&originWidth=464&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2770&status=done&style=none&taskId=u18ff0879-fb2d-43be-8615-26d818ff149&title=&width=464)<br />这是因为品牌名(name)属性设置了更新跟添加时的非空校验,但是这两个分组又是必要的,所以修改showStatus的校验分组,针对showStatus新建一个校验分组UpdateStatusGroup替换掉原来的UpdateGroup。同时新建一个针对修改状态的方法:
  8. ```java
  9. /**
  10. * 修改状态
  11. */
  12. @RequestMapping("/update/status")
  13. //@RequiresPermissions("gulimallproduct:brand:update")
  14. public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
  15. brandService.updateById(brand);
  16. return R.ok();
  17. }

同时记得修改前端项目的修改状态请求地址。
image.png