SpringBoot学习记录,一个小项目实战

1、什么是Spring boot

Spring Boot 是一个基于 Spring 的快速开发框架,它通过"自动配置"和"约定优于配置"的思想,把原本复杂的 Spring 项目简化成开箱即用的形式(比如内置服务器、自动依赖管理),让你几乎不用写大量 XML 或繁琐配置,就能快速搭建并运行一个后端应用(如 Web 服务或接口)。

"起步依赖(Starter )"就是:把某一类常用功能需要的所有依赖提前帮你打包配置好,你只需要引入一个依赖即可。

xml 复制代码
<!--        这个是spring web的起步依赖,版本由parent父工程或者dependencymanager决定-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

自动配置:遵循约定大约配置的原则,在boot程序启动后,一些bean对象会自动注入到ioc容器,不需要手动声明,简化开发

什么是微服务

微服务(Microservices)是一种软件架构风格,它把一个大型应用拆分成多个小而独立的服务,每个服务只负责单一业务功能(比如用户、订单、支付),并通过接口(通常是 HTTP API)相互通信;这样做的好处是各个服务可以独立开发、部署和扩展,提高系统的灵活性和可维护性,但也会带来服务间通信、部署和运维复杂度增加的问题。

2、第一个SpringBoot项目

1、快速上手

你可以使用下面这个网站快速构建一个springboot项目

start.spring.io/

idea也内置了这个功能,你可以把服务改为 start.aliyun.com ,这是阿里针对国内的优化版

直接启动启动类就运行起来了,如果失败考虑你有没有添加web-starter依赖,不要指定version,让spring自己选择,否则可能出各类奇怪问题

建一个控制类,控制类等的包必须和启动类同级别,否则会无法注册,因为springboot会默认扫描其启动类所在的包及其子包

使用下面的操作可以通过jar包运行程序

2、配置

1、properties和pom.xml

properties 复制代码
# 端口
server.port=8081

# 项目名
spring.application.name=demo

# 数据库
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

# 日志
logging.level.root=INFO
logging.level.com.zhang=DEBUG

很多操作不可计数,记住最常用的就行

  • 在资源目录下新建一个banner.txt,可以为控制台修改启动画

你可以在这个网站找到很多ascii码艺术画为编码添加情趣 www.bootschool.net/ascii

markdown 复制代码
 ████        █████        ████████     █████ █████  
░░███      ███░░░███     ███░░░░███   ░░███ ░░███   
 ░███     ███   ░░███   ░░░    ░███    ░███  ░███ █ 
 ░███    ░███    ░███      ███████     ░███████████ 
 ░███    ░███    ░███     ███░░░░      ░░░░░░░███░█ 
 ░███    ░░███   ███     ███      █          ░███░  
 █████    ░░░█████░     ░██████████          █████  
░░░░░       ░░░░░░      ░░░░░░░░░░          ░░░░░   
                                                    
                         大吉大利            永无BUG  

2、原理:Spring Boot 的自动配置就像"点外卖套餐"

你只需要在项目xml里引入一个 starter(比如 web),就相当于点了一份"套餐"。

Spring Boot 在启动时,会去一个"菜单清单"(以前是 spring.factories,现在是自动配置类列表,点import文件)里找:这个套餐都需要哪些东西(比如 Tomcat、Spring MVC、JSON 处理等)。

然后它会判断当前环境是否满足条件(比如有没有相关依赖、有没有你自己定义的 Bean),如果满足,就自动帮你把这些组件(Bean)全部创建好并放进容器里。

最终效果就是:

你不用自己写一堆配置,Spring Boot 已经根据你选的"套餐"帮你全配好了
引入 starter → 启动扫描 → 条件判断 → 自动装配 → 可直接使用

  • Spring Boot 自动配置基于 @EnableAutoConfiguration 实现。

  • 启动时会通过 AutoConfigurationImportSelector

  • 读取 META-INF 下的自动配置类,

  • 然后根据 @Conditional 系列条件注解,

  • 按需将满足条件的 Bean 注册到 IOC 容器中。

  • 这样开发者只需要引入 starter,

  • 很多配置就能自动完成。

3、如何自定义starter?

在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot的starter。

我们尝试实现自己的mybatis起步依赖的功能,也就需要上面的两个模块

1、自动配置依赖

这个是MyBatis 的自动配置模块。应当自动创建 SqlSessionFactory这样的Bean,自动帮你配置 MyBatis

  • autoconfigure 模块需要"使用那些技术的类",所以必须导入对应依赖。
xml 复制代码
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter</artifactId>
   <version>4.0.6</version>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jdbc</artifactId>
   <version>4.0.6</version>
   <scope>compile</scope>
</dependency>
<dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis</artifactId>
   <version>3.5.19</version>
</dependency>
<dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis-spring</artifactId>
   <version>4.0.0</version>
</dependency>
<dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>3.8.1</version>
   <scope>test</scope>
</dependency>
2、编写自动配置
java 复制代码
package com.zhang.config;

import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;
import java.util.List;

@AutoConfiguration//告诉 Spring Boot:"这个类可以参与自动配置"
public class MyBatisAutoConfig {
    @Bean //"把这个方法返回的对象交给 Spring 管理"。
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
//        datasource是spring-boot-starter-jdbc依赖自动注入的
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(BeanFactory beanFactory) {
//        Spring 容器自己创建 BeanFactory而来
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
//        获取 Spring Boot "自动配置基础包"。也就是:@SpringBootApplication 所在包 以及它的子包。
        List<String> packages = AutoConfigurationPackages.get(beanFactory);
//        get(0)作用package com.zhang得到com.zhang
        String p = packages.get(0);
//        "去扫描 com.zhang 包下的 Mapper 接口"
        mapperScannerConfigurer.setBasePackage(p);
//          解决的是:"扫到什么算 Mapper?"(筛选条件)
        mapperScannerConfigurer.setAnnotationClass(Mapper.class);
        return mapperScannerConfigurer;

    }
}

告诉 Spring Boot:

"有哪些自动配置类需要加载",所以要编写一个AutoConfiguration.imports 文件

3、编写starter模块
  • 在xml中添加自动配置的依赖

starter 中引入 autoconfigure,

是因为 starter 本身只负责依赖聚合,

真正的自动配置逻辑写在 autoconfigure 模块中。 用户只需要引 starter,

就会间接引入 autoconfigure,

Spring Boot 才能自动加载配置类,实现自动装配。

