谷粒商城笔记-环境配置(2)——文件上传、java参数验证、递归,分页、事务

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 公共依赖

View Code

             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

  1. 登录阿里云账号,进入对象存储界面(对象存储OSS)
  2. 在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

            本次尝试使用的是上传文件流;

View Code

   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);

     }
 }
View Code

             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;
    }
}
View Code

        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();
}
View Code                       

                        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;
    }
}
View Code

                        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());
    }
}
View Code

                              常见的异常统一处理类型

/**
 * 异常处理器 在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();
   }
}
View Code

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": "优惠卷名字不能为空"
    }
}
View Code

         19.3分组校验功能(多场景校验)

                    a.创建分组接口,无需具体的实现类

package com.atguigu.common.valid;

public interface AddGroup {
}
public interface UpdateGroup {
}
View Code

                    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": "优惠卷名字不能为空"
    }
}
View Code

                       根据测试结果分析:

由保存与修改的验证结果,看出
在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加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{};
}
View Code

 因为上面的message值对应的最终字符串需要去ValidationMessages.properties中获得,所以我们在common中新建文件                                          ValidationMessages.properties      

           com.atguigu.common.valid.ListValue.message=必须提交指定的值 [0,1,2,3,4]

                   b.自定义校验器

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<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);
    }
}
View Code

                          实现统一接口: 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": "优惠券图片不能为空"
    }
}
View Code

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

原文地址:https://www.cnblogs.com/q994321263/p/15368959.html