[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传

584 阅读9分钟

导航

[react] Hooks

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick
[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传

(一) 前置知识

(1) 一些单词

transactional 事务
pattern 模式
completion 完成 结束 n
implement 实现 ( class 类名 implements 接口名 )
abstract 抽象的 adj
override 覆盖 重写
leaf 叶子
servlet 小程序服务 小应用程序
consume 消耗
configuration 配置

(2) interface 接口

  • interface
    • 描述类有什么功能,但不具体实现,实现是在类中去实现
    • 在interface中方法因为没有方法体,所以是要加 abstract 来修饰
    • abstract 是抽象的意思adj
    • 注意
      • 接口不能被实例化,所以没有构造方法
      • 可以通过多态的方式实例化子类(实现类)
      • 接口中只有成员常量,没有成员变量
      • 接口于接口的关系是 extends 比如 => 接口1 extends 接口2
      • 共性继承类,扩展实现类
  • implements
    • ( 类和接口 ) 是 ( 实现关系 ),用 implements 表示,implement 就是实现的意思
    • 单实现
      • class 实现类类名 implements 接口名
    • 多实现
      • class 实现类类名 implements 接口1,接口2,接口3
  • 实现类
    • 在接口中也经定义了类的方法,但没有实现
    • 实现类,就是去实现接口中的方法的类
    • 通过 class 实现类类名 implements 接口名 的方式来写实现类
  • 关系
    • ( 接口与接口 ) 是 ( 继承 ) 关系
    • ( 类与接口 ) 是 ( 实现 ) 关系


(3) springboot 的静态资源访问

  • 静态资源相关-官方文档
  • 静态资源目录
    • resources 文件夹下的 4 个文件夹
      • static
      • public
      • resources
      • META-INF/resources
  • 访问 ( 当前项目根路径 ) 访问静态资源
    • 根路径 + 静态资源名
  • 原理
    • 静态映射/**
    • 请求进来,先找 ( controller ) 处理,不能处理的所有请求再又交给 ( 静态资源处理器 ) 来处理,再找不到就会404
    • 问题:如果controller中有个api接口是 /a.jpg,而我们的 resources/static/a.jpg 也有同名的静态资源,会发生什么?
    • 答案:会返回controller的api方法的返回值,而不会访问静态资源
  • 静态资源访问前缀
    • 静态资源设置前缀:spring.mvc.static-path-pattern=/resources/**
    • 访问静态资源:当前项目 + 前缀 + 静态资源
    • 例子:http://localhost:7777/resources/66.jpg
  • 指定自定义的静态资源目录 ( 默认静态文件夹路径 )
    • spring.web.resources.static-locations=[classpath:/7resources/]

(4) @Configuration @Bean

  • @Configuration 标注的类是 ( 配置类 )
  • @Bean 是向容器中注册组件,并且是单列的
src/main/java/com.example.demo/config/MyConfig.java
-------

/**
 * 1. @Configuration 标注的类就是 配置类
 * 2。配置类里,使用 @Bean 标注在方法上给容器注册组件,注册的组件默认也是单实例的
 * 3. 注意:@Configuration标注的类,该类本身也是一个组件,即 MyConfig 也是一个组件,即 ( 配置类本身也是一个组件 )
 *
 * 4. proxyBeanMethods 代理bean的方法
 *    Full(proxyBeanMethods = true) 单例,可以用于 ( 组件依赖 )
 *    Lite(proxyBeanMethods = false) 
 */
// 告诉 SpringBoot 这是一个 ( 配置类 ),等用于以前的配置文件
// @Configuration 配置类
@Configuration(proxyBeanMethods = true)
public class MyConfig {

    // @Bean
    // @Bean 给容器中添加组件
    // 1. 以方法名作为组件的id => user1
    // 2. 返回类型就是组件类型 => UserBean
    // 3. 返回的值就是组件在容器中的实例 => new UserBean()
    // @Bean("userX") 的参数可以自定义id
    @Bean("userX")
    public UserBean user1() {
        return new UserBean("woow_wu7", 20);
    }
}

(5) @ConfigurationProperties @Component

  • 要让配置文件和bean对象进行绑定
  • 第一种方法
    • @ConfigurationProperties + @Component
    • @ConfigurationProperties 可以把 application.propertiesapplication.yml 文件中的配置的参数注入到组件中
(1) src/main/java/bean/AppMessageBean.java
-------
// 只有在容器中的组件才能获取 SpringBoot 的强大功能
// 该 bean 对象主要是测试 @ConfigurationProperties 和 @Component 两个注解
@Data
@Component
@ConfigurationProperties(prefix = "myapp") // prefix="myapp" 这个前缀的值是在 application.yml 文件中配置的
public class AppMessageBean {
    private String name;
    private String email;
    private String author;
}



(2) src/main/java/com.example.demo/controller/testController.java
-------
@RestController
@Slf4j
public class TestController {
    @Autowired
    AppMessageBean appMessageBean;
    // (1)
    // 测试: @ConfigurationProperties 和 @Component 两个注解
    // 教程: https://www.cnblogs.com/jimoer/p/11374229.html
    @GetMapping("/@ConfigurationProperties")
    public AppMessageBean getAuthorName() {
        System.out.println(appMessageBean);
        String author = appMessageBean.getAuthor();
        System.out.println(author);
        return  appMessageBean;
    }
}
  • 第二种方法
    • 需要在配置类中
    • @EnableConfigurationProperties(Car.class)
      • 开启 Car 属性绑定功能
      • 把 Car 自动注册到容器中

(6) @ConfigurationProperties 和 @Value的区别

  • @Value
    • 主要用于单个属性
    • 并且配合要求较高,使用羊肉串模式,比如 my-app-name 这样
(1)src/main/java/com.example.demo/bean/ValueTestBean.java
-------
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component // @Value 同样需要使用 @Component 将组件注册到容器中,对比 @ConfigurationProperties
public class ValueTestBean {
    private String name;

    private String email;

    @Value("${myapp.author}") // 用于单个属性
    private String author;
}



(2) 在controller中测试
-------
    @GetMapping("/@Value")
    public ValueTestBean getValueAuthorName() {
        System.out.println(valueTestBean);
        String author = valueTestBean.getAuthor();
        System.out.println(author);
        return valueTestBean;
    }

(二) 配置 mybatis 的 xml 形式

(1) 引入 mybatis-spring-boot-starter 的maven依赖 ( 也叫场景启动器 )

<!-- mysql -->
<!--- 利用mybatis操作mysql需要三个库 ( mysql + jdbc + mybatis ) -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>
  <scope>runtime</scope>
</dependency>

<!-- jdbc依赖,(Java Database Connectivity) java数据连接 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

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

(2) 在 ( application.yml ) 文件中设置 mybatis ( 全局配置文件 ) 和 ( sql映射文件 )

  • application.propertiesapplication.yml 都可以
    • 优先级 application.properties > application.yml
    • 推荐使用 application.yml
  • 第一步:在resources文件夹中新建mybatis文件夹
    • 新建全局配置文件:mybatis/mybatis-config.xml
    • 新建mapper的sql映射文件:mybatis/mapper/MybatisTestMapper.xml
  • 第二步:在 application.yml 文件中配置 mybatis 的 全局配置文件sql映射文件

- 第一步:在resources文件夹中新建mybatis文件夹
  - 新建全局配置文件:`mybatis/mybatis-config.xml`
  - 新建mapper的sql映射文件:`mybatis/mapper/MybatisTestMapper.java`
(1) resources/mybatis/mybatis-config.yml
-------
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
</configuration>


(2) resources/mybatis/mapper/MybatisTestMapper.xml
-------
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.MybatisTestMapper">
    <!-- public MybatisTestBean getMybatisTest(int id); -->
    <!-- id 是方法名 -->
    <!-- resultType 是方法的返回值类型,通过 copy path => copy reference 可以快速生成 -->
    <select id="getMybatisTest" resultType="com.example.demo.bean.MybatisTestBean">
        select * from mybatis_test where id=#{id}
    </select>
</mapper>


- 第二步:在 application.yml 文件中配置 mybatis 的 `全局配置文件` 和 `sql映射文件`
(3) application.yml
-------
# 配置 mybatis 规则
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml # mybatis全局配置文件
  mapper-locations: classpath:mybatis/mapper/*.xml # mybatis的sql映射文件

(3) 正常写 controller => service => mapper

MybatisTestController
-------
@RestController
public class MybatisTestController {

    @Autowired
    MybatisTestService mybatisTestService;

    @GetMapping("/mybatis")
    public MybatisTestBean getMybatisTestMessage(@RequestParam(name="id") int id) {
        return mybatisTestService.getMybatisTest(id);
    }
}
MybatisTestService
-------
@Service
public class MybatisTestService {

    @Autowired
    MybatisTestMapper mybatisTestMapper;

    public MybatisTestBean getMybatisTest(int id) {
        System.out.println(id);
        return mybatisTestMapper.getMybatisTest(id);
    }
}
MybatisTestMapper.java
-------

@Mapper
public interface MybatisTestMapper {
    public MybatisTestBean getMybatisTest(int id);
}

(三) 拦截器

(1) HandlerInterceptor 拦截器的三个方法

  • preHandle 在目标方法执行前执行,即 controller 方法执行前执行
  • postHandle 在目标方法执行完成后执行
  • afterCompletion 在页面渲染后执行

(2) 具体的实现过程

(2.1) 编写一个拦截器, 实现 HandlerInterceptor 接口

  • 注意是 implements HandlerInterceptor
src/main/java/com.exmaple.demo/interceptor/LoginInterceptor.java
-------

/**
 * 拦截器
 * 1. 编写一个拦截器,实现 HandlerInterceptor 接口
 * 2. 把拦截器注册到容器中 ( 实现 WebMvcConfigurer 的  addInterceptors 方法)
 * 3. 指定拦截规则 【如果拦截所有,静态资源也会被拦截,可以用 excludePathPatterns 方法放行】
 */
/**
 * 登陆检查
 * @Slf4j
 * 1. @Slf4j 是 lombok提供的功能
 * 2. @Slf4j 注入后就可以使用 log.info()
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    // 目标方法执行之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle拦截器 - 拦截的路径是{}", request.getRequestURI());
        return true; // true 表示放行,false表示拦截
    }

    // 目标方法执行完成之后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle拦截器 - ModelAndView", modelAndView);
    }

    // 页面渲染以后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion拦截器", ex);
    }
}

(2.2) 把拦截器注册到容器中,( 实现 WebMvcConfigurer 的 addInterceptors 方法)

(2.3) 指定拦截规则 【如果拦截所有,静态资源也会被拦截,可以用 excludePathPatterns 方法放行】

src/main/java/com.example.demo/config/AdminWebConfig.java
-------

/**
 * 拦截器
 * 1. 编写一个拦截器,实现 HandlerInterceptor 接口
 * 2. 把拦截器注册到容器中 ( 实现 WebMvcConfigurer 的  addInterceptors 方法)
 * 3. 指定拦截规则 【如果拦截所有,静态资源也会被拦截,可以用 excludePathPatterns 方法放行】
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截 => 拦截所有请求,包括静态资源
                .excludePathPatterns("/", "/login", "css/**", "/fonts/**", "/images/**", "/js/**"); // 放行,放行了static文件夹下的所有静态资源
                // 问题:如何能访问到 resources/static/images/8.jpg
                // 回答:http://localhost:7777/images/8.jpg
    }
}

(四) 文件上传

(1) @RequestPart("input框的name属性对应的值")

  • @RequestPart这个注解用在 multipart/form-data 表单提交请求的方法上

(2) 实现步骤

(2.1) springboot中如果需要返回html页面,需要 spring-boot-starter-thymeleaf 场景启动器

<!-- spring-boot-starter-thymeleaf -->
<!-- 主要用于显示resources/templates中的html -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

(2.2) resources/templates/fileUpload.xml

<!DOCTYPE html>
<!--注意:xmls:th 的值-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>测试页面</div>
    <!-- th:action="@{/upload}" 提交的controller对应的path -->
    <!-- enctype="multipart/form-data" -->
    <form th:action="@{/upload}" method="post" enctype="multipart/form-data">
        <div>
            <span>单头像上传</span>
            <input type="file" name="single">
        </div>
        <div>
            <span>多头像上传</span>
            <!-- multiple表示开启多个上传 -->
            <input type="file" name="multiple" multiple>
        </div>
        <button type="submit">上传</button>
    </form>
</body>
</html>

(2.3) src/main/java/com.example.demo/controller/FileUpload.java

@Controller
@Slf4j
public class FileUpload {

    @GetMapping("/fileUpload")
    public String handleFile(
    ) {
        // 1. 这里返回的是 resources/templates/fileUpload.html
        // 2. 需要安装 spring-boot-starter-thymeleaf 这个maven依赖
        return "fileUpload";
    }

    // MultipartFile 会自动封装上传上来的文件
    @PostMapping("/upload")
    public String upload(
            @RequestPart("single") MultipartFile single,
            @RequestPart("multiple") MultipartFile[] multiple
    ) throws IOException {
        log.info("上传的单文件{}", single);
        log.info("上传的多文件{}", multiple);
        if (!single.isEmpty()) {
            String originalFilename = single.getOriginalFilename(); // 获取原始文件名
            single.transferTo(new File("F:\\Java-workspace\\uploadFolder\\" + originalFilename)); 
            // 保存到 ( F/java-workspace/uploadFolder ) 文件夹
        }
        if (multiple.length > 0) {
            for (MultipartFile file : multiple) { // for循环
                if (!file.isEmpty()) {
                    String originalFilename = file.getOriginalFilename(); // 原始文件名
                    long size = file.getSize()/1024; // 文件大小,默认但是为字节,1MB = 1024KB = 1024 * 1024 byte
                    log.info("文件名{}. 大小{}KB", originalFilename, size);
                    file.transferTo(new File("F:\\Java-workspace\\uploadFolder\\" + originalFilename));
                }
            }
        }
        return "fileUpload"; // 返回 fileUpload.html
    }
    
    
    // (3)
    // 前后端分离的接口
    // 注意点
    // 1. consumes 一定要设置成 "multipart/form-data" 因为前端 antd 中的 Upload 组件是用的 form-data 方式在上传
    // 2. 前端上传时 Upload 组件一定要设置 name 属性,因为 name 的值是和这里的 @RequestPart("前端name属性的值") 一一对应
    @PostMapping(value = "/frontendUpload", consumes = "multipart/form-data")
    public String frontendUpload(
            // @RequestParam("file") MultipartFile avatars
            @RequestPart("file") MultipartFile avatars
    ) {
        System.out.println(avatars);
        return "上传成功";
    }
}

(2.4) 前后端分离 react页面的上传代码如下

<Upload
  action="/api/frontendUpload" // action还是会走代理
  listType="picture-card"
  fileList={fileList}
  onPreview={handlePreview}
  onChange={handleChange}
  name="file" // 注意,name一定要约束好,因为后端 @RequestPart("file") 是根据前端传递的name值来判断的
>


项目源码