两个模块,自动配置只需要配置和imports,starter只需要xml

4、测试

上述步骤就完成了starter编写,我们代替官方的mybatisstarter依赖试试

4、yml配置

SpringBoot使用一个全局的配置文件,配置文件名称是固定的

  • application.properties

    • 语法结构:key=value
  • application.yml

    • 语法结构:key:空格 value

配置文件的作用:修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;

YAML是 "YAML Ain't a Markup Language"(YAML不是一种置标语言)的递归缩写。

在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种置标语言)

1、标记语言

以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml

yaml配置:

yaml 复制代码
server:
  prot: 8080

xml配置:

xml 复制代码
<server>
  <port>8081<port>
</server>

在springboot中更推荐使用yaml配置,因为它对比properties和xml更加简洁清爽,更适合表达"有层级的配置",写得更少、更直观,所以在 Spring Boot 里更推荐,两个配置可以同时生效

yml 复制代码
# k=v

# 普通的key-value
name: qinjiang

# 对象
student:
  name: qinjiang
  age: 3

#行内写法
student: {name: qinjiang,age: 3}

# 数组
pets:
  - cat
  - dog
  - pig

pets: [cat,dog,pig]
2、用yaml注入实体类

一般java中我们可以new给对象赋值,spring中我们知道可以通过- @value("${?}") - 和自动注入给字段和对象赋值,在springboot中又多了可以用yaml配置文件注入对象,如下图

  • 配置yml和配置properties都可以获取到值 , 强烈推荐 yml

  • 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value("${?.?}")

  • 如果说,我们专门编写了一个JavaBean来和配置文件进行映射,就直接使用

    @configurationProperties,不要犹豫!

3、jsr303校验

在下面这个图里使用了@Validated 数据校验,可以看到你要是在yaml配置或者其他的什么,为这个字段添加了不为邮箱格式的数据,就会爆红并返回message

分类 注解 示例 作用
非空 @NotNull @NotNull 不能为 null
非空字符串 @NotBlank @NotBlank 不能为 null 且去空格后不为空
非空集合 @NotEmpty @NotEmpty 不能为 null 且长度 > 0
数值范围 @Min @Min(18) 最小值
数值范围 @Max @Max(60) 最大值
精确范围 @DecimalMin @DecimalMin("0.1") 小数最小值
精确范围 @DecimalMax @DecimalMax("100.0") 小数最大值
正数/负数 @Positive @Positive 必须 > 0
非负数 @PositiveOrZero @PositiveOrZero ≥ 0
长度 @Size @Size(min=3,max=10) 字符串/集合长度
格式 @Email @Email 邮箱格式
正则 @Pattern @Pattern(regexp="^1[3-9]\d{9}$") 自定义规则(手机号等)
时间 @Past @Past 必须是过去时间
时间 @Future @Future 必须是未来时间
4、多环境配置
  • 现代springboot开发时,如果有多个不一样的配置文件,怎么方便的切换环境?
yml 复制代码
application.yml            ← 默认配置
application-dev.yml        ← 开发环境
application-test.yml       ← 测试环境
application-prod.yml       ← 生产环境
  • 在默认的配置中指定我们要使用哪个环境
yml 复制代码
spring:
  profiles:
    active: dev
  • 指定之后就只有它生效
  • 还有把多个环境写到一个yml文件的办法
yml 复制代码
spring:
  profiles:
    active: dev

---
spring:
  config:
    activate:
      on-profile: dev
server:
  port: 8081

---
spring:
  config:
    activate:
      on-profile: prod
server:
  port: 8080

可以用"一个 yml"练习 , 但最好尽早习惯多文件结构

3、手动创建SpringBoot应用

如果在没有各种快速构建工具,手动应该怎么做呢?

1、新建一个maven项目

使用quickstart骨架就行

2、引入相关的依赖

springboot父依赖

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.6</version>
</parent>

或者用管理依赖

xml 复制代码
<dependencyManagement>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.4.6</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

    </dependencies>
</dependencyManagement>

最后导入你想要的依赖,比如web开发

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>4.0.6</version>
</dependency>

3、创建启动类与配置文件

启动类名字一般是项目名加Application

java 复制代码
@SpringBootApplication
public class SpringCreateManualApplication
{
    public static void main( String[] args )
    {

        SpringApplication.run(SpringCreateManualApplication.class, args);
    }
}

4、整合MyBatis

1、导入相关依赖与配置

xml 复制代码
<!--        mybatis起步依赖-->
        <!-- Source: https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>4.0.1</version>
        </dependency>
<!--        mysql驱动依赖-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>9.6.0</version>
        </dependency>
yml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis
    username: root
    password: 123456

2、编写相关业务逻辑

java 复制代码
@Mapper
public interface UserMapper {
    @Select("select * from mybatis.user where id = #{id}")
    User getUserById(int id);
}
java 复制代码
public interface UserService {
    User getUserById(int id);
}
java 复制代码
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public User getUserById(int id) {
        return userMapper.getUserById(id);
    }
}
java 复制代码
@RestController
public class UserController {
    @Autowired
    private UserServiceImpl userService;
    @GetMapping("/findById")
    public User findById(Integer id) {
        return userService.getUserById(id);
    }
}

3、springboot wed开发

1、静态资源

在springboot项目结构中,js,html等静态资源有三个可供你选择的位置,

还有一个供给依赖包classpath:/META-INF/resources/

js 复制代码
src/main/resources/
├── static/          ✅ 最常用
├── public/          ✅ 也可以用
├── resources/       ✅ 很少用(但存在)

90% 项目:只用 static/ 放一切前端资源;public/ 放极少数站点根文件;resources/ 基本不用

在以上三个目录任选建一个index.html就是默认页,最推荐做法只用:static/index.html

2、模板引擎Thymeleaf

1、Thymeleaf是什么

  • 模板引擎和前后端分离是两种不同的页面生成方式:
  • 模板引擎属于"后端渲染",由 Spring Boot 等后端把数据填进 HTML 模板(如 Thymeleaf)直接生成完整页面返回;
  • 而前后端分离是"前端渲染",后端只提供 JSON 数据接口,前端用 Vue.js 或 React 在浏览器中把数据渲染成页面。两者本质区别在于"页面由谁生成"(后端 vs 前端),实际项目通常二选一为主,但也可以在不同模块中混合使用。
  • Thymeleaf 和 JSP 做的是同一类事,但 Thymeleaf 是"现代版、更规范、更好维护"的替代方案
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

