苍穹外卖知识点大总结(4万字)
前言:
这篇文章是我对苍穹外卖的大总结哦,也是我未来的复习资料
目录
1.项目简单介绍:
本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台 和 小程序端应用 两部分。

系统管理后台:主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。
这个只是一个商家的操作系统,但是用户端可以好多人登录
小程序端:主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。
技术选型:
2.Nginx反向代理
-
正向代理:代理客户端
-
你(客户端)通过一个代理服务器去访问谷歌(目标服务器)。谷歌只知道代理服务器访问了它,而不知道你的存在。
-
作用: 为客户端服务,用于突破访问限制(如翻墙)、隐藏客户端身份、缓存等。
-
比喻: 就像一个租房中介,你(租客)通过中介去联系房东,房东不知道你的真实身份,只和中介打交道。
-
-
反向代理:代理服务端
-
你(客户端)访问
example.com,这个域名背后实际上是一个 Nginx 服务器(反向代理)。Nginx 接收你的请求后,悄悄地将请求转发给背后真正处理请求的应用服务器(比如 Node.js, Java, Python 应用),然后将应用服务器的响应返回给你。 -
作用: 为服务端服务,用于负载均衡、安全、缓存静态内容、SSL 终结等。
-
比喻: 就像一个大公司的前台/总机。你打电话给公司总机,总机根据你的需求(比如按分机号)将电话转接到相应的部门(内部服务器)。你不需要知道具体是哪个员工接的电话。
-
核心配置详解:
-
端口(Port)是什么?
-
端口就像是服务器的"门牌号",用于区分不同的网络服务。
-
一台服务器可以提供多种服务(如网站、邮件、FTP等),每个服务使用不同的端口。
-
-
80 端口的特殊意义:
-
80 端口是 HTTP 协议的默认端口。
-
当你在浏览器输入
http://example.com时,浏览器实际上访问的是http://example.com:80(省略了:80,因为它是默认的)。
-
-
实际应用场景:
nginx
server { listen 80; # 监听 80 端口(HTTP) server_name localhost; # 服务器名称是 localhost # 当有人通过 http://localhost 访问时,返回下面的页面 location / { root /usr/share/nginx/html; index index.html index.htm; } }
Nginx 的反向代理配置主要使用 proxy_pass 指令。
一个最简单的配置示例:
nginx:
server {
listen 80;
server_name example.com;
location / {
# 核心指令:将请求代理到后端服务器
proxy_pass http://localhost:3000;
# 以下是一些常用且重要的附加配置
# 1. 设置传递给后端服务器的头信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # 传递客户端的真实IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递转发链路上的IP列表
proxy_set_header X-Forwarded-Proto $scheme; # 传递客户端使用的协议(http/https)
# 2. 一些超时和缓冲区的配置
proxy_connect_timeout 30s; # 与后端服务器建立连接的超时时间
proxy_send_timeout 30s; # 向后端服务器发送请求的超时时间
proxy_read_timeout 30s; # 从后端服务器读取响应的超时时间
# 3. 缓冲区(可选,对于大响应体有用,但会增加内存使用)
# proxy_buffering on;
# proxy_buffer_size 4k;
# proxy_buffers 8 4k;
}
}
配置解释:
-
server_name example.com;: 监听对example.com的请求。 -
location /: 匹配所有以/开头的请求路径。 -
proxy_pass http://localhost:3000;: 这是核心,告诉 Nginx 将所有匹配的请求转发到http://localhost:3000这个上游服务。 -
proxy_set_header ...: 这是至关重要的配置。因为 Nginx 在转发请求时,默认会修改或丢失一些原始的客户端信息。通过这些设置,我们告诉后端服务器客户端的真实情况。-
X-Real-IP: 后端应用可以通过这个头知道客户端的真实 IP,否则日志里记录的都是 Nginx 服务器的 IP。 -
X-Forwarded-For: 记录了整个代理路径上的所有 IP,对于追踪请求来源非常有用。 -
X-Forwarded-Proto: 告诉后端应用客户端使用的是 HTTP 还是 HTTPS。如果你的 Nginx 前面还有一层 SSL 终结的负载均衡器,这个配置就尤其重要。
-

负载均衡策略:
Nginx 可以轻松地将请求分发到一组后端服务器。

使用 upstream 块定义服务器组
# 在 http 块中定义一个名为 ‘backend_servers’ 的上游组
http {
upstream backend_servers {
server 192.168.1.10:8080 weight=3; # weight表示权重,越高被分发的请求越多
server 192.168.1.11:8080;
server 192.168.1.12:8080;
# 还可以配置健康检查、备份服务器等
# server 192.168.1.13:8080 backup; # 备份服务器,只有当其他服务器都不可用时才启用
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend_servers; # 这里使用 upstream 的名字
# 同样需要设置 proxy_set_header 等指令
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
-
轮询: 默认方式,按顺序逐一分配。
-
加权轮询: 如上例,
weight值越高,被分配到的几率越大。适用于服务器性能不均的场景。 -
IP 哈希: 根据客户端 IP 的哈希值分配,能保证同一个客户端的请求总是落到同一台后端服务器上,常用于解决 Session 问题。
nginx: upstream backend_servers { ip_hash; server 192.168.1.10:8080; server 192.168.1.11:8080; }
3. Swagger,knife4j与Apifox
这些都是利用接口文档进行测试的,其实postman也可以
Swagger是一种基于OpenAPI规范的工具,用于设计、生成和文档化RESTful API
Apifox:
Apifox是一款集API设计、调试、Mock、自动化测试与文档管理于一体的综合平台。它支持多种协议,提供可视化界面,可自动生成API文档和Mock数据,方便前后端开发。
我用的就是这个,挺好用的,推荐

4.项目架构思想
也是用到了maven的分模块开发

maven:

1.直接继承依赖
如果父工程在 <dependencies> 中声明了依赖,子工程会自动继承
父工程 pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>a</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>
<modules>
<module>b</module>
</modules>
</project>
子工程 b 的 pom.xml
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>a</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>b</artifactId>
<!-- 自动继承 spring-core 依赖 -->
</project>
2.依赖管理(推荐方式)
更常见的做法是使用 <dependencyManagement>
父工程
<project>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
子工程
<project>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<!-- 不需要写版本号,从父工程继承 -->
</dependency>
</dependencies>
</project>
DTO,VO和POJO:
sky-pojo: 模块中存放的是一些 entity、DTO、VO
POJO:一种简单的Java实体类,不包含业务逻辑
DTO,VO 是一个简单的 Java 对象,用于封装一组数据。
DTO通常包含字段和相应的 getter 和 setter 方法,但不包含业务逻辑或行为。
在 RESTful API 中,使用 DTO 封装返回的数据,确保只返回必要的信息
VO是前端需要展示的数据,由后端查询数据库,返回对应数据
注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据
5.全局异常处理器
全局异常处理器是一种用于集中处理应用程序中所有异常的机制。
在Spring框架中,可以通过@ExceptionHandler注解来定义一个全局异常处理器方法。当程序运行时抛出异常时,Spring会自动调用该方法来处理异常,而不是直接抛出到客户端。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.info("异常信息:{}",ex.getMessage());
if (ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}
else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
}
核心机制:
-
@ControllerAdvice:-
这是一个组件注解,它的作用是拦截所有被
@Controller和@RestController注解的控制器中抛出的异常。 -
它将类标记为一个全局的、跨多个控制器的异常处理器、数据绑定处理器等。你可以把它想象成一个“全局的切面(AOP)”,专门用于处理控制器层面的问题。
-
-
@ResponseBody:-
这个注解告诉 Spring,该方法的返回值应该直接写入 HTTP 响应体(Response Body)中,而不是被解析为一个视图名称或进行跳转。
-
Spring 会使用内置的
HttpMessageConverter(如MappingJackson2HttpMessageConverter)将返回值(通常是对象或集合)序列化成 JSON/XML 等格式。
-
-
@RestControllerAdvice=@ControllerAdvice+@ResponseBody:-
因此,被
@RestControllerAdvice注解的类中所有的方法都默认带有@ResponseBody语义。 -
这意味着,你在这个类里编写的异常处理方法,其返回值会自动被转换为 JSON 并发送给客户端。
-
在该项目中
设定了一个工具类,把我们会遇到的每一种的异常命名为相对应的类名,这样更加清晰
设置一个BaseException 继承运行时异常 RuntimeException
一有异常就全局异常处理器捕捉到进行处理
6.日志记录
-
什么是日志?
-
日志就好比生活中的日记,可以随时随地记录你生活中的点点滴滴。
-
程序中的日志,是用来记录应用程序的运行信息、状态信息、错误信息的。
-

-
JUL:这是JavaSE平台提供的官方日志框架,也被称为JUL。配置相对简单,但不够灵活,性能较差。
-
Log4j:一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标。
-
Logback:基于Log4j升级而来,提供了更多的功能和配置选项,性能由于Log4j。
-
Slf4j:(Simple Logging Facade for Java)简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。
准备工作:引入logback的依赖(springboot中无需引入,在springboot中已经传递了此依赖)
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
由于 Maven 的依赖传递机制,当你声明 logback-classic 时,它会自动将另外两个核心模块引入到你的项目中。下表清晰地展示了这一关系:
| 你直接声明的依赖 | 它会帮你悄悄引入的依赖 |
|---|---|
logback-classic |
slf4j-api 和 logback-core |
Logback日志级别
日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(优先级由低到高):
| 日志级别 | 说明 | 记录方式 |
| trace | 追踪,记录程序运行轨迹 【使用很少】 | log.trace("...") |
| debug | 调试,记录程序调试过程中的信息,实际应用中一般将其视为最低级别 【使用较多】 | log.debug("...") |
| info | 记录一般信息,描述程序运行的关键事件,如:网络连接、io操作 【使用较多】 | log.info("...") |
| warn | 警告信息,记录潜在有害的情况 【使用较多】 | log.warn("...") |
| error | 错误信息 【使用较多】 | log.error("...") |
配置
1) logback.xml 文件配置
2)直接在 application.properties :
logging.level.com.yourpackage.UserService=DEBUG
lombok中提供的@Slf4j注解,可以简化定义日志记录器这步操作。添加了该注解,就相当于在类中定义了日志记录器,就下面这句代码:
private static Logger log = LoggerFactory. getLogger(Xxx. class);
没有设置级别默认使用根日志
logging:
level:
# 项目相关包
com.hmdp: debug
com.hmdp.controller: info
com.hmdp.service: debug
com.hmdp.mapper: trace
# 框架相关包
org.springframework: warn
org.springframework.web: debug
org.mybatis: debug
org.apache.ibatis: debug
# 数据库相关
org.hibernate.SQL: debug
org.hibernate.type.descriptor.sql.BasicBinder: trace
# 根日志
root: info
# 文件输出
file:
name: logs/application.log
max-size: 10MB
max-history: 30
# 日志格式
pattern:
console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(%-5level) %clr(%logger{36}){cyan} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
7.http协议
请求简单介绍
对于http协议
请求协议:浏览器将数据以请求格式发送到服务器。包括:请求行、请求头 、请求体
get请求只有请求行和请求体
post请求则是三个都有
1)请求行:请求方式、资源路径、协议/版本组成(之间使用空格分隔)
2)请求头:
| Host | 表示请求的主机名 |
| User-Agent | 浏览器版本。 例如:Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79 ,IE浏览器的标识类似Mozilla/5.0 (Windows NT ...)like Gecko |
| Accept | 表示浏览器能接收的资源类型,如text/*,image/*或者*/*表示所有; |
| Accept-Language | 表示浏览器偏好的语言,服务器可以据此返回不同语言的网页; |
| Accept-Encoding | 表示浏览器可以支持的压缩类型,例如gzip, deflat |
| Content-Type | 请求主体的数据类型 |
| Content-Length | 数据主体的大小(单位:字节) |
偶尔还带token
3)请求体:则是post请求里面带的参数
2.响应
响应几乎是一样的:响应行,响应头, 响应体
1)响应行: 协议及版本、响应状态码、状态码描述
2)响应头:
| Content-Type | 表示该响应内容的类型,例如text/html,image/jpeg |
| Content-Length | 表示该响应内容的长度(字节数); |
| Content-Encoding | 表示该响应压缩算法,例如gzip ; |
| Cache-Control: | 指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒 ; |
| Set-Cookie: | 告诉浏览器为当前页面所在的域设置cookie |
3)响应体 :响应数据
状态码
| 状态码分类 | 说明 |
| 1xx | 响应中 --- 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 |
| 2xx | 成功 --- 表示请求已经被成功接收,处理已完成 |
| 3xx | 重定向 --- 重定向到其它地方,让客户端再发起一个请求以完成整个处理 |
| 4xx | 客户端错误 --- 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 |
| 5xx | 服务器端错误 --- 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 |
关于响应状态码,我们先主要认识三个状态码,其余的等后期用到了再去掌握:
-
200 ok客户端请求成功 -
404 Not Found请求资源不存在 -
500 Internal Server Error服务端发生不可预期的错误
8.jwt令牌/过滤器/拦截器/ThreadLocal
jwt.令牌登录
JWT全称 JSON Web Token (官网:https://jwt.io/)
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
. 首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖
jwt就是把三个部分进行了base64编码,xxxxxxx.yyyyyyy.zzzzzz,x就是头部,y就是负载,z就是签名签名 = HA256( base64(header) + "." + base64(payload), 你的密钥 ) 然后将签名附在JWT的第三部分。这个签名是Header和Payload的“数字指纹”。
这个密钥是你自己所设置的,利用密钥才有可能解密第三部分,这样验证时只有一样的令牌才会通过,如果不一样就会拒绝(检验作用)
首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
另外,在登陆时会用到两个技术(都可以用)对令牌进行验证
Filter过滤器
Interceptor拦截器
讲解:
1)先自定义拦截器
实现HandlerInterceptor接口,并重写其所有方法
//自定义拦截器
@Component
public class DemoInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //true表示放行
}
//目标资源方法执行后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ... ");
}
//视图渲染完毕后执行,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion .... ");
}
}
注意:
-
preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行
-
postHandle方法:目标资源方法执行后执行
-
afterCompletion方法:视图渲染完毕后执行,最后执行
2). 注册配置拦截器
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}
这里也是可以设置不拦截那些路径的
实例:
**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
}
**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户的id:", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
下面我们来详细拆解一下为什么必须注册:
| 步骤 | 使用的技术 | 作用 |
|---|---|---|
| 1. 创建Bean | @Component |
让Spring容器管理拦截器实例的生命周期,使其可以被依赖注入。 |
| 2. 注册并配置 | WebMvcConfigurer.addInterceptors |
告知Spring MVC框架将此Bean用作拦截器,并定义其拦截规则,将其插入到请求处理流程中。 |
一个生动的比喻
-
@Component:就像你拥有一张工程师资格证书。你把这个资格证放在了人才市场(Spring容器)里。人才市场知道有你这么个人才存在。 -
WebMvcConfigurer注册:就像一家公司(Spring MVC框架)去人才市场招聘,它需要一个“安全门卫”(拦截器)的职位。公司的HR(WebConfig类)找到了你,看了你的资格证,然后对你说:“好的,我正式聘用你为我们公司的安全门卫,你的职责是检查所有进入大楼的人(/**)”。
如果你只有资格证书,但没有被公司聘用并分配具体岗位,你是不会去站岗的。同样,拦截器类只有 @Component 注解而没有被注册,它就不会执行任何拦截逻辑。

当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应
通过以上讲解,我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据
ThreadLocal
*温馨小贴士:关于threadlocal**
如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离
ThreadLocal 是 Java 提供的一个类,它为每个使用该变量的线程都提供了一个独立的变量副本。这意味着,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
简单来说,ThreadLocal 解决了变量在线程间的隔离问题,而 synchronized 解决了变量在线程间的共享问题。
| 方法 | 作用 | 返回值 | 重要说明 |
|---|---|---|---|
get() |
获取当前线程的变量副本 | T |
如果之前没set过,且没设置initialValue,返回null |
set(T value) |
设置当前线程的变量副本 | void |
|
remove() |
移除当前线程的变量副本 | void |
重要:防止内存泄漏 |
initialValue() |
提供初始值 | T |
protected方法,需要子类重写 |
withInitial(Supplier) |
创建带初始值的ThreadLocal | ThreadLocal<S> |
Java 8新增的静态方法,推荐使用 |
Web容器的线程处理机制
text
客户端请求 → Tomcat线程池 → 分配线程T1 → 完整处理请求 → 返回响应 → 线程T1回归线程池
关键点:
-
每个HTTP请求都会被分配一个独立的线程来处理
-
这个线程会完整地执行整个请求链路:
-
拦截器 → Controller → Service → Dao → 返回拦截器 → 返回响应
-
-
在整个请求处理期间,都是同一个线程在执行
text
用户请求
│
↓
Tomcat分配线程T1
│
↓
JwtTokenUserInterceptor.preHandle()
│
↓ ┌─────────────────┐
解析JWT令牌 │ ThreadLocal │
│ │ 线程T1: 123 │
↓ └─────────────────┘
BaseContext.setCurrentId(123)
│
↓
Controller方法
│
↓ ┌─────────────────┐
BaseContext.getCurrentId() → │ ThreadLocal │ → 返回123
│ │ 线程T1: 123 │
↓ └─────────────────┘
Service方法
│
↓ ┌─────────────────┐
BaseContext.getCurrentId() → │ ThreadLocal │ → 返回123
│ │ 线程T1: 123 │
↓ └─────────────────┘
返回响应
│
↓
线程T1回归线程池
ThreadLocal被清理(最好手动remove)
9.AOP思想
AOP简介
AOP是一种编程范式,旨在通过分离横切关注点(如日志记录、事务管理、安全检查等)来提高代码的模块化和可维护性。它将这些横切逻辑从业务逻辑中分离出来,集中管理,从而减少代码冗余和提高代码清晰度。
作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
AOP核心概念
1. 连接点(JoinPoint)
连接点是 AOP 中可被拦截的程序执行点,在 Spring AOP 中特指方法的执行。它不仅包含方法本身,还封装了方法执行时的所有上下文信息:
- 方法的参数列表
- 方法所属的类实例
- 方法的返回值(如果已执行完毕)
- 抛出的异常(如果有)
可以理解为:连接点是程序运行过程中可能被 AOP 切入的 "时机点",而 Spring AOP 的JoinPoint接口则提供了访问这些信息的方法。在实际应用中,几乎所有的业务方法都可能成为连接点,具体是否被切入则由切入点决定。
2. 通知(Advice)
通知是 AOP 中定义的具体增强逻辑,是在多个方法中重复出现的共性功能,主要有以下五类:
- 前置通知(Before):在连接点方法执行前执行
- 后置通知(After):无论连接点方法是否正常执行,都会在其之后执行
- 返回通知(AfterReturning):在连接点方法正常返回后执行
- 异常通知(AfterThrowing):在连接点方法抛出异常后执行
- 环绕通知(Around):包围连接点方法,可在其前后执行自定义逻辑,功能最强大
例如统计方法执行耗时的功能,就可以通过环绕通知实现:在方法执行前记录开始时间,执行后计算时间差,这部分逻辑被单独封装为一个通知方法。
3. 切入点(PointCut)
切入点是用于筛选连接点的条件,决定了哪些连接点会被应用通知。它通过切入点表达式来定义匹配规则,常见的表达式类型有:
- 按方法签名匹配:如execution(* com.example.service.*.*(..))
- 按注解匹配:如@annotation(com.example.annotation.Log)
- 按类型匹配:如within(com.example.service.*)
4. 切面(Aspect)
切面是通知和切入点的结合体,它明确了 "在什么时机、对什么方法、做什么增强":
- 包含了通知(要做什么增强)
- 包含了切入点(对什么方法增强)
- 定义了通知与切入点的关联关系(在方法执行的什么阶段增强)
在 Spring 中,切面通常通过@Aspect注解标识的类来实现,一个切面可以包含多个通知和多个切入点的组合。
5. 目标对象(Target)
目标对象是指被 AOP 增强的原始对象,即通知所应用的对象。它是业务逻辑的实际执行者,AOP 通过动态代理的方式对目标对象进行包装,在不修改其原始代码的情况下实现功能增强。
Spring的AOP底层实现:

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象,在代理对象当中就会对目标对象当中的原始方法进行功能的增强,故而对原代码无侵入性
通知执行顺序
在使用通知时的注意事项:
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。

@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2
外卖例子:公共字段自动填充
1)先定义了一个enum枚举类
OperationType value() 表示在一个注解中定义了一个名为 value 的元素,其类型是 OperationType 枚举。
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
典型上下文
这种声明通常出现在自定义注解中,例如:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AutoFil {
// 定义一个名为value的元素,其类型是OperationType枚举
OperationType value();
2)定义一个名为value的元素,其类型是OperationType枚举 如何使用呢?在想用的方法上面添加 @AutoFill(value = OperationType.INSERT) // 使用注解并提供OperationType值
3) 然后再创建切面类
1. execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) -
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略 throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的 异常)
- * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数, 也可以通配 包、类、方法名的一部分
- .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
总结我们这次就是execution(* com.sky.mapper.*.*(..))
2.切入点表达式第二个方法自定义注解@annotation(自己自定义的注解类名) 用的时候在使用地方加注解就可以
其实只写第二个也是可以的,不过加上个execution缩小范围,加快内存运行速率

4) 然后利用反射获取数据进行修改
5)有关Class对象:
class 是Java关键字,用于定义类。Class(首字母大写)是一个Java类,是java.lang.Class类的全限定名。
它们完全不同,但又有密切关系。让我用比喻来解释:
生动的比喻:汽车制造厂
想象一个汽车制造厂:
-
class Car { ... }- 这就像汽车的设计图纸
-
它定义了汽车应该有什么属性(颜色、型号)和方法(启动、加速)
-
它只是一个蓝图,不是实际存在的物体
-
Car myCar = new Car();- 这就像根据图纸制造出一辆真实的汽车
-
myCar是一个具体的、可以使用的对象实例
-
Class<?> carClass = Car.class;- 这就像图纸的档案管理员
-
这个管理员不是图纸本身,也不是汽车
-
但他知道关于这张图纸的一切信息:能造什么车、有什么功能、需要什么材料
每一个被jvm加载的类都有一个class对象
| 概念 | 含义 | 比喻 |
|---|---|---|
| 类 (class) | 代码中定义的模板 | 汽车的设计图纸 |
Class 对象 |
JVM生成的,用于表示类元数据的对象 | 图纸的管理员/档案 |
获取 Class 对象 |
获取类的元数据信息 | 找到这个管理员 |
| 实例对象 (Instance) | 根据类模板创建的具体对象 | 根据图纸制造的实实在在的汽车 |
| 创建实例对象 | 通过 Class 对象的方法(newInstance, Constructor) |
命令管理员根据图纸造一辆新车 |
| 特性 | class 关键字 |
Class 类 |
|---|---|---|
| 本质 | Java关键字,语法标记 | 具体的Java类 (java.lang.Class) |
| 用途 | 用于定义新的类 | 用于表示已加载的类和接口 |
| 实例化 | 不能实例化,只是语法 | 可以获取其实例(如 String.class) |
| 包含信息 | 定义类的结构 | 包含类的元数据(方法、字段、注解等) |
| 比喻 | 设计图纸 | 图纸的管理员/档案 |
10.文件上传
先配置阿里云oss服务,这里跳过了
AliOssUtil类是没有办法加@Component注解交给ioc容器管理的,这是因为当你使用@Component注解时,Spring默认会尝试使用无参构造函数来创建Bean实例,或者尝试自动注入所需的参数,但是在我们的这个类中,无法直接获得这些参数的具体值,创建bean会失败的
像ioc容器,在spring启动的时候,会默认

开这个启动类得注解后,回有一个@ComponentScan默认扫描当下包及其子包
当我们没办法把他交给第三方容器管理时改怎么办呢?
1.@ComponentScan组件扫描(添加扫描,这个不可以,因为无法创建bean
2.@Import导入普通类导入配置类@Import(AliOssUti.class)
//导入的类会被Spring加载到IOC容器中
3.配置类@Configuration
当Spring应用程序启动时,它会扫描所有被 @Configuration 注解的类。Spring发现配置类会对其进行特殊处理,执行@Bean方法:Spring会调用所有被 @Bean 注解标记的方法。Spring会拦截这些方法的调用。它将方法的返回值(即 new这个对象)接管过来,并将其注册到自己的IoC容器中。
public class HeaderConfig {
@Bean
public HeaderParser headerParser(){
return new HeaderParser();
}}

还有一个小技巧
在配置数据库和配置刚刚的阿里云oss的时候,没有直接的写道yml中,而是新加了dev,这样无论如何需要改动是可以直接改配置文件
11.map传参数
1)<foreach>
在 MyBatis 的 XMLMapper 文件中,<foreach> 标签的基本结构如下:
xml
<foreach collection="需要遍历的集合"
item="单个元素名称"
index="索引(可选)"
open="开始符号"
separator="间隔符号"
close="结束符号">
#{单个元素名称}
</foreach>
补充:MyBatis 参数绑定规则
当你传入一个 List 类型参数给 MyBatis 时,如果没有使用 @Param 注解,MyBatis 会自动将其绑定为关键字 list
在MyBatis中,collection属性不一定必须写"list",默认值是list而已,如果使用@Param注解明确指定参数名,则使用注解中的名称
如果不使用注解,单个List/Collection参数默认可用"list"或"collection"
如果不使用注解,单个数组参数默认可用"array"
如果是多个参数,必须使用@Param注解指定名称
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sky.mapper.SetmealDishMapper">
<!-- 情况1:使用@Param("ids")注解 -->
<select id="getSetmealIdsByDishIds1" resultType="long">
select setmeal_id from setmeal_dish where dish_id in
<foreach item="dishId" collection="ids" open="(" separator="," close=")">
#{dishId}
</foreach>
</select>
<!-- 情况2:不使用注解,单个List参数 -->
<select id="getSetmealIdsByDishIds2" resultType="long">
select setmeal_id from setmeal_dish where dish_id in
<foreach item="dishId" collection="list" open="(" separator="," close=")">
#{dishId}
</foreach>
</select>
<!-- 情况3:多个参数,必须使用@Param注解 -->
<select id="getSetmealIdsByDishIds3" resultType="long">
select setmeal_id from setmeal_dish where dish_id in
<foreach item="dishId" collection="ids" open="(" separator="," close=")">
#{dishId}
</foreach>
and status = #{status}
</select>
</mapper>
package com.sky.mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface SetmealDishMapper {
// 使用@Param注解明确指定参数名
List<Long> getSetmealIdsByDishIds1(@Param("ids") List<Long> dishIds);
// 不使用注解,单个List参数
List<Long> getSetmealIdsByDishIds2(List<Long> dishIds);
// 多个参数,使用@Param注解
List<Long> getSetmealIdsByDishIds3(@Param("ids") List<Long> dishIds,
@Param("status") Integer status);
}
2)resultMap
MyBatis的ResultMap配置,用于定义如何将数据库查询结果映射到Java对象
例子:
对应的Java实体类
Emp类(员工类)
public class Emp {
private Long id;
private String username;
private String password;
private String name;
private Integer gender;
private String phone;
private String job;
private BigDecimal salary;
private String image;
private LocalDate entryDate;
private Long deptId;
private LocalDateTime createTime;
private LocalDateTime updateTime;
// 一对多关系:一个员工有多个工作经历
private List<EmpExpr> exprList;
// getter和setter方法...
}
EmpExpr类(工作经历类)
public class EmpExpr {
private Long id;
private String company; // 公司名称
private String job; // 职位
private LocalDate begin; // 开始时间
private LocalDate end; // 结束时间
private Long empId; // 关联的员工ID
// getter和setter方法...
}
数据库表结构
CREATE TABLE emp (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(50),
name VARCHAR(50),
gender TINYINT,
phone VARCHAR(20),
job VARCHAR(50),
salary DECIMAL(10,2),
image VARCHAR(200),
entry_date DATE,
dept_id BIGINT,
create_time DATETIME,
update_time DATETIME
);
CREATE TABLE emp_expr (
id BIGINT PRIMARY KEY,
emp_id BIGINT, -- 外键,关联emp表的id
company VARCHAR(100), -- 公司名称
job VARCHAR(50), -- 职位
begin DATE, -- 开始时间
end DATE, -- 结束时间
FOREIGN KEY (emp_id) REFERENCES emp(id)
);
1. ResultMap定义
<resultMap id="empResultMap" type="com.itheima.pojo.Emp">
-
id="empResultMap":给这个映射规则起个名字,方便其他地方引用 -
type="com.itheima.pojo.Emp":指定要映射的Java实体类
2. 基本字段映射
<id column="id" property="id" />
<result column="username" property="username" />
<!-- 其他字段... -->
-
<id>:映射主键字段 -
<result>:映射普通字段 -
column:数据库表中的列名 -
property:Java实体类中的属性名
3. 一对多关系映射(核心部分)
<collection property="exprList" ofType="com.itheima.pojo.EmpExpr">
<id column="ee_id" property="id"/>
<result column="ee_company" property="company"/>
<!-- 其他字段... -->
</collection>
这是最关键的部分,表示一个员工对应多个工作经历的一对多关系:
-
property="exprList":Emp实体类中存放工作经历列表的属性名 -
ofType="com.itheima.pojo.EmpExpr":集合中每个元素的类型 -
使用别名(如
ee_id)来避免字段名冲突
4. SQL查询语句
<select id="getById" resultMap="empResultMap">
select e.*,
ee.id ee_id,
ee.emp_id ee_empid,
ee.begin ee_begin,
ee.end ee_end,
ee.company ee_company,
ee.job ee_job
from emp e left join emp_expr ee on e.id = ee.emp_id
where e.id = #{id}
</select>
-
使用左连接查询员工表和工作经历表
-
为工作经历表的字段设置别名(加
ee_前缀) -
使用
resultMap="empResultMap"引用上面定义的映射规则
查询结果
Emp{
id: 1,
name: "张三",
job: "经理",
exprList: [
EmpExpr{company: "阿里", job: "开发", begin: "2020-01", end: "2022-01"},
EmpExpr{company: "腾讯", job: "架构", begin: "2022-02", end: "2023-12"}
]
}
这个ResultMap实现了:
-
基本字段映射:将数据库列映射到Java对象属性
-
一对多关系映射:将员工的工作经历数据组装到List集合中
-
避免字段冲突:通过别名区分不同表的同名字段
-
完整的对象组装:一次查询即可获取员工及其所有工作经历信息
这是一种典型的MyBatis高级映射用法,特别适用于处理复杂的关系数据查询
3)传参数
MyBatis的Mapper层中完全可以传递两个或多个参数。以下是几种常见的实现方式:
1. 使用 @Param 注解(推荐)
java
// Mapper接口
public interface UserMapper {
List<User> findUsersByNameAndAge(@Param("name") String name,
@Param("age") Integer age);
}
xml
<!-- Mapper XML -->
<select id="findUsersByNameAndAge" resultType="User">
SELECT * FROM user
WHERE name = #{name} AND age = #{age}
</select>
2. 使用 Map 传递参数
java
// Mapper接口
List<User> findUsersByMap(Map<String, Object> params);
java
// 调用方式
Map<String, Object> params = new HashMap<>();
params.put("name", "张三");
params.put("age", 25);
userMapper.findUsersByMap(params);
xml
<!-- Mapper XML -->
<select id="findUsersByMap" parameterType="map" resultType="User">
SELECT * FROM user
WHERE name = #{name} AND age = #{age}
</select>
3. 使用 JavaBean 对象
java
// 参数对象
public class UserQuery {
private String name;
private Integer age;
// getter/setter省略
}
java
// Mapper接口
List<User> findUsersByQuery(UserQuery query);
xml
<!-- Mapper XML -->
<select id="findUsersByQuery" parameterType="UserQuery" resultType="User">
SELECT * FROM user
WHERE name = #{name} AND age = #{age}
</select>
4. 直接使用参数顺序(不推荐)
java
// Mapper接口
List<User> findUsersByNameAndAge(String name, Integer age);
xml
<!-- Mapper XML -->
<select id="findUsersByNameAndAge" resultType="User">
SELECT * FROM user
WHERE name =
12.redis
Redis是一种键值型的NoSql数据库,这里有两个关键字:
-
键值型
-
NoSql

认识NoSQL
NoSql可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库。
结构化与非结构化
传统关系型数据库是结构化数据,每一张表都有严格的约束信息:字段名、字段数据类型、字段约束等等信息,插入的数据必须遵守这些约束:

而NoSql则对数据库格式没有严格约束,往往形式松散,自由。
可以是键值型,文档型,图格式:

关联和非关联
传统数据库的表与表之间往往存在关联,例如外键

而非关系型数据库不存在关联关系,要维护关系要么靠代码中的业务逻辑,要么靠数据之间耦合:
{
id: 1,
name: "张三",
orders: [
{
id: 1,
item: {
id: 10, title: "荣耀6", price: 4999
}
},
{
id: 2,
item: {
id: 20, title: "小米11", price: 3999
}
}
]
}
查询方式
传统关系型数据库会基于Sql语句做查询,语法有统一标准;
而不同的非关系数据库查询语法差异极大,五花八门各种各样。

事务

总结

存储方式
- 关系型数据库基于磁盘进行存储,会有大量的磁盘IO,对性能有一定影响
- 非关系型数据库,他们的操作更多的是依赖于内存来操作,内存的读写速度会非常快,性能自然会好一些
扩展性
- 关系型数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。
- 非关系型数据库可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。
- 关系型数据库因为表之间存在关联关系,如果做水平扩展会给数据查询带来很多麻烦
介绍:
windows启动:
我们启动redis的时候要输入
redis-server.exe redis.windows.conf
每一次输入这个是很麻烦的,我们可以创建一个创建批处理文件(推荐)
-
在
D:\software\redis2目录下新建一个文本文件(我的是在这个位置,改成自己对应的redis文件位置就可以) -
重命名为
start-redis.bat
linux启动:
在redis篇中有
1.配置信息(java操作redis)
1)导入maven坐标
spring data redis
2)yml文件中配置信息
redis:
host: localhost
port: 6379
password: 123456
database: 10
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。可以通过修改Redis配置文件来指定数据库的数量
3)配置类
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建RedisTemplate对象...");
// 创建RedisTemplate对象
RedisTemplate redisTemplate = new RedisTemplate<>();
// 设置连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 创建redis key 的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
利用配置类将RedisTemplate对象交给ioc容器
// 创建redis key 的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());这里利用的序列化(序列化器的作用是将Java对象转换成字节序列,以便在Redis中存储。同样,反序列化则是将字节序列转换成Java对象。)
如果不设置序列化RedisTemplate 会使用默认的 JdkSerializationRedisSerializer,默认序列化器会将键序列化成二进制格式,在 Redis 中查看时会是乱码(如:\xac\xed\x00\x05t\x00\x03key)
使用 StringRedisSerializer 后,键会保持为可读的字符串形式(如:user:123)
完整的配置应该像这样:
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建RedisTemplate对象...");
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置键的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置Hash键的序列化器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置值的序列化器 - 使用JSON序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 设置Hash值的序列化器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
讲解:
1>Spring Boot 自带的默认序列化
java
// 使用Spring默认配置的RedisTemplate @Autowired private RedisTemplate<String, Object> redisTemplate; // 默认使用JDK序列化,存储的是二进制数据 redisTemplate.opsForValue().set("user:1", userObject);特点:
使用JDK序列化机制
存储的是二进制格式,不可读
在不同语言间不兼容
序列化后的值在redis-cli中看到的是乱码
2>自定义RedisTemplate的序列化
见上文
常用的序列化方案对比
序列化方式 优点 缺点 Jackson2JsonRedisSerializer 可读性好,跨语言 稍慢,存储空间稍大 GenericJackson2JsonRedisSerializer 保留类型信息 存储空间更大 StringRedisSerializer 高效,简单 只能存字符串 JdkSerializationRedisSerializer (默认) Java对象直接序列化 不可读,不跨语言 StringRedisTemplate和自定义序列化的RedisTemplate
@Autowired private StringRedisTemplate stringRedisTemplate; // 需要引入Jackson库 private static final ObjectMapper objectMapper = new ObjectMapper(); public void saveUser() throws JsonProcessingException { User user = new User("李四", 25, "北京"); // 手动将对象序列化为JSON字符串 String userJson = objectMapper.writeValueAsString(user); // 将JSON字符串存入Redis stringRedisTemplate.opsForValue().set("user:200", userJson); } public User getUser() throws JsonProcessingException { // 从Redis获取JSON字符串 String userJson = stringRedisTemplate.opsForValue().get("user:200"); if (userJson != null) { // 手动将JSON字符串反序列化为Java对象 return objectMapper.readValue(userJson, User.class); } return null; }在Spring Data Redis中,
StringRedisTemplate可以理解为是为了简化纯字符串操作而设计的一个特化版本的RedisTemplate。它开箱即用,无需额外配置,在很多场景下都非常方便。为了让你快速抓住核心,这里有一个对比表格:
特性 StringRedisTemplate 自定义序列化的RedisTemplate 序列化方式 所有键值均使用StringRedisSerializer 可灵活配置(如JSON、JDK等) 存储格式 存入和读出的都是可读的字符串 取决于配置(JSON为可读字符串,JDK为二进制) 数据类型支持 仅支持String类型的键和值 支持各种Java对象 数据互通性 与默认 RedisTemplate数据不互通与使用相同序列化方式的客户端互通 适用场景 1. 操作纯字符串
2. 需要人工查看Redis数据
3. 手动序列化/反序列化JSON1. 直接存储Java对象
2. 项目内复杂数据类型交互
为什么它只支持String? 因为它内部使用的是
StringRedisSerializer,这个序列化器只接受String类型的数据,并将其转换为字节数组。如果你试图直接存入一个对象,会报错。
数据为什么不互通? 默认的
RedisTemplate使用JdkSerializationRedisSerializer,它会在键值前加上特殊前缀(如\xac\xed\x00\x05t)。而StringRedisTemplate没有这些。用RedisTemplate存一个键,用StringRedisTemplate是读不出来的,反之亦然序列化过程解析
1. Jackson2JsonRedisSerializer 的作用
java
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);这个serializer的作用是:
将Java对象转换为JSON格式的字节数组
将JSON字节数组转换回Java对象
2. 实际存储内容示例
假设有一个User对象:
java
public class User { private String name; private int age; private String city; // 构造方法、getter/setter省略 }存储过程:
java
User user = new User("张三", 25, "北京"); redisTemplate.opsForValue().set("user:1", user);在Redis中实际存储的值:
json
{ "@class": "com.example.User", "name": "张三", "age": 25, "city": "北京" }Redis图形化界面显示效果
使用 Redis Desktop Manager 或其他工具查看:
Key的显示:
text
user:1 (可读的字符串)Value的显示:
如果工具支持JSON解析: 会显示格式化的JSON
如果工具不支持: 会显示JSON字符串:
text
"{\"@class\":\"com.example.User\",\"name\":\"张三\",\"age\":25,\"city\":\"北京\"}"序列化细节深入
1. 默认包含类型信息
java
// 默认情况下,Jackson2JsonRedisSerializer会添加@class字段 // 这确保了反序列化时能知道原始类型 { "@class": "com.example.User", // 类型信息 "name": "张三", "age": 25, "city": "北京" }传入普通字符串的情况
1. 存储普通字符串示例
java
// 使用配置了Jackson2JsonRedisSerializer的RedisTemplate redisTemplate.opsForValue().set("message", "Hello World");2. 实际存储内容
在Redis中存储的值:
text
"Hello World"注意: 这里存储的是带双引号的JSON字符串,而不是原始字符串!
为什么会有双引号?
序列化过程解析:
java
// 你传入的是:String "Hello World" // Jackson2JsonRedisSerializer处理: // 1. 识别到这是String类型 // 2. 将其序列化为JSON格式的字符串 // 3. 结果: "\"Hello World\"" (JSON字符串表示)不同数据类型的序列化结果对比
传入的数据类型 序列化后的Redis值 说明 "Hello World"(String)"Hello World"带双引号的JSON字符串 123(Integer)123直接存储数字,无引号 true(Boolean)true直接存储布尔值 new User("张三", 25){"@class":"User","name":"张三","age":25}JSON对象 List.of("a", "b")["a","b"]JSON数组 在Redis图形化界面中的显示
存储字符串 "Hello World":
text
Key: message Value: "Hello World" (包含双引号)与StringRedisTemplate的对比:
使用Jackson2JsonRedisSerializer:
java
redisTemplate.opsForValue().set("key1", "hello"); // 存储为: "hello"使用StringRedisTemplate:
java
stringRedisTemplate.opsForValue().set("key2", "hello"); // 存储为: hello与手动序列化的对比
配置的方式(自动序列化):
java
// 直接存储对象,自动序列化为JSON redisTemplate.opsForValue().set("user:1", userObject);手动的方式:
java
// 需要手动转换 ObjectMapper mapper = new ObjectMapper(); String userJson = mapper.writeValueAsString(userObject); stringRedisTemplate.opsForValue().set("user:1", userJson);StringRedisTemplate 存储 "hello" 的结果:
· ✅ 不会转为JSON
· ✅ 存储为原始字符串 "hello"
· ✅ 在Redis界面显示为: hello (没有引号)Jackson2JsonRedisSerializer 存储 "hello" 的结果:
· ✅ 会转为JSON格式
· ✅ 存储为JSON字符串 "\"hello\""
· ✅ 在Redis界面显示为: "hello" (有双引号)StringRedisSerializer 的设计目的:
· 专门为字符串优化
· 保持数据的"原样",不做任何格式转换
· 性能更高,存储空间更小Jackson2JsonRedisSerializer 的设计目的:
· 统一处理所有Java类型
· 保证任何数据类型都能正确序列化为JSON格式
· 字符串在JSON中必须用双引号包围
Q: 序列化后到底是转化为什么东西了?
A: 转换为JSON格式的字符串,但在Redis中存储为字节数组Q: 在Redis图形化界面value显示是json吗?
A: 是的,显示为JSON字符串(如果工具支持JSON解析,会格式化显示;否则显示原始JSON字符串)Q: 这个serializer的作用?
A: 自动在Java对象 ⇄ JSON字符串之间进行转换,让你可以直接操作Java对象,而不用关心序列化细节
默认的 RedisTemplateStringRedisTemplate类型 RedisTemplate<Object, Object>RedisTemplate<String, String>序列化方式 默认 JdkSerializationRedisSerializer(JDK序列化)默认 StringRedisSerializer(字符串序列化)数据存储形式 字节数组(Redis中显示为乱码) 可读的字符串 数据互通性 与 StringRedisTemplate数据不共通与默认 RedisTemplate数据不互通// 注入默认的RedisTemplate @Autowired private RedisTemplate<Object, Object> redisTemplate; // 注入StringRedisTemplate @Autowired private StringRedisTemplate stringRedisTemplat解决决序列化问题
如果你不希望使用默认的JDK序列化,可以考虑以下方法:
直接使用
StringRedisTemplate
如果你的数据主要是字符串,或者你习惯将对象序列化为JSON字符串后存储,这是最直接简单的选择。自定义
RedisTemplate序列化
这是更通用的解决方案。你可以创建一个配置类,自定义RedisTemplate的序列化方式,例如使用JSON序列化:1. 默认情况(自动配置)
java
// Spring Boot自动创建并放入容器的实例 @Bean // Spring Boot自动配置的 public RedisTemplate<Object, Object> redisTemplate() { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用默认的JDK序列化 template.setDefaultSerializer(new JdkSerializationRedisSerializer()); return template; }2. 自定义配置类覆盖
java
@Configuration public class RedisConfig { // 这个方法会覆盖Spring Boot的默认配置 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用自定义的序列化方式 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } }🗂️ IoC容器中的变化
配置前:
text
IoC容器中有: └── redisTemplate (Spring Boot默认创建的) └── 使用JDK序列化配置后:
text
IoC容器中有: └── redisTemplate (你的配置类创建的) └── 使用String + JSON序列化覆盖后功能的变化
是的,完全能执行写入Redis的功能! 覆盖后不仅保留了所有原有功能,只是改变了序列化方式。
功能模块 默认RedisTemplate 自定义RedisTemplate 变化 连接管理 ✅ 有 ✅ 有 不变 命令执行 ✅ 有 ✅ 有 不变 数据操作(opsForValue等) ✅ 有 ✅ 有 不变 事务支持 ✅ 有 ✅ 有 不变 序列化方式 JDK序列化 JSON序列化 改变 自定义配置只是"换零件",不是"换机器"
连接工厂(RedisConnectionFactory):没变,还是原来那个
Redis连接管理:没变,还是原来那套
命令执行逻辑:没变,还是原来那个
序列化器:换了,从JDK序列化换成了JSON序列化
2. redis基础 and Java中操作redis
redis Redis是一个基于内存的 key-value 结构数据库。
基于内存存储,读写性能高
适合存储热点数据(热点商品、资讯、新闻)


1)字符串操作命令

Redis 字符串类型
常用命令:
SET key value 设置指定key的值
GET key 获取指定key的值
SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
SETNX key value 只有在 key 不存在时设置 key 的值
/**
* 操作字符串类型的数据
*/
@Test
public void testString(){
// set get setex setnx
redisTemplate.opsForValue().set("name","小明");
String city = (String) redisTemplate.opsForValue().get("name");
System.out.println(city);
redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES);
redisTemplate.opsForValue().setIfAbsent("lock","1");
redisTemplate.opsForValue().setIfAbsent("lock","2");
}
2). 操作哈希类型数据
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,

