Java后端系统学习路线α阶段--白卷项目优化(三)-完结篇

88 阅读11分钟

白卷项目的10个优化事项,码云仓库地址:gitee.com/qinstudy/wj

大榜:前面,我们讨论了白卷项目的相关优化项,接下来一起收个尾,对剩下部分优化,主要是下面3个事项:后端开发的常用注解、文件操作、博客管理功能。

1、白卷项目-后端开发的常用注解

小汪:好啊,今天把白卷的优化项搞完,把事情打个结。白卷项目的后端是基于Spring Boot框架的,用到了框架的一些注解,有@Controller、@RestController、@Service等。

大榜:是啊,注解确实还不少,我刚开始接触Spring Boot时,容易搞混。为了不搞混,我们把Spring相关的东西捋一捋,主要有Spring Framework、Spring MVC、Spring Boot、Spring Cloud。

小汪:这个我清楚。Spring Framework就是我们常说的Spring框架,像Spring MVC、Spring Boot、Spring Cloud都是基于Spring底层实现的,可以说Spring是所有框架的基石。

大榜:是滴了,Spring MVC是以Spring为底层,专门用于Java Web开发的框架,Spring MVC有很多注解,用于简化Web开发流程。比如注入到IOC容器的注解:@Controller、@RestController、@Service、@Repository;比如url映射的注解:@RequestMapping、@GettingMapping、@PostMapping;请求参数的注解:@RequestBody、@RequestParam、@PathVariable;响应数据为json格式的注解:@ResponseBody。如下图:

image.png

小汪:这个我知道哈,毕竟我工作就是Java Web开发,而且白卷是一个前后端分离项目,前端和后端是通过json格式数据进行交互,所以我们一般使用@RestController注解,该注解包含了@Controller、@ResponseBody,表明这是一个后端Http接口,且响应数据格式为json。

大榜:你说的我可以听懂,但如果对于初学Spring的小伙伴们,就需要去学习注解的入门示例了,好的做法是在网上找Spring注解的使用教程,一定要多动手实践,使用对比差异法来加深理解哟。

小汪:榜哥又拿出对比差异法了,厉害呀。对比差异法,是不是这样的学习方式,比如@RequestBody、@RequestParam有什么区别,什么情况下用@RequestBody,什么情况下@RequestParam,或者进一步,如果我们用错了注解,程序的预期结果是什么样的呢?

大榜:哈哈哈,是滴了。你已经搞懂了对比差异法,可以“出山”了。

小汪:哈哈哈,天天在榜哥的熏陶下讨论学习,必须懂啊!接下来,我们是不是讨论文件上传、下载的功能了。

2、文件上传、下载、查看

大榜:是滴了。白卷中,只是对文件上传做了基本介绍,可以看这一篇:图片上传与项目的打包部署,对文件下载并没有做介绍,所以我想着把文件上传和下载一并讨论了,对比差异法来学习嘛。你看,白卷中文件上传接口,代码是这样的:

@CrossOrigin
@PostMapping("/api/covers")
public String coversUpload(MultipartFile file) throws IOException {
    // 本地磁盘的路径
    String filePath = folderPath;
    File imageFolder = new File(filePath);
​
    String suffixName = "";
    // 获取图片的后缀名称
    String originalFilename = file.getOriginalFilename();
    suffixName = originalFilename.substring(originalFilename.lastIndexOf("."));
​
    log.info("上传图书的封面的后缀名称为:{};封面原始名称为:{}", suffixName, originalFilename);
​
    File f = new File(imageFolder,
                      StringUtils.getRandomString(6) + suffixName);
    if (!f.getParentFile().exists()) {
        f.getParentFile().mkdirs();
    }
​
    // 将MultipartFile文件,保存到创建的f中,也就是保存到本地磁盘上。
    file.transferTo(f);
​
    String imageUrl = "http://localhost:8443/api/file/" + f.getName();
    // 将文件资源路径返回给前端,这样前端就可以根据资源路径来查找图片了
    return imageUrl;
}

上面的图片上传接口中,代码接收前端的文件流数据,然后后端生成文件名称,并将文件流数据保存到后端服务器的磁盘上。

而且,当图书文件上传成功之后,我们还需要在前台查看这些图书信息。所以,我们还需要做路径映射,也就是将前端访问的图片URL,与后端的静态资源路径("d:/img/"),建立映射关系,代码是这样的:

 /**
     * 重新指定静态资源的映射关系
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        /**
         * 将前端访问的图片URL,与后端的静态资源路径("d:/img/"),建立映射关系。
         * 如果注释掉下面的静态资源映射的代码,则前端页面的图书封面为空。所以我们需要加上此处的映射关系
         */
        registry.addResourceHandler("/api/file/**").addResourceLocations("file:" + folderPath);