使用模板引擎时在资源文件夹新建templates目录,给模板引擎放动态页面

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--src/main/resources/templates/test.html-->
<p th:text="${msg}">test</p>

<!--不转译text,相当于传过来标签会被变html的标签-->
<p th:utext="${msg}">test2</p>

<!--接受集合,循环数据给array,再text转译这个array,你也可以像jsp,vue那样取数据-->
<li th:each="array:${arrays}" th:text="${array}">array</li>
<!--<li th:each="array:${arrays}" >[[${array}]]</li>-->

</body>
</html>
java 复制代码
@GetMapping("/hello")
public String hello(Model model) {
    model.addAttribute("msg", "<h1>Hello World</h1>");

    model.addAttribute("arrays", Arrays.asList("a", "b", "c"));
    return "test";
}
表达式 作用
${} 访问普通变量
*{} 访问表单对象属性
@{} 生成 URL 路径
#{} 读取国际化(配置文件)
~{} 模板片段引用(布局/复用)

2、Thymeleaf标签如何用

Thymeleaf 不是"新标签",而是给 HTML 标签加 th:* 属性

  • 文本
bash 复制代码
<p th:text="${msg}"></p>

<p> 标签加属性


  • 链接
css 复制代码
<a th:href="@{/login}">登录</a>

  • 循环
bash 复制代码
<li th:each="user : ${users}"></li>

  • 条件
bash 复制代码
<div th:if="${isLogin}"></div>

4、实战

1、环境搭建

yml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
    username: root
    password: 123456

2、注册接口

1、定义相关的实体类和对应的结果类

这个 Result<T> 本质上是:

一个"统一返回结果类"

也就是后端接口不管成功还是失败,都返回统一格式给前端。

java 复制代码
public class Result<T> {
    private Integer code;//业务状态码 0-成功 1-失败
    private String message;//提示信息
    private T data;//响应数据

    public Result() {
    }

    public Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    //快速返回操作成功响应结果(带响应数据)
    //public static <泛型声明> 返回值类型 方法名(参数)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
        }

    //快速返回操作成功响应结果
    public static Result success() { return new Result(0, "操作成功", null); }

    public static Result error(String message) { return new Result(1, message, null); }

2、定义注册业务相关逻辑接口

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserServiceImpl userService;

    @PostMapping("/register")
    public Result register(String username, String password) {
        User u = userService.findByUSerName(username);
        if (u != null) {//用户名存在
            return Result.error("用户名已存在");
        }
        else {
            String md= MD5Util.md5(password);//先加密,把加密数据存入数据库
            userService.register(username, md);
            return Result.success();

        }

    }
}
java 复制代码
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User findByUSerName(String userName) {
        return userMapper.findUserByName(userName);
    }

    @Override
    public Boolean register(String username, String password) {
        return userMapper.register(username, password);
    }
}
java 复制代码
@Mapper
public interface UserMapper {
    @Select("select * from big_event.user where username=#{username}")
    User findUserByName(String userName) ;
    @Insert("insert into big_event.user(username,password,create_time,update_time) " +
            "values (#{username},#{password},now(),now())")
    Boolean register(String username, String password);
}

其中的MD5加密工具类如下,不过由于md5如今没有加盐,已经不再安全,实际密码存储不建议用

java 复制代码
package com.zhang.bigevent.utils;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

public class MD5Util {

    /**
     * 32位 MD5 加密(小写)
     * 16位和32位 MD5,本质上其实是同一个东西。仅仅在展示上有一点区别
     * md5在现在的密码存储已经不再安全,简单的密码撞库就可以破解,BCrypt等是更好的方法
     */
    public static String md5(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");

            byte[] bytes = md.digest(text.getBytes(StandardCharsets.UTF_8));

            StringBuilder sb = new StringBuilder();

            for (byte b : bytes) {
                String hex = Integer.toHexString(b & 0xff);

                if (hex.length() == 1) {
                    sb.append("0");
                }

                sb.append(hex);
            }

            return sb.toString();

        } catch (Exception e) {
            throw new RuntimeException("MD5 加密失败", e);
        }
    }

    /**
     * 16位 MD5
     */
    public static String md5_16(String text) {
        return md5(text).substring(8, 24);
    }


}

3、对数据进行校验--Spring Validation的使用

  • 在上面的案例里,我们没有对用户名密码进行任何长度或者字符上的约束,这显然不符合实际业务,于是我们需要对数据进行校验

Spring Validation 是Spring 提供的一个参数校验框架,使用预定义的注解完成参数校验

  1. 引入Spring Validation 起步依赖

  2. 在参数前面添加@Pattern注解

    使用正则表达式限制参数格式,例如限制用户名必须是 5~16 位字母数字。

java 复制代码
@Pattern(regexp = "^[a-zA-Z0-9_]{5,16}$")
String username
  1. 在Controller类上添加@Validated注解

    开启 Spring 的参数校验功能,让 @Pattern@NotNull 等校验注解真正生效。

java 复制代码
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    @Autowired
    private UserServiceImpl userService;

    @PostMapping("/register")
    public Result register(@Pattern(regexp = "^\S{5,16}$") String username,@Pattern(regexp = "^\S{5,16}$") String password) {
        User u = userService.findByUSerName(username);
        if (u != null) {//用户名存在
            return Result.error("用户名已存在");
        }
        else {
            String md= MD5Util.md5(password);//先加密,把加密数据存入数据库
            userService.register(username, md);
            return Result.success();

        }

    }
}

这样我们的用户名与密码就被约束了,但是出现不符合要求的数据时会抛出一个500错误,这个500放回到前端不符合我们统一格式的要求,因此需要定义一个全局异常处理器

全局异常处理器

全局异常处理器(Global Exception Handler):

本质上是:

统一处理项目中出现的异常,SpringBoot 用来"统一接管项目报错"的机制

避免:

  • 到处写 try-catch
  • 报错信息混乱
  • 直接返回 500 白页
  • 前后端返回格式不统一
java 复制代码
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
//声明这是一个全局异常处理类,监听整个 SpringBoot 项目中的异常,返回 JSON 数据,不返回页面
@RestControllerAdvice
public class GlobalExceptionHandler {
//    指定当前方法处理哪种异常,Exception.class表示所有异常
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
//        打印异常详细信息,方便我们调试
        e.printStackTrace();
//        如果异常有信息,返回异常信息;否则,返回"操作失败"
        return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
    }

}

