文章目录

  • 一、简介
    • 1、概述
    • 2、环境与技术介绍
    • 3、简单的文件上传
  • 二、文件校验与上传实战
    • 1、 前提准备
    • 2、 文件枚举类
    • 3、 自定义文件校验注解
    • 4、 文件校验切面
    • 5、 文件上传工具类
    • 6、 控制类
    • 7、 配置文件
    • 8、 文件的前端显示
  • 三、阿里云OSS文件上传
    • 1、 阿里云oss配置
    • 2、 Java整合oss
    • 3、 注意事项

一、简介

1、概述

文件上传是Web项目的一个基本功能,一般是通过上传文件的后缀名进行格式校验,但是由于文件的后缀是可以手动更改的,黑客可以通过修改后缀名入侵文件服务器,因此后缀名校验不是一种严格有效的文件校验方式。如果想要对上传文件进行严格的格式校验,则需要通过文件头进行校验,即魔数文件头是位于文件开头的一段承担一定任务的数据,一般都在开头的部分,其作用就是为了描述一个文件的一些重要的属性,其可以作为是一类特定文件的标识。

2、环境与技术介绍

SpringBoot2.5.6,AOP思想

使用切面编程,在文件上传之前,通过自定义注解首先进行自定义文件类型判断,若判断不通过,则通过全全局自定义异常返回,通过所有检查后才进行文件的上传,同时通过ConditionalOnProperty注解可以在application.yml中进行注解文件的打开或关闭,即校验文件功能的开启与关闭。

