品牌数据表对应数据库gulimall-pms
中的pms-brand
数据表结构
之前的三级分类中,自己设计界面进行增删改查。其实在逆向工程renren-generator
生成的代码中,会生成对应数据表的增删改查界面。
1. 新增品牌管理菜单
2. 搭建基础增删改查界面
使用逆向生成的代码
将两个vue界面复制到modules/product中
重启项目,打开品牌维护界面,基础增删改查已经就绪
由于系统默认会做权限控制,有一些按钮会做权限判断是否显示
将权限控制设置为永远返回true
重新查看页面效果
3. 优化细节—显示状态[0-不显示;1-显示]
删除[0-不显示;1-显示]
列表brand.vue
显示状态
新增更新brand-add-or-update.vue
显示状态
将显示状态改为用switch
组件控制
列表brand.vue
显示状态
<el-table-columnprop="showStatus"header-align="center"align="center"label="显示状态"><template slot-scope="scope"><el-switchv-model="scope.row.showStatus"active-color="#13ce66"inactive-color="#ff4949"></el-switch></template></el-table-column>
新增更新brand-add-or-update.vue
显示状态
<el-form-item label="显示状态" prop="showStatus"><el-switchv-model="dataForm.showStatus"active-color="#13ce66"inactive-color="#ff4949"></el-switch></el-form-item>
效果
监听Switch
组件点击事件
列表brand.vue
显示状态
<el-switchv-model="scope.row.showStatus"active-color="#13ce66"inactive-color="#ff4949"@change="updateBrandStatus(scope.row)":active-value="1":inactive-value="0">
直接使用逆向生成代码中的product/brand/update
接口来更新数据
updateBrandStatus(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({type: "success",message: "状态更新成功",});});},
4. 文件上传
分布式文件上传将所有的文件存储服务在统一位置处理。
4.1 阿里云对象存储OSS
4.1.1. 简介
对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。
4.1.2. 使用步骤
开通阿里云对象存储服务/product/oss
进入控制台,可以查看API文档
资源术语
推荐:一个项目创建一个Bucket
。
创建Bucket
4.2. 图片上传方式
4.2.1. 方式一:普通上传方式
这种方式用户上传还要经过自己的应用服务器,额外操作。
4.2.2. 方式一:服务端签名后直传
4.2.3. 项目采用上传方式
阿里云存储对象账号密码存储在自己的应用服务器中前端向阿里云发送数据的时候,首先向服务器请求Policy上传策略,服务器根据账号密码生成防伪签名(防伪策略,令牌,地址等)前端携带防伪签名访问OSS,如果正确接收上传请求。4.3. 文件上传实现
官方文档
4.3.1. 安装SDK,引入依赖
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.10.2</version></dependency>
4.3.2. 测试文件上传
复制文件上传代码
// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-。String endpoint = "yourEndpoint";// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。String accessKeyId = "yourAccessKeyId";String accessKeySecret = "yourAccessKeySecret";// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);// 创建PutObjectRequest对象。// 填写Bucket名称、Object完整路径和本地文件的完整路径。Object完整路径中不能包含Bucket名称。// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。PutObjectRequest putObjectRequest = new PutObjectRequest("examplebucket", "exampleobject.txt", new File("D:\\localpath\\examplefile.txt"));// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。// ObjectMetadata metadata = new ObjectMetadata();// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());// metadata.setObjectAcl(CannedAccessControlList.Private);// putObjectRequest.setMetadata(metadata);// 上传文件。ossClient.putObject(putObjectRequest);// 关闭OSSClient。ossClient.shutdown();
参数
endpoint:地域节点
accessKeyId、accessKeySecret:
管理AccessKey
使用子用户AccessKey
创建用户
设置账号
开通后生成accessKeyId、accessKeySecret
新建账户没有任何权限,添加权限
完整代码
@Testpublic void testUpload() {String endpoint = "xxx";String accessKeyId = "xxx";String accessKeySecret = "xxx";OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);PutObjectRequest putObjectRequest = new PutObjectRequest("gulimall-kaisarh", "login.png", new File("C:\\Users\\Administrator\\Pictures\\login.png"));ossClient.putObject(putObjectRequest);ossClient.shutdown();System.out.println("上传完成");}
测试
4.4. SpringCloud Alibaba-OSS实现对象存储
4.4.1. 引入SpringCloud Alibaba-OSS
由于很多服务都可能使用文件上传,因此直接在gulimall-common
中引入
--引入spring-alibaba-oss--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alicloud-oss</artifactId></dependency>
4.4.2. 配置阿里云oss 相关的账号信息
spring: cloud:alicloud: oss:endpoint: oss-cn-access-key: xxxxxxsecret-key: xxxxxx
注意:必须申请 RAM 账号信息,并且分配 OSS 操作权限
4.4.3 测试使用OssClient 上传
@AutowiredOSSClient ossClient;@Testpublic void testUpload() throws FileNotFoundException {InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Pictures\\removeAll.png");ossClient.putObject("gulimall-kaisarh", "removeAll.png", inputStream);ossClient.shutdown();System.out.println("上传完成");}
5. 创建微服务,整合第三方功能
5.1. 创建微服务gulimall-third-party
5.2. 修改依赖
<dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version></dependency>
将对象存储依赖放到gulimall-third-party
中
5.3. 将gulimall-third-parthy
添加到nacos中
5.3.1. 新建命名空间
新建配置oss.yml
完善配置
5.3.2. 配置gulimall-third-party
配置中心
spring.application.name=gulimall-third-partyspring.cloud.nacos.config.server-addr=127.0.0.1:8848spring.cloud.nacos.config.namespace=e5b7c2f9-afd4-4750-94c7-f0be48b97fb8spring.cloud.nacos.config.ext-config[0].data-id=oss.ymlspring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUPspring.cloud.nacos.config.ext-config[0].refresh=true
5.3.3. 配置gulimall-third-party
注册中心
spring:cloud:nacos:discovery:server-addr: 127.0.0.1:8848application:name: gulimall-third-partyserver:port: 30000
开启服务注册发现
去除数据源mybatis依赖
<exclusions><exclusion><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></exclusion></exclusions>
5.4. 启动项目
启动项目后可以在Nacos
的服务列表查询
测试上传通过
6. 服务端签名后直传
6.1. 文档
服务端签名后直传文档
6.2. 流程介绍
Web端向服务端请求签名,然后直接上传,不会对服务端产生压力,而且安全可靠。
6.3. 代码
Java代码
在gulimall-third-part
微服务中创建controller并添加@RestController
注解
编写获取签名接口
package com.atguigu.gulimall.thirdparty.controller;import com.aliyun.oss.OSS;import com.mon.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.nio.charset.StandardCharsets;import java.text.SimpleDateFormat;import java.util.Date;import java.util.LinkedHashMap;import java.util.Map;@RestControllerpublic class OssController {@AutowiredOSS ossClient;@Value("${spring.cloud.alicloud.oss.endpoint}")private String endpoint;@Value("${spring.cloud.alicloud.oss.bucket}")private String bucket;@Value("${spring.cloud.alicloud.access-key}")private String accessId;@RequestMapping("/oss/policy")public Map<String, String> policy() {String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。// String callbackUrl = "http://88.88.88.88:8888";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);// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。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(StandardCharsets.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));// respMap.put("expire", formatISO8601Date(expiration));} catch (Exception e) {// Assert.fail(e.getMessage());System.out.println(e.getMessage());} finally {ossClient.shutdown();}return respMap;}}
启动测试测试接口
expire:过期时间dir:上传文件名host:上传文件地址policy:签名
6.4. 配置网关
- id: third_part_routeuri: lb://gulimall-third-partypredicates:## 前端项目发送请求,带有/api前缀- Path=/api/thirdparty/**filters:- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
测试:测试接口
7. OOS前后端联调上传功能
文件上传组件el-upload
的action
属性对应Bucket
域名
更新后端接口,返回R
对象
R policy() {···return R.ok().put("data", respMap);}
配置OSS跨域
multiUpload.vue
<template><div><el-uploadaction="http://gulimall-kaisarh.oss-cn-":data="dataObj":list-type="listType":file-list="fileList":before-upload="beforeUpload":on-remove="handleRemove":on-success="handleUploadSuccess":on-preview="handlePreview":limit="maxCount":on-exceed="handleExceed":show-file-list="showFile"><i class="el-icon-plus"></i></el-upload><el-dialog :visible.sync="dialogVisible"><img width="100%" :src="dialogImageUrl" alt /></el-dialog></div></template><script>import {policy } from "./policy";import {getUUID } from "@/utils";export default {name: "multiUpload",props: {//图片属性数组value: Array,//最大上传图片数量maxCount: {type: Number,default: 30,},listType: {type: String,default: "picture-card",},showFile: {type: Boolean,default: true,},},data() {return {dataObj: {policy: "",signature: "",key: "",ossaccessKeyId: "",dir: "",host: "",uuid: "",},dialogVisible: false,dialogImageUrl: null,};},computed: {fileList() {let fileList = [];for (let i = 0; i < this.value.length; i++) {fileList.push({url: this.value[i] });}return fileList;},},mounted() {},methods: {emitInput(fileList) {let value = [];for (let i = 0; i < fileList.length; i++) {value.push(fileList[i].url);}this.$emit("input", value);},handleRemove(file, fileList) {this.emitInput(fileList);},handlePreview(file) {this.dialogVisible = true;this.dialogImageUrl = file.url;},beforeUpload(file) {let _self = this;return new Promise((resolve, reject) => {policy().then((response) => {console.log("这是什么${filename}");_self.dataObj.policy = response.data.policy;_self.dataObj.signature = response.data.signature;_self.dataObj.ossaccessKeyId = response.data.accessid;_self.dataObj.key = response.data.dir + getUUID() + "_${filename}";_self.dataObj.dir = response.data.dir;_self.dataObj.host = response.data.host;resolve(true);}).catch((err) => {console.log("出错了...", err);reject(false);});});},handleUploadSuccess(res, file) {this.fileList.push({name: file.name,// url: this.dataObj.host + "/" + this.dataObj.dir + "/" + file.name; 替换${filename}为真正的文件名url:this.dataObj.host +"/" +this.dataObj.key.replace("${filename}", file.name),});this.emitInput(this.fileList);},handleExceed(files, fileList) {this.$message({message: "最多只能上传" + this.maxCount + "张图片",type: "warning",duration: 1000,});},},};</script><style></style>
policy.js
import http from '@/utils/httpRequest.js'export function policy() {return new Promise((resolve, reject) => {http({url: http.adornUrl("/thirdparty/oss/policy"),method: "get",params: http.adornParams({})}).then(({data }) => {resolve(data);})});}
singleUpload.vue
<template><div><el-uploadaction="http://gulimall-kaisarh.oss-cn-":data="dataObj"list-type="picture":multiple="false":show-file-list="showFileList":file-list="fileList":before-upload="beforeUpload":on-remove="handleRemove":on-success="handleUploadSuccess":on-preview="handlePreview"><el-button size="small" type="primary">点击上传</el-button><div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div></el-upload><el-dialog :visible.sync="dialogVisible"><img width="100%" :src="fileList[0].url" alt="" /></el-dialog></div></template><script>import {policy } from "./policy";import {getUUID } from "@/utils";export default {name: "singleUpload",props: {value: String,},computed: {imageUrl() {return this.value;},imageName() {if (this.value != null && this.value !== "") {return this.value.substr(this.value.lastIndexOf("/") + 1);} else {return null;}},fileList() {return [{name: this.imageName,url: this.imageUrl,},];},showFileList: {get: function () {return (this.value !== null && this.value !== "" && this.value !== undefined);},set: function (newValue) {},},},data() {return {dataObj: {policy: "",signature: "",key: "",ossaccessKeyId: "",dir: "",host: "",// callback:'',},dialogVisible: false,};},methods: {emitInput(val) {this.$emit("input", val);},handleRemove(file, fileList) {this.emitInput("");},handlePreview(file) {this.dialogVisible = true;},beforeUpload(file) {let _self = this;return new Promise((resolve, reject) => {policy().then((response) => {console.log("响应的数据", response);_self.dataObj.policy = response.data.policy;_self.dataObj.signature = response.data.signature;_self.dataObj.ossaccessKeyId = response.data.accessid;_self.dataObj.key = response.data.dir + getUUID() + "_${filename}";_self.dataObj.dir = response.data.dir;_self.dataObj.host = response.data.host;console.log("响应的数据222。。。", _self.dataObj);resolve(true);}).catch((err) => {reject(false);});});},handleUploadSuccess(res, file) {console.log("上传成功...");this.showFileList = true;this.fileList.pop();this.fileList.push({name: file.name,url:this.dataObj.host +"/" +this.dataObj.key.replace("${filename}", file.name),});this.emitInput(this.fileList[0].url);},},};</script><style></style>
brand-add-or-update.vue
<template>···<el-form-item label="品牌logo地址" prop="logo"><single-upload v-model="dataForm.logo"></single-upload></el-form-item>···</template><script>import singleUpload from "../../../components/upload/singleUpload.vue";export default {components: {singleUpload },···};</script>
8. 品牌管理功能完善
8.1. 新增品牌界面
设置switch
显示状态开关,激活为0,不激活为1
<el-form-item label="显示状态" prop="showStatus"><el-switchv-model="dataForm.showStatus"active-color="#13ce66"inactive-color="#ff4949":active-value="1":inactive-value="0"></el-switch></el-form-item>
新增数据的时候做校验
dataRule: {···firstLetter: [{validator: (rule, value, callback) => {if (value === "") {callback(new Error("首字母必须填写!"));} else if (!^[a-zA-Z]$.test(value)) {callback(new Error("首字母必须a-z或A-Z!"));} else {callback();}},trigger: "blur",},],sort: [{validator: (rule, value, callback) => {console.log(value);console.log(typeof value);if (value === "") {callback(new Error("排序字段必须填写!"));} else if (!Number.isInteger(value) || value < 0) {callback(new Error("排序字段必须是大于等于0的整数"));} else {callback();}},trigger: "blur",},],},
8.2. 品牌列表界面
设置品牌logo为图片
<template>···<el-table-columnprop="logo"header-align="center"align="center"label="品牌logo"><template slot-scope="scope"><img:src="scope.row.logo"alt="logo"style="width: 100px; height: 80px"/></template></el-table-column>···</template>
9. JSR303服务端数据校验
给Bean添加校验注解
注解可以参考javax\validation\constraints
使用注解@Valid
告知SpringMVC进行校验,开启校验功能
Postman测试
效果:校验错误后会有默认响应
错误信息可以通过注解自定义
给校验的bean后紧跟一个BindingResult
就可以获取到校验结果,可以自定义错误返回信息
/*** 保存*/@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 -> {// FieldError 获取错误提示String defaultMessage = item.getDefaultMessage();// 获取错误属性名称String field = item.getField();map.put(field, defaultMessage);});return R.error(400, "提交的数据不合法").put("data", map);} else {brandService.save(brand);return R.ok();}}
给其他字段增加校验注解
package com.atguigu.gulimall.product.entity;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import java.io.Serializable;import java.util.Date;import lombok.Data;import org.hibernate.validator.constraints.URL;import javax.validation.constraints.*;/*** 品牌** @author KaiSarH* @email huankai7@* @date -04-15 16:20:25*/@Data@TableName("pms_brand")public class BrandEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 品牌id*/@TableIdprivate Long brandId;/*** 品牌名*/@NotBlank(message = "品牌名必须提交")private String name;/*** 品牌logo地址*/@URL(message = "logo必须是一个合法的url地址")@NotEmptyprivate String logo;/*** 介绍*/private String descript;/*** 显示状态[0-不显示;1-显示]*/private Integer showStatus;/*** 检索首字母*/@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")private String firstLetter;/*** 排序*/@Min(value = 0, message = "排序必须大于等于0")private Integer sort;}
10. 统一异常处理
很多业务都需要进行数据验证,代码大部分都是重复的,可以做统一异常处理
使用SpringMVC提供的@ControllerAdvice
使用步骤:
抽取异常处理类
使用@ControllerAdvice
注解标识
使用basePackages
标识哪个位置出现异常进行处理
接口使用BindingResult
会接收错误,感应异常。
删除掉后就不再对异常进行处理,而是直接抛出异常。
GulimallExceptionControllerAdvice
的作用就是感应异常,集中处理。
错误信息以JSON
格式返回,需要给类添加@ResponseBody
注解
@ResponseBody
注解和@ControllerAdvice(basePackages
注解可以合并为@RestControllerAdvice
注解
//@ResponseBody//@ControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller")@RestControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller")
统一处理MethodArgumentNotValidException
异常
package com.atguigu.gulimall.product.exception;import mon.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//@ResponseBody//@ControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller")@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")public class GulimallExceptionControllerAdvice {// 准确匹配某种异常@ExceptionHandler(value = MethodArgumentNotValidException.class)public R handleVaildException(MethodArgumentNotValidException e) {log.error("数据校验出现问题:{},异常类型:{}", e.getMessage(), e.getClass());BindingResult bindingResult = e.getBindingResult();Map<String, String> errorMap = new HashMap<>();bindingResult.getFieldErrors().forEach(fieldError -> {errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());});return R.error(400, "数据校验出现问题").put("data", errorMap);}// 无法准确匹配后处理@ExceptionHandler(value = Throwable.class)public R handleException(Throwable throwable) {return R.error();}}
11. 全局状态码枚举类
错误码和错误信息定义类
错误码定义规则为 5 为数字 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常维护错误码后需要维护错误描述,将他们定义为枚举形式
错误码列表:
10: 通用
001:参数格式校验
11: 商品
12: 订单
13: 购物车
14: 物流
状态码在很多地方都需要,因此在gulimall-common
中定义枚举类BizCodeEnume
package mon.exception;/****错误码和错误信息定义类*1. 错误码定义规则为 5 为数字*2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常*3. 维护错误码后需要维护错误描述,将他们定义为枚举形式*错误码列表:*10: 通用*001:参数格式校验*11: 商品*12: 订单*13: 购物车*14: 物流*/public enum BizCodeEnume {UNKNOW_EXCEPTION(10000, "系统未知异常"),VAILE_EXCEPTION(10001, "参数格式校验失败");private int code;private String msg;BizCodeEnume(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}}
定义完成后,在接口中不需要再额外写状态码
原来返回状态码方式:
return R.error(400, "数据校验出现问题").put("data", errorMap);
定义全局状态码枚举类后返回状态码方式:
return R.error(BizCodeEnume.VAILE_EXCEPTION.getCode(), BizCodeEnume.VAILE_EXCEPTION.getMsg()).put("data", errorMap);
12. JSR303分组校验
JSR303
分组校验可以完成多场景复杂校验。
在新增数据和修改数据的时候,校验的字段可能不同。
例如:
新增品牌的时候,由于ID是自动生成的自增长ID,所以新增的时候不携带ID
修改品牌的时候,需要根据ID进行修改
无论是新增还是修改,品牌名都不能为空
某些字段如logo在新增的时候需要录入必须提交,修改的时候可以不用必须提交
解决:使用JSR303分组校验功能
在gulimall-common
中定义不同分组,例如新增分组和修改分组。
使用groups
属性给校验注解标注什么情况下需要校验。groups
为情况数组,可以添加一个或多个。
/**** 品牌id*/@NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class})@Null(message = "新增不能指定id", groups = {AddGroup.class})@TableIdprivate Long brandId;/*** 品牌名*/@NotBlank(message = "品牌名必须提交", groups = {UpdateGroup.class, AddGroup.class})private String name;
在Controller
中,使用@Validated
注解标识接口进行哪一组的校验
/*** 保存*/@RequestMapping("/save")public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand/*, BindingResult result*/) {brandService.save(brand);return R.ok();}/*** 修改*/@RequestMapping("/update")public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand) {brandService.updateById(brand);return R.ok();}
注意
默认没有指定分组
的校验注解,例如下图中@NotEmpty
在分组校验@Validated
情况下不会生效,只会在不分组的情况下生效。
13. JSR303自定义校验
在一些特殊情况下,例如显示状态[0-不显示 1-显示],没有内置的校验注解使用,需要自己写校验方法。
使用@Pattern
注解正则表达式自定义校验
使用过程:
编写一个自定义的校验注解
编写一个自定义的校验器
关联自定义的校验器和自定义的校验注解
让校验器校验校验注解标识的字段
13.1. 编写一个自定义的校验注解
希望有一个注解@ListValue(values = {0, 1})
,用来规定字段可以使用的值(0和1).
创建ListValue
校验注解
注解必须拥有三个属性
message:当校验出错后,错误信息去哪取groups:支持分组校验payload:自定义负载信息
注解必须有以下原信息数据
@Documented@Constraint(validatedBy = {})@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)
Target:注解可以标注在哪些位置Retention:时机,可以在运行时获取到Constraint:注解使用哪个校验器进行校验,可以指定校验器
导入相关包
Payload、Constraint
依赖validation-api
,在pom.xml
导入依赖
基础注解ListValue.java
package mon.valid;import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.*;@Documented@Constraint(validatedBy = {})@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)public @interface ListValue {String message() default "{javax.validation.constraints.NotEmpty.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};}
定义完成后,可以在BrandEntity.java0
中引入使用
在ListValue.java
中指定value
数组
int[] values() default {};
指定错误信息
将message
的默认值改为ListValue全类名.message
String message() default "{mon.valid.ListValue.message}";
创建配置文件ValidationMessages.properties
在配置文件中配置错误信息
13.2. 编写一个自定义的校验器
创建校验器类文件ListValueConstraintValidator.java
ListValueConstraintValidator
实现接口ConstraintValidator
ConstraintValidator
接口包含两个泛型,第一个为对应注解,第二个为校验数据类型
实现ConstraintValidator
接口两个方法
initialize
初始化方法 参数constraintAnnotation
包含默认合法的值
isValid
校验方法 参数integer
为提交过来需要检验的值
校验器代码
package mon.valid;import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;import java.util.HashSet;import java.util.Set;public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {private Set<Integer> set = new HashSet<>();// 初始化方法@Overridepublic void initialize(ListValue constraintAnnotation) {// 合法的值int[] values = constraintAnnotation.values();// 将合法值全部放到set中,便于查找是否存在for (int value : values) {set.add(value);}}// 判断是否校验成功@Override/*** @params value 提交过来需要检验的值* @params context 校验的上下文环境信息* @return*/public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {return set.contains(value);}}
注解与代码数据对应关系
注解使用的时候,会使用values = {0, 1}
指定值
@ListValue(values = {0, 1})
这些值对应的就是initialize
初始化方法中的合法值。
而isValid
校验方法中的值,指的是客户端传递过来需要校验的值。
13.3. 关联校验器和校验注解
在校验注解位置,使用@Constraint
注解指定校验器
@Constraint(validatedBy = {ListValueConstraintValidator.class})
可以指定多个校验器,适配不同类型的校验
@Constraint(validatedBy = {A.class,B.class,C.class})
13.4. 完整校验注解代码与校验器
13.4.1. 校验注解
package mon.valid;import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.*;@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 "{mon.valid.ListValue.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};int[] values() default {};}
13.4.2. 校验器
package mon.valid;import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;import java.util.HashSet;import java.util.Set;public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {private Set<Integer> set = new HashSet<>();// 初始化方法@Overridepublic void initialize(ListValue constraintAnnotation) {// 合法的值int[] values = constraintAnnotation.values();// 将合法值全部放到set中,便于查找是否存在for (int value : values) {set.add(value);}}// 判断是否校验成功@Override/*** @params value 提交过来需要检验的值* @params context 校验的上下文环境信息* @return*/public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {return set.contains(value);}}
13.4.3. 注解使用
@Data@TableName("pms_brand")public class BrandEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 显示状态[0-不显示;1-显示]*/@ListValue(values = {0, 1})private Integer showStatus;}
13.5. 测试
指定在添加的时候必须携带
/*** 显示状态[0-不显示;1-显示]*/@ListValue(values = {0, 1}, groups = {AddGroup.class})private Integer showStatus;
save
测试
错误测试
正确测试
14. 将修改品牌状态单独抽取一个方法
添加修改状态分组
新建接口,指定使用UpdateStatusGroup
分组
/*** 修改品牌显示状态*/@RequestMapping("/update/status")public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand) {brandService.updateById(brand);return R.ok();}
在BrandEntity
中进行配置,在UpdateStatusGroup
组只判断showStatus
/*** 显示状态[0-不显示;1-显示]*/@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})@ListValue(values = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})private Integer showStatus;
修改前端项目更新状态请求
测试成功