常用命令:
HSET key field value 将哈希表 key 中的字段 field 的值设为 value
HGET key field 获取存储在哈希表中指定字段的值
HDEL key field 删除存储在哈希表中的指定字段
HKEYS key获取哈希表中所有字段
HVALS key获取哈希表中所有值
/**
* 操作哈希类型的数据
*/
@Test
public void testHash(){
//hset hget hdel hkeys hvals
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("100","name","tom");
hashOperations.put("100","age","20");
String name = (String) hashOperations.get("100", "name");
System.out.println(name);
Set keys = hashOperations.keys("100");
System.out.println(keys);
List values = hashOperations.values("100");
System.out.println(values);
hashOperations.delete("100","age");
}
3). 操作列表类型数据
Redis 列表是简单的字符串列表,按照插入顺序排序

常用命令:
LPUSH key value1 [value2] 将一个或多个值插入到列表头部
LRANGE key start stop 获取列表指定范围内的元素
RPOP key 移除并获取列表最后一个元素
LLEN key 获取列表长度
/**
* 操作列表类型的数据
*/
@Test
public void testList(){
//lpush lrange rpop llen
ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPushAll("mylist","a","b","c");
listOperations.leftPush("mylist","d");
List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);
listOperations.rightPop("mylist");
Long size = listOperations.size("mylist");
System.out.println(size);
}
4). 操作集合类型数据
Redis set 是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据