3、简单的文件上传

    @Value("${file.staticPath}")private String staticPath;@Value("${file.uploadFolder}")private String uploadFolder;/*** 上传文件,比较通用的方法,这里我写在这里可以进行参考修改* 其他方法*/public String uploadFile(MultipartFile multipartFile, String dir) {try {//上传的文件:aaa.jpgString realFileName = multipartFile.getOriginalFilename();//2:藏图文件名的后级String imgSuffix = realFileName.substring(realFileName.lastIndexOf("."));//3:生成的唯一的文件名:能不能用中文名:不能因为统一用英文命名。String newFileName = UUID.randomUUID().toString() + imgSuffix;//4:日期目录SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");String datePath = dateFormat.format(new Date());//5:服务路径String serverName = uploadFolder;//6:指定文件上传以后的目录File targetPath = new File(serverName + dir, datePath);if (!targetPath.exists()) {targetPath.mkdirs();}//6:指定文件上传以后的服务器的完整的文件名File targetFileName = new File(targetPath, newFileName);//7:文件上传到指定的目录multipartFile.transferTo(targetFileName);// 返回的自由选择,可以选择Map进行返回String fileName = dir + File.separator + datePath + File.separator + newFileName;return staticPath + File.separator + fileName;} catch (IOException e) {e.printStackTrace();return "fail";}}

yml中进行配置

file:staticPatternPath: /upload/**uploadFolder: /www/upload/staticPath: http://www.shawn22.xyz:8080

二、文件校验与上传实战

1、 前提准备

SpringBoot Log4j2日志

SpringBoot自定义全局异常

2、 文件枚举类

包含了每种文件的后缀名与头部魔数

/*** 文件类型* 文件魔数* @author Shawn* @date 2021/11/23*/
@Getter
public enum FileType {/*** JPEG  (jpg)*/JPEG("JPEG", "FFD8FF"),JPG("JPG", "FFD8FF"),/*** PNG*/PNG("PNG", "89504E47"),/*** GIF*/GIF("GIF", "47494638"),/*** TIFF (tif)*/TIFF("TIF", "49492A00"),/*** Windows bitmap (bmp)*/BMP("BMP", "424D"),/*** 16色位图(bmp)*/BMP_16("BMP", "424D228C010000000000"),/*** 24位位图(bmp)*/BMP_24("BMP", "424D8240090000000000"),/*** 256色位图(bmp)*/BMP_256("BMP", "424D8E1B030000000000"),/*** CAD  (dwg)*/DWG("DWG", "41433130"),/*** Adobe photoshop  (psd)*/PSD("PSD", "38425053"),/*** Rich Text Format  (rtf)*/RTF("RTF", "7B5C727466"),/*** XML*/XML("XML", "3C3F786D6C"),/*** HTML (html)*/HTML("HTML", "68746D6C3E"),/*** Email [thorough only] (eml)*/EML("EML", "44656C69766572792D646174653A"),/*** Outlook Express (dbx)*/DBX("DBX", "CFAD12FEC5FD746F "),/*** Outlook (pst)*/PST("", "2142444E"),/*** doc;xls;dot;ppt;xla;ppa;pps;pot;msi;sdw;db*/OLE2("OLE2", "0xD0CF11E0A1B11AE1"),/*** Microsoft Word/Excel 注意:word 和 excel的文件头一样*/XLS("XLS", "D0CF11E0"),/*** Microsoft Word/Excel 注意:word 和 excel的文件头一样*/DOC("DOC", "D0CF11E0"),/*** Microsoft Word/Excel 2007以上版本文件 注意:word 和 excel的文件头一样*/DOCX("DOCX", "504B0304"),/*** Microsoft Word/Excel 2007以上版本文件 注意:word 和 excel的文件头一样 504B030414000600080000002100*/XLSX("XLSX", "504B0304"),/*** Microsoft Access (mdb)*/MDB("MDB", "5374616E64617264204A"),/*** Adobe Acrobat (pdf) 255044462D312E*/PDF("PDF", "25504446"),/*** Windows Password  (pwl)*/PWL("PWL", "E3828596"),/*** WAVE (wav)*/WAV("WAV", "57415645"),/*** AVI*/AVI("AVI", "41564920"),/*** Real Audio (ram)*/RAM("RAM", "2E7261FD"),/*** Real Media (rm) rmvb/rm相同*/RM("RM", "2E524D46"),/*** Real Media (rm) rmvb/rm相同*/RMVB("RMVB", "2E524D46000000120001"),/*** MPEG (mpg)*/MPG("MPG", "000001BA"),/*** Quicktime  (mov)*/MOV("MOV", "6D6F6F76"),/*** MIDI (mid)*/MID("MID", "4D546864"),/*** MP4*/MP4("MP4", "00000020667479706D70"),/*** MP3*/MP3("MP3", "49443303000000002176"),/*** FLV*/FLV("FLV", "464C5601050000000900"),/*** torrent*/TORRENT("TORRENT", "6431303A637265617465"),/*** JSP Archive*/JSP("JSP", "3C2540207061676520"),/*** JAVA Archive*/JAVA("JAVA", "7061636B61676520"),/*** CLASS Archive*/CLASS("CLASS", "CAFEBABE0000002E00"),/*** JAR Archive*/JAR("JAR", "504B03040A000000"),/*** MF Archive*/MF("MF", "4D616E69666573742D56"),/*** EXE Archive*/EXE("EXE", "4D5A9000030000000400"),/*** ELF Executable*/ELF("ELF", "7F454C4601010100"),/*** Lotus 123 v1*/WK1("WK1", "2000604060"),/*** Lotus 123 v3*/WK3("WK3", "00001A0000100400"),/*** Lotus 123 v5*/WK4("WK4", "00001A0002100400"),/*** Lotus WordPro v9*/LWP("LWP", "576F726450726F"),/*** Sage(sly.or.srt.or.slt;sly;srt;slt)*/SLY("SLY", "53520100");/*** 后缀 大写字母*/private final String suffix;/*** 魔数*/private final String magicNumber;FileType(String suffix, String magicNumber) {this.suffix = suffix;this.magicNumber = magicNumber;}@NonNullpublic static FileType getBySuffix(String suffix) throws FileUploadException {for (FileType fileType : FileType.values()) {if (fileType.getSuffix().equals(suffix.toUpperCase())) {return fileType;}}throw new FileUploadException("不支持的文件后缀 : " + suffix);}
}

3、 自定义文件校验注解

/*** 文件检查** @author Shawn* @date 2021/11/23*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface FileCheck {/*** 校验不通过提示信息** @return*/String message() default "不支持的文件格式";/*** 校验方式*/CheckType type() default CheckType.SUFFIX;/*** 支持的文件后缀** @return*/String[] supportedSuffixes() default {};/*** 支持的文件类型** @return*/FileType[] supportedFileTypes() default {};enum CheckType {/*** 仅校验后缀*/SUFFIX,/*** 校验文件头(魔数)*/MAGIC_NUMBER,/*** 同时校验后缀和文件头*/SUFFIX_MAGIC_NUMBER}
}

4、 文件校验切面

/*** @author Shawn* @date 2021年11月23日9:32* prefix为配置文件中的前缀,* name为配置的名字* havingValue是与配置的值对比值,当两个值相同返回true,配置类生效* 需要在yml中进行配置:前缀+名字,值为true,表示该配置文件生效**/
@Aspect
@Slf4j
@Component
@ConditionalOnProperty(prefix = "file-check", name = "enabled", havingValue = "true")
public class FileCheckAspect {/*** 目标方法:被@FileCheck注解的方法即为目标方法* 其中@annotation中的值,需要和target方法中参数名称相同(必须相同,但是名称任意)** @param joinPoint  连接点* @param annotation 文件检查*/@Before("@annotation(annotation)")public void before(JoinPoint joinPoint, FileCheck annotation) throws FileUploadException {final String[] suffixes = annotation.supportedSuffixes();final FileCheck.CheckType type = annotation.type();final FileType[] fileTypes = annotation.supportedFileTypes();final String message = annotation.message();// 支持的文件后缀和文件类型有一个为空则返回if (ArrayUtils.isEmpty(suffixes) && ArrayUtils.isEmpty(fileTypes)) {return;}Object[] args = joinPoint.getArgs();//文件后缀转成set集合Set<String> suffixSet = new HashSet<>(Arrays.asList(suffixes));for (FileType fileType : fileTypes) {suffixSet.add(fileType.getSuffix());}//文件类型转成set集合Set<FileType> fileTypeSet = new HashSet<>(Arrays.asList(fileTypes));for (String suffix : suffixes) {fileTypeSet.add(FileType.getBySuffix(suffix));}//对参数是文件的进行校验for (Object arg : args) {if (arg instanceof MultipartFile) {doCheck((MultipartFile) arg, type, suffixSet, fileTypeSet, message);} else if (arg instanceof MultipartFile[]) {for (MultipartFile file : (MultipartFile[]) arg) {doCheck(file, type, suffixSet, fileTypeSet, message);}}}}/*** 根据指定的检查类型对文件进行校验*/private void doCheck(MultipartFile file, FileCheck.CheckType type, Set<String> suffixSet, Set<FileType> fileTypeSet, String message) throws FileUploadException {if (type == FileCheck.CheckType.SUFFIX) {doCheckSuffix(file, suffixSet, message);} else if (type == FileCheck.CheckType.MAGIC_NUMBER) {doCheckMagicNumber(file, fileTypeSet, message);} else {doCheckSuffix(file, suffixSet, message);doCheckMagicNumber(file, fileTypeSet, message);}}/*** 验证文件魔数*/private void doCheckMagicNumber(MultipartFile file, Set<FileType> fileTypeSet, String message) throws FileUploadException {String magicNumber = readMagicNumber(file);String fileName = file.getOriginalFilename();String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1).toUpperCase();for (FileType fileType : fileTypeSet) {if (magicNumber.startsWith(fileType.getMagicNumber()) && fileType.getSuffix().toUpperCase().equalsIgnoreCase(fileSuffix)) {return;}}log.error("文件头格式错误:{}", magicNumber);throw new FileUploadException(message);}/*** 验证文件后缀*/private void doCheckSuffix(MultipartFile file, Set<String> suffixSet, String message) throws FileUploadException {String fileName = file.getOriginalFilename();String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1).toUpperCase();for (String suffix : suffixSet) {if (suffix.toUpperCase().equalsIgnoreCase(fileSuffix)) {return;}}log.error("文件后缀格式错误:{}", message);throw new FileUploadException(message);}/*** 读取文件,获取文件头*/private String readMagicNumber(MultipartFile file) throws FileUploadException {try (InputStream is = file.getInputStream()) {byte[] fileHeader = new byte[4];is.read(fileHeader, 0, 4);return byteArray2Hex(fileHeader);} catch (IOException e) {log.error("文件读取错误:{0}", e);throw new FileUploadException("读取文件失败!");}}/*** 字节数组转十六进制*/private String byteArray2Hex(byte[] data) {StringBuilder stringBuilder = new StringBuilder();if (ArrayUtils.isEmpty(data)) {return null;}for (byte datum : data) {int v = datum & 0xFF;String hv = Integer.toHexString(v).toUpperCase();if (hv.length() < 2) {stringBuilder.append(0);}stringBuilder.append(hv);}return stringBuilder.toString();}}

5、 文件上传工具类

/*** 文件上传工具类* @author Shawn* @date 2021年11月22日19:45**/
public class FileUtils {private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);/*** 文件上传** @param file 文件* @return {@link String}* @throws Exception 异常*/public static String fileUpload(Integer type, Integer userId,MultipartFile file) throws FileUploadException {// 获取文件名,带后缀String originalFilename = file.getOriginalFilename();// 获取文件的后缀格式String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();String filePrefix = String.valueOf(System.currentTimeMillis()).concat(String.valueOf(type)).concat(String.valueOf(userId));String newFileName = filePrefix.concat(".").concat(fileSuffix);String dirPath;// 判断上传类型if(type == 0 ){dirPath = FileLocationEnum.LocalVideoLocation.getLocation();}else{dirPath = FileLocationEnum.LocalPicLocation.getLocation();}String path = dirPath + newFileName;File destFile = new File(dirPath + newFileName);if (!destFile.getParentFile().exists()) {destFile.getParentFile().mkdirs();}try {file.transferTo(destFile);logger.info("单次上传文件成功");// 将相对路径返回给前端return path;} catch (IOException e) {logger.error("upload pic error");throw new FileUploadException("上传文件错误");}}/*** 文件上传的图片** @param type   类型,图片为1,视频为0* @param userId 用户id* @param files   文件* @return {@link List<String>}*/public static List<String> fileUploadWithPics(int type, Integer userId, MultipartFile[] files) throws FileUploadException {List<String> picList = new ArrayList<>();for (MultipartFile file:files) {picList.add(fileUpload(type,userId,file));}logger.info("多图片文件上传成功");return picList;}}

6、 控制类

这里提供了一个视频上传接口和多图片上传接口

/*** @author Shawn* @date 2021年11月22日21:09**/
@RestController
@RequestMapping("/file")
public class FileUploadController {/*** 文件上传的图片* 同时校验后缀和文件头* @param userId 用户id* @param file   文件* @return {@link ResultVO<?>}* @throws Exception 异常*/@PostMapping("/fileuploadwithpics")@FileCheck(message = "不支持的图片格式",supportedSuffixes = {"png", "jpg",  "jpeg"},type = FileCheck.CheckType.SUFFIX_MAGIC_NUMBER,supportedFileTypes = {FileType.PNG, FileType.JPG, FileType.JPEG})public ResultVO<?> fileUploadWithPics(Integer userId, @RequestParam("pics") MultipartFile[] MultipartFile) throws Exception {if(userId==null){return new ResultVO<>(400,"缺少userId参数");}// 1表示图片,0 表示视频List<String> result = FileUtils.fileUploadWithPics(1, userId, MultipartFile);Map<String, List<String>> map = new HashMap<>(4);map.put("picUrl",result);return new ResultVO<>(map);}/*** 文件上传视频* 仅校验后缀* @param userId 用户id* @param file   文件* @return {@link ResultVO<?>}* @throws Exception 异常*/@PostMapping("/fileuploadwithvideo")@FileCheck(message = "不支持的视频格式",type = FileCheck.CheckType.SUFFIX,supportedSuffixes = {"mp4","gif"})public ResultVO<?> fileUploadWithVideo(Integer userId, @RequestParam("video") MultipartFile file) throws Exception {if(userId==null){return new ResultVO<>(400,"缺少userId参数");}String s = FileUtils.fileUpload(0, userId, file);Map<String, String> map = new HashMap<>(4);map.put("videoUrl",s);return new ResultVO<>(map);}}

7、 配置文件

application.yml进行配置

spring:servlet:multipart:enabled: true# 单个文件大小,m默认1Mmax-file-size: 10MB# 总上传文件大小,默认10Mmax-request-size: 30MB# 文件多少时写入磁盘,默认为0,有文件就写入# file-size-threshold: 10MB

8、 文件的前端显示

一种是Nginx进行映射,这种方式比较常见;另一种是SpringBoot自带的映射穿透,需要在application配置好映射关系,或者在java里配置好映射关系。

若视频放在D:\social\文件夹下,最终资源访问路径http://ip:port/social/xxxx

Yml配置文件方式

spring:mvc:static-path-pattern: /social/**web:resources:static-locations: file:D:\social\

javaBean配置方式

/**#application.yml中的配置file:staticPatternPath: /social/**uploadFolder: file:D:\social\
*///这个注解必须加,将该bean交给Spring管理,否则无法解析@Value
@Component
public class WebMvcConfig implements WebMvcConfigurer {@Value("${file.staticPatternPath}")private String staticPatternPath;@Value("${file.uploadFolder}")private String uploadFolder;// 这个方法是springboot中springMvc让程序开发者去配置文件上传的额外的静态资源服务的配置@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// staticPatternPath 是访问路径,后面的是上传的资源路径// uploadFolder 是文件存储位置,而文件保存在uploadFolder 目录下registry.addResourceHandler(staticPatternPath).addResourceLocations(uploadFolder);}
}

三、阿里云OSS文件上传

1、 阿里云oss配置

首先开通阿里云oss,选择公共读,这样别人才可以读到我们的文件,但这样可能会导致上行流量剧增

创建玩Bucket后,需要配置一下ssl证书和已备案自定义域名,否则浏览器只能下载,不能读

最后获取AccessKey和SecretKey。进入 AccessKey管理 ,进入之后选择开始使用子用户AccessKey(推荐,这样安全),创建子用户,选择openAPI访问,创建完成后,添加AliyunOSSFullAccess权限

2、 Java整合oss

官方教程:https://help.aliyun.com/document_detail/84778.html

下面简单说一下配置,首先配置maven

<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.10.2</version>
</dependency>

创建上传方法

public static String uploadFile(MultipartFile multipartFile) {// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。String endpoint = "oss-cn-hangzhou.aliyuncs.com";// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。String accessKeyId = "";String accessKeySecret = "";// 你的桶名字String bucketName = "";// 你的自定义域名,需要备案和配好ssl证书String domainName = "";// 桶里面你的根目录String rootPath = "lamp";OSS ossClient = null;try {// 创建OSSClient实例。ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);// 获取文件上传的流InputStream inputStream = multipartFile.getInputStream();// 构建指定目录,按日期分类SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");String datePath = dateFormat.format(new Date());// 获取文件名String originName = multipartFile.getOriginalFilename();String filename = UUID.randomUUID().toString();String suffix = originName.substring(originName.lastIndexOf("."));String newName = filename + suffix;String fileUrl = rootPath + "/" + datePath + "/" + newName;// 上传文件ossClient.putObject(bucketName, fileUrl, inputStream);return "https://" + domainName + "/" + fileUrl;} catch (IOException e) {e.printStackTrace();return "fail";} finally {// 关闭OSSClient。ossClient.shutdown();}}

3、 注意事项

使用 OSS 默认域名访问 html、图片资源,会有以附件形式下载的情况。若需要浏览器直接访问,需使用自定义域名进行访问,同时保证已经配置好ssl证书;同时oss桶还可以用来做图床

其他请参考官方文档


参考文献:

https://www.jianshu.com/p/be3f4c26c39a

https://www.cnblogs.com/zys2019/p/15394599.html

https://www.bilibili.com/video/BV1C3411b7wt?p=15&spm_id_from=pageDriver

SpringBoot文件上传与校验相关推荐

  1. 补习系列(11)-springboot 文件上传原理

    一.文件上传原理 一个文件上传的过程如下图所示: 浏览器发起HTTP POST请求,指定请求头: Content-Type: multipart/form-data 服务端解析请求内容,执行文件保存处 ...

  2. SpringBoot文件上传源码解析

    一.SpringMVC文件上传源码分析前言(这部分我觉得原作者写的很好) 该如何研究SpringMVC的文件上传的源码呢? 研究源码并不是仅仅知道程序是怎样运行的,而应该从宏观的角度.不同的立场去看待 ...

  3. 解决Springboot文件上传报错,java.io.FileNotFoundException: D:\System\Temp\tomcat.819...00.tmp (系统找不到指定的文件。)

    Springboot文件上传,csdn上的方法无非是下面这两个: imgFile.transferTo(imageFolder); // 方法一/*** 方法二* FileUtils.copyInpu ...

  4. springboot文件上传下载实战 ——文件上传、下载、在线打开、删除

    springboot文件上传下载实战 文件上传 文件上传核心 UserFileController 文件上传测试 文件下载与在线打开 文件下载.在线打开核心 UserFileController 文件 ...

  5. springboot文件上传下载实战 —— 登录功能、展示所有文件

    springboot文件上传下载实战 创建项目 pom.xml 数据库建表与环境准备 建表SQL 配置文件 application.properties 整体架构 前端页面 登录页面 login.ht ...

  6. SpringBoot文件上传异常之提示The temporary upload location xxx is not valid

    SpringBoot文件上传异常之提示The temporary upload location xxx is not valid 参考文章: (1)SpringBoot文件上传异常之提示The te ...

  7. SpringBoot 文件上传 通过Content-Type和文件头判断文件类型

    SpringBoot 文件上传 通过Content-Type和文件头判断文件类型 一.关于MIME MIME的全称是Multipurpose Internet Mail Extensions,即多用途 ...

  8. springboot文件上传、下载使用ftp工具将文件上传至服务器

    springboot文件上传.下载使用ftp工具 首先在服务器搭建ftp服务 配置文件(在application.properties中) # Single file max size multipa ...

  9. springboot文件上传,单文件上传和多文件上传,以及数据遍历和回显

    springboot文件上传,单文件上传和多文件上传 项目结构及pom.xml 创建文件表单页面 编写javabean 编写controller映射 MultipartFile类 @RequestPa ...

最新文章

  1. 美国国家科学委员会发布学术研发报告
  2. MySQL之帮助的使用
  3. 如何加入IETF 如何发表自己的RFC
  4. 吴继业:LinkedIn商业分析部如何运用大数据实现商业价值
  5. matlab如何使用cu文件,Matlab编译cuda的.cu文件
  6. 注释嵌套注释_DIY注释
  7. Arbitrage——判断正环Bellman-Ford/SPFA
  8. Effective Java 电子书 apk版本下载
  9. 4001.基于双向链表的双向冒泡排序法
  10. 趣文:有趣的 Linux 命令
  11. 组策略参考文档1-共享打印机
  12. php图像处理缩略图,17.ThinkPHP 扩展库:图像处理--生成缩略图
  13. 功能强大的全新虚拟商品自动发货商城源码
  14. c语言程序后退_单片机控制小车循迹(前进、后退、左右转)
  15. Android 微信聊天页面
  16. win7双屏幕,双任务栏
  17. 详细了解SQLITE 优缺点 性能测试
  18. html 条纹背景,CSS3 一组条纹背景图案
  19. 沙漠 草原 湖泊 羊群 骆驼(2)
  20. 复盘二: 了解自我和管理自我,诚惶诚恐,保持敬畏-- 宁向东的清华管理学课总结

热门文章

  1. 聚类分析在用户行为中的实例_聚类分析案例之市场细分
  2. 小程序中如何关注公众号
  3. upload-labs靶场通关指南(16-17关)
  4. 常见的打印机无法打印问题
  5. ApeCoin计划推出自己的区块链,Messari分析师们怎么看?
  6. css表格表头对角线,用div+css模拟类excel表格对角线(斜线)
  7. ClasspathResource路径问题解决
  8. 试用《Cascadeur》:一款基于物理的角色动画软件
  9. AVS2实时编码器xavs2的运行
  10. 傅里叶变换的通俗理解