项目自学记录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的内部事务的异常,会影响外部事务的回滚
断点续传
流程如下:
- 前端上传前先把文件分成块
- 一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传。先校验这一块,这一块没有才上传。
- 各分块上传完成最后在服务端合并文件
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
比如调度中心下面有三台机器,只有分片广播的路由策略才能同时将任务分配给三个机器,让三个机器都运行。
把任务队列分为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/