常用命令:
SADD key member1 [member2] 向集合添加一个或多个成员
SMEMBERS key 返回集合中的所有成员
SCARD key获取集合的成员数
SINTER key1 [key2]返回所有给定集合的交集
SUNION key1 [key2]返回所有给定集合的并集
SREM key member1 [member2]删除集合中一个或多个成员
/**
* 操作集合类型的数据
*/
@Test
public void testSet(){
//sadd smembers scard sinter sunion srem
SetOperations setOperations = redisTemplate.opsForSet();
setOperations.add("set1","a","b","c","d");
setOperations.add("set2","a","b","x","y");
Set members = setOperations.members("set1");
System.out.println(members);
Long size = setOperations.size("set1");
System.out.println(size);
Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);
Set union = setOperations.union("set1", "set2");
System.out.println(union);
setOperations.remove("set1","a","b");
}
5). 操作有序集合类型数据
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。

常用命令:
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
ZREM key member [member ....] 移除有序集合中的一个或多个成员
/**
* 操作有序集合类型的数据
*/
@Test
public void testZset(){
//zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1","a",10);
zSetOperations.add("zset1","b",12);
zSetOperations.add("zset1","c",9);
Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);
zSetOperations.incrementScore("zset1","c",10);
zSetOperations.remove("zset1","a","b");
}
6). 通用命令操作