3、登录接口

1、Controller业务

java 复制代码
@PostMapping("/login")
public Result login(
        @Pattern(regexp = "^\S{5,16}$") String username,
        @Pattern(regexp = "^\S{5,16}$") String password) {
    User u = userService.findByUSerName(username);
    if (u == null) {
        return Result.error("该用户不存在");
    }
    //由于数据库存储的是加密后的密码,所以我们要把输入的密码加密再与数据库比对
    if (MD5Util.md5(password).equals(u.getPassword())) {
        return Result.success("jwt 令牌");//后面再说这是什么
    }
    return Result.error("密码错误");
}

2、登录认证---令牌

在只存在上面的业务逻辑时,一个没有登陆的人也可以访问一些内部接口,因此需要认证

拦截器是一个办法,现在这里介绍另一种办法---令牌

  • Token 就是:服务器发给客户端的"登录凭证"以后客户端拿着它:证明"我已经登录过了"
  • JWT,全称:JSON Web Token (jwt.io/),定义了一种简洁的、自包含的格式,用于通信双方以json数据格式安全的传输信息。
xml 复制代码
<!-- Source: https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.5.0</version>
    <scope>compile</scope>
</dependency>
  1. 组成

    • Header(头), 记录令牌类型和签名算法等
    • PayLoad(荷载),携带自定义的信息
    • Signature(签名),对头部和荷载进行加密计算得来
  2. 使用

    • 引入java-jwt坐标
    • 调用API生成和校验令牌
    • 解析令牌抛出异常,就证明令牌被篡改或者过期了
java 复制代码
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtil {
//定义 JWT 签名密钥。作用:生成 token 时加密,验证 token 时解密,必须一致。否则:token 无法验证
    private static final String KEY = "zhang";
    //接收业务数据,生成token并返回,claims就是你要放进 token 的用户数据。
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()//创建 token
                .withClaim("claims", claims)//向 token 中放数据。
//                设置 token 过期时间。整体意思:当前时间 + 12小时,token 十二小时后失效。
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
//                使用:HMAC256算法。用:KEY = "zhang"进行签名。
                .sign(Algorithm.HMAC256(KEY));
    }

    //接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))//要求 必须使用同一个 KEY 验证签名。
                .build()//创建 verifier(验证器)。
                .verify(token)//真正开始验证。
                .getClaim("claims")//获取 claims
                .asMap();//转为 Map
    }
}
1、在登陆后生成token
java 复制代码
@PostMapping("/login")
public Result login(
        @Pattern(regexp = "^\S{5,16}$") String username,
        @Pattern(regexp = "^\S{5,16}$") String password) {
    User u = userService.findByUSerName(username);
    if (u == null) {
        return Result.error("该用户不存在");
    }
    //由于数据库存储的是加密后的密码,所以我们要把输入的密码加密再与数据库比对
    if (MD5Util.md5(password).equals(u.getPassword())) {
        HashMap<String, Object> claims = new HashMap<>();//map来存储数据
        claims.put("id", u.getId());
        claims.put("username", username);
        String token = JwtUtil.genToken(claims);//工具类得到token
        return Result.success(token);//封装到result返回前端
    }
    return Result.error("密码错误");
}
2、在需要验证的功能添加验证

由于还没写文章的功能,下面的是模拟token验证的情况

java 复制代码
@RestController
@RequestMapping("/article")
public class ArticleController {
    @GetMapping("/list")
//    因为登录后的用户会把token一直存在请求头中,因此我们要从请求头中去取
    public Result getArticles(@RequestHeader(name = "Authorization") String token
    , HttpServletResponse resp) {
        try {
            Map<String, Object> claims = JwtUtil.parseToken(token);
            return Result.success("所有的文章数据");//如果上面验证没没抛出错误,说明已经登录,执行业务
        } catch (Exception e) {
           resp.setStatus(401);//执行到这里说明未登录,故而设定错误状态码,返回错误信息
            return Result.error("未登录");
        }

    }
}
3、使用拦截器简化我们的代码

假使我们的程序有大量的controller,一个个解析token太麻烦。于是拦截器可以实现简化

java 复制代码
@Component
public class LoginInterceptors implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");//得到令牌
        try {
            JwtUtil.parseToken(token);//验证令牌
            return true;//放行
        } catch (Exception e) {
            response.setStatus(401);
            return false;//阻止
        }
    }
}

在高并发场景下不建议用catch,因为异常的开销比if判断大,所以用if判断token是不是null更节省性能,这是GPT老师说的,供参考

java 复制代码
// 1. 获取请求头中的 token  
String token = request.getHeader("token");  
  
// 2. 判断 token 是否存在  
if(token == null || token.isEmpty()){  
response.setStatus(401);  
return false;  
}  
  
// 3. 验证 token  
try{  
JwtUtil.parseToken(token);  
}catch (Exception e){  
response.setStatus(401);  
return false;  
}  
  
// 4. 放行  
return true;

注册拦截器

java 复制代码
package com.zhang.bigevent.config;

import com.zhang.bigevent.interceptors.LoginInterceptors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptors loginInterceptors;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        除了登录注册,其他的都验证token
        registry.addInterceptor(loginInterceptors)
                .excludePathPatterns("/user/login","/user/register");
    }
}

现在我们的controller代码可以专注它自己的事情了

java 复制代码
    @GetMapping("/list")
//    因为登录后的用户会把token一直存在请求头中,因此我们要从请求头中去取
    public Result getArticles( ) {
//        try {
//            Map<String, Object> claims = JwtUtil.parseToken(token);
//            return Result.success("所有的文章数据");//如果上面验证没没抛出错误,说明已经登录,执行业务
//        } catch (Exception e) {
//           resp.setStatus(401);//执行到这里说明未登录,故而设定错误状态码,返回错误信息
//            return Result.error("未登录");
//        }
        return Result.success("所有的文章数据");
    }

4、获取用户详细信息

因为token存了用户名,所以可以通过用户名得到user

java 复制代码
@GetMapping("/userinfo")
public Result<User> UserInfo(@RequestHeader(name = "Authorization") String token) {
    Map<String, Object> claims = JwtUtil.parseToken(token);
    String username = (String) claims.get("username");
    User user = userService.findByUSerName(username);
    return Result.success(user);
}

在测试时需要在postman的headers添加Authorization。为了方便你可以在接口统一配置,这样所有方法都能得到token

然而你这么做返回的user数据会把密码这种数据也一起返回。虽然是加密后的,但也是不安全,故而做出以下改进

