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项目
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 提供的一个参数校验框架,使用预定义的注解完成参数校验
-
引入Spring Validation 起步依赖
-
在参数前面添加@Pattern注解
使用正则表达式限制参数格式,例如限制用户名必须是 5~16 位字母数字。
java
@Pattern(regexp = "^[a-zA-Z0-9_]{5,16}$")
String username
-
在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>
-
组成
- Header(头), 记录令牌类型和签名算法等
- PayLoad(荷载),携带自定义的信息
- Signature(签名),对头部和荷载进行加密计算得来
-
使用
- 引入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、

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、分组校验
把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项
- 定义分组
- 定义校验项时指定归属的分组
- 校验时指定要校验的分组
解决上一个小节的问题
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,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

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;//阻止
}
}