通用命令
Redis的通用命令是不分数据类型的,都可以使用的命令!
KEYS pattern 查找所有符合给定模式( pattern)的 key
EXISTS key 检查给定 key 是否存在
TYPE key 返回 key 所储存的值的类型
DEL key 该命令用于在 key 存在是删除 key
/**
* 通用命令操作
*/
@Test
public void testCommon(){
//keys exists type del
Set keys = redisTemplate.keys("*");
System.out.println(keys);
Boolean name = redisTemplate.hasKey("name");
Boolean set1 = redisTemplate.hasKey("set1");
for (Object key : keys) {
DataType type = redisTemplate.type(key);
System.out.println(type.name());
}
redisTemplate.delete("mylist");
}
13.HttpClient/微信登录
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
HttpClient作用:
-
发送HTTP请求
-
接收响应数据
HttpClient应用场景:
当我们在使用扫描支付、查看地图、获取验证码、查看天气等功能时
其实,应用程序本身并未实现这些功能,都是在应用程序里访问提供这些功能的服务,访问这些服务需要发送HTTP请求,并且接收响应数据,可通过HttpClient来实现。
HttpClient的maven坐标:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
HttpClient的核心API:
- HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
- HttpClients:可认为是构建器,可创建HttpClient对象。
- CloseableHttpClient:实现类,实现了HttpClient接口。
- HttpGet:Get方式请求类型。
- HttpPost:Post方式请求类型。
HttpClient发送请求步骤:
- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求
微信登录
https://developers.weixin.qq.com/miniprogram/dev/framework/
步骤:

步骤分析:
- 小程序端,调用wx.login()获取code,就是授权码。
- 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器。
- 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带对应参数。
- 开发者服务端,接收微信接口服务返回的数据。opendId是微信用户的唯一标识。
- 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端。
- 小程序端,收到自定义登录态,存储storage。
- 小程序端,后绪通过wx.request()发起业务请求时,携带token。
- 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
- 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
1)先把需要用到的几个数据封装到了Properties里面(jwt和wechat)
在使用的时候可以直接注入

2)先登录校验,校验通过再下发jwt令牌,和前面emp中实现的下发令牌思路是一样的(利用封装好的jwt工具类)
3)service完善微信登陆功能
通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。
HTTPS 调用
GET https://api.weixin.qq.com/sns/jscode2session
请求参数
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appid | string | 是 | 小程序 appId |
| secret | string | 是 | 小程序 appSecret |
| js_code | string | 是 | 登录时获取的 code,可通过wx.login获取 |
| grant_type | string | 是 | 授权类型,此处只需填写 authorization_code |
返回参数
| 属性 | 类型 | 说明 |
|---|---|---|
| session_key | string | 会话密钥 |
| unionid | string | 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。 |
| errmsg | string | 错误信息,请求失败时返回 |
| openid | string | 用户唯一标识 |
| errcode | int32 | 错误码,请求失败时返回 |