java 复制代码
public class User {

    private Integer id;
    private String username;
    @JsonIgnore//这样实体类作为json数据返回前端时,会忽略password
    private String password;
    private String nickname;
    private String email;
驼峰下划线问题

我们可以看到createTime这种字段不会为空,为什么显示为null呢? 因为数据库命名是下划线命名,而实体类是驼峰,这时我们要在配置文件添加camel

yml 复制代码
mybatis:
  configuration:
    map-underscore-to-camel-case: true

ThreadLocal优化数据存取

ThreadLocal 可以理解成"线程自己的变量盒子"。

每个线程都能往里面存数据,但只能拿到自己存的那份,别的线程看不到,因此不会互相干扰。它常用于保存当前登录用户、数据库连接等"当前线程专属的数据"。 提供线程局部变量

  • 用来存取数据: set()/get()
  • 使用ThreadLocal存储的数据, 线程安全

下面这个new线程的代码可以这么理解:"创建一个叫线程1的新员工,然后 start() 让他开始干活,干的内容就是 {} 里的代码。"

上图看出它确实实现了线程隔离的效果

  • 很多业务都需要获取当前登录用户的信息。为了避免在 Controller、Service、DAO 之间层层传递用户 id,
  • 可以在拦截器中解析 token,将用户信息存入 ThreadLocal,后续业务代码直接通过 get() 获取当前线程中的用户数据,
  • 从而简化参数传递。请求结束后再调用 remove() 清理数据,防止线程复用导致数据污染。

为此新建一个工具类

java 复制代码
package com.zhang.bigevent.utils;

public class ThreadLocalUtil {
    //提供全局唯一ThreadLocal对象,
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    //根据键获取值
    public static <T> T get(){
        return (T) THREAD_LOCAL.get();
    }

    //存储键值对
    public static void set(Object value) { THREAD_LOCAL.set(value); }

    //清除ThreadLocal 防止内存泄漏
    public static void remove() { THREAD_LOCAL.remove(); }
}

在拦截器里配置Threadlocal

java 复制代码
@Component
public class LoginInterceptors implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");//得到令牌
        try {
            Map<String, Object> claims = JwtUtil.parseToken(token);//验证令牌
            ThreadLocalUtil.set(claims);
            return true;//放行
        } catch (Exception e) {
            System.out.println("拦截了");
            response.setStatus(401);
            return false;//阻止
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        ThreadLocalUtil.remove();
    }
}

为什么清除threadlocal的数据要在拦截器的after方法里?

是因为每次拦截器都会在开始方法里put数据,故而每个请求都会把这个数据又装一遍,所以在after里清除不会影响下一次的使用

java 复制代码
@GetMapping("/userinfo")
public Result<User> UserInfo() {
    Map<String,Object> map = ThreadLocalUtil.get();//得到线程存的map数据
    String username = (String) map.get("username");//得到username
    User user = userService.findByUSerName(username);
    return Result.success(user);
}

5、更新用户基本信息

1、基本信息修改

java 复制代码
@PutMapping("/update")//@PutMapping 可以理解成:专门对应 HTTP PUT 请求的映射注解,也就是更新
public Result update(@RequestBody User user){//RequestBody可以要求前端把零散的字段封装成对象
    userService.update(user);
    return Result.success();
}
java 复制代码
@Override
public Integer update(User user) {
    user.setUpdateTime(LocalDateTime.now());//设定修改时间,每次操作必须改的
    return userMapper.update(user);
}
java 复制代码
@Update("update big_event.user set nickname=#{username},email=#{email}," +
        "update_time=#{updateTime} where id=#{id}")
Integer update(User user);
对实体类字段进行校验

在上面的代码里没有对nickname,email的格式进行约束,现在要解决这个问题,

相对比,Spring Framework 里的 Spring Validation(参见4.2.3) 本质上就是:

利用 @NotNull@Size@Email 这类校验注解,自动帮你做参数校验。

java 复制代码
public class User {
    @NotNull
    private Integer id;
    private String username;
    @JsonIgnore//这样实体类作为json数据返回前端时,会忽略password
    private String password;
    @NotEmpty
    @Pattern(regexp = "^\S{1,10}$")
    private String nickname;
    @NotEmpty
    @Email
    private String email;
    private String userPic;

还需要在controller添加 @Validated注解,这样实体类里的约束注解才能生效

java 复制代码
@PutMapping("/update")//@PutMapping 可以理解成:专门对应 HTTP PUT 请求的映射注解,也就是更新
public Result update(@RequestBody @Validated User user){//RequestBody可以要求前端把零散的字段封装成对象
    userService.update(user);
    return Result.success();
}

2、更新用户头像

java 复制代码
//    @PatchMapping 用来处理:HTTP PATCH 请求。它的语义通常是:"部分修改资源"。Put是完整修改,patch:补丁
    @PatchMapping("/updateAvatar")
    public Result updateAvatar(@RequestParam String avatarUrl) {//@RequestParam不指定参数名的情况下,它是会根据后面的形参名来在请求里找
        userService.updateAvatar(avatarUrl);
        return Result.success();
    }
java 复制代码
  @Override
    public void updateAvatar(String avatarUrl) {
        Map<String,Object> claims = ThreadLocalUtil.get();
//        获取当前的登录用户的id,好在mapper层的语句中执行where子句
        Integer id = (Integer) claims.get("id");
        userMapper.updateAvatar(avatarUrl,id);
    }
java 复制代码
@Update("update big_event.user set user_pic=#{avatarUrl},update_time=now() where id=#{id}")
void updateAvatar(String avatarUrl,Integer id);

这里有和上面业务一样的问题,没有验证头像url是不是链接,所以需要改进

java 复制代码
@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam @URL String avatarUrl) {//@RequestParam不指定参数名的情况下,它是会根据后面的形参名来在请求里找
    userService.updateAvatar(avatarUrl);
    return Result.success();
}

同样你也可以在实体类字段上用@URL,然后在controller参数前加@Validated

3、更新用户密码

java 复制代码
    @PatchMapping("/updatePwd")
