一、前言

在完成图书馆管理系统和表白墙案例的学习后,我们继续完成更复杂的博客系统

我们先来看这个博客系统的架构图,看看这个项目是怎么分层的

上面这张图就是这个项目的整个结构,请求从浏览器发出,经过拦截器,Controller,Service,Mapper,最后到达MySQL数据库。

搭建这个项目需要用到的工具:

JDK17,Maven,MySQL,navicat,IntelliJ IDEA

二、数据库设计

先创建数据库java_blog_spring

-- 创建数据库
CREATE DATABASE IF NOT EXISTS java_blog_spring 
    DEFAULT CHARACTER SET utf8mb4 
    DEFAULT COLLATE utf8mb4_unicode_ci;

USE java_blog_spring;

我们要设计两张表,因为这个博客系统有两个实体

一个是用户,写博客的人,即user,用户表

一个是blog,被写出来的文章,即blog,博客表

所以我们要在java_blog_spring中创建这两个表

创建user表

CREATE TABLE user (
    id          INT PRIMARY KEY AUTO_INCREMENT,   -- ① 主键
    user_name   VARCHAR(128) NOT NULL,            -- ② 用户名  
    password    VARCHAR(128) NOT NULL,            -- ③ 密码
    github_url  VARCHAR(256),                     -- ④ GitHub主页
    delete_flag INT DEFAULT 0,                    -- ⑤ 删除标记
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- ⑥ 创建时间
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- ⑦ 更新时间
);

其中密码存的是加密后的明文,delete_flag=0说明博客正常,=1说明博客已2被删除

 方便后续找回已删除的博客

UPDATE blog SET delete_flag = 0 WHERE id = ?

创建blog表

