18.阿里云OSS文件上传功能
18.1 创建三方服务
- 创建微服务gulimall-third-part:
组织名:com.atguigu.gulimall;模块名:gulimall-third-part;包名:com.atguigu.gulimall.thirdpart
Name:gulimall.thirdpart 。之后在下一步的依赖中添加web—>spring web,Spring Cloud Routing—> open Feign
- 添加公用配置依赖:
a.添加common 公共依赖
b.nacos注册中心,配置中心
注册中心: 添加pom引用(common中包含),新建配置文件(bootstrap.properties)添加配置,开启注册中心(在启动服务main函数类上)@EnableDiscoveryClient
配置文件:
spring.application.name=gulimall-third-part
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
server.port= 88
18.2 OSS阿里云存储
分布式系统中的文件存储:https://www.jianshu.com/p/1be6bf51566f
阿里云教程:https://help.aliyun.com/learn/learningpath/oss.html?spm=5176.7933691.J_7985555940.3.53e94c59v339me
阿里云使用使用官网:https://help.aliyun.com/document_detail/32009.html
- 登录阿里云账号,进入对象存储界面(对象存储OSS)
- 在Bucket 列表创建一个gulimaill-hello2的bucket,创建过程中需要的参数介绍:https://help.aliyun.com/document_detail/31827.html
3.创建访问密钥 在鼠标放到用户头像显示的AccessKey 管理——>选择开始使用子用户管理AccessKey——>创建用户名与显示名都为gulimall的用户之后, 访问方式Open API 调用访问——为新用户添加权限——>创建新的AccessKey,之后保存id与密码。
AliyunOSSFullAccess |
管理对象存储服务(OSS)权限 |
4.用文件上传方式上传文件:参考地址:https://help.aliyun.com/document_detail/84781.html
本次尝试使用的是上传文件流;
18.3 Spring Alibaba OSS 封装的组件
Spring Alibaba OSS 封装的组件:https://github.com/alibaba/aliyun-spring-boot/tree/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample
1、添加pom文件的引用
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
</dependency>
2、在配置文件中.直接添加配置
// application.properties
alibaba.cloud.access-key=your-ak
alibaba.cloud.secret-key=your-sk
alibaba.cloud.oss.endpoint=***
3、在微服务中的调用: OSSClient 以服务注册的方式注入。
@Service
public class YourService {
@Autowired
private OSSClient ossClient;
public void saveFile() {
String bucketName = "gulimaill-hello2";
// 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
InputStream inputStream = new FileInputStream("/D:\testa2.txt");
// 依次填写Bucket名称(例如examplebucket)和Object完整路径(例如exampledir/exampleobject.txt)。Object完整路径中不能包含Bucket名称。
ossClient.putObject(bucketName, "testa2.txt", inputStream);
}
}
4、配置文件配置在nacos配置中心中
根据nacos中的配置,配置成配置文件
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=d3791198-6eea-45e8-8e33-8e1a37bc5f34
spring.cloud.nacos.config.ext-config[0].data-id=gulimall-third-part-oss.yml
spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.ext-config[0].refresh=true
18.4 分布式系统中的文件上传模型
参考文档:https://help.aliyun.com/document_detail/31926.html
1、服务器模拟policy,微服务中生成一个通用方法
@RestController
public class OSSController {
@Autowired
OSS 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> doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/* String accessId = "<yourAccessKeyId>"; // 请填写您的AccessKeyId。
String accessKey = "<yourAccessKeySecret>"; // 请填写您的AccessKeySecret。
String endpoint = "oss-cn-hangzhou.aliyuncs.com"; // 请填写您的 endpoint。*/
//String bucket = "bucket-name"; // 请填写您的 bucketname 。
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
// callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
//String callbackUrl = "http://88.88.88.88:8888";
//修改为按照时间格式的方式
String dateFormat=new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir =dateFormat+"/"; // 用户上传文件时指定的前缀。
// 创建OSSClient实例。
//OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
Map<String, String> respMap = new LinkedHashMap<String, String>();
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.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;
}
}
2、测试结果:
调用链接:http://localhost:30000/oss/policy
返回结果:
{"accessid":"LTAI5tEVEUcQMFAYHM1p1Rwe","policy":"etJleHBpcmF0aW9uIjoiMjAyMS0xMC0wNVQwMjoyNDo1Mi4xNThaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIxLTEwLTA1LyJdXX0=","signature":"/j/0l4ZFpq2MQ4rSGCBsL4tt6N4=","dir":"2021-10-05/","host":"https://gulimaill-hello2.oss-cn-shanghai.aliyuncs.com","expire":"1633400692"}
3、网关配置:
policy的链接访问统一从: http://localhost:88/api/thirdpart/oss/policy
在网关配置中添加:
- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/${segment}
19.JAVA参数校验(Valid,Validated,自定义检验)
JSR-303校验
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
参考文件:https://www.jianshu.com/p/554533f88370
19.1 添加pom文件
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
19.2一般验证机制 @Valid
a.目前JSR-303 可以验证的种类:
b.简单使用方法:
controller中加校验注解@Valid,开启校验,给校验的Bean后,紧跟一个BindResult。就可以获取到校验的结果。在文件ValidationMessages.properties,可以查看到整个错误规则。ValidationMessages中存在中文注解。
/*
* 用来验证测试用
* */
@RequestMapping("/validateTest")
public R validateTest(@Valid @RequestBody CouponEntity coupon, 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);
}
return R.ok();
}
c.错误码的定义:
统一错误码定义:10000 前两位为业务场景,后三位为错误代码
package com.atguigu.common.exception;
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_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;
}
}
d. 统一异常处理@ExceptionHandler
常用到的4个注解:
@ControllerAdvice标注在类上,通过“basePackages”能够说明处理哪些路径下的异常。
@ResponseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML。
@RestControllerAdvice 是@ControllerAdvice+@responseBody的功能。
@ExceptionHandler(value = 异常类型.class)标注在方法上,表示处理的异常类型。
以下为自定义的异常统一处理类型
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.coupon.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.coupon.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,"自己写的异常汇总方法");
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("错误:",throwable);
//return R.error(400,"自己写的异常汇总方法");
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
常见的异常统一处理类型
/**
* 异常处理器 在common中汇总统一异常:
*
* @author Mark sunlightcs@gmail.com
*/
@RestControllerAdvice
public class RRExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理自定义异常
*/
@ExceptionHandler(RRException.class)
public R handleRRException(RRException e){
R r = new R();
r.put("code", e.getCode());
r.put("msg", e.getMessage());
return r;
}
@ExceptionHandler(NoHandlerFoundException.class)
public R handlerNoFoundException(Exception e) {
logger.error(e.getMessage(), e);
return R.error(404, "路径不存在,请检查路径是否正确");
}
@ExceptionHandler(DuplicateKeyException.class)
public R handleDuplicateKeyException(DuplicateKeyException e){
logger.error(e.getMessage(), e);
return R.error("数据库中已存在该记录");
}
@ExceptionHandler(AuthorizationException.class)
public R handleAuthorizationException(AuthorizationException e){
logger.error(e.getMessage(), e);
return R.error("没有权限,请联系管理员授权");
}
@ExceptionHandler(Exception.class)
public R handleException(Exception e){
logger.error(e.getMessage(), e);
return R.error();
}
}
e.测试用例:
/**
* 优惠卷名字
*/
@NotBlank(message ="优惠卷名字不能为空" )
@NotNull
private String couponName;
访问链接:http://localhost:7000/coupon/coupon/validatetest2
request:{"id":3,"couponName":"","couponType":7 }
respnse:
{
"msg": "参数格式校验失败",
"code": 10001,
"data": {
"couponName": "优惠卷名字不能为空"
}
}
19.3分组校验功能(多场景校验)
a.创建分组接口,无需具体的实现类
package com.atguigu.common.valid;
public interface AddGroup {
}
public interface UpdateGroup {
}
b.vo实体上添加group,分组信息
在controller的方法上或者方法参数上写要处理的分组的接口信息,如@Validated(AddGroup.class)
c.测试用例
/**
* 优惠券图片
*/
@NotBlank(message ="优惠券图片不能为空",groups = {AddGroup.class})
private String couponImg;
/**
* 优惠卷名字
*/
@NotBlank(message ="优惠卷名字不能为空",groups = {AddGroup.class, UpdateGroup.class})
private String couponName;
/**
* 备注
*/
@NotBlank(message ="备注不能为空",groups = {UpdateGroup.class})
private String note;
@NotBlank(message ="优惠券图片2不能为空")
private String couponImg2;
/*
* 用来测试分组验证
* */
@RequestMapping("/validatetest3save")
public R validateTest3save(@Validated(AddGroup.class) @RequestBody CouponEntity coupon){
return R.ok();
}
/*
* 用来测试分组验证
* */
@RequestMapping("/validatetest3update")
public R validateTest3update(@Validated({UpdateGroup.class}) @RequestBody CouponEntity coupon){
return R.ok();
}
原验证规则
/*
* 用来验证测试用
* */
@RequestMapping("/validatetest2")
public R validateTest2(@Valid @RequestBody CouponEntity coupon){
return R.ok();
}
原验证规则:
http://localhost:7000/coupon/coupon/validatetest2
{"id":3,"couponName":"","couponType":7 }
{
"msg": "参数格式校验失败",
"code": 10001,
"data": {
"couponImg2": "优惠券图片2不能为空"
}
}
保存结果:
http://localhost:7000/coupon/coupon/validatetest3save
{"id":3,"couponName":"","couponType":7 }
{
"msg": "参数格式校验失败",
"code": 10001,
"data": {
"couponName": "优惠卷名字不能为空",
"couponImg": "优惠券图片不能为空"
}
}
更新结果:
http://localhost:7000/coupon/coupon/validatetest3update
{"id":3,"couponName":"","couponType":7 }
{
"msg": "参数格式校验失败",
"code": 10001,
"data": {
"note": "备注不能为空",
"couponName": "优惠卷名字不能为空"
}
}
根据测试结果分析:
由保存与修改的验证结果,看出
在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
19.4自定义验证机制 @Valid
a.自定义注解:需要默认的三个方法(message(),groups(),payload())
package com.atguigu.common.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 "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default{};
}
因为上面的message值对应的最终字符串需要去ValidationMessages.properties中获得,所以我们在common中新建文件 ValidationMessages.properties
com.atguigu.common.valid.ListValue.message=必须提交指定的值 [0,1,2,3,4]
b.自定义校验器
View Codepackage 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<ListValue,Integer> { private Set<Integer> set =new HashSet<>(); //初始化操作 @Override public void initialize(ListValue constraintAnnotation) { int [] vals= constraintAnnotation.vals(); for (int val:vals){ set.add(val); } } //验证规则是否正常的通过 @Override public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) { return set.contains(integer); } }
实现统一接口: ConstraintValidator, 第一个泛型参数是所对应的校验注解类型,第二个是校验对象类型
c. 自定义注解与校验器的关联
@Constraint(validatedBy = { ListValueConstraintValidator.class})
d.测试用例:
/**
* 优惠卷类型[0->全场赠券;1->会员赠券;2->购物赠券;3->注册赠券]
*/
@ListValue(vals={0,1,2,3},groups = {AddGroup.class},message ="必须是指定的通用类型" )
private Integer couponType;
/**
测试结果:
http://localhost:7000/coupon/coupon/validatetest3save
{"id":3,"couponName":"","couponType":7 }
{
"msg": "参数格式校验失败",
"code": 10001,
"data": {
"couponName": "优惠卷名字不能为空",
"couponType": "必须是指定的通用类型",
"couponImg": "优惠券图片不能为空"
}
}
20. 实践案例总结
20.1MyBatis-Plus的逻辑删除以及常用的注解
a.@TableField 该字段只是实体中的字段,与数据库表字段没有实际的映射关系
@TableField(exist = false)
private String couponImg2;
b.执行SQL的日志打印功能:在yml文件中添加配置
logging:
level:
com.atguigu.gulimall: debug
注意事项:Debug前与: 要有一个空格。
c. 逻辑删除操作正常的配置
1). 在yml文件中添加配置。
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
2). 在DAO数据库对象上添加注解。
/**
* 发布状态[0-未发布,1-已发布],用来测试逻辑删除的功能
*/
@TableLogic(value = "1",delval = "0")
private Integer publish;
3).测试用例:
执行删除操作:
@RequestMapping("/delete/{id}")
public R deleteByid(@PathVariable("id") Long id){
couponService.removeByIds(Arrays.asList(id));
return R.ok();
}
执行的sql日志打印出来:
执行查询操作:
/**
* 信息
*/
@RequestMapping("/info/{id}")
//@RequiresPermissions("coupon:coupon:info")
public R info(@PathVariable("id") Long id){
CouponEntity coupon = couponService.getById(id);
return R.ok().put("coupon", coupon);
}
执行的sql日志打印出来::
SELECT id,coupon_type,coupon_img,coupon_name,note,num,amount,per_limit,min_point,start_time,end_time,use_type,publish_count,use_count,receive_count,enable_start_time,enable_end_time,code,member_level,publish FROM sms_coupon WHERE id=? AND publish=1
测试结果分析:
逻辑删除的添加,delete,remove操作删除为逻辑更新,其他的操作执行语句在执行先会默认添加逻辑删除的条件帅选。
d.常用的分页插件 Pageutils的正常使用
public PageUtils queryPage(Map<String, Object> params) {
IPage<CouponEntity> page = this.page(
new Query<CouponEntity>().getPage(params),
new QueryWrapper<CouponEntity>()
);
return new PageUtils(page);
}
分页查询:pageUtils,MyBatis Plus分页查询功能参考文档:https://blog.csdn.net/hancoder/article/details/113787197参考官网:https://mp.baomidou.com/guide/page.html