14.Spring Cache技术(缓存)
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 支持的缓存实现
| 缓存实现 | 特点 | 使用场景 |
|---|---|---|
| Redis | 分布式、持久化 | 集群环境、数据持久化 |
| Caffeine | 高性能、内存 | 单机应用、高性能需求 |
| Ehcache | 功能丰富、成熟 | 企业级应用 |
| ConcurrentMap | 简单、内置 | 测试、简单场景 |
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
-
EHCache
-
Caffeine
-
Redis(常用)
起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version>
</dependency>
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
常用注解
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
| 注解 | 说明 | 适用场景 |
|---|---|---|
| @EnableCaching | 开启缓存注解功能,通常加在启动类上 | |
| @Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 | 查询方法,如 findUserById(Long id) |
| @CachePut | 将方法的返回值放到缓存中 | 更新/插入方法,如 updateUser(User user), createUser(User user) |
| @CacheEvict | 将一条或多条数据从缓存中删除 | 删除/更新方法,如 deleteUser(Long id) |
看上去@Cacheable @CachePut两个很像,具体有什么区别呢?
核心区别:目的不同
-
@Cacheable:主要目的是“读取”优化。它的逻辑是:“如果缓存有,就直接返回;如果缓存没有,才执行方法,并把结果放入缓存”。它的侧重点是“查”和“避免重复计算”。 -
@CachePut:主要目的是“更新”缓存。它的逻辑是:“不管缓存有没有,都会执行方法,并且一定会把方法返回的结果放入缓存,覆盖旧值”。它的侧重点是“存”和“强制更新”。
为什么不能只用 @Cacheable?
想象一个典型的场景:更新用户信息。
-
场景:你有一个
updateUser(User user)方法,用于更新数据库中的用户信息。 -
目标:更新数据库后,希望缓存中的旧用户数据也被更新,以保证下次读取时拿到的是最新数据。
如果你错误地使用 @Cacheable 来注解 updateUser 方法:
-
你调用
updateUser时,@Cacheable会先检查缓存中key=user.id是否存在。 -
如果缓存里恰好有这条数据(很可能有),那么
updateUser方法体根本不会执行! 数据库更新操作被跳过,直接返回了缓存里的旧User对象。这会导致严重的业务逻辑错误(数据库没更新,却告诉你更新成功了)。
这就是 @Cacheable 的“先查缓存”机制在“写操作”上带来的灾难性后果。
对于上面的更新场景,应该使用 @CachePut:
-
每次调用
updateUser,方法体都会必然执行,数据库得到更新。 -
方法执行成功后,返回的新User对象会被强制放入缓存,替换掉旧的缓存数据。
-
这样就保证了缓存与数据库的最终一致性。
实际运用:
@CachePut