CREATE TABLE blog (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    title       VARCHAR(256) NOT NULL,            -- ① 标题
    content     MEDIUMTEXT,                       -- ② 内容(Markdown)
    user_id     INT NOT NULL,                     -- ③ 作者ID(外键)
    delete_flag INT DEFAULT 0,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

content使用的markdown格式,MEDIUMTEXT 数据类型能存16MB,能满足文章的存储需求

这里采用的是逻辑删除,只是把delete_flag标记为1,没有真正删除数据,可以随时恢复数据

三、pom.xml

我们先看pom文件在整体架构图:

1.Maven是什么?

想象你写了一个购物清单,需要别人帮你按找清单采购需要的物品。Maven就是那个清单,上面pom.xml的五个部分就是需要采购的物品。

Maven作为清单,会自动去中央仓库中下载,不用我们手动下载

2.项目坐标

<groupId>com.bite</groupId>
<artifactId>spring-blog</artifactId>
<version>0.0.1-SNAPSHOT</version>

有了groupId,artifactId,version,maven就能从仓库璃找到唯一的jar包,可以依赖jar包中已经写好的功能,不用自己再重新写一遍。

groupId 是组织/团队名,通常是公司域名倒过来写

artifactId 是项目模块名

version是版本号

3.父pom

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.5</version>
</parent>

是pom.xml中最关键的两行

Spring 官方提供了一个“父POM”,帮我们管理所有spring相关的jar包版本号。

我们的依赖<dependency>里没有写版本号,是英文父pom中已经定义好了继承了父pom的版本,不会出现版本冲突问题。

4.依赖们

--spring-boot-starter-web 是Web的核心

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

是SpringBoot的全家桶之一,自动引入:

内嵌Tomcat (把Tomcat打包成一个jar包,程序启动Tomcat自动启动)Tomcat启动后,java程序能够接收 http 请求和发送响应

SpringMVC  让我们能写@RestController和@RequestMapping(定义 URL 与 HTTP 方法的映射规则)

Jackson  让Java对象和json对象互相转换

--mybatis-spring-boot-starter

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>4.0.1</version>
</dependency>
 

操作数据库,博客数据在mysql中,mybatis负责与数据库建立连接

MyBatis不属于Spring,父pom不管它的版本

--mysql-connector-j 是数据库驱动

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

是MySQL的官方的JDBC驱动

--lombok 

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

不用手写getter,setter,toString,只需要加上@Data

在打包时会把Lombok排除,因为它的工作只在编译期间完成(生成代码),运行时不需要

--JJWT三件套--登录令牌

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>        <!-- API 接口 -->
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>       <!-- 实现 -->
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>    <!-- JSON 序列化 -->
    <scope>runtime</scope>
</dependency>

作用:用户登录后,服务器生成一个加密令牌返回给浏览器,后续请求带上这个令牌,服务器就知道"你是谁"了。

--测试依赖类

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

两个都是 scope=test,打包成 jar 时不会带进去。前者提供 JUnit 5 测试框架,后者让测试环境能识别 MyBatis 的 Mapper。

四、application.yml配置文件怎么写

1.yml文件是什么格式?

yml是用缩进表示层级的配置文件格式,yml用缩进表示“谁属于谁”

java的poperties文件时另一种写法,是key=value平铺的写法

# properties 写法(平铺)
spring.datasource.url=jdbc:mysql://...
spring.datasource.username=root
 

# yml 写法(层级)
spring:
  datasource:
    url: jdbc:mysql://...
    username: root
 

2.spring.datasource  数据库连接配置

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_blog_spring?characterEncoding=utf8&useSSL=false
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver

-127.0.0.1:3306是MySQL的默认端口

-java_blog_spring是数据库名

-characterEncoding=utf8   字符编码设为utf8,防止中文乱码

-useSSL=false  关闭SSL加密

-driver-class-name指定MySQL的驱动名

com.mysql.cj.jdbc.Driver是MySQL Connnector/J 8 的驱动,老版本是com.mysql.jdbc.Driver

spring.mvc.favicon.enable=false --关闭默认图标

spring:
  mvc:
    favicon:
      enable: false
  • 当用户访问你网站的任何一个页面(比如 http://localhost:8080/hello),浏览器会自动额外发一个请求去问:/favicon.ico。这个文件就是浏览器标签页上显示的那个小图标(比如知乎的小蓝标、GitHub 的小猫)

  • 就是告诉 Spring Boot:你别再插手 /favicon.ico 请求了

  • 禁掉之后:

  • 浏览器依然会请求 /favicon.ico(这是浏览器的天性,禁不掉)。但 Spring Boot 不再自动去 classpath 里找图标,也不再打印那条“找不到图标”的警告日志。

3.spring.profiles.active=dev --多环境切换

spring:
  profiles:
    active: dev

实际开发中,至少有两套环境

环境 用途 去表
dev(开发) 本地跑代码 连接本地数据库、日志开debug、端口随意
prod(生产) 上线给用户用 连线上数据库、日志只打error、端口80/443

active:dev的意思是当前激活dev环境

Spring Boot会去加载名为application-dev.yml的配置文件,它里面配置会覆盖/补充主application.yml(当前项目没有,至少留个坑位)

4.mybatis配置块

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true

配置1:log-impl:org.apache.ibatis.logging.stdout.StdOutImpl

作用:打印SQL日志,MyBatis执行SQL语句时,把SQL语句直接打印到控制台

==> Preparing: SELECT * FROM blog WHERE id = ?

==> Parameters: 1(Integer)

<== Total: 1

配置2:map-underscore-to-camel-case: true --下划线转驼峰

MyBatis的自动映射功能

数据库字段名习惯用下划线:user_id,Java对象属性名习惯用驼峰:userId,开了这个配置后,MyBatis查询时会自动把下划线字段名樱色到驼峰属性名,不用手写@Result注解

5.logging配置块

logging:
  file:
    name: spring-blog
  logback:
    rollingpolicy:
      max-file-size: 100KB
      file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i
  level:
    com:
      example:
        demo: debug

rollingpolicy--日志滚动策略

日志文件不能无限增大,超过max-file-siza:100KB,就会自动新建一个文件,就按file-name-pattern归档

${LOG_FILE}.%d{yyyy-MM-dd}.%i 这个模式的意思是:

  • %d{yyyy-MM-dd}:按日期归档
  • %i:同一天内的序号(第几个滚动文件)

最终文件名类似:spring-blog.2026-05-27.0(第一天的第一个滚动文件)

level --日志级别

level:
  com:
    example:
      demo: debug

意思是:com.example.demo 包下所有类的日志级别设为 debug

日志级别从高到低:ERROR > WARN > INFO > DEBUG > TRACE

程序启动流程:

  1. Spring Boot 启动,读取 application.yml
  2. 看到 profiles.active=dev,再去读 application-dev.yml(目前还不存在,是个待补全的坑)
  3. 根据 spring.datasource 配置,建立数据库连接池
  4. 根据 mybatis.configuration 配置,初始化 MyBatis(开启驼峰映射、SQL 日志打印)
  5. 根据 logging 配置,初始化日志系统,开始往 spring-blog.log 写日志

五、入口类

@SpringBootApplication                          // ← 这行是全部秘密
public class SpringBlogApplication {

    public static void main(String[] args) {     // ← 普通 java 程序入口
        SpringApplication.run(                   // ← 启动 Spring 容器
            SpringBlogApplication.class,          // ← 告诉 Spring:"从我这个类的包路径开始扫描"
            args                                 // ← 命令行参数
        );
    }
}

@SpringBootApplication是三个注解的组合

@SpringBootConfiguration

@EnableAutoComfiguration

@ComponentScan

这三类注解相当于一个注解@SpringBootApplication

1.@ComponentScan --扫描自己的类

Spring启动时,从SpringBlogApplication所在的com.bite.springblog包开始扫描所有子包

扫描子包时,寻找这些注解:

@RestController   @Service   @Mapper    @Configuration   @Component

com.bite.springblog              ← SpringBlogApplication 在这里
├── controller/
│   ├── BlogController.java     ← @RestController → 找到了!
│   ├── UserController.java     ← @RestController → 找到了!
│   └── FileController.java     ← @RestController → 找到了!
├── service/
│   └── BlogService.java        ← @Service → 找到了!
├── mapper/
│   ├── BlogMapper.java         ← @Mapper → 找到了!
│   └── UserMapper.java         ← @Mapper → 找到了!
└── config/
    ├── WebConfig.java          ← @Configuration → 找到了!
    └── ResponseAdvice.java     ← @RestControllerAdvice → 找到了!

入口类必须放在最外层包,否则同级的包扫描不到

2.EnableAutoConfiguration --自动化大师

我们引入了什么jar包,它就帮我们创建什么Bean

具体流程:

  1. Spring Boot 启动时,扫描所有引入的 jar 包中 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件
  2. 每个 starter 包都在这个文件里写了自己要自动创建的配置类
  3. Spring Boot 用 @ConditionalOnClass 等条件注解判断:classpath 里有没有这个类?有就创建,没有就跳过

本项目中,自动装配的决策过程:

| spring-boot-starter-web 在 classpath? | → 启动内嵌 Tomcat,注册 DispatcherServlet |

| mybatis-spring-boot-starter 在 classpath? | → 创建 SqlSessionFactory,扫描 Mapper 接口 |

| mysql-connector-j 在 classpath? | → 根据 application.yml 创建 DataSource 连接池 |

| Jackson 在 classpath? | → 注册 JSON 消息转换器 |

如果你把 mysql-connector-j 从 pom.xml 里删掉,启动时 DataSource 就创建失败了,因为自动装配发现 classpath 里没有 MySQL 驱动类

3.@SpringBootConfiguration --给入口类赋能

告诉Spring和你,这个类可以定义Bean,意味着你可以直接在入口类中写@Bean方法:

@SpringBootApplication
public class SpringBlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBlogApplication.class, args);
    }

    // 可以直接在这里注册自定义 Bean
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