​
        log.info("静态资源映射关系添加成功!{}", registry);
    }

这样,就实现了图片文件的上传和图片查看功能了。

小汪:榜哥,你这个图片上传的接口,代码是这样:

@CrossOrigin
@PostMapping("/api/covers")
public String coversUpload(MultipartFile file) throws IOException {
    ...
}

MultipartFile,表示一个文件流数据,我感觉只能上传一张图片把?

大榜:你说得很对。上面的写法只能上传一张图片。比如老板给你一个需求,栏目1中 用户需要上传个人头像,栏目2中 用户需要上传生活照,而且是多张生活照。面对这个需求,我们就不能使用coversUpload(MultipartFile file)了,正确的代码是这样的:

/**
     * 图片上传:Spring的 MultipartFile类
     * 处理用户上传的表单数据
     * MultipartFile 自动封装上传过来的文件
     * @param email
     * @param username
     * @param headerImg 头像,只能有一张,所以后端直接使用MultipartFile类来接收
     * @param photos 生活照,可能有多张,所以后端使用MultipartFile[]数组 来接收 多张生活照片
     * @return
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
​
                         @RequestPart("headerImg") MultipartFile headerImg,
                         @RequestPart("photos") MultipartFile[] photos) throws IOException {
​
        log.info("上传的信息:email={},username={},headerImg的大小={},photos生活照的数量={}",
                email,username,headerImg.getSize(),photos.length);
​
        return "success";
    }

小汪:代码的逻辑我懂了,由于个人头像headerImg ,只能有一张,所以后端直接使用MultipartFile类来接收,代码是这样的:

@RequestPart("headerImg") MultipartFile headerImg,

@RequestPart("headerImg")注解,表明该文件是用于个人照headerImg 的上传。

生活照photos 可能有多张,所以后端使用MultipartFile[]数组 来接收多张生活照片,代码是这样:

 @RequestPart("photos") MultipartFile[] photos)

@RequestPart("photos")注解,表示该文件是用于生活照的上传。其实,你代码的注释也讲得很清楚。

大榜:好的注释既可以帮助自己快速回顾代码,也可以帮助到他人。可以说是赠人玫瑰,手留余香,哈哈哈。

小汪:这比喻,小弟佩服得五体投地。那文件下载是什么样呢?

大榜:文件下载,前端传递文件名称fileName,后端接收文件名称,并在后端服务器中查找是否存在该文件,若存在,以二进制流得形式输出给前端,代码是这样的:

/**
     * 文件下载:设置文件下载的二进制格式,response.setContentType("application/octet-stream");
     *
     * 参考链接:https://quellanan.blog.csdn.net/article/details/102785895?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1-102785895-blog-106453424.topblog&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1-102785895-blog-106453424.topblog&utm_relevant_index=1
     * @param fileName
     * @param response
     * @return
     */
    @ResponseBody
    @GetMapping("/download")
    public String fileDownload(@RequestParam("fileName") String fileName, HttpServletResponse response) {
        // 服务器上的文件存储的根目录:d:/img/
        String downloadFilePath = folderPath;
​
        File file = new File(downloadFilePath + fileName);
        if(!file.exists()){
            log.error("文件在服务器中不存在,输入的文件名:{}", fileName);
            return "下载文件不存在";
        }
​
        response.reset();
        // 设置图片下载的二进制格式
        response.setContentType("application/octet-stream");
​
        response.setCharacterEncoding("utf-8");
        response.setContentLength((int) file.length());
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName );
​
        BufferedInputStream bufferedInputStream = null;
        OutputStream outputStream = null;
​
        try {
            bufferedInputStream = new BufferedInputStream(new FileInputStream(file));
​
            byte[] buff = new byte[1024];
            outputStream  = response.getOutputStream();
​
            // 将输入流数据,写入到输出流中
            int len = -1;
            while ((len = bufferedInputStream.read(buff)) != -1) {
                outputStream.write(buff, 0, len);
                outputStream.flush();
            }
​
        } catch (IOException e) {
            log.error("文件下载失败,异常:",e);
            return "下载失败";
​
        } finally {
            if (bufferedInputStream != null) {
                try {
                    bufferedInputStream.close();
                } catch (IOException e) {
                    log.error("关闭bufferedInputStream输入流对象失败", e);
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    log.error("关闭输出流对象outputStream失败", e);
                }
            }
        }
