Github地址:https://github.com/YuyanCai/mall-study
OSS是对象存储服务,有什么用呢?把图片存储到云服务器上能让所有人都访问到!
详细操作可查官方文档,下面只写关键代码
[SDK示例 (aliyun.com)](https://help.aliyun.com/document_detail/32008.html)
一、创建子用户测试用例
官方推荐使用子账户的AccessID和SecurityID,因为如果直接给账户的AccessID和SecurityID的话,如果不小心被其他人获取到了,那账户可是有全部权限的!!!
所以这里通过建立子账户,给子账户分配部分权限实习。
这里通过子账户管理OSS的时候,要给子账户添加操控OSS资源的权限
这里是必须要做的,因为子账户默认是没有任何权限的,必须手动给他赋予权限

二、引入依赖
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.1.0version>
dependency>
三、测试用例
@SpringBootTest
class MallProductApplicationTests {
@Test
public void testUploads() throws FileNotFoundException {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "。。。";
String accessKeySecret = "。。。";
// 填写Bucket名称,例如examplebucket。
String bucketName = "pyy-mall";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "2022/testPhoto.txt";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "C:\\Users\\Jack\\Desktop\\R-C.jfif";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

https://github.com/alibaba/aliyun-spring-boot/blob/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample/README-zh.md
一、引入依赖
我们不是进行依赖管理了吗?为什么还要显示写出2.1.1版本
这是因为这个包没有最新的包,只有和2.1.1匹配的
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alicloud-ossartifactId>
<version>2.1.1.RELEASEversion>
dependency>
二、在配置文件中配置 OSS 服务对应的 accessKey、secretKey 和 endpoint
alicloud:
access-key: xxx
secret-key: xxx
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
三、注入OSSClient测试
@Resource
private OSSClient ossClient;
@Test
public void testUploads() throws FileNotFoundException {
// 上传文件流。
InputStream inputStream = new FileInputStream("C:\\Users\\Jack\\Desktop\\LeetCode_Sharing.png");
ossClient.putObject("pyy-mall", "2022/testPhoto2.png", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传完成...");
}

新模块存放所有的第三方服务,像短信服务、图片服务、视频服务等等
引入依赖如下:
这里去除了mp的依赖,因为引入mp就需要配置数据库服务器地址
一、配置文件
<dependencies>
<dependency>
<groupId>com.caq.mallgroupId>
<artifactId>mall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alicloud-ossartifactId>
<version>2.1.1.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>

二、启动类添加注解
将第三方的包也注册到注册中心
主启动类@EnableDiscoveryClient
application.yml配置文件如下:
#用来指定注册中心地址
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
alicloud:
access-key: ...
secret-key: ...
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket: pyy-mall
bootstrap.yml文件指定注册中心
#用来指定配置中心地址
spring:
application:
name: mall-third-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: 2bbd2076-36c8-44b2-9c2e-17ce5406afb7
file-extension: yaml
extension-configs:
- data-id: mall-third-service.yml
group: DEFAULT_GROUP
refresh: true
三、测试
@SpringBootTest
class MallThirdServiceApplicationTests {
@Resource
OSSClient ossClient;
@Test
void contextLoads() throws FileNotFoundException {
// 上传文件流。
InputStream inputStream = new FileInputStream("C:\\Users\\Jack\\Desktop\\LeetCode_Sharing.png");
ossClient.putObject("pyy-mall", "2022/testPhoto3.png", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传完成...");
}
}
没问题!
四、改善上传
服务端签名后直传
采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。
因此,OSS提供了服务端签名后直传的方案。
controller如下:
这里定义返回类为R是为了统一返回结果,到后面也会用到
package com.caq.mall.thirdservice.controller;
@RestController
@RequestMapping("oss")
public class OssController {
@Resource
private OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")
public String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
public String bucket;
@Value("${spring.cloud.alicloud.access-key}")
public String accessId;
private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
@GetMapping("/policy")
public R getPolicy(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
// callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
// String callbackUrl = "http://88.88.88.88:8888";
String dir = format.format(new Date())+"/"; // 用户上传文件时指定的前缀。以日期格式存储
// 创建OSSClient实例。
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("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 R.ok().put("data",respMap);
}
}
测试这个请求,http://localhost:9988/oss/policy,成功获取

五、设置网关代理
- id: mall-third-service
uri: lb://mall-third-service
predicates:
- Path=/api/thirdservice/**
filters:
- RewritePath= /api/thirdservice/(?>.*),/$\{segment}
测试这个请求,http://localhost:88/api/thirdservice/oss/policy

至此,我们的功能都没问题了,那么现在就来前端的代码
单文件上传组件
点击上传
只能上传jpg/png文件,且不超过10MB
多文件上传组件
服务端签名
import http from '@/utils/httpRequest.js'
export function policy() {
return new Promise((resolve,reject)=>{
http({
url: http.adornUrl("/third-party/oss/policy"),
method: "get",
params: http.adornParams({})
}).then(({ data }) => {
resolve(data);
})
});
}

图片可以正常上传和显示


就是规定添加的属性要符合规定,不然会出现想不到的异常!
例如:添加品牌选项框中,设置检索首字母那么我们就要规定首字母不能是多个字母只能是a-z或A-Z之间的一个
那么我们就可以对这个输入框进行绑定,如下

实现效果如下:

后端的处理前端传来的数据时,虽然前端已做限制但是还不够严谨,例如我们可以跳过页面通过一些工具直接发送请求也可以完成添加等操作,所以后端也需要做数据校验!
java中也提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,@Email,@NotNull等注解。
一、添加依赖
后面可能其他模块也能用到,所以这里把依赖添加到common模块
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
<version>2.3.2.RELEASEversion>
dependency>
这个依赖提供了NotNull,@NotBlank和@NotEmpty这些判断
二、给需要校验的bean添加注解
/**
* 品牌
*
* @author xiaocai
* @email mildcaq@gmail.com
* @date 2022-07-27 21:05:30
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank
private String name;
/**
* 品牌logo地址
*/
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(0)
private Integer sort;
}
未在控制类中指定方法开启校验时如下:

controller中给请求方法加校验注解@Valid,开启校验,
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody @Valid BrandEntity brand){
brandService.save(brand);
return R.ok();
}
这里我错误信息只返回是Bad Request
视频中老师所讲的是有详细信息的,这个差异应该是版本原因不影响!

这种返回的错误结果并不符合我们的业务需要。我们想让捕捉这个错误的详细信息,并且能够统一返回我们自定义的信息!
三、通过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();
}
再次测试
好啦,这下把错误信息都捕获到喽!

但是,这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。
四、统一异常处理
可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
(1)抽取一个异常处理类
详细信息都写在了注释里,可以作为参考!!!
@Slf4j
@RestControllerAdvice(basePackages = "com.caq.mall.product.controller")
public class MallExceptionAdvice {
//指定的包下所有的校验异常都会被这个方法捕捉
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException exception) {
//定义map,存放所有错误信息
Map<String, String> map = new HashMap<>();
//通过BindResult捕获校验结果
BindingResult bindingResult = exception.getBindingResult();
//遍历校验结果中所有字段的错误,字段为key,错误信息为value存放到map中
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)测试: http://localhost:88/api/product/brand/save
接下来我们去掉我们控制类中save方法的校验,看看统一异常处理能否生效
还是可以哦!

(3)错误状态码
正规开发过程中,错误状态码有着严格的定义规则
/***
* 错误码和错误信息定义类
* 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;
}
}
(4)默认异常处理
上面的统一异常处理只是针对了校验相关的错误,那么如果是其他异常呢?
那就再来个默认的异常处理呗
@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());
}
一、给校验注解,标注上groups,指定什么情况下才需要进行校验
如:指定在更新和添加的时候,都需要进行校验,我们对id进行限制
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
这里的UpdateGroup和AddGroup都需要收到创建一下,为了演示可以只创建不写内容
二、使用@Validated注解
@Validated(AddGroup.class)指定新增的时候注解才会生效
其他的注解字段,即使标注校检也不生效
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
三、测试
因为指定了新增不能指定id,但是我们测试的时候加id了所以返回错误信息

测试其他字段
可以看到即使name字段加非空了,我们测试用空值也是可以生效的
说明在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。

一、导入依赖
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>2.0.1.Finalversion>
dependency>
二、编写自定义注解
(1)注解的格式不会写怎么办?
直接复制其他注解的形式
(2)特别说明
(3)@Constraint( validatedBy = {})的说明
validatedBy指定这个注解由哪一个校验器校验,详细信息如图:

(4)配置注解错误返回信息
在resource文件下创建:ValidationMessages.properties
com.zsy.common.valid.ListValue.message=必须提交指定的值
(5)自定义的校验器
详细信息还是注意代码中的注释
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private final Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* 参数:自定义注解的详细信息
*/
@Override
public void initialize(ListValue constraintAnnotation) {
//constraintAnnotation.vals()意思是获得你注解里的参数
int[] values = constraintAnnotation.vals();
//把获取到的参数放到set集合里
for (int v al : values) {
set.add(val);
}
}
/**
* 判断是否校验成功
* @param value 需要校验的值
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
//这里的Integer value参数是指你注解里提交过来的参数
//之后判断集合里是否有这个传进来的值,如果有返回true,没的话返回false并返回错误信息
return set.contains(value);
}
}
(6)关联自定义校验器
通过validatedBy = {ListValueConstraintValidator.class}去指定即可!
那如果以后@ListValue注解支持的属性类型变为double了,我们只需要在指定新的校验器即可
/**
* 自定义校验注解 声明可以取那些值
*/
@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.caq.common.validation.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default {};
}
三、测试
我们给状态字段指定分组检验,让它增加的时候才进行校验
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals = {0, 1}, groups = {AddGroup.class})
private Integer showStatus;
(1)读取properties文件内容乱码

设置好,清理target,重新编译
再次测试

(1)做检验的字段
/**
* 品牌
* @author xiaocai
* @email mildcaq@gmail.com
* @date 2022-07-27 21:05:30
*/
@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;
}
(2)controller中共三个方法做了数据校验
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class})@RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
@RequestMapping("/update/status")
public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
(三)测试前后端的校验
测试状态修改

测试修改

测试新增