key的写法如下(常用的是第一个)
#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;
#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;
#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
@Cacheable
一般我们用跟@Cacheput相同的第一个
@CacheEvict
缓存套餐:
(其实只想把用户端的数据存到redis中,因为只有用户端会在短时间内有巨大访问量)存储的是查询出来的每个分类下的套餐)
用户端查询:
在用户端的分类查询套餐上加@Cacheable
不用管返回值,这个注解会自动的把Result<List<Setmeal>>封装到redis中
15.spring task(定时)
Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位:定时任务框架
作用:定时自动执行某段Java代码
spring task有哪些应用场景呢?
1). 信用卡每月还款提醒
2). 银行贷款每月还款提醒
3). 火车票售票系统处理未支付订单
4). 入职纪念日为用户发送通知
其实只要是需要定时处理的场景都可以使用spring task
cron表达式:
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
举例:
2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022
| 秒 | 分钟 | 小时 | 日 | 月 | 周 | 年 |
| 0 | 0 | 9 | 12 | 10 | ? | 2022 |
说明:一般日和周的值不同时设置,其中一个设置,另一个用?表示。
cron表达式在线生成器:https://cron.qqe2.com/
可以直接在这个网站上面,只要根据自己的要求去生成corn表达式即可。所以一般就不用自己去编写这个表达式。
通配符:
* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值;
/ 符号前表示开始时间,符号后表示每次递增的值0!!!!!
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
使用步骤:
1). 导入maven坐标 spring-context(已存在)