​
        log.info("文件下载成功,文件名称:{}", fileName);
        return "下载成功";
    }

代码中,我们设置了响应数据的消息类型为"application/octet-stream",它是一种二进制格式,这样图片、文件传输给前端,就不会乱码。

小汪:对于文件下载的响应数据的输出格式为"application/octet-stream",那文件上传请求的消息类型是什么呢?

大榜:文件上传请求的消息类型是multipart/form-data。总的来说,对于前后端分离项目,如果是一般的增删改查接口,前端和后端交互的消息类型为:application/json。如果是特殊的接口,如文件上传,前端传入到消息类型是multipart/form-data格式,这也是HTTP协议约定的文件上传的协议格式;如果是文件下载,前端只需要传入查询条件,后端根据查询条件返回文件流给前端,所以后端响应数据的消息类型为application/octet-stream。具体的实践示例,可以参考 HTTP的消息类型content-type的常见格式(application/json、multipart/form-data、application/octet-stream)

小汪:榜哥,你这个实践示例,一看就是自己实践了的,对于前后端分离项目,一般只有这3种消息类型:application/json、multipart/form-data、application/octet-stream,学到了。

大榜:如果不是前后端分离项目,即前后端一体化项目,可以还有一种格式,就是表单提交。比如登录时的表单提交,前端传入的请求头的消息类型为:application/x-www-form-urlencoded。该格式的特点,将form数据转换成一个字符串(name1=value1&name2=value2…),上面的实践示例中也讲到了。

小汪:那接下来,是不是到博客管理的优化点了?

3、博客管理功能

大榜:是啊。白卷是一个图书与博客管理系统,博客管理功能的开发,可以看这一篇:博客功能开发

小汪:我看了作者写的这篇博客,很容易就入门了。前端使用了开源的mavon-editor 编辑器,来进行博客文章的编辑、md格式转换;后端建立数据库的jotter_article表,来保存博客文章,该表中包含了下面几个字段:文章 html、md 原文、文章摘要。文章html字段用来存储前端编辑并保存的html格式的文章,md原文字段,用来存储md格式的文章,文章摘要字段用来存储摘要信息。

大榜:你讲得很详细,看来实践过啊。作者的博客管理功能开发,确实很入门。不过,你实践的时候,有没有发现查看博客文章时,加载的比较慢,所以,我们可以采用缓存,将博客文章缓存起来,提高查询速度。缓存一般有本地缓存和Redis缓存,我们都可以去尝试下,这个我们以后再讨论。

小汪:榜哥,又挖坑了。我记得,白卷项目中用户、角色、权限与菜单的分配,使得不同用户登录后,可以看到不同的导航栏菜单,我们也没有讨论。还有,白卷项目的部署及监控,我们也没讨论。

大榜:哈哈哈,没想到你还记得呢。我们之前的文章中,还留了分布式Session、Spring的扩展功能(如非Web层的全局异常处理器),都没有一起讨论。

小汪:哈哈哈,是啊,看来榜哥也没忘记呀。这些坑,我们放在Java后端学习路线的β阶段 来攻克,好不好。

大榜:好啊,β阶段我们一起去踩完上面的这些坑,就会变得更强了。β阶段,我打算按照下面6项来开展:

1)秒杀项目实战及复盘总结;

2)Java语言中反射、多线程并发

3)数据库的用户、角色、权限、菜单的表结构设计及实践;

4)Redis中分布式锁

5)消息中间件RabbitMQ

6)Spring扩展功能:非Web层的全局异常处理器、Aware回调接口、bean对象创建的扩展。

小汪:榜哥想得很远啊,已经准备好了β阶段的学习路线了,我要跟着你一块学习,共同进步!

4、总结

至此,后端学习路线的α阶段就告一段落,一路上,小汪和大榜一起讨论了Java SE基础Java EE基础白卷项目复盘及优化。由于自己刚开始写博客文章,有些知识点介绍的不是很清楚、模棱两可,后面我会慢慢打磨,争取把知识清楚明白地说出来,讲解知识做到麻雀虽小,五脏俱全,这也是我的用户名"麻雀学习"的由来。

后面,我们将进入Java后端学习路线的β阶段,欢迎各位小伙伴们多多支持,一起来讨论学习。

5、参考内容

Vue + Spring Boot 项目实战(十八):博客功能开发