//    把前端传来的 JSON 请求体 自动转换成一个 Map<String,String>
    public Result updatePWD(@RequestBody Map<String,String> params) {
        String oldPwd = params.get("old_pwd");
        String newPwd = params.get("new_pwd");
        String rePwd = params.get("re_pwd");
        if (StringUtils.isEmpty(oldPwd) || StringUtils.isEmpty(newPwd) || StringUtils.isEmpty(rePwd)) {
            return Result.error("缺少必要参数");
        }
        Map<String, Object> claims = ThreadLocalUtil.get();
        String username = (String) claims.get("username");
//        由于数据库里是加密的,所以要和加密的比
        if ( !userService.findByUSerName(username).getPassword().equals(MD5Util.md5(oldPwd))){
            return Result.error("原密码错误");
        }
        if (!newPwd.equals(rePwd)) {
            return Result.error("两次密码填写内容不一致");
        }
        userService.updatePwd(newPwd);
        return Result.success();

    }
java 复制代码
    @Override
    public void updatePwd(String newPwd) {
        Map<String,Object> claims = ThreadLocalUtil.get();
//        获取当前的登录用户的id,好在mapper层的语句中执行where子句
        Integer id = (Integer) claims.get("id");
//        因为密码不能改成明文的,不能在数据库存明文
        userMapper.updatePwd(MD5Util.md5(newPwd),id);
    }
java 复制代码
@Update("update big_event.user set big_event.user.password=#{newPwd},big_event.user.update_time=now() where big_event.user.id=#{id}")
void updatePwd(String newPwd, Integer id);

6、文章模块

1、新增文章分类

java 复制代码
@RestController
@RequestMapping("/category")
public class CategoryController {
    @Autowired
    private CategoryServiceImpl categoryService;
    @PostMapping
    public Result add(@RequestBody @Validated Category category) {
        categoryService.add(category);
        return Result.success();
    }
}
java 复制代码
@NotEmpty
private String categoryName;
@NotEmpty
private String categoryAlias;
java 复制代码
@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    private CategoryMapper categoryMapper;
    @Override
    public void add(Category category) {
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateTime(LocalDateTime.now());
        Map<String,Object> claims = ThreadLocalUtil.get();
        Integer id = (Integer) claims.get("id");
        category.setCreateUser(id);
        categoryMapper.add(category);
    }
}
java 复制代码
@Mapper
public interface CategoryMapper {
    @Insert("insert into big_event.category (big_event.category.id, big_event.category.category_name, big_event.category.category_alias, big_event.category.create_user, big_event.category.create_time, big_event.category.update_time)" +
            "values (#{id},#{categoryName},#{categoryAlias},#{createUser},#{createTime},#{updateTime})")
    void add(Category category);
}

2、查询当前用户的分类

你可以看到我这两个接口都没指定不一样的url,但是只要发送类型不同,一个post,一个get就可以访问不同的

java 复制代码
@GetMapping
public Result<List<Category>> getAll() {
    List<Category> list = categoryService.list();
    return Result.success(list);
}
java 复制代码
@Override
public List<Category> list() {
    Map<String,Object> claims = ThreadLocalUtil.get();
    Integer id = (Integer) claims.get("id");
    return categoryMapper.list(id);
}
java 复制代码
@Select("select * from category where id=#{id}")
List<Category> list(Integer id);

实体类里指定时间类型

java 复制代码
@JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
  private LocalDateTime createTime;
  @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
  private LocalDateTime updateTime;

3、获取文章分类详情

java 复制代码
@GetMapping("/detail")
public Result<Category> detail(@RequestParam int id) {
    Category category=categoryService.detail(id);
    return Result.success(category);
}
java 复制代码
@Override
public Category detail(int id) {
    return categoryMapper.detail(id);
}
java 复制代码
@Select("select * from category where id=#{id}")
Category detail(int id);

4、修改分类

java 复制代码
@PutMapping
public Result update(@RequestBody @Validated Category category) {
    categoryService.update(category);
    return Result.success();
}
java 复制代码
@Override
public void update(Category category) {
    category.setUpdateTime(LocalDateTime.now());
    categoryMapper.update(category);
}
java 复制代码
@Update("update category set category_name=#{categoryName}" +
        ",category_alias=#{categoryAlias} where id=#{id}")
void update(Category category);

由于我们依据id来修改,所以id不能为空

java 复制代码
public class Category {
    @NotNull
    private Integer id;
    @NotEmpty
    private String categoryName;
    @NotEmpty
    private String categoryAlias;
注解 null "" " "
@NotNull
@NotEmpty
@NotBlank

但是这个添加了id不能为null,这和新增分类冲突了,要解决这个问题

5、分组校验

把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项

  1. 定义分组
  2. 定义校验项时指定归属的分组
  3. 校验时指定要校验的分组

解决上一个小节的问题

java 复制代码
public class Category {
    @NotNull(groups = {Update.class})
    private Integer id;
    @NotEmpty(groups = {Update.class, Add.class})
    private String categoryName;
    @NotEmpty(groups = {Add.class, Update.class})
    private String categoryAlias;
  private Integer createUser;
  @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime createTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime updateTime;

    public interface Add{

    }
    public interface Update{}
java 复制代码
@PutMapping
public Result update(@RequestBody @Validated(Category.Update.class) Category category) {
    categoryService.update(category);
    return Result.success();
}
java 复制代码
@PostMapping
public Result add(@RequestBody @Validated(Category.Add.class) Category category) {
    categoryService.add(category);
    return Result.success();
}

上面是比较麻烦的写法实际上:

  • 只要定义了一个未指定接口类约束,它就属于默认分组,你自定义的分组继承了默认分组就得到它的约束
  • 下面这个例子实现了和上面实体类代码一样的效果,id的约束指定了故而不在default,其他的都在
java 复制代码
public class Category {
    @NotNull(groups = {Update.class})
    private Integer id;
    @NotEmpty
    private String categoryName;
    @NotEmpty
    private String categoryAlias;
  private Integer createUser;
  @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime createTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")
    private LocalDateTime updateTime;
    public interface Add extends Default {}
    public interface Update extends Default {}

这个模块还有删除分类的方法省略了

java 复制代码
@Delete("delete from category where id=#{id}")
void delete(Integer id);

6、新增文章

java 复制代码
@RestController
@RequestMapping("/article")
public class ArticleController {
   @Autowired
   private ArticleService articleService;

