前言:

这篇文章是我对苍穹外卖的大总结哦,也是我未来的复习资料

目录

1.项目简单介绍:

2.Nginx反向代理

3. Swagger,knife4j与Apifox

4.项目架构思想

5.全局异常处理器

6.日志记录

7.http协议

8.jwt令牌/过滤器/拦截器/ThreadLocal

 9.AOP思想

10.文件上传

11.map传参数 

12.redis

13.HttpClient/微信登录

14.Spring Cache技术(缓存)

15.spring task(定时)

16.WebSocket

17.Apache ECharts(数据可视化)

18.Apache POI 


1.项目简单介绍:

本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台 和 小程序端应用 两部分。

系统管理后台:主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。

这个只是一个商家的操作系统,但是用户端可以好多人登录

小程序端:主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。

技术选型:

2.Nginx反向代理

  • 正向代理:代理客户端

    • 你(客户端)通过一个代理服务器去访问谷歌(目标服务器)。谷歌只知道代理服务器访问了它,而不知道你的存在。

    • 作用: 为客户端服务,用于突破访问限制(如翻墙)、隐藏客户端身份、缓存等。

    • 比喻: 就像一个租房中介,你(租客)通过中介去联系房东,房东不知道你的真实身份,只和中介打交道。

  • 反向代理:代理服务端

    • 你(客户端)访问 example.com,这个域名背后实际上是一个 Nginx 服务器(反向代理)。Nginx 接收你的请求后,悄悄地将请求转发给背后真正处理请求的应用服务器(比如 Node.js, Java, Python 应用),然后将应用服务器的响应返回给你。

    • 作用: 为服务端服务,用于负载均衡、安全、缓存静态内容、SSL 终结等。

    • 比喻: 就像一个大公司的前台/总机。你打电话给公司总机,总机根据你的需求(比如按分机号)将电话转接到相应的部门(内部服务器)。你不需要知道具体是哪个员工接的电话。

核心配置详解:

  1. 端口(Port)是什么?

    • 端口就像是服务器的"门牌号",用于区分不同的网络服务。

    • 一台服务器可以提供多种服务(如网站、邮件、FTP等),每个服务使用不同的端口。

  2. 80 端口的特殊意义:

    • 80 端口是 HTTP 协议的默认端口

    • 当你在浏览器输入 http://example.com 时,浏览器实际上访问的是 http://example.com:80(省略了 :80,因为它是默认的)。

  3. 实际应用场景:

    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);
            }
    
        }
    
    }

    核心机制:

    1. @ControllerAdvice:

      • 这是一个组件注解,它的作用是拦截所有被 @Controller 和 @RestController 注解的控制器中抛出的异常。

      • 它将类标记为一个全局的、跨多个控制器的异常处理器、数据绑定处理器等。你可以把它想象成一个“全局的切面(AOP)”,专门用于处理控制器层面的问题。

    2. @ResponseBody:

      • 这个注解告诉 Spring,该方法的返回值应该直接写入 HTTP 响应体(Response Body)中,而不是被解析为一个视图名称或进行跳转。

      • Spring 会使用内置的 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)将返回值(通常是对象或集合)序列化成 JSON/XML 等格式。

    3. @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类的全限定名。

    它们完全不同,但又有密切关系。让我用比喻来解释:

    生动的比喻:汽车制造厂

    想象一个汽车制造厂:

    1. class Car { ... } - 这就像汽车的设计图纸

    • 它定义了汽车应该有什么属性(颜色、型号)和方法(启动、加速)

    • 它只是一个蓝图,不是实际存在的物体

    1. Car myCar = new Car(); - 这就像根据图纸制造出一辆真实的汽车

    • myCar是一个具体的、可以使用的对象实例

    1. Class<?> carClass = Car.class; - 这就像图纸的档案管理员

    • 这个管理员不是图纸本身,也不是汽车

    • 但他知道关于这张图纸的一切信息:能造什么车、有什么功能、需要什么材料

    每一个被jvm加载的类都有一个class对象

    概念 含义 比喻
    类 (class) 代码中定义的模板 汽车的设计图纸
    Class 对象 JVM生成的,用于表示类元数据的对象 图纸的管理员/档案
    获取 Class 对象 获取类的元数据信息 找到这个管理员
    实例对象 (Instance) 根据类模板创建的具体对象 根据图纸制造的实实在在的汽车
    创建实例对象 通过 Class 对象的方法(newInstanceConstructor) 命令管理员根据图纸造一辆新车
    特性 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实现了:

    1. 基本字段映射:将数据库列映射到Java对象属性

    2. 一对多关系映射:将员工的工作经历数据组装到List集合中

    3. 避免字段冲突:通过别名区分不同表的同名字段

    4. 完整的对象组装:一次查询即可获取员工及其所有工作经历信息

    这是一种典型的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

    每一次输入这个是很麻烦的,我们可以创建一个创建批处理文件(推荐)

    1. 在 D:\software\redis2 目录下新建一个文本文件(我的是在这个位置,改成自己对应的redis文件位置就可以)

    2. 重命名为 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. 手动序列化/反序列化JSON
    1. 直接存储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对象,而不用关心序列化细节

    默认的 RedisTemplate StringRedisTemplate
    类型 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序列化,可以考虑以下方法:

    1. 直接使用 StringRedisTemplate
      如果你的数据主要是字符串,或者你习惯将对象序列化为JSON字符串后存储,这是最直接简单的选择。

    2. 自定义 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/

    步骤:

    步骤分析:

    1. 小程序端,调用wx.login()获取code,就是授权码。
    2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器。
    3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带对应参数。
    4. 开发者服务端,接收微信接口服务返回的数据。opendId是微信用户的唯一标识。
    5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端。
    6. 小程序端,收到自定义登录态,存储storage。
    7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
    8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
    9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

    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

    想象一个典型的场景:更新用户信息

    1. 场景:你有一个 updateUser(User user) 方法,用于更新数据库中的用户信息。

    2. 目标:更新数据库后,希望缓存中的旧用户数据也被更新,以保证下次读取时拿到的是最新数据。

    如果你错误地使用 @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.总结

    本文到此结束,苍穹外卖也要告一段落了!

    Logo

    中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

    更多推荐