但一般不这样写

总结:

Spring Application.run( )做了什么?

这段代码启动时

  1. 创建 Spring 容器(ApplicationContext)
  2. 读取 application.yml,加载所有配置项
  3. 自动装配@EnableAutoConfiguration):根据 classpath 创建 Tomcat、DataSource、SqlSessionFactory 等
  4. 组件扫描@ComponentScan):找到你自己的 Controller、Service、Mapper
  5. 依赖注入:把所有组件之间的引用关系连好(Controller 需要 Service,Service 需要 Mapper)
  6. 启动内嵌 Tomcat,绑定端口(默认 8080)
  7. 应用就绪,等待 HTTP 请求

控制台最后打印的那行就是启动完成的标志:

Started SpringBlogApplication in 3.456 seconds

六、分层架构

例:Spring Boot三层架构--删除博客

DELETE /blog/delete?blogId=1

                   |

@RequestMapping("/delete")

public Boolean delete(Integer blogId){

      ...

      blogService.deleteBlog(blogId)

}

BlogController.java

                   |

public Integer deleteBlog(Integer blogId) {

        return blogMapper.deleteBlog(blogId);

BlogService.java

                   |

@Update("update blog set delete_flag = 1 where id = #{id}")

Integer deleteBlog(@Param("id") Integer id);

BlogMapper.java

                   |

blog 表 delete_flag 字段置为 1

只负责
Controller BlogController 和HTTP打交道,管进出
Service BlogService 业务逻辑,管对不对
Mapper BlogMapper 和数据库打交道,管存取数据

1.第一层:Model(数据模型)

@Data           // Lombok:自动生成 getter/setter/toString/equals/hashCode
public class BlogInfo {
    private Integer id;
    private String title;
    private String content;
    private Integer userId;
    private boolean isLoginUser;   // 不在数据库里,前端判断是否显示编辑按钮用
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;

    // 覆盖 Lombok 默认的 getCreateTime,返回格式化字符串
    public String getCreateTime() {
        return DateUtils.dateformat(createTime);
    }
}

@Data注解帮我们自动生成getter/setter/toString,使Java文件更加整洁干净

2.第二层:Mapper 只管SQL

@Mapper                // MyBatis 扫描这个接口,自动生成实现类
public interface BlogMapper {

    @Select("select * from blog where delete_flag = 0")
    List<BlogInfo> selectAll();

    @Select("select * from blog where id = #{id}")
    BlogInfo selectById(Integer id);

    @Update("update blog set title = #{title}, content = #{content} where id = #{id}")
    Integer updateBlog(BlogInfo blogInfo);

    @Insert("insert into blog (title, content, user_id) values (#{title}, #{content}, #{userId})")
    Integer insertBlog(BlogInfo blogInfo);

    @Update("update blog set delete_flag = 1 where id = #{id}")
    Integer deleteBlog(@Param("id") Integer id);
}

(1)Mapper是接口,不是类

interface BlogMapper没有class,不用写实现代码,只需要写方法声明签名+SQL注解

(2)#{xxx}是占位符

#{id}是SQL的占位符?,MyBatis会从方法参数里取id值填进去,防止SQL注入。

区别于 ${ } 直接拼接字符串,有注入风险。

(3)方法参数的对应规则

  • 传入 BlogInfo blogInfo → 用 #{title}#{userId} 直接取属性名(走 getter)
  • 传入基本类型 Integer id 且只有一个参数 → 直接 #{id} 即可
  • 传入基本类型但方法有多个参数 → 必须加 @Param("id") 明确告诉 MyBatis 这个参数叫什么

(例如 Integer update(Integer id, String title)),MyBatis 就没法知道 #{id} 应该用第一个参数还是第二个参数。此时如果不加 @Param,MyBatis 会提供默认命名:#{arg0}#{arg1} 或 #{param1}#{param2}。)

 (4)为什么 deleteBlog 用 UPDATE 不用 DELETE

update blog set delete_flag = 1 where id = #{id}

这就是逻辑删除:数据还在表里,只是 delete_flag 改成了 1,查询时过滤掉 delete_flag = 1 的行。这样数据可以恢复,也能保留历史记录。

3.第三层:Service业务逻辑层

@Slf4j
@Service             // 标记为 Service Bean,Spring 管理它的生命周期
public class BlogService {

    @Autowired       // Spring 自动注入 BlogMapper 实现类
    private BlogMapper blogMapper;

    public List<BlogInfo> getList() {
        return blogMapper.selectAll();    // 直接转发,无业务逻辑
    }

    public boolean insertBook(BlogInfo blogInfo) {
        try {
            Integer result = blogMapper.insertBlog(blogInfo);
            if (result == 1) {
                return true;
            }
        } catch (Exception e) {
            log.error("添加图书失败, e:", e);   // 记录错误日志
        }
        return false;
    }

    public Integer deleteBlog(Integer blogId) {
        return blogMapper.deleteBlog(blogId);
    }
}

(1)@Slf4j是Lombok框架中一个实用的注释,它能帮助我们再类中自动创建一个日志记录器log,避免手动编写日志对象声明代码,让代码更简洁。

log.info("获取博客列表");
log.error("添加博客失败, e:", e);

加上该注解之后就可以直接再代码中使用log对象来记录日志了

(2)@Service告诉Spring要创造Service对象并管理它的声明周期,并且供其他类使用

没有@Service的话还需要手动NewService对象

UserService userService = new UserService();

(3)@Autowried依赖注入

因为没有new BlogMapper对象,所以Spring发现BlogService需要BlogMapper时就会自动去容器里找,把它注入进来

(4)保证业务逻辑正确

如发布博客时,标题和内容不能为空,插入失败时catch异常返回false

Mapper只关心SQL执行成功了没有,不会管userId是哪里来的

4.第四层:Controller  最上层,和HTTP打交道

@Slf4j
@RequestMapping("/blog")       // 这个类下所有接口路径前缀都是 /blog
@RestController                // = @Controller + @ResponseBody
public class BlogController {

    @Autowired
    private BlogService blogService;

    // GET /blog/delete?blogId=1
    @RequestMapping("/delete")
    public Boolean delete(Integer blogId) {
        log.info("删除博客, blogId:{}", blogId);

        // 参数校验:这是 Controller 的职责
        if (blogId == null || blogId <= 0) {
            return false;
        }

        Integer rows = blogService.deleteBlog(blogId);

        // 判断是否真的删了
        if (rows == null || rows <= 0) {
            log.error("删除博客失败, blogId:{}, 影响行数:{}", blogId, rows);
            return false;
        }
        return true;
    }
}

路由映射

@ResquestMapper("/blog")  //类映射

@ResquestMapper(“delete”)//方法映射

拼起来就是/blog/delete。浏览器访问的就是这个URL。

七、粘贴功能

图片的粘贴功能涉及前端的两个文件和后端的两个类

首先看static/blog_edit.html和static/blog_update.html这两个类中的粘贴功能

包位置:src/main/resources/static/

// 1. 初始化绑定——等 Editor.md 加载完后执行
function bindPasteImage() {
    var cm = editor.cm;   // 获取 CodeMirror 实例(注意是 editor.cm,不是 editor.codemirror)
    if (!cm) {
        setTimeout(bindPasteImage, 1000);  // 未初始化则1秒后重试
        return;
    }
    // 监听 DOM 的 paste 事件(不能用 cm.on,必须用 addEventListener)
    cm.getWrapperElement().addEventListener("paste", function(e) {
        var items = e.clipboardData && e.clipboardData.items;
        for (var i = 0; i < items.length; i++) {
            if (items[i].type.indexOf("image") !== -1) {
                e.preventDefault();
                uploadPasteImage(items[i].getAsFile());
                break;
            }
        }
    });
}

// 2. 上传图片
function uploadPasteImage(file) {
    // 先插入占位符
    var placeholder = "\n![上传中...]()\n";
    var cm = editor.cm;
    cm.replaceSelection(placeholder);

    var formData = new FormData();
    formData.append("editormd-image-file", file);  // 字段名必须是这个

    $.ajax({
        url: "/upload",
        type: "POST",
        data: formData,
        processData: false,
        contentType: false,
        success: function(response) {
            var result = response.data || response;  // 兼容 ResponseAdvice 包装
            if (result.success === 1) {
                // 替换占位符为真实图片链接
                var content = cm.getValue();
                cm.setValue(content.replace(placeholder, "\n![](" + result.url + ")\n"));
            }
        }
    });
}

这里涉及到三个部分

第一部分是 bindPasteImage(),是绑定粘贴图片功能的函数

cm 是 CodeMirror 的简写,是一个编辑器

CodeMirror 是一个开源网页代码的编辑器库。editor.cm 就是拿到 CodeMirror 的实例对象,通过它获取编辑器的 DOM 容器、监听事件等,来完成编辑区绑定粘贴事件。

看到 if(!cm) 是为了保证编辑器 cm 已经加载好了,如果没有加载好,就调用 setTimeOut 这个 JavaScript 中的延时定时器,延迟 1000 毫秒之后再执行这个 bindPasteImage 函数。保证编辑器已经准备好了,再去执行图片粘贴功能,避免编辑器没加载好直接粘贴导致报错。

第二部分是监听 DOM 的 paste 事件

cm.getWrapperElement() 是为了在页面中找到编辑器的编辑框。就是把整个页面的导航页,侧边栏等排除在外,精准定位到中间的文本编辑框,给这个编辑框实现 “监听”功能,只要在这个编辑框内按 Ctrl+V 就能触发后面的图片上传逻辑。如果在该页面的其他位置粘贴,就不会误触上传。

这个 addEventListener() 这个方法,就是给刚刚的文本编辑器加上一个监听器。其中监听的是 “paste” 事件,就是监听按 Ctrl+V 这个动作,监听到这个动作之后就执行 function(e) 这个回调函数。(回调函数就是把一个函数当作参数传给另一个函数,等预定动作发生之后,再执行这个函数。就是等目标事件发生之后才被调用的函数)

这个回调函数里的 e.clipboardData() 是事件对象里专门用来读取剪贴板的属性,本质是一个对象。

items 是剪贴板里所有数据项的集合,我们复制的图片、文本会作为一个个独立的项存在这里,再通过 clipboardData 拿到总容器,在 items 属性里也能精准地找到图片数据。

为什么要写成 e.clipboardData && e.clipboardData.items; 呢?是为了兼容浏览器的“短路判断”,,有些低版本的浏览器中 e 对象中根本没有 clipboardData 这个对象,所以要加上 &&,要先判断 e.clipboardData 是否存在,如果存在再取 items,不存在就会返回 undefined,不会让代码崩掉。

items 里面存了粘贴板里不同格式的数据项。如果单纯复制文本,items 里就只存有文本项,如果复制带图的内容,就会同时存在图片、文本甚至 HTML 格式的项。所以我们要在集合中“筛选出”图片项来处理,其他项跳过。

剪切板的设计是尽可能保留所有格式。如果在网页上复制一张图,浏览器会同时保存图片上的二进制数据、带 img 标签的 HTML 代码、甚至是图片的 alt 文本描述,这些都会变成 items 里的不同项,但顺序不一定固定。但如果是用系统自带的截图工具,或者在微信上保存图片,就只会保存图片的二进制数据项,不会带多余的 HTML 或者文本格式。

这里我发现我的博客系统在粘贴网页图片时,会报错

在粘贴微信和系统截图时都不会,通过Cased by发现,是因为max-file-size单个文件和max-request-size请求的最大大小只有1MB,太小了需要在application中修改它们俩的大小,修改完后可以正常上传

然后就是for循环遍历items对象,在对象中找到第一个包含字符串“image”的items对象

第三部分是上传图片方法 uploadPasteImage(flie)

更多推荐