项目自学记录9


项目自学记录9

跨域问题

协议+域名+端口,只要有一个不同,就是跨域。

添加一个过滤器,选择springframework.web下面的CorsFilter。类似如下:

@Configuration
public class WebMvcConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //允许白名单域名进行跨域调用
        corsConfiguration.addAllowedOrigin("*");
        //允许post请求方法跨域调用
        corsConfiguration.addAllowedMethod(HttpMethod.POST);
        //放行全部原始头信息
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.setExposedHeaders(Arrays.asList("header1", "header2"));
        corsConfiguration.setMaxAge(3600L);
        //允许跨越发送cookie
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/*", corsConfiguration);
        return corsConfigurationSource;
    }
}

Nginx的反向代理要了解一下

sql树形结构

方法一:表的自连接。方法二:递归

表的自连接需要确定树的深度。

SELECT
    one.id one_id,
    one.label one_label,
    two.id two_id,
    two.label two_label
FROM
    course_category one
    INNER JOIN course_category two ON two.parentid=one.id
    INNER JOIN course_category three ON three.parentid=two.id
    WHERE one.id='1' AND one.is_show='1' AND two.is_show='1'
    ORDER BY one.orderby,two.orderby

递归

with recursive temp as (
select * from  course_category p where  id= '1'
 union all
select t.* from course_category t inner join temp on temp.id = t.parentid
)
select *  from temp order by temp.id, temp.orderby

sql注入,为什么

从下面可以看到最后的a后面多了一个斜杠。

要看Mybatis的源码

MyBatis在#{}传递参数时,是借助setString()方法来完成,${}则不是

如果是单引号,会在SQL语句中添加一个斜杠

最终传递的参数为:'aaa\' or 1=1 --,在数据库中肯定是查不到的。

什么情况下用${}? 需要插入的东西为sql时,或者为表名时。就不能用#{}

stream的tomap

Map<Integer, String> collect = list.stream()
        .collect(Collectors.toMap(Student::getId, Student::getName, (n1, n2) -> n2));

第一个参数,key是谁,第二个参数,value是谁,第三个参数,如果有俩key一样了,让后面的覆盖前面的。

校验框架

超越用过,spring-boot-starter-validation。提供了JSR303校验支持。

在Dto类中的一个属性上加注解,@NotEmpty(message=”课程名称不能为空”)

在Controller层的函数入参中,要加@Validated

还要针对弹出的错误信息在异常类中创建函数

BindingResult bindingResult = e.getBindingResult();
List<String> errors = new ArrayList<>();
bindingResult.getFieldErrors().stream().forEach(item->{
    errors.add(item.getDefaultMessage());
});
String errMessage = StringUtils.join(errors,",");

分组校验,定义一些常用的组,更新用组1,增加用组1和组2

@NotEmpty(message=”课程名称不能为空”,groups={ValidationGroups.Insert.class})

组为自己定义的类,里面有三个接口Insert,Update,Delete

@Validated(ValidationGroups.Insert.class)

resultMap映射

类似如下:

配置文件优先级

项目应用名配置文件> 扩展配置文件 > 共享配置文件 > 本地配置文件。 Nacos上配的优先级高于本地的。

如果想让本地配置文件优先,也要在nacos里配

spring:
  cloud:
    config:
      override-none: true

上传后文件校验

要比较本地的上传之前的,和本地的下载以后的。不要直接访问云端,直接访问云端md5校验不会通过。

非事务调用事务

函数1调用函数2,函数2是这个类自身的。即this.函数2,this不是代理对象,即使函数2加了事务注解,也没用。

解决方案:注入的都是代理对象,要把自己的Service注入,然后把函数2提出为一个接口,用注入的service.func2,这样就是代理对象,就会触发事务。

其他事务失效场景

@Transactional标记的方法不是public

抛出的异常类型不是RuntimeException。在rollbackFor里配置

事务方法内部调用事务方法,如果函数2设置@Transactional(propagation=Propagation.REQUIRES_NEW),可能无法开启新事务。因为你不是通过代理对象去调用的。函数1调用函数2,函数1是一个事务。

1.标志REQUIRES_NEW会新开启事务,外层事务不会影响内部事务的提交/回滚
2.标志REQUIRES_NEW的内部事务的异常,会影响外部事务的回滚

断点续传

流程如下:

  1. 前端上传前先把文件分成块
  2. 一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传。先校验这一块,这一块没有才上传。
  3. 各分块上传完成最后在服务端合并文件
public class BigFileTest {

    //分块测试
    @Test
    public void testChunk() throws IOException {
        //源文件
        File sourceFile = new File("D:\\develop\\upload\\1.项目背景.mp4");
        //分块文件存储路径
        String chunkFilePath = "D:\\develop\\upload\\chunk\\";
        //分块文件大小
        int chunkSize = 1024 * 1024 * 5;
        //分块文件个数
        int chunkNum = (int) Math.ceil(sourceFile.length() * 1.0 / chunkSize);
        //使用流从源文件读数据,向分块文件中写数据
        RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r");
        //缓存区
        byte[] bytes = new byte[1024];
        for (int i = 0; i < chunkNum; i++) {
            File chunkFile = new File(chunkFilePath + i);
            //分块文件写入流
            RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw");
            int len = -1;
            while ((len=raf_r.read(bytes))!=-1){
                raf_rw.write(bytes,0,len);
                if(chunkFile.length()>=chunkSize){
                    break;
                }
            }
            raf_rw.close();
        }
        raf_r.close();

    }

    //将分块进行合并
    @Test
    public void testMerge() throws IOException {
        //块文件目录
        File chunkFolder = new File("D:\\develop\\upload\\chunk");
        //源文件
        File sourceFile = new File("D:\\develop\\upload\\1.项目背景.mp4");
        //合并后的文件
        File mergeFile = new File("D:\\develop\\upload\\1.项目背景_2.mp4");

        //取出所有分块文件
        File[] files = chunkFolder.listFiles();
        //将数组转成list
        List<File> filesList = Arrays.asList(files);
        //对分块文件排序
        Collections.sort(filesList, new Comparator<File>() {
            @Override
            public int compare(File o1, File o2) {
                return Integer.parseInt(o1.getName())-Integer.parseInt(o2.getName());
            }
        });
        //向合并文件写的流
        RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw");
        //缓存区
        byte[] bytes = new byte[1024];
        //遍历分块文件,向合并 的文件写
        for (File file : filesList) {
            //读分块的流
            RandomAccessFile raf_r = new RandomAccessFile(file, "r");
            int len = -1;
            while ((len=raf_r.read(bytes))!=-1){
                raf_rw.write(bytes,0,len);
            }
            raf_r.close();

        }
        raf_rw.close();
        //合并文件完成后对合并的文件md5校验
        FileInputStream fileInputStream_merge = new FileInputStream(mergeFile);
        FileInputStream fileInputStream_source = new FileInputStream(sourceFile);
        String md5_merge = DigestUtils.md5Hex(fileInputStream_merge);
        String md5_source = DigestUtils.md5Hex(fileInputStream_source);
        if(md5_merge.equals(md5_source)){
            System.out.println("文件合并成功");
        }

    }


}

在MinIO中进行断点续传等操作

分块上传,在minIO中合并,

public class MinioTest {

    MinioClient minioClient =
            MinioClient.builder()
                    .endpoint("http://192.168.101.65:9000")
                    .credentials("minioadmin", "minioadmin")
                    .build();

    @Test
    public void test_upload() throws Exception {

        //通过扩展名得到媒体资源类型 mimeType
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
        if(extensionMatch!=null){
            mimeType = extensionMatch.getMimeType();
        }

        //上传文件的参数信息
        UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                .bucket("testbucket")//桶
                .filename("D:\\develop\\upload\\1.mp4") //指定本地文件路径
//                .object("1.mp4")//对象名 在桶下存储该文件
                .object("test/01/1.mp4")//对象名 放在子目录下
                .contentType(mimeType)//设置媒体文件类型
                .build();

        //上传文件
        minioClient.uploadObject(uploadObjectArgs);



    }
    //删除文件
    @Test
    public void test_delete() throws Exception {

        //RemoveObjectArgs
        RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket("testbucket").object("1.mp4").build();

        //删除文件
        minioClient.removeObject(removeObjectArgs);



    }

    //查询文件 从minio中下载
    @Test
    public void test_getFile() throws Exception {

        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test/01/1.mp4").build();
        //查询远程服务获取到一个流对象
        FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
        //指定输出流
        FileOutputStream outputStream = new FileOutputStream(new File("D:\\develop\\upload\\1a.mp4"));
        IOUtils.copy(inputStream,outputStream);

        //校验文件的完整性对文件的内容进行md5
        FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\develop\\upload\\1.mp4"));
        String source_md5 = DigestUtils.md5Hex(fileInputStream1);
        FileInputStream fileInputStream = new FileInputStream(new File("D:\\develop\\upload\\1a.mp4"));
        String local_md5 = DigestUtils.md5Hex(fileInputStream);
        if(source_md5.equals(local_md5)){
            System.out.println("下载成功");
        }

    }


    //将分块文件上传到minio
    @Test
    public void uploadChunk() throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

        for (int i = 0; i < 6; i++) {
            //上传文件的参数信息
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket("testbucket")//桶
                    .filename("D:\\develop\\upload\\chunk\\"+i) //指定本地文件路径
                    .object("chunk/"+i)//对象名 放在子目录下
                    .build();

            //上传文件
            minioClient.uploadObject(uploadObjectArgs);
            System.out.println("上传分块"+i+"成功");
        }

    }

    //调用minio接口合并分块
    @Test
    public void testMerge() throws Exception {

//        List<ComposeSource> sources = new ArrayList<>();
//        for (int i = 0; i < 30; i++) {
//            //指定分块文件的信息
//            ComposeSource composeSource = ComposeSource.builder().bucket("testbucket").object("chunk/" + i).build();
//            sources.add(composeSource);
//        }

        List<ComposeSource> sources = Stream.iterate(0, i -> ++i).limit(6).map(i -> ComposeSource.builder().bucket("testbucket").object("chunk/" + i).build()).collect(Collectors.toList());

        //指定合并后的objectName等信息
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .bucket("testbucket")
                .object("merge01.mp4")
                .sources(sources)//指定源文件
                .build();
        //合并文件,
        //报错size 1048576 must be greater than 5242880,minio默认的分块文件大小为5M
        minioClient.composeObject(composeObjectArgs);

    }

}

定时任务xxl-job

XXL-JOB任务执行流程

比如调度中心下面有三台机器,只有分片广播的路由策略才能同时将任务分配给三个机器,让三个机器都运行。

把任务队列分为3部分,分别分块给0,1,2三台机器。

分布式锁

synchronized只在一个虚拟机里,不能分布式。

让多个虚拟机去抢一把锁。分布式锁用数据库,redis,zookeeper都能实现。数据库可以是主键,也可以是某一个字段不为’4’这种(抢到锁就改为4)

Runtime.getRuntime() .availableProcessors();

jdk文档中这么写到,返回jvm虚拟机可用核心数。并且后面还有一段注释:这个值有可能在虚拟机的特定调用期间更改。我们平时对于此函数的直观印象为:返回机器的CPU数,这个应该是一个常量值

使用CountDownLatch来计数。一种典型的用法就是把一个大任务拆分N个部分,让多个线程(Worker)执行,每个线程(Worker)执行完自己的部分计数器就减1,当所有子部分都完成后,Driver 才继续向下执行才继续执行。

countDownLatch.await(30,TimeUnit.MINUTES); 最多30分钟,等30min就自动解除了。

本地事务只考虑自身的数据库。分布式事务还可以考虑其他组件。

在微服务架构下, 无论是单服务多数据库,还是订单服务远程调用库存服务,都会存在分布式事务。

CAP中,要考虑满足CP还是AP,CP是强一致性,AP则要保证最终一致性。

使用Seata框架基于AT模式和TCC模式都可以实现CP。

AP的话可以用消息队列的方式,失败后自动重试;也可以使用任务调度的方案。

如果使用ssl-job任务调度的方案,可以专门建立一个表ma_message,来记录需要调度哪些信息。本地消息表+任务调度机制。在同一个数据库中,向子模块course_publish写数据的同时,也要向mq_message写。

使用一个sdk工具包,进行mq_message的crud

远程调用

内容管理 调用 媒资管理

在启动类加注解@EnableFeignClients(basePackages={“com.xuecheng.content.feignclient”})

同时需要新定义一个接口,接口里是对应的函数,接口上加个@FeignClient注解

熔断是当下游服务异常时一种保护系统的手段。就不再让你访问媒资管理了。

降级是熔断后上游服务处理熔断的方法,比如内容管理调用一个本地方法。

降级要在@FeignClient中加个fallback属性,或者加fallbackFactory。

认证授权和支付我直接跳过

直接看别人的博客吧

Git仓库(求star):https://github.com/Cyborg2077/xuecheng-plus
项目基础环境搭建: https://cyborg2077.github.io/2023/01/29/XuechengOnlinePart1/
内容管理模块(含实战内容):https://cyborg2077.github.io/2023/02/02/XuechengOnlinePart2/
媒资管理模块:https://cyborg2077.github.io/2023/02/10/XuechengOnlinePart3/
课程发布模块:https://cyborg2077.github.io/2023/02/28/XuechengOnlinePart4/
认证授权模块(含实战内容,解决微信登录):https://cyborg2077.github.io/2023/03/08/XuechengOnlinePart5/
选课学习模块:https://cyborg2077.github.io/2023/03/17/XuechengOnlinePart6/
项目优化:https://cyborg2077.github.io/2023/03/23/XuechengOnlinePart7/

黑马头条也看过,本身百度网盘的资料就很好,就不再多补充了。


文章作者: 爱敲代码の鱼儿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 爱敲代码の鱼儿 !
  目录