   @PostMapping
   public Result add(@RequestBody @Validated Article article) {
      articleService.add(article);
      return Result.success();
   }
java 复制代码
@Autowired
private ArticleMapper articleMapper;
@Override
public void add(Article article) {
    article.setCreateTime(LocalDateTime.now());
    article.setUpdateTime(LocalDateTime.now());
    Map<String,Object> claims=ThreadLocalUtil.get();
    Integer id = (Integer) claims.get("id");
    article.setCreateUser(id);
    articleMapper.add(article);

}
java 复制代码
@Insert("insert into article (title, content, cover_img, state, category_id,create_user,create_time,update_time)" +
        "values (#{title},#{content},#{coverImg},#{state},#{categoryId},#{createUser},#{createTime},#{updateTime})")
void add(Article article);
java 复制代码
public class Article {

    private Integer id;
    @NotEmpty
    @Pattern(regexp = "^\S{1,10}$")
    private String title;
    @NotEmpty
    private String content;
    @NotEmpty
    @URL
    private String coverImg;
    private String state;
    @NotNull
    private Integer categoryId;

如果validate提供的校验不满足要求怎么办

自定义校验

7、分页查询文章

这个类用来放返回结果,一个字段是返回数据总数,一个是list集合

java 复制代码
public class PageBean <T>{
   private Long total;
   private List<T> items;


    public PageBean() {
    }

    public PageBean(Long total, List<T> items) {
        this.total = total;
        this.items = items;
    }
xml 复制代码
<!-- Source: https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>2.1.0</version>
    <scope>compile</scope>
</dependency>
java 复制代码
@GetMapping
public Result<PageBean<Article>> list(
        Integer pageNum,
        Integer pageSize,
        // required = false表示这个参数可以不传
        @RequestParam(required = false) Integer categoryId,
        @RequestParam(required = false) String state
){
   PageBean<Article> pb=articleService.list(pageNum,pageSize,categoryId,state);
   return Result.success(pb);
}
java 复制代码
   @Override
    public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
        PageBean<Article> pb = new PageBean<>();
//"告诉 PageHelper:
//接下来马上要执行的第一个 select 查询,
//请自动帮我加上分页 limit。" PageHelper本质是一个拦截器,你后面的mapper执行select * from article
//        它会给他变成select * from article limit pageNum,pageSize
//        pageNum = 当前页
//      pageSize = 每页条数
//limit 第一个参数 = 需要跳过多少条数据(offset),也就是从那个参数开始
        PageHelper.startPage(pageNum,pageSize);
        Map<String,Object> o = ThreadLocalUtil.get();
        Integer  userId = (Integer) o.get("id");
        List<Article> as=articleMapper.list(userId,categoryId,state);
// 虽然变量类型写的是 List,
//但 PageHelper 实际返回的是 Page 对象,这个对象是PAgeHelper带来的
//所以可以强转后获取分页信息。
        Page<Article> p= (Page<Article>) as;
        pb.setTotal(p.getTotal());
        pb.setItems(p.getResult());
//不用 Page 直接返回,
//是因为 Page 是分页插件内部对象;为了解耦不要直接返回框架内部对象
//PageBean 才是项目自己的统一分页响应格式。
        return pb;
    }

下面这个是mapper

java 复制代码
List<Article> list(Integer userId, Integer categoryId, String state);

下面这个是xml配置文件,实现动态查询

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实际相当于JDBC中的UserDaoImpl-->
<!--namespace=绑定一个对应的dao/mapper接口-->
<mapper namespace="com.zhang.bigevent.mapper.ArticleMapper">
    <select id="list" resultType="com.zhang.bigevent.pojo.Article">
        select * from article
        <where>
            <if test="categoryId!=null">
                and  category_id=#{categoryId}
            </if>
            <if test="categoryId!=null">
                and state=#{state}
            </if>
            and create_user=#{userId}
        </where>
    </select>

</mapper>

7、文件上传

java 复制代码
@RestController
public class FileUploadController {

    @PostMapping("/upload")
    public Result<String> upload( MultipartFile file) throws IOException {
//        得到文件的名字
        String originalFilename = file.getOriginalFilename();
//        通过原名字和UUID组合,确保每一个文件名都不一样,也就不会两个操作一个覆盖另一个
        String filename= UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
//        把上传的文件数据放在一个新地方。\这个分隔符可以用File.separator 代替只要在父目录和filename代替一个分隔符就行
//        更推荐这种写法 ,new File(父目录, 子文件名),Java 会自动帮你处理路径分隔符。
        file.transferTo(new File("D:\code\project\bigevent\src\main\resources\static\upload",filename));
//  file.transferTo(new File("D:\code\project\bigevent\src\main\resources\static\upload"+File.separator+filename));
//    file.transferTo(new File("D:\code\project\bigevent\src\main\resources\static\upload\"+filename));    
        return Result.success(filename);
    }
}

MultipartFile 是 Spring 框架中用于"接收上传文件"的接口,位于:

它主要用于处理:

  • 图片上传
  • 文件上传
  • 表单中的 file 类型数据

例如前端:

html 复制代码
<input type="file" name="file">

提交后,后端就可以用:

java 复制代码
MultipartFile file

接收这个文件。

场景 常用方法
获取文件名 getOriginalFilename()
判断是否上传 isEmpty()
获取文件类型 getContentType()
保存文件 transferTo()
获取文件内容 getBytes()
大文件处理 getInputStream()

1、阿里云OSS

"云"本质上就是:

别人的服务器 + 网络服务

也就是:

  • 不是存你自己电脑
  • 而是存到互联网的数据中心服务器上

例如:

  • Alibaba Cloud
  • Tencent Cloud
  • Amazon Web Services
  • Google Cloud

这些公司都有大量机房。你存在本地的上传数据,开发可以,但不能被其他用户自由简便的访问,使用专门的云服务提供商可以解决这些问题

阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

cn.aliyun.com/ 配置

help.aliyun.com/zh/oss/user...

xml 复制代码
<!-- Source: https://mvnrepository.com/artifact/com.aliyun/alibabacloud-oss-v2 -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>alibabacloud-oss-v2</artifactId>
    <version>0.3.0</version>
    <scope>compile</scope>
</dependency>

下面是一个基于官网简单上传写的,实际开发推荐key写到环境变量

java 复制代码
public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        String accessKeyId = "LTAI51t227oPKMrgcsQF3ccGMs4";
        String accessKeySecret = "yKU03uRCbB6yNdgyDsv1Byrt2ZphzbPh";

        OSS ossClient = new OSSClientBuilder().build(
                endpoint,
                accessKeyId,
                accessKeySecret
        );
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "big-event-zhanglei";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "001.png";
        // 填写本地文件的完整路径,例如D:\localpath\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
        String filePath= "D:\图片\OIP.jpg";
        // 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
        String region = "cn-beijing";