2). 启动类添加注解 @EnableScheduling 开启任务调度
3). 自定义定时任务类
16.WebSocket
简单介绍:
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
-
HTTP是短连接
-
WebSocket是长连接
-
HTTP通信是单向的,基于请求响应模式
-
WebSocket支持双向通信
-
HTTP和WebSocket底层都是TCP连接
-

思考:既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?
WebSocket缺点:
服务器长期维护长连接需要一定的成本
各个浏览器支持程度不一
WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
WebSocket应用场景:
1). 视频弹幕
2). 网页聊天
3). 体育实况更新
4). 股票基金报价实时更新
代码开发(代码简单介绍)
前端:(html示例界面)
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
@Component 和 @ServerEndpoint 的作用是不同的:
@Component 的作用
-
将类标记为Spring Bean,由Spring容器管理
-
支持依赖注入(@Autowired等)
-
但不负责WebSocket端点的注册
@ServerEndpoint 的作用
-
定义WebSocket端点路径和配置
-
需要额外的注册机制才能生效
解决方案
您需要 ServerEndpointExporter 来桥接两者:
WebSocketConfiguration.java
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
完整的工作流程
java
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
// 1. @Component 让Spring管理这个Bean
// 2. @ServerEndpoint 定义WebSocket端点
// 3. ServerEndpointExporter 负责将两者关联起来
private static Map<String, Session> sessionMap = new HashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
sessionMap.put(sid, session);
}
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
// 处理消息
}
@OnClose
public void onClose(@PathParam("sid") String sid) {
sessionMap.remove(sid);
}
}
package com.sky.webSocket;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
为什么需要 ServerEndpointExporter?
-
@ServerEndpoint 是Java WebSocket标准注解,Spring默认不扫描它
-
ServerEndpointExporter 专门扫描 @ServerEndpoint 注解
-
它将WebSocket端点注册到Servlet容器中
-
同时确保这些端点能享受Spring的特性(如依赖注入)
17.Apache ECharts(数据可视化)
Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。 官网地址:Apache ECharts
常见效果展示:
1). 柱形图
2). 饼形图
3). 折线图
总结:不管是哪种形式的图形,最本质的东西实际上是数据,它其实是对数据的一种可视化展示。
18.Apache POI
Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。

Apache POI的maven坐标:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.16</version>
</dependency>
代码简单介绍:
将数据写入Excel文件
public class POITest {
/**
* 基于POI向Excel文件写入数据
* @throws Exception
*/
public static void write() throws Exception{
//在内存中创建一个Excel文件对象
XSSFWorkbook excel = new XSSFWorkbook();
//创建Sheet页
XSSFSheet sheet = excel.createSheet("itcast");
//在Sheet页中创建行,0表示第1行
XSSFRow row1 = sheet.createRow(0);
//创建单元格并在单元格中设置值,单元格编号也是从0开始,1表示第2个单元格
row1.createCell(1).setCellValue("姓名");
row1.createCell(2).setCellValue("城市");
XSSFRow row2 = sheet.createRow(1);
row2.createCell(1).setCellValue("张三");
row2.createCell(2).setCellValue("北京");
XSSFRow row3 = sheet.createRow(2);
row3.createCell(1).setCellValue("李四");
row3.createCell(2).setCellValue("上海");
FileOutputStream out = new FileOutputStream(new File("D:\\itcast.xlsx"));
//通过输出流将内存中的Excel文件写入到磁盘上
excel.write(out);
//关闭资源
out.flush();
out.close();
excel.close();
}
public static void main(String[] args) throws Exception {
write();
}
}

读取Excel文件中的数据
public class POITest {
/**
* 基于POI读取Excel文件
* @throws Exception
*/
public static void read() throws Exception{
FileInputStream in = new FileInputStream(new File("D:\\itcast.xlsx"));
//通过输入流读取指定的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//获取Excel文件的第1个Sheet页
XSSFSheet sheet = excel.getSheetAt(0);
//获取Sheet页中的最后一行的行号
int lastRowNum = sheet.getLastRowNum();
for (int i = 0; i <= lastRowNum; i++) {
//获取Sheet页中的行
XSSFRow titleRow = sheet.getRow(i);
//获取行的第2个单元格
XSSFCell cell1 = titleRow.getCell(1);
//获取单元格中的文本内容
String cellValue1 = cell1.getStringCellValue();
//获取行的第3个单元格
XSSFCell cell2 = titleRow.getCell(2);
//获取单元格中的文本内容
String cellValue2 = cell2.getStringCellValue();
System.out.println(cellValue1 + " " +cellValue2);
}
//关闭资源
in.close();
excel.close();
}
public static void main(String[] args) throws Exception {
read();
}
}

19.总结
本文到此结束,苍穹外卖也要告一段落了!
更多推荐




所有评论(0)