        try {
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(filePath));
            // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            // putObjectRequest.setMetadata(metadata);

            // 上传文件。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

把这个示例改成工具类

java 复制代码
package com.zhang.bigevent.utils;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;

import java.io.File;
import java.io.InputStream;

public class AliOssUtil {
    private static String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
    private static String accessKeyId = "LTAI5t7oPKMrgcs332QF3ccGMs4";
    private static String accessKeySecret = "yKU03uRCbB6yNdgyDs12vByrtZphzbPh";
    // 填写Bucket名称,例如examplebucket。
    private static String bucketName = "big-event-zhanglei";
    private static  String region = "cn-beijing";
    public static String uploadFile(String objectName, InputStream filePath) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String url = "";

        OSS ossClient = new OSSClientBuilder().build(
                endpoint,
                accessKeyId,
                accessKeySecret
        );

        try {
//                                              InputStream 可以代替 new File(...) 作为"文件数据来源"。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, filePath);

            PutObjectResult result = ossClient.putObject(putObjectRequest);
//https://big-event-zhanglei.oss-cn-beijing.aliyuncs.com/001.png
            url="https://"+bucketName+"."+endpoint.substring(endpoint.lastIndexOf("/")+1)+
            "/"+objectName;

        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return url;
    }
}

new File和inputstream都可以作为文件数据的来源,这个工具类之所以有流作为参数,是因为控制类的参数MultipartFile有getInputstream的方法

java 复制代码
import java.util.UUID;

@RestController
public class FileUploadController {

    @PostMapping("/upload")
    public Result<String> upload( MultipartFile file) throws Exception {
//        得到文件的名字
        String originalFilename = file.getOriginalFilename();
//        通过原名字和UUID组合,确保每一个文件名都不一样,也就不会两个操作一个覆盖另一个
        String filename= UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));

        String s = AliOssUtil.uploadFile(filename, file.getInputStream());
        System.out.println(s);
        return Result.success(s);
    }
}

8、登录优化

在登录后,如果你修改了密码,那么原密码的令牌就应该失效了,然而令牌在内存里生成时是 Map,但一旦发出去就变成了不可变的加密字符串,我们无法直接操作它,故而我们这样实现这个改进

Redis

Redis 是数据库,但和我们熟悉的 MySQL 完全不同 ,Redis 是一个开源的内存数据结构存储系统,用作数据库、缓存和消息中间件。

  • Redis 是一种基于内存的 NoSQL 键值数据库,它的核心特点是读写速度极快(微秒级),支持字符串、哈希、列表、集合等多种数据结构。
  • 它最常用于缓存热点数据 (如用户信息、页面内容)以减轻数据库压力,同时也被广泛用作分布式会话存储 (如存登录 Token,实现主动失效)、计数器 (如点赞数、限流)和分布式锁
  • 与传统关系型数据库(如 MySQL)不同,Redis 主要将数据存储在内存中,因此能提供极高的并发读写能力,但存储容量受内存限制,通常作为数据库的"加速层"配合使用。

使用前你需要在官网下载,并点击redis-sever启动服务 github.com/microsoftar...

xml 复制代码
<!-- Source: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>4.0.6</version>
    <scope>compile</scope>
</dependency>
yml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
    username: root
    password: 123456
  data:
    redis:
      host: localhost
      port: 6379
      

执行一个测试

java 复制代码
@SpringBootTest//这个注解的作用就是test前注册spring容器
public class RedisTest {
    @Autowired
//StringRedisTemplate 是 Spring Data Redis 提供的一个专门用于处理字符串数据的工具类
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void test() {
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        operations.set("username", "zhangsan");

    }
}

1、基于redis的登录优化

java 复制代码
@Autowired
private StringRedisTemplate stringRedisTemplate;

@PostMapping("/login")
public Result login(
        @Pattern(regexp = "^\S{5,16}$") String username,
        @Pattern(regexp = "^\S{5,16}$") String password) {
    User u = userService.findByUSerName(username);
    if (u == null) {
        return Result.error("该用户不存在");
    }
    //由于数据库存储的是加密后的密码,所以我们要把输入的密码加密再与数据库比对
    if (MD5Util.md5(password).equals(u.getPassword())) {
        HashMap<String, Object> claims = new HashMap<>();//map来存储数据
        claims.put("id", u.getId());
        claims.put("username", username);
        String token = JwtUtil.genToken(claims);//工具类得到token

        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        //直接把键和值设置为token,避免了重复,也方便比对,过期时间与token一致
        operations.set(token, token,12, TimeUnit.HOURS);
        return Result.success(token);//封装到result返回前端
    }
    return Result.error("密码错误");
}
java 复制代码
@Component
public class LoginInterceptors implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");//得到令牌
        try {
        //token名直接为键名优点就在于,本地token直接取
        //,通过能不能取出就知道有没有失效,不用取出再比对
            String redisToken = stringRedisTemplate.opsForValue().get(token);
            if (redisToken == null) {//为空说明已经失效,直接抛出错误
                throw new RuntimeException();
            }
            Map<String, Object> claims = JwtUtil.parseToken(token);//验证令牌
            ThreadLocalUtil.set(claims);
            return true;//放行
        } catch (Exception e) {
            System.out.println("拦截了");
            response.setStatus(401);
            return false;//阻止
        }
    }
相关推荐
小江的记录本6 小时前
【Java基础】反射与注解:核心原理、自定义注解、注解解析方式(附《思维导图》+《面试高频考点清单》)
java·数据结构·python·mysql·spring·面试·maven
ch.ju6 小时前
Java Programming Chapter 4——Composition of classes
java·开发语言
日月云棠6 小时前
5 高级配置:多注册中心与异步化编程
java·后端
敖正炀6 小时前
BlockingQueue 与生产者-消费者模式:并发数据传递的源码内核
java
敖正炀6 小时前
Stream API 惰性求值与内部迭代
java
日月云棠6 小时前
4 高级配置:容错策略、降级保护与流量控制
java·后端
人道领域6 小时前
Java基础热门八股总结:八种基本数据类型 + 装箱拆箱 + 缓存机制,(90%的Java新手都搞不清的装箱拆箱问题)
java·开发语言·python
jameslogo6 小时前
如何用RocketMQTemplate发送事务消息
java·spring boot·rocketmq
菜鸟小九6 小时前
JUC补充(ThreadLocal、completableFuture)
java·开发语言