框架

Java后端详解-总体篇(包含除框架外大部分内容,例如数据库、集合、java基础、JUC、JVM等)

Spring生态

场景 项目名称 一句话职责 典型依赖(starter)
应用骨架 Spring Boot 免 XML、内嵌容器、一键启动 spring-boot-starter
Web Spring MVC REST/ MVC 接口、文件上传、内容协商 spring-boot-starter-web
数据访问 Spring Data JPA 一键 DAO,几乎零 SQL spring-boot-starter-data-jpa
数据访问 Spring Data Redis 模板+注解缓存,哨群集成都好使 spring-boot-starter-data-redis
数据访问 Spring Data MongoDB 文档数据库同步、异步 Repositories spring-boot-starter-data-mongodb
消息 Spring Kafka 生产/消费/流式处理,offset 自动提交 spring-kafka
消息 Spring AMQP (RabbitMQ) 可靠路由、死信、延迟队列 spring-boot-starter-amqp
安全 Spring Security 登录认证、JWT、OAuth2、方法级授权 spring-boot-starter-security
网关 Spring Cloud Gateway 统一路由、限流、灰度、跨域 spring-cloud-starter-gateway
注册中心 Spring Cloud Netflix Eureka 服务注册与发现(也可 Nacos/Consul) spring-cloud-starter-netflix-eureka-server/client
调用 Spring Cloud OpenFeign 声明式 HTTP 客户端,像调本地方法 spring-cloud-starter-openfeign
熔断 Spring Cloud Circuit Breaker (Resilience4j) 失败率阈值、舱壁、重试、降级 spring-cloud-starter-circuitbreaker-resilience4j
配置中心 Spring Cloud Config 集中管理 yml/properties,动态刷新 spring-cloud-config-server / config-client
链路追踪 Spring Cloud Sleuth → Micrometer Tracing 自动生成 TraceId,对接 Zipkin/Jaeger micrometer-tracing-bridge-otel
任务 Spring Batch 海量数据批处理,分片、chunk、重试 spring-boot-starter-batch
集成 Spring Integration 企业级 EIP(路由、拆分、聚合) spring-integration-core
测试 Spring Boot Test 切片测试、MockMVC、@DataJpaTest spring-boot-starter-test
响应式 Spring WebFlux Netty 驱动,Reactive Streams 背压 spring-boot-starter-webflux
响应式数据 Spring Data R2DBC 响应式关系型数据库 spring-boot-starter-data-r2dbc
服务器推送 Spring GraphQL 类型安全的一站式 GraphQL 框架 spring-boot-starter-graphql

Spring

基本概念

Spring 是 轻量级、一站式 Java 后端“底座”,核心理念:IOC(控制反转)+ AOP(面向切面),帮助管理对象、解耦业务、屏蔽样板代码

IOC

概念

IOC:Inversion of Control:控制反转

作用:创建和管理对象的依赖关系,降低耦合、解除代码间的依赖关系

在Spring项目中,各个类需要的对象不再通过new 对象获取,而是通过将需要生成对象的类的信息交给Spring容器,后者根据信息创建对象。各个类需要对象时直接通过依赖注入(DI)即可直接获得Spring创建的对象

Spring容器是IOC容器的具体实现

IOC容器是抽象概念,Spring容器基于IOC思想实现

IOC思想基于IOC容器完成,IOC容器底层即对象工厂

IOC实现思路:

  1. 创建工厂,读取xml配置文件

    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    
  2. 解析器解析xml文件,获取xml文件中的id以及class属性值

    <bean id="userDao" class="com.example.dao.UserDaoImpl"/>
    
  3. 使用反射机制,获取对象

    Class clz = Class.forName("class属性对应的全路径");
    Object obj = clz.newInstance();
    
  4. 准备IOC容器,存放对象

    public static Map<String,Object> map = new HashMap<>();
    map.put("id属性对应的值",obj);
    

IOC容器核心接口

  1. BeanFactory
  2. ApplicationContext

IOC配置方法

方式 示例 特点
XML 配置 <bean id="userDao" class="..."> 老项目常用,集中配置
注解配置 @Component, @Autowired 简洁,现代主流
Java 配置 @Configuration, @Bean 类型安全,可重构,推荐新项目

后两种常用在Springboot中,xml配置属于Spring早版本使用的原始方法

基于原始xml

xml中定义bean标签,后者配置对象信息交给Spring管理

默认情况下,Spring容器调用类的无参构造创建对象

<beans ...>
    <!-- 1. 声明 bean -->
    <bean id="userDao" class="com.example.dao.UserDaoImpl"/>

    <!-- 2. 依赖注入 -->
    <bean id="userService" class="com.example.service.UserService">
        <constructor-arg ref="userDao"/>   <!-- 构造器注入 -->
    </bean>
</beans>
基于注解
@Repository          // 等价于 <bean id="userDao" ...>
public class UserDaoImpl implements UserDao { }

@Service             // 等价于 <bean id="userService" ...>
public class UserService {
    private final UserDao userDao;

    @Autowired       // 等价于 <constructor-arg ref="userDao"/>
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

如果是这种方式,则需要先定义扫描包两种方式

  1. 注解扫描

    @ComponentScan

  2. 手动扫描

    ApplicationContext ctx =
    new AnnotationConfigApplicationContext(“com.example”);

基于配置类
@Configuration
public class AppConfig {
    @Bean
    public UserDao userDao() {          // 方法名即 beanName
        return new UserDaoImpl();
    }

    @Bean
    public UserService userService() {  // 显式组装依赖
        return new UserService(userDao());
    }
}

通过上三种方式将对象生成好了在容器中,我们可以通过以下方式获取指定对象

ApplicationContext ctx =
        new ClassPathXmlApplicationContext("beans.xml");
UserService us = ctx.getBean("userService", UserService.class);

//前者对应xml方式,后者对应注解和配置类中方式

ApplicationContext ctx =
        new AnnotationConfigApplicationContext(AppConfig.class);
UserService us = ctx.getBean(UserService.class);
DI

依赖注入,将容器中的对象拿出来

@Autowaired

AOP

概念

Spring AOP 让你在不修改源代码的前提下,给程序统一添加功能(如日志、事务、权限控制)

概念 说明
Aspect(切面) 横切关注点的模块化,比如日志切面、事务切面。
Join Point(连接点) 程序执行过程中的某个点,比如方法调用、异常抛出。Spring 只支持方法级别的连接点
Advice(通知) 切面在连接点上执行的动作,比如“方法执行前”或“方法执行后”要做的事。
Pointcut(切点) 匹配连接点的表达式,比如“所有 service 包下的方法”。
Weaving(织入) 把切面应用到目标对象的过程。Spring 使用运行时织入(通过代理)。
Target(目标对象) 被代理的对象。
Proxy(代理对象) Spring 生成的代理类,负责调用目标方法和切面逻辑。

定义通知的注解

类型 注解 执行时机
前置通知 @Before 方法执行前
后置通知 @After 方法执行后(无论是否异常)
返回通知 @AfterReturning 方法成功返回
异常通知 @AfterThrowing 方法抛出异常
环绕通知 @Around 方法执行前后都可控制(最强大)
基于注解
@Aspect
@Component
public class PerformanceAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行目标方法
        long duration = System.currentTimeMillis() - start;
        System.out.println(joinPoint.getSignature() + " executed in " + duration + "ms");
        return result;
    }
}

需要注意:

  1. 切面类要定义为Spring Bean,用@Component
  2. Spring是运行时织入,AspectJ是编译时织入,后者功能更强

八股

Spring Bean生命周期

四个阶段:

  1. 实例化 Instantiation

    创建 Bean 的实例

  2. 属性赋值 Populate Properties

    为 Bean 的属性设置值(依赖注入)

    这里的依赖注入包括注入普通的属性,也包括Bean的相互依赖注入,这也就涉及到循环依赖问题

  3. 初始化 Initialization

    步骤 描述
    1. BeanNameAware 如果实现了 BeanNameAware,调用 setBeanName()
    2. BeanFactoryAware 如果实现了 BeanFactoryAware,调用 setBeanFactory()
    3. ApplicationContextAware 如果实现了 ApplicationContextAware,调用 setApplicationContext()
    4. BeanPostProcessor(前置处理) 调用所有 BeanPostProcessor.postProcessBeforeInitialization()
    5. @PostConstruct / InitializingBean 先执行 @PostConstruct,再执行 afterPropertiesSet()
    6. 自定义 init-method 如果配置了 init-method,调用它
    7. BeanPostProcessor(后置处理) 调用所有 BeanPostProcessor.postProcessAfterInitialization()
  4. 销毁 Destruction

    容器关闭时

    步骤 描述
    @PreDestroy 先执行 @PreDestroy
    DisposableBean 再执行 destroy()
    自定义 destroy-method 最后调用配置的 destroy-method
  • BeanPostProcessor 是 Spring 提供的最强大扩展点之一,AOP 就是靠它实现的。
  • @PostConstruct@PreDestroy 是 JSR-250 注解,推荐优先使用。
  • init-methoddestroy-method 是 XML 或 @Bean 的配置方式,适合无源码的类。
循环依赖

通过三级缓存解决

SpringMVC

基本概念

SpringMVC 是 Spring 框架的一个子模块,全称是 Spring Web MVC,用于构建基于 MVC 设计模式的 Web 应用

执行流程

  1. 请求经过tomcat等servlet容器后到达 DispatcherServlet
  2. DispatcherServlet 找 HandlerMapping 拿 HandlerExecutionChain( Handler + 拦截器列表)
  3. 找 HandlerAdapter
  4. 执行拦截器 preHandle
  5. HandlerAdapter 调用 Controller 方法、拿到 ModelAndView(或 JSON,用 @ResponseBody 时直接写响应)
  6. 执行拦截器 postHandle
  7. 视图解析器 ViewResolver 把逻辑视图解析成真实视图(JSP、Thymeleaf 等)并渲染(或消息转换器直接把 JSON 写回)
  8. 执行拦截器 afterCompletion(无论是否异常都会进)
组件 职责
DispatcherServlet 统一入口,把请求分发给“映射器-适配器-视图解析器”整条链。
HandlerMapping 根据 URL、method、headers 找对应的处理器(Controller 方法)。默认实现 RequestMappingHandlerMapping。
HandlerAdapter 让 DispatcherServlet 不依赖具体类型就能“调” Controller。常用 RequestMappingHandlerAdapter。
HandlerInterceptor 3 个回调:preHandle(前置)、postHandle(后置)、afterCompletion(完成)。
ModelAndView 老版本容器:装模型数据 + 逻辑视图名;@RestController 时代退居二线。
ViewResolver 把“user/list”解析成 /WEB-INF/jsp/user/list.jsp。
HttpMessageConverter 负责 @ResponseBody / @RequestBody 的 JSON、XML、Protobuf 等序列化/反序列化。

拦截器 vs 过滤器
过滤器在 Servlet 层,依赖 Servlet 容器;拦截器在 Spring 容器内,能访问 Model、Handler、异常等,粒度更细。后续springboot模块中会讲拦截器

Servlet 容器(Tomcat) → 自定义 Filter(Servlet 层) → DispatcherServlet(SpringMVC 层) → Controller

请求到后端的执行流程

浏览器
│ DNS解析 + TCP三次握手,HTTP报文解析成若干TCP段

操作系统协议栈
│ 接收TCP段,重组,放到Tomcat监听端口的对应Socket的接收缓冲区

Tomcat Connector(Poller)
│ 把数据读进ByteBuffer,交给Http11Processor,servlet容器的开始

Http11Processor
│ 按HTTP协议切出请求行、头、体,封装成标准的HttpServletRequest

Mapper → 找到对应 Web 应用(jar包)


ApplicationFilterChain
│ 执行过滤器链

DispatcherServlet(SpringMVC 入口)

SpringBoot

本篇重点

若需要直接在IDEA中创建SpringBoot项目,则需要:

IDEA2020.3版本以后

安装了Spring Boot Assistant插件

基本概念

spring+web -> springmvc简化升级 -> springboot

springboot天然支持springcloud,可用于构建微服务架构

概念 说明
自动配置(Auto Configuration) 根据项目依赖自动配置 Spring 应用,减少手动配置。例如:引入了 spring-boot-starter-web,就会自动配置 Tomcat、Spring MVC 等。
起步依赖(Starter Dependencies) 一系列预定义的依赖组合,简化 Maven/Gradle 配置。例如:spring-boot-starter-data-jpa 就包含了 JPA、Hibernate、Spring Data JPA 等。
内嵌服务器(Embedded Server) 默认内嵌 Tomcat(也支持 Jetty、Undertow),无需部署 WAR 文件到外部服务器,直接运行 main() 方法即可启动。
Spring Boot Starter 一种依赖封装方式,命名规则为 spring-boot-starter-*,如 spring-boot-starter-securityspring-boot-starter-test 等。
Spring Boot Application 使用 @SpringBootApplication 注解标注的主类,是 Spring Boot 应用的入口。它整合了 @Configuration@EnableAutoConfiguration@ComponentScan
外部化配置(Externalized Configuration) 支持通过 application.propertiesapplication.yml 文件配置应用参数,支持多环境配置(如 application-dev.yml)。
Spring Boot Actuator 提供生产级监控和管理功能,如健康检查、指标收集、线程 dump、环境信息等。

八股

自动装配原理

启动类的@SpringBootApplication注解中包含了@EnableAutoConfiguration,后者包含了@Import注解引入了AutoConfigurationImportSelector选择器,选择器会读取 META-INF/spring/…imports 文件并加载所有自动配置类,该文件列出了所有自动配置类的全限定名。自动配置类中会包含各种条件注解用于判断是否生效(@Condition等),当符合条件时,就会注册Bean加入到Spring容器中。如果Bean中需要额外配置,则会通过属性配置等注解从配置文件中获取值注入到Bean中

Spring Boot 自动装配 = 条件注解 + 配置类 + classpath 依赖 + 属性绑定

ThreadLocal

线程私有

在springboot中,一个请求对应一个线程,ThreadLocal让我们可以将这次请求经常需要的数据保存起来,充当一个全局数据的作用

例如后续的token,我们解析token后拿到用户的基本信息,即可将信息保存在一个ThreadLcoal中,后续不管是Controller还是service等,只要是本次请求中的,我们都可以直接从ThreadLcoal中获取

但ThreadLocal需要注意内存问题

涉及到强弱引用、ThreadLocal底层存储结构等问题

简单来说,如果是我们手动创建使用ThreadLocal,使用后如果没有手动清除,就容易出现内存问题

而后续讲解的SpringSecurity中自带一个ThreadLocal,更安全

创建项目

请添加图片描述

这里sdk对应jdk版本

java对应java语言版本

选择依赖:

请添加图片描述

其中Spring Boot DevTools可用于热部署

请添加图片描述

其中Spring Session用于改变原本会话保存在服务器内存,将会话保存到数据库中

IDEA配置

基础配置
  1. 配置JDK

    既可以自己下载jdk后添加到idea,也可以直接在idea中下载指定版本的jdk

    请添加图片描述

  2. 配置maven

    先下载maven,再将路径填写到idea如下位置

    请添加图片描述

  3. 配置编码

    如下图,基本都改为UTF-8即可

    请添加图片描述

  4. 配置数据库语言

    请添加图片描述

  5. 快捷键

IDEA快捷键

编辑

功能 Win/Linux
万能搜索类/文件/符号 两下 Shift
智能补全 Ctrl+Space
类型补全(new 后) Ctrl+Shift+Space
快速修复/生成 Alt+Enter
自动生成代码(getter、构造器等) Alt+Insert
复制整行 Ctrl+D
删除整行 Ctrl+Y
移动语句 Ctrl+Shift+↑/↓
环绕(if/try/while…) Ctrl+Alt+T
格式化代码 Ctrl+Alt+L

导航定位

功能 Win/Linux
跳到定义 Ctrl+BCtrl+Click
查看实现类 Ctrl+Alt+B
弹出用法列表 Ctrl+Alt+F7
返回/前进 Ctrl+Alt+←/→
最近文件 Ctrl+E
结构弹窗(大纲) Ctrl+F12

运行调试

功能 Win/Linux
运行 Shift+F10
调试 Shift+F9
断点/取消 Ctrl+F8
单步进入 F7
单步跳过 F8
运行到光标 Alt+F9

重构

功能 Win/Linux
重命名变量/类/文件 Shift+F6
抽取方法 Ctrl+Alt+M
抽取变量 Ctrl+Alt+V
内联 Ctrl+Alt+N
  1. 个性化配置
个性化配置
  • 下图位置可以修改代码字体风格

    请添加图片描述

  • 下图位置可以修改代码中绝大部分内容的颜色

    请添加图片描述

项目基础配置

pom.xml基础配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath/>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>Test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- 2. 核心基础依赖:自动配置、日志等 -->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 3. Web开发依赖:Spring MVC + 嵌入式Tomcat -->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 4. 测试依赖:JUnit5 + Mockito-->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope> <!-- 仅测试环境生效 -->
        </dependency>

    </dependencies>

    <properties>
        <java.version>17</java.version>
    </properties>

</project>

启动类基本配置

@SpringBootApplication
public class SlcApplication {
    public static void main(String[] args) {
        SpringApplication.run(SlcApplication.class,args);
    }
}

yml配置文件

server:
	port: 8080

层次结构

在完成上述项目基础配置后,即可搭建以下层次结构,实现最基本的后端结构

三层:Controller、Service、Mapper

对应:接口、业务、数据

Controller

表现层

作用:

  1. 接收前端参数(JSON / 表单 / 文件 / 路径 / 查询串)
  2. 调用 Service 做业务
  3. 把结果包成统一响应格式并还给前端
基本使用
@RestController                 // @Controller + @ResponseBody
@RequestMapping("/api/user")    // 类前缀
public class UserController {

    @Autowired	//@Resource
    private UserService userService;   // 3. 调用业务

    /* ----- 2. 各种参数接收方式 ----- */
    @GetMapping("/{id}")                    // 路径参数
    public R<UserVo> detail(@PathVariable Long id) {
        return R.ok(userService.find(id));
    }

    @GetMapping("/list")                    // 查询串 ?page=1&size=10
    public R<List<UserVo>> list(PageQo qo) { // 对象里含 page/size
        return R.ok(userService.list(qo));
    }

    @PostMapping                           // JSON 体 {"name":"Tom"}
    public R<Long> create(@Valid @RequestBody UserDto dto) {
        return R.ok(userService.create(dto));
    }

    @PostMapping("/upload")                // 文件上传
    public R<String> upload(@RequestPart("file") MultipartFile file) throws IOException {
        String url = fileService.upload(file);
        return R.ok(url);
    }

    @DeleteMapping("/{id}")                // 删除
    public R<Void> delete(@PathVariable Long id) {
        userService.remove(id);
        return R.ok();
    }
}
常用注解
注解 作用 / 场景说明
@RestController 组合注解 = @Controller + @ResponseBody;整个类所有方法直接返回 JSON,不再走视图解析器。
@Controller 标记为 Spring Bean 且为控制器;若返回页面,需配合模板引擎(Thymeleaf、FreeMarker)。
@ResponseBody 可标注方法 / 类;把返回值序列化后写入 HTTP Body,常和 @Controller 搭配单独控制 JSON。
@RequestMapping 通用路径映射,可指定 method = GET / POST 等;细粒度推荐 Get / Post / Put / DeleteMapping。
@GetMapping 等价于 @RequestMapping(method = GET);查询 / 读取资源。
@PostMapping 等价于 @RequestMapping(method = POST);新增 / 提交资源。
@PutMapping 等价于 @RequestMapping(method = PUT);全量更新。
@DeleteMapping 等价于 @RequestMapping(method = DELETE);删除资源。
@PathVariable 取 REST 风格路径参数,如 /user/{id}
@RequestParam 取查询字符串或表单字段;可设 required = false / defaultValue
@RequestBody 把完整 JSON / XML 报文体反序列化为 Java 对象;仅用于 POST / PUT 且 Content-Type = application/json
@RequestPart 接收 multipart/form-data 里的单项(文件或复杂字段);常用于文件上传。
@Valid 开启 JSR-303 参数校验;需配合 DTO 内嵌 @NotNull 等注解。
@Validated Spring 增强版 @Valid;支持分组校验、方法级校验。
@ResponseStatus 自定义 HTTP 响应状态码;如 @ResponseStatus(HttpStatus.CREATED) 返回 201。

其次还有@Autowired和@Resource,这两个注解都可以实现自动注入

但前者由spring提供,后者由javaEE提供

前者优先根据类型注入,容器中有多个同类型对象则根据名称注入,后者优先根据名称注入

前者可以配合@Qualifier指定名称

前者有required属性,后者有name属性

需要注意@RequestParam和@RequestVariable的区别

注解 拼在 URL 哪一段 参数长什么样 例子 典型场景
@RequestParam 查询串(? 之后) 键=值,用 & 分隔 /user?id=1&name=Tom 传统 GET 表单、搜索过滤
@PathVariable 路径段(/ 之后) 无键名,就是一段 /user/1/Tom RESTful 风格资源定位
Rest风格

表现层中定义方法的约定,例如下图,将"/users"放在@RequestMapper中,后续方法上基本不用再加路径

操作 HTTP 方法 资源路径 语义
查询全部 GET /users 读取集合
查询单个 GET /users/{id} 读取单个资源
新增 POST /users 在集合下新增资源
全量更新 PUT /users/{id} 替换整个资源
部分更新 PATCH /users/{id} 只改部分字段
删除 DELETE /users/{id} 移除资源
Service

业务层

分接口和实现类,在接口中定义业务方法,每个接口可能有多个实现类,即业务方法可能有多种实现方法,便于修改。

表现层调用业务层,业务层调持久层

service层结构主要是

请添加图片描述

表现层中注入service层接口,接口对应多个实现类,用于后续可以方便切换service的具体业务实现,在有多个实现类时,一定要手动指定选择哪个实现类,可通过:

  1. 名称匹配
  2. @Qualifier显示指定
  3. 实现类上@Primary,表示默认使用实现类
Mapper

持久层

MyBatis 是 Java 的 DAO 框架之一
Mapper 层是“拿 MyBatis 当实现”的 DAO 层,并非“调用 MyBatis”,Mapper层本身就是 DAO

以下是mapper层示例

//    @Select("select * from user " +
//            "where id in " +
//            "(select followed_person_id from follow_person where follow_person_id = #{id})")
//    List<User> getFollowedPerson(Integer id);

    //优化sql结果:
    @Select("SELECT u.* " +
            "FROM user u " +
            "JOIN follow_person f ON u.id = f.followed_person_id " +
            "WHERE f.follow_person_id = #{id}")
    List<User> getFollowedPerson(@Param("id") Integer id);

@Param可以确保使用多个参数,单参数可以不加

Maven

在前面基础配置中说明了maven在idea中的配置位置

定义:Apache Maven 是一个基于项目对象模型(POM)的构建和依赖管理工具

功能:写好 pom.xml,Maven 帮你下载 jar、编译、测试、打包、部署

概念:

  1. pom:首先明确pom.xml即项目的说明书,描述了 坐标、依赖、插件、构建规则
  2. GAV:坐标,每个坐标对应一个jar
  3. maven生命周期:validate → compile → test → package → verify → install → deploy
  4. Plugin:插件,Maven只负责调度插件,具体功能由Maven中自带的各种插件完成

总结:通过编写pom.xml,将下载jar包并拷入lib等交给maven。

关于使用maven中的生命周期来打包生成jar,后续在SpringBoot/部署中再说

请添加图片描述

POM详解
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <!-- 1. 当前 POM 模型版本(固定 4.0.0) -->
  <modelVersion>4.0.0</modelVersion>

  <!-- 2. 项目坐标 = 身份证号 -->
  <groupId>com.example</groupId>      <!-- 组织/公司域名倒写 -->
  <artifactId>demo-boot</artifactId>  <!-- 模块/项目名 -->
  <version>1.0.0-SNAPSHOT</version> <!-- 版本号(SNAPSHOT=开发版) -->
  <packaging>jar</packaging>          <!-- 打什么包:jar / war / pom -->

  <!-- 3. 统一属性:一处定义,到处引用 -->
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>3.4.4</spring-boot.version>
  </properties>

  <!-- 4. 依赖管理:只声明版本,不实际引入 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <!-- 5. 真正要用的 jar -->
  <dependencies>
    <!-- web 场景 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 测试 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <!-- 6. 插件列表 -->
  <build>
    <plugins>
      <!-- 编译插件:指定 Java17 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
        <configuration>
          <source>17</source>
          <target>17</target>
        </configuration>
      </plugin>

      <!-- Spring Boot 打包插件:打 fat-jar -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>   <!-- 把普通 jar 重打成可执行 jar -->
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
区块 关键标签 说明
坐标 groupId / artifactId / version 唯一定位一个 jar,被其他项目引用时就是 GAV
属性 properties 统一版本号/编码,避免硬编码,支持 ${} 占位
依赖管理 dependencyManagement 只锁版本,不导包;子模块或当前 <dependencies> 再写一次即可省略 version
依赖 dependencies 真正要用的 jar;scope 控制生命周期阶段
插件 build → plugins 绑定到生命周期阶段,完成编译、测试、打包、部署等具体活
scope 生效阶段 例子
compile 全阶段(默认) spring-core
provided 编译+测试,运行由容器提供 servlet-api
runtime 运行/测试才用 jdbc 驱动
test 仅测试 junit
system 本地路径 jar(已废弃,别用)

Json

后端响应给前端的数据通常都是Json格式

基本概念

JSON(JavaScript Object Notation)一种轻量级、跨语言、纯文本 的数据交换格式。

Json基本格式:

{
  "id": 1,
  "name": "Alice",
  "skills": ["Java", "Python"],
  "boss": null,
  "dept": {
    "no": 100,
    "name": "研发部"
  }
}
SpringBoot响应Json
基本概念

和后续会讲到的日志差不多,Springboot自己引入了Json相关依赖

当我们引入spring-boot-starter-test依赖

请添加图片描述

其内部引入

请添加图片描述

其内部又引入

请添加图片描述

总结:只要我们引入了web依赖,jackson就会自带

jackson即SpringBoot默认使用的json解析框架

除了jackson,还会有fastJson等替代框架,如果要替换默认,引入fastJson依赖并将fastJson注册成HTTP消息转换器代替jackson即可

不同数据类型返回的Json格式

在SpringBoot项目中只需要在Controller类上添加@ResponseBody或者@RestController

即可自动将Controller中方法返回的类型转为Json格式返回给前端

@RestController
@Slf4j
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        log.info("访问了 /hello 接口");
        return "Hello SLF4J";	//这里的返回值决定了前端获取到的数据
    }
}

因此我们在定义Controller时,需要考虑好方法的返回值,方便前端使用

这就和下一部分的数据封装有关系了。

下面说明当我们在Controller方法中返回各种类型的数据,前端会接收到什么

  1. 基本类型

    Java 返回值 HTTP 响应体(JSON) 说明
    return 1; 1 不是 { "value" : 1 },就是裸数字
    return true; true 裸布尔
    return "abc"; "abc" 裸字符串,带双引号
  2. 对象、集合、Map

    Java 返回值 JSON
    User {id=1, name="Tom"} {"id":1,"name":"Tom"}
    List<User> [{"id":1,"name":"Tom"},{"id":2,"name":"Lucy"}]
    Map<String,Object> {"k1":"v1","k2":123} Map 直接变对象
    Set<String> ["a","b","c"] 集合统一变数组
  3. 特殊类型

    Java 返回值 JSON 说明
    Optional<User> {"id":1,"name":"Tom"}null 非空就拆包,空就是 null
    OptionalInt / OptionalDouble 123null 同样拆包
    Page<User>(Spring Data) { "content":[...], "totalElements":100, "totalPages":10, "size":10, "number":0 } 自带分页字段
    ResponseEntity<T> T 决定 + 自定义状态码/头 体部仍是 T 的 JSON
    Mono<T> / Flux<T>(WebFlux) T 或数组相同 阻塞收集后序列化
  4. 空值

    Java 返回值 JSON
    null null 前端收到字面量 null
    Collections.emptyList() [] 空数组
    new ArrayList<>() [] 空数组
    Optional.empty() null 拆包后变成 null
额外配置(jackson处理null)

当我们从数据库获取到数据,数据经过业务层、表现层后被jackson解析成json格式数据,如果解析时数据中有null值,则前端获取的json中也会有null,这可能导致前端出问题,因此需要额外配置。

当然,理论上数据库设计中应当先规避存储null值

关于对null的处理

  1. 将null值字段直接去掉

  2. 将null换成固定值,如空字符串""

    @Configuration
    public class JacksonNullConfig {
    
        /* 1. 定义 null 替换器 */
        private static class NullToStringSerializer extends JsonSerializer<Object> {
            @Override
            public void serialize(Object value, JsonGenerator g,
                                SerializerProvider provider) throws IOException {
                // 这里写你要的“指定值”
                g.writeString("--");
            }
        }
    
        /* 2. 把替换器塞进 ObjectMapper */
        @Bean
        public Jackson2ObjectMapperBuilderCustomizer customizeNull() {
            return builder -> {
                SimpleModule module = new SimpleModule();
                // 全局:所有 String 类型出现 null 就写 "--"
                module.addSerializer(String.class, new StdSerializer<String>(String.class) {
                    @Override
                    public void serialize(String v, JsonGenerator g,
                                        SerializerProvider p) throws IOException {
                        g.writeString(v == null ? "--" : v);
                    }
                });
                builder.modules(module);
            };
        }
    }
    

数据封装

响应封装

前面说了后端向前端响应Json格式数据,这统一了数据类型,但是前端不同的请求获取到的数据还是会有差异,例如前端请求时,可能会响应一个json键值对,也可能会响应一个json列表。其次,每次响应结果只包含返回数据,若需要返回提示信息给用户,则后端必须自己手写提示信息并通过额外一次请求返回给前端。

因此对于响应结果,需要统一格式并且考虑除了数据库数据,前端还需要的额外信息

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private int code;
    private String msg;
    private T data;

    //快捷工厂方法
    public static <T> R<T> ok(T data) {
        return new Result<>(200, "ok", data);
    }
    public static <T> R<T> fail(int code, String msg) {
        return new Result<>(code, msg, null);
    }
}

定义Result类后,我们需要确保Controller中所有方法的返回值都是Result类型,前端后续获取到的响应结果就统一了格式,而且后端便于传递提示信息。

对于Result类:

code:业务状态码

meg:提示信息

data:业务数据,泛型,确保任何数据都可以封装成Result

除此以外还可以定义:

//本次请求唯一追踪号
//定位日志、排错、链路追踪必备
private String traceId;

//耗时(性能分析) 单位:毫秒
private Long cost;
分页封装

下面PafeInfo类封装了分页信息,需要时传给Result类中的data字段

private PageInfo page;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageInfo implements Serializable {
    private long page;        // 当前页码(前端传)
    private long size;        // 每页条数(前端传)
    private long total;       // 总记录数(SQL count 查出来)
    private long pages;       // 总页数(计算得出)
}

PageInfo 之所以实现 Serializable,是为了让对象在需要“跨进程、跨网络、跨 JVM”传输或持久化时,能被 Java 的原生序列化机制直接转成字节流,而不会出现 NotSerializableException

如果不实现Serializable,后续如果要把PageInfo类对象放入redis中,则可能报错

这些分页信息可由Mybatis或者MP中分页API获取,后端再手动封装成PageInfo对象即可。

若前端请求需要分页,则PageInfo字段中存分页信息

若不需要分页,PageInfo字段置null

异常封装

如果前端请求在业务过程中出现异常,例如密码错误、定位失败等等,则后端应当返回提示信息。

  1. 首先需要确定,即使是返回异常数据给前端,依旧要使用前面的Result

其次在业务中,我们编写代码自然是先判断用户行为是否正确,正确则下一步,异常则抛出异常(throw exception)并结束业务

  1. 如果我们要在异常出现时结束业务并返回提示信息,则需要想办法捕获全局异常,当捕获到异常,则返回数据给前端

全局异常处理在后续

总之出现异常时也需要返回Result,且使用code和msg两个字段即可,对应Result.fail()工厂方法

配置文件

springboot中三种配置文件,properties和yml通常用于外部化配置,对应配置引入的外部依赖等,给外部依赖中的类提供属性配置(将配置中定义的参数传给类对象中)

xml通常作为补充,可用于mybatis中存放sql文件(详情在Java DAO),xml在之前的Spring中也用于定义Bean

配置文件优先级:

  1. 启动参数
  2. 环境变量
  3. application-{profile}.properties/yml
  4. application.properties/yml
  5. 默认配置(@Value 默认值等(@Vaule也可以用于属性配置))
properties

key-value结构

# 注释
app.name=MyApplication
app.version=1.0.0
db.host=localhost
db.port=3306
db.user=root
db.password=secret

如果是传统方法实现属性配置:

Properties props = new Properties();
props.load(new FileInputStream("config.properties"));
String dbHost = props.getProperty("db.host");

在springboot中可通过注解方式快速配置

xml

可扩展标记语言(eXtensible Markup Language)

标签结构

最小可用文件结构如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 注释:上面的是xml声明,定义版本和编码,必须放在第一行 -->
<root>
    <user id="1">
        <name>张三</name>
        <age>18</age>
    </user>
</root>

xml通常作为补充,可用于mybatis中存放sql文件(详情在Java DAO),xml在之前的Spring中也用于定义Bean

yml

层次结构

yml和yaml基本一致

相比于properties和xml,更简洁,层次更清晰

我们通常使用yml

springboot中application.yml是全局配置,一定会被加载,如果有application-dev.yml等,都只有在被设置了环境才会被加载。

Spring Boot 2.x+ 官方首选:application.yml 优先级高于 application.properties

基本结构示例:

server:
  port: 8080              # 键值对写法:key: 空格value
  servlet:
    context-path: /api    # 层级靠缩进,禁止Tab,只能用空格
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456

冒号后面必须有空格

缩进只用空格,空格数随便

不同数据类型示例

# 字符串
name: yml
desc: "Line1\nLine2"      # 双引号会转义\n
desc2: 'Line1\nLine2'     # 单引号原样输出

# 整数、布尔、空值
age: 18
enable: true              # true/false
money: 3.14
empty: ~                  # 等价于 null

# 列表(两种)
langs:
  - java
  - go
  - python
ports: [80, 443, 8080]    # 行内写法,逗号后面一定要空格

# Map / 对象
person:
  name: 张三
  age: 20

# 多文档在一个文件(高级)
spring:
  profiles:
    active: dev   # 默认激活dev
---
spring:
  profiles: dev		# 也可以写在application-dev.yml
server:
  port: 8081
---
spring:
  profiles: prod	# 也可以写在application-prod.yml
server:
  port: 80
属性配置

用处:将配置类中的数据拉取到类里面

可用yml,也可用properties

类中属性注入的三种方法:

使用以下的注解,都需要确保当前类是Spring bean

默认只能将属性写在application.yml中

若想将属性写在其他自定义配置文件中并被读取,可在application.yml中配置以下内容

spring:
  config:
    import: "classpath:extra.yml"   # 也可 file:/opt/config/extra.yml
  1. @Value 单个注入

    @Value("${app.name}")
    private String appName;
    
  2. @ConfigurationProperties 批量绑定

    @ConfigurationProperties(prefix = "app") // 前缀=app
    //写在类上
    
  3. 直接注入

    @Autowired
    private AppProperties props;
    
    public String createToken(String username){
            return Jwts.builder()
                       .setSubject(username)
                       .signWith(SignatureAlgorithm.HS256, props.getSecurity().getJwt().getSecret())
                       .compact();
        }
    
关于环境切换

我可以写多个配置文件,配置文件中对于相同的属性怕,我们配置不同的值,例如dev环境用8081端口,prod环境用8082端口

当我们启动项目时,指定使用的配置文件即可

  1. 应用yml

    可以通过在application.yml中spring.profiles.active=dev来设置环境

  2. 环境变量

    export SPRING_PROFILES_ACTIVE=prod
    java -jar app.jar

  3. 命令行添加

    java -jar app.jar --spring.profiles.active=prod

三种优先级从1到3,越来越高

全局异常处理

功能:对于整个项目,只要抛出异常就捕获

首先明确,异常自然有很多种,针对不同的异常,要有不同的提示信息,这个提示信息自然不能每个要抛出异常的地方都写一遍

  1. 因此首先要定义各种各样的业务异常

    @Getter
    public class BizException extends RuntimeException {
        private final int code;
        public BizException(int code, String msg) {
            super(msg);
            this.code = code;
        }
    }
    

    通过继承类,就可以定义异常

    关于上面的super(msg):

    自定义类继承RuntimeException,RuntimeException继承Throwable类

    Throwable中已经定义了一个detailMessage字段,和我们想要定义的msg字段功能一样,因此不需要我们在显式定义一个,直接用父类的字段即可

    而当我们要取detailMessage时,要用BizException.getMessage()

    我们业务中要做的就是定义需要的各种异常(将这么多的异常类放在自定义一个包里面),并在指定位置先判断再抛出即可。

    if (stock < amount) {
        throw new BizException(4001, "库存不足");
    }
    
  2. 编写全局异常处理器(捕获所有异常)

    不仅要处理自定义异常,也要处理原生异常

    @Slf4j
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        /** 2.1 业务异常 → 友好提示 */
        @ExceptionHandler(BizException.class)
        public Result<Void> handleBiz(BizException e) {
            log.warn("业务异常 code={}, msg={}", e.getCode(), e.getMessage());
            return Result.fail(e.getCode(), e.getMessage());
        }
    
        /** 2.2 参数校验失败 → 4000 */
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public Result<Void> handleValid(MethodArgumentNotValidException e) {
            String msg = e.getBindingResult()
                          .getFieldErrors()
                          .stream()
                          .map(DefaultMessageSourceResolvable::getDefaultMessage)
                          .collect(Collectors.joining(","));
            return Result.fail(4000, msg);
        }
    
        /** 2.3 兜底:所有未预料异常 → 5000 */
        @ExceptionHandler(Throwable.class)
        public Result<Void> handleAll(Throwable e) {
            log.error("系统异常", e);
            return Result.fail(5000, "系统繁忙,请稍后再试");
        }
    }
    

拦截器Interceptor

作用在于,对于指定的请求进行拦截,当匹配自定义的逻辑后再放行

拦截器和过滤器:

Interceptor 是 Spring MVC 框架的,工作在 DispatcherServlet 与 Controller 之间

Filter 是 Servlet 规范(JSR-340)的,工作在 Web 容器/Servlet 前后

就JWT登入验证而言,更适合用过滤器实现(后续会使用springsecurity生成过滤器链)

执行顺序:

  1. HTTP
  2. Tomcat
  3. Filter
  4. DispatcherServlet(SpringMVC)
  5. preHandler(拦截器1 -> 2 -> … -> 拦截器n的preHandler)
  6. Controller方法
  7. postHandler(逆序)
  8. 渲染试图
  9. afterCompletion(逆序)
使用

要在springboot中使用拦截器,顺序:定义、注册、放行

  1. 定义拦截器(实现HandlerInterceptor)

    public class LoginInterceptor implements HandlerInterceptor {
    
        // 目标方法执行前
        @Override
        public boolean preHandle(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler) throws Exception {
    
            // 1. 放行静态资源 /error /swagger 等
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
    
            // 2. 拿登录令牌
            String token = request.getHeader("Authorization");
            if (token == null || !token.startsWith("Bearer ")) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;          // 中断后续
            }
    
            // 3. 解析 & 存到 ThreadLocal
            String userId = JwtUtils.parse(token.substring(7));
            UserContext.set(userId);   // ThreadLocal
            return true;               // true=继续流程;false=到此为止
        }
    
        
        // 目标方法执行后,视图渲染前(@RestController 基本用不到)
        @Override
        public void postHandle(HttpServletRequest request,
                               HttpServletResponse response,
                               Object handler,
                               ModelAndView modelAndView) throws Exception {
            // 可以往 Model 里塞公共数据
        }
    
        
        // 整个请求完成后(渲染后 / 异常后都会走)
        @Override
        public void afterCompletion(HttpServletRequest request,
                                    HttpServletResponse response,
                                    Object handler,
                                    Exception ex) throws Exception {
            UserContext.clear();       // 清理 ThreadLocal,防内存泄漏
        }
    }
    

    定义拦截器中可重写三个方法:

    1. preHandle
    2. postHandle
    3. afterCompletion

    三个方法对应不同的拦截时间点

    preHandle 方法最后返回值为boolean类型,当返回为true,则放行,反之拦截

  2. 注册(配置类实现WebMvcConfigurer)

    @Configuration
    @RequiredArgsConstructor
    public class WebConfig implements WebMvcConfigurer {
    
        private final LoginInterceptor loginInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //注册拦截器
            registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/**")            // 设置这个拦截器拦截的api
                    .excludePathPatterns(              // 放行清单
                        "/user/login",
                        "/user/register",
                        "/error",
                        "/swagger-ui/**",
                        "/webjars/**",
                        "/**/*.html", "/**/*.css", "/**/*.js");
        }
    }
    

    registry.addInterceptor(拦截器)时,该拦截器就被登记顺序和规则,Spring MVC内部的HandlerExecutionChain会根据登记信息生成有序拦截器链

过滤器Filter

Servlet 规范中的 Filter,它工作在 DispatcherServlet 之前,可拦截所有 HTTP 请求(含静态资源)

监听器中不可以直接注入Bean,除非先给过滤器加@Component,让spring容器管理

使用

定义过滤器

@Slf4j
public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        log.info("Filter 收到请求: {}", req.getRequestURI());
        chain.doFilter(request, response);   // 放行
        log.info("Filter 响应结束");
    }
}

注册

注册的方法一:通过配置类,可指定order执行顺序

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<DemoFilter> demoFilter() {
        FilterRegistrationBean<DemoFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new DemoFilter());
        bean.addUrlPatterns("/*");          // 拦截范围
        bean.setOrder(1);                   // 数字越小越先执行
        return bean;
    }
}

注册的方法二:直接在过滤器类上面添加注解

但是无法定义过滤器顺序

@WebFilter(urlPatterns = "/*", filterName = "annoFilter")

使用注解注册过滤器,还要在启动类上添加@ServletComponentScan

监听器Listener

发布订阅模式

三个身份:事件、发布者、监听器

监听器有三种:

名称 所属规范 / 框架 作用位置 常见用途
Servlet 规范 Listener javax.servlet.* Servlet 容器(Tomcat) 监听 Web 三大域对象 的创建与销毁:ServletContext、HttpSession、ServletRequest
Spring 事件监听器 org.springframework.context IOC 容器内部 监听 业务/框架自定义事件,解耦异步任务
Spring Boot 生命周期监听器 org.springframework.boot.context SpringApplication 启动阶段 容器刷新前 修改环境、打印 banner、埋点
Servlet 规范 Listener

常用接口

  • ServletContextListener —— 项目启动/关闭
  • HttpSessionListener —— 会话创建/销毁
  • ServletRequestListener —— 请求进入/离开

使用时直接实现对应的接口

@Slf4j
@WebListener
public class SessionCounter implements HttpSessionListener {

    private final AtomicInteger counter = new AtomicInteger(0);

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        int c = counter.incrementAndGet();
        log.info("新会话创建,当前在线:{}", c);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        int c = counter.decrementAndGet();
        log.info("会话销毁,当前在线:{}", c);
    }
}

启动类加 @ServletComponentScan 即可生效

Spring 事件监听器

三个身份:事件、发布者、监听器

  1. 定义事件

    public class OrderPaidEvent extends ApplicationEvent {
        private final String orderNo;
        public OrderPaidEvent(Object source, String orderNo) {
            super(source);
            this.orderNo = orderNo;
        }
        public String getOrderNo() { return orderNo; }
    }
    

    事件和实体类差不多,用来承载数据

  2. 发布事件

    @Autowired
    private ApplicationEventPublisher publisher;
    
    public void pay(String orderNo){
        // 业务逻辑...
        publisher.publishEvent(new OrderPaidEvent(this, orderNo));
    }
    
  3. 监听事件

    @Component
    public class OrderPaidListener {
    
        @EventListener
        public void onOrderPaid(OrderPaidEvent event){
            log.info("收到订单已支付事件:{}", event.getOrderNo());
        }
    
        // 异步执行,启动类加 @EnableAsync 才能异步
        @EventListener
        @Async
        public void sendEmail(OrderPaidEvent event){
            log.info("异步发邮件:{}", event.getOrderNo());
        }
    }
    

当我了解了Spring 事件监听器后,我感觉他这种发布订阅模式很像消息队列,都是在需要的时候发布消息,监听器统一处理消息。只是可能消息队列还可以用来存数据。查询后差别如下:

Spring 事件机制 = 进程内、同步/异步、轻量级、无持久化、无堆积、无分布式 的 “内存级 MQ”;
真正的 MQ = 跨进程、可堆积、可持久化、可广播、可削峰填谷 的 “分布式基础设施”。

简单来说,二者模式类似,但Spring事件是在我们单个springboot项目中的,项目运行后,它也就只在单个进程中有效果。但MQ是一个中间件,它是别人编写的,它自己就是一个进程,它可以接受多个进程来充当生产者或者消费者。前者只在一个进程中有效,后者自己还能存数据,且接受多进程访问。

Spring Boot 生命周期监听器
  1. 监听 应用已准备就绪事件

    public class ReadyListener implements ApplicationListener<ApplicationReadyEvent> {
        @Override
        public void onApplicationEvent(ApplicationReadyEvent event) {
            System.out.println("✅ SpringBoot 已完全启动,端口:"
                + event.getApplicationContext()
                        .getEnvironment()
                        .getProperty("server.port"));
        }
    }
    
  2. 代码注册

    @SpringBootApplication
    public class App {
        public static void main(String[] args) {
            SpringApplication app = new SpringApplication(App.class);
            app.addListeners(new ReadyListener());
            app.run(args);
        }
    }
    

定时任务

  1. 在启动类上加注解@EnableScheduling

  2. 定义定时任务

    package com.group4.class5.common.task;
    
    import com.group4.class5.modules.user.mapper.UserInfoMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    @Component
    public class UserSchedule {
    
        @Autowired
        private UserInfoMapper userInfoMapper;
    
    
    //    @Scheduled(cron = "*/5 * * * * ?")      //每五秒执行一次
        @Scheduled(cron = "0 0 0 * * ?")    //每天0点执行清空用户判决次数
        public void cleanUserJudgeNum() {
            //清空用户积分
            userInfoMapper.cleanUserJudgeNum();
        }
    }
    
常用时间设置
  1. cron表达式

    最灵活

    @Scheduled(cron = "0 15 10 15 * ?")   // 每月 15 号 10:15:00 执行
    public void cronJob() { /* ... */ }
    
  2. fixedRate

    固定间隔(毫秒),从上一次启动开始

    @Scheduled(fixedRate = 5_000)   // 每 5 秒跑一次,不管上次跑没跑完
    
  3. fixedDelay

    固定延迟(毫秒),从上一次结束开始

    @Scheduled(fixedDelay = 5_000)  // 上次跑完后等 5 秒再跑
    

AOP

概念

AOP中注意三个身份:

  1. 切面:切面类,在该类中定义切点和通知
  2. 切入点:匹配需要被增强的方法,一个切入点可以匹配一个乃至多个方法
  3. 通知:在切入点指定的方法基础上,新增的代码、功能

AOP理念在于扩展功能,定义好切点即可给对应的方法进行扩展

定义切点可以通过直接指定路径以及指定自定义注解来实现

自定义注解
@Target(ElementType.METHOD)		//表示只能添加在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExec {}
使用
  1. 先引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  2. 定义切面类,在切面类中定义切点和通知

    //定义切面类
    @Aspect
    @Component
    public class LoggingAspect {
        //定义切点,通过指定路径
        @Pointcut("execution(* com.example.demo.service.*.*(..))")
    	public void serviceLayer() {}
        
        //定义切点,通过自定义注解
        @Pointcut("@annotation(com.demo.aop.LogExec)")
        public void logPointcut() {}
        
        //定义切点,两者同用
        @Pointcut("@annotation(LogExec)&&execution(* com.example.demo.service.*.*(..))")
        public void logPointcut() {}
        
        
        //定义通知
        @Before("serviceLayer()")
        public void logBefore(JoinPoint joinPoint) {
            System.out.println("Before method: " + joinPoint.getSignature().getName());
        }
    
        @AfterReturning(pointcut = "serviceLayer()", returning = "result")
        public void logAfterReturning(JoinPoint joinPoint, Object result) {
            System.out.println("After returning method: " + joinPoint.getSignature().getName() + " with result: " + result);
        }
    
        @AfterThrowing(pointcut = "serviceLayer()", throwing = "error")
        public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
            System.out.println("After throwing method: " + joinPoint.getSignature().getName() + " with error: " + error);
        }
        
    }
    
  3. 启动类添加注解

    @EnableAspectJAutoProxy
    

事务

Spring Boot 事务 = “加一个 @Transactional 就完事”,底层由 AOP 拦截、事务管理器真正调用 JDBC 的 commit/rollback

在需要的方法或者类上加个@Transactional即可

日志

基本概念及使用

Springboot中,任何起步依赖都已经默认引入了slf4jLogback

slf4j:日志框架

Logback:日志系统、日志实现(还有Log4j等)

简单来说:我们使用日志时要调用slf4j的API,而slf4j会调用Logback等日志实现。而当我们想更换日志实现时,只需要排除Logback依赖即可

slf4j的存在让我们可以随意选择日志实现,而使用时只需要考虑slf4j的API

在项目中使用时,只需要通过slf4jLoggerFactory类就可以产生日志对象

通过调用日志对象的API就可以产生日志

//因为默认引入了slf4j,需要时直接创建log对象即可
private static final Logger log = LoggerFactory.getLogger(HelloController.class);

//使用时
log.info("提示信息");
log.debug("调试信息");
log.warn("警告信息");
log.error("错误信息");

而更方便的,可以直接在需要使用日志的类上添加@Slf4j注解,注解底层和上述创建log对象完全一样,使用注解后,即可直接使用log对象调用API

@Slf4j
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        //直接使用log对象
        log.info("访问了 /hello 接口");
        return "Hello SLF4J";
    }
}
额外配置

关于日志的配置,既可以在yml中配置,也可以在xml中配置,只要配置后,springboot即可直接读取到,但文件名称不要写错。

  1. application.yml
  2. logback-spring.xml

在application.yml中直接配置常用的配置

# 文件路径、级别、控制台颜色全配好
logging:
  level:
    root: info
    com.example.demo: debug      # 指定包级别
  file:
    name: logs/app.log           # 会自动按天切割(Spring Boot 2.5+)
  pattern:
    console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

在logback-spring.xml中配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 读 Spring 属性 -->
    <springProperty scope="context" name="LOG_HOME" source="logging.file.path" defaultValue="logs"/>
    <springProfile name="dev">
        <logger name="com.example" level="DEBUG"/>
    </springProfile>

    <!-- 控制台 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 按天+大小滚动文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

序列化

基本概念

序列化是把内存数据变成可跨进程、跨语言的字节流

序列化之后对象才能跨网络、跨进程、跨时间被传输、存储和复活

序列化 = 把内存对象“翻译成”标准字节协议

简单来说:网络传输只能传字节流,若不序列化而直接将内存数据传输,则会导致无法传输、解析错误等问题

序列化会出现在很多场景,只要是需要通过网络传输,基本都要考虑序列化,但是Mysql和后续的redis不同,前者自动帮我们序列化了,而redis需要我们手动序列化

接口序列化(@RestController → JSON)

在springboot中,引入起步依赖后,会自动用jackson序列化,前面Json模块中说过了

Redis 序列化(缓存/分布式会话)

写一个redis配置类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        StringRedisSerializer keySer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valSer = new GenericJackson2JsonRedisSerializer(
                new ObjectMapper()
                    .registerModule(new JavaTimeModule())   // 支持 LocalDate
                    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        );

        template.setKeySerializer(keySer);
        template.setHashKeySerializer(keySer);
        template.setValueSerializer(valSer);
        template.setHashValueSerializer(valSer);
        template.afterPropertiesSet();
        return template;
    }
}
分布式消息(RocketMQ/Kafka)
远程调用(Dubbo/OpenFeign)

跨域问题

CORS机制 跨域资源共享(Cross-Origin Resource Sharing)

浏览器在访问不同域名的后端接口时,浏览器会先发送一个预检请求(OPTIONS请求),询问后端是否运行这个跨域访问,后端需要在响应头指定,告诉浏览器哪个前端域名是被运行的,即这个前端发送过来的请求,后端接不接受。

解决跨域问题主要通过两种方法:

  1. nginx反向代理

    前端请求 -> nginx ->后端

  2. 后端配置类或者添加@Cross

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")          // 所有接口
                .allowedOriginPatterns("*") // 或指定域名
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(false)   // 不带 cookie 可 false
                .maxAge(3600);
    }
}

WebSocket

很重要!

具体概念在网络/应用层/websocket

使用:

  1. 前端发送http请求,带上特定请求头(前端通过览器内置的 WebSocket 对象发送请求即可)

  2. 后端引入依赖

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
  3. 配置 WebSocket 端点

    配置类,注册 WebSocket 处理器和端点

    @Configuration
    @EnableWebSocket // 启用WebSocket支持
    public class WebSocketConfig implements WebSocketConfigurer {
    
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            // 注册WebSocket端点:ws://localhost:8080/chat
            // setAllowedOrigins("*") 允许跨域(生产环境需指定具体域名)
            registry.addHandler(new ChatWebSocketHandler(), "/chat")
                    .setAllowedOrigins("*");
        }
    }
    
    
  4. 实现消息处理逻辑

    消息处理器,处理客户端连接、消息收发和断开连接事件

    public class ChatWebSocketHandler extends TextWebSocketHandler {
    
        // 存储所有在线客户端的会话(线程安全)
        private static final Set<WebSocketSession> onlineSessions = 
            Collections.synchronizedSet(new HashSet<>());
    
        // 客户端连接成功时触发
        @Override
        public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            onlineSessions.add(session);
            System.out.println("新客户端连接,当前在线人数:" + onlineSessions.size());
            // 向新连接的客户端发送欢迎消息
            session.sendMessage(new TextMessage("欢迎加入聊天室!当前在线人数:" + onlineSessions.size()));
        }
    
        // 收到客户端消息时触发
        @Override
        protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
            String clientMessage = message.getPayload();
            System.out.println("收到消息:" + clientMessage);
            
            // 广播消息给所有在线客户端(包括发送者)
            broadcast("用户" + session.getId().substring(0, 6) + ":" + clientMessage);
        }
    
        // 客户端断开连接时触发
        @Override
        public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
            onlineSessions.remove(session);
            System.out.println("客户端断开连接,当前在线人数:" + onlineSessions.size());
            // 通知所有在线客户端有人离开
            broadcast("用户" + session.getId().substring(0, 6) + "已离开,当前在线人数:" + onlineSessions.size());
        }
    
        // 广播消息给所有在线客户端
        private void broadcast(String message) throws IOException {
            for (WebSocketSession session : onlineSessions) {
                if (session.isOpen()) { // 确保连接处于打开状态
                    session.sendMessage(new TextMessage(message));
                }
            }
        }
    }
    

异步处理

  1. 配置类上添加@EnableAsync激活异步功能

  2. 在需要异步处理的方法上添加@Async

    到需要执行被异步注解标记的方法时,另开线程来运行该方法

通常用于:

  1. 处理耗时操作

  2. 解耦任务执行:将非核心流程和核心业务流程异步分离

  3. 提高资源利用率:

    在使用异步处理时,最好自定义线程池代替默认的Spring的简易线程池,避免频繁创建线程的开销

ThreadLocal

作用:充当一个全局的存储空间

线程私有,每个线程对应一个请求,即可用ThreadLocal存储处理请求相关业务需要的数据

内存泄漏问题:

先说ThreadLocal底层结构

  • 每个 Thread 对象内部有一个 ThreadLocalMap 成员变量(线程的私有变量)
  • ThreadLocalMap 是一个哈希表,key 是 ThreadLocal 实例value 是我们存入的值(如 threadLocal.set(value) 中的 value)

内存泄漏:

核心原因是 ThreadLocalMap 中的 Entry (键值对)对 ThreadLocal 是弱引用(WeakReference),但对 value 是强引用

内存泄露的触发流程:

  1. ThreadLocal 实例不再被使用,此时没有强引用指向它
  2. GC 发生时,弱引用的 key(ThreadLocal 实例)会被回收,ThreadLocalMap 中会出现 key 为 null 的 Entry
  3. 但 Entry 的 value 仍被强引用关联(Thread → ThreadLocalMap → Entry → value),且这个 value 永远无法被访问(因为 key 已 null)
  4. 如果 线程长期存活(如线程池中的核心线程),这些 key 为 null 的 value 会一直占用内存,最终导致内存泄露

解决方法:

手动调用 remove() 方法

ThreadLocal<String> threadLocal = new ThreadLocal<>();
try {
    threadLocal.set("value");
    // 业务逻辑
} finally {
    threadLocal.remove(); // 清除当前线程的 value,避免内存泄露
}

或者直接用别人封装好的,例如后面springSecurity中就自带ThreadLcoal实例,我们用就完了,不要考虑其他

Session

概念

常用于登入校验

常和cookie关联

角色有:请求、session、Session ID、Cookie、响应

当用户第一次访问网站时,服务器为用户创建一个独一无二的session并生成一个Session ID,后续创建一个名为JSESSINID的cookie并将Session ID存进去,服务端将这个cookie带在响应头上返回给前端,用户后续的请求都带上这个Session ID

我们只需要判断并创建session,服务端会自动生成sessionid并通过cookie传给前端,前端后续请求也会自动带上存了session ID的cookie

cookie存储在客户端浏览器,session存储在服务器端

无论是请求头还是响应头,保存的都是cookie,cookie里面有session ID

注意:

  1. session只是临时的,用户登入的凭证,session自带过期时间,同一个人不同时间登入会产生不同的session
  2. 用户登入时判断session是否存在,后续将会从数据库中读取该用户的信息存入session中,便于后续业务直接从session中获取当前用户的信息
  3. session会在用户退出登入、关闭浏览器、达到超时时间后被销毁
使用
//获取session对象,根据请求头是否携带session ID判断是否登入
HttpSession session = request.getSession();	//用在登入
HttpSession session = request.getSession(false);	//用在判断是否登入
//前者没有session则创建session,后则没有则返回null

//存数据到session
session.setAttribute(key,value);

//从session中读取数据
session.getAttribute(key);

//销毁
session.removeAttribute(key);

//销毁整个session
session.invalidate();

JWT

JWT(JSON Web Token)是一种JSON格式安全传输信息的简洁、自包含的令牌。

对应登入问题

可以代替sessionID的方式

也可以用redis存储token的方式代替JWT

但考虑到我们后续要用到springsecurity,因此优先使用JWT

JWT是无状态,而session和redis实现登入校验的方式都是有状态

“无状态” = 服务器 不存储 任何客户端上下文(Session、登录记录、令牌黑名单……)。
每个请求都必须自带完整凭证(这里是 JWT),服务器仅凭凭证就能完成认证与授权,请求之间没有任何内存关联

而session通常在服务器存储sessionID,redis存储UUID等

总结:session和redis方式都会在服务器端存储数据,而JWT生成的token会直接包含部分用户信息传给前端,服务器端不保存token

关于token,即用户登入后获得的唯一标识,可以用来判断是否是正常登入的用户,避免受到攻击。

token由后端生成,前端用户第一次请求后,后端生成token并返回给前端,前端后续请求都将token放在HTTP请求头中的Authorization字段中,后端对于后续请求则将请求头中的Authorization字段解析并拿取token来判断是否正常。

登入问题:

当用户登入后,我们需要给这个用户一个token(唯一标识),该用户在后续请求中都带上这个token,让我们后端知道这是合法用户且制定了这是哪个用户,让用户登入后多次请求不会被拒绝,也让后端快速确定用户信息,提供对应服务

基本概念

JWT生成的token结构:Header.Payload.Signature

Header:Json格式,包含typ(说明是JWT格式)和alg(指明签名算法)两个字段

Payload:载荷,Json格式,其中字段包含标准注册字段(exp过期时间、iat签发时间、sub指向的用户id)和自定义字段(用户信息,如username)

注意:jwt中的token中的前两部分并不安全,他们都只是通过基础编码得到的字符串,很容易被解析,因此只保存username这种数据

Signature:签名,用来判断token是否存在,算法如下:将前两部分分别通过Base64URL编码后拼接在一起成字符串,通过前面定义好的算法和保存在后端的密钥对这个字符串进行加密得到Signature

当用户发来请求时,携带token,根据token进行解密,获取token前两部分,根据算法和密钥再加密一次,结果和传来的签名对比,相同则放行

关于JWT算法升级

将token放在https://jwt.io/可以得到算法规则

jjwt 的 Keys.hmacShaKeyFor 会在 ≥ 48 字节时自动升级算法到 HS384/512(token的Header的alg字段)

使用
  1. 使用JWT要先引入依赖
<!-- JWT 0.12.5 三件套 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
  1. 然后创建jwt工具类,便于快速生成token
@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expire:7200}")
    private long expireSeconds;

    private Key getKey() {
        byte[] bytes = secret.getBytes(StandardCharsets.UTF_8);
        // 截32字节
        byte[] keyBytes = Arrays.copyOf(bytes, 32);
        return new SecretKeySpec(keyBytes, "HMACSHA256");
    }

    /** 仅用于登录成功后给前端签发 token */
    public String generateToken(String username, List<String> roles) {
        return Jwts.builder()
                .subject(username)
                .claim("roles", roles)   // 直接放字符串列表
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + expireSeconds * 1000))
                .signWith(getKey())
                .compact();
    }
}

SpringSecurity

Spring Security = 认证 + 授权 + Web 攻击防护 + 会话安全 + 密码安全 + 安全事件总线 + 扩展生态

这是一个基于Spring框架的安全认证和授权框架,可以帮助我们维护项目安全以及管理具体角色能访问什么页面

底层采用过滤器链

Spring Security 的过滤器默认 order = -100,数字越小越靠前

每层过滤器解决一个问题

我们使用时,对某几个过滤器中实现的功能进行覆盖即可

如果不是前后端分离,springsecurity会根据登入结果进行页面跳转,起到了路由的作用

对于前后端分离架构,后端对于登入结果,不执行跳转页面的功能,而只返回json数据(有token)给前端,前端自己来跳转
前后端分离 = Security 只负责鉴证 + 发 token,不再管页面跳转
前端自己发登录请求 → 拿 token → 自己路由 → 每次请求带 Authorization: Bearer xxx 即可

Spring Security 的 BearerTokenAuthenticationFilter 只认 “Bearer” 前缀 + 空格

在SpringSecurity配置类中,我们会定义登入页、登出页的请求路径,前端编写登入页并根据请求路径发送登入请求即可。后端会根据前端发送的请求中的账密进行判断,登入成功则返回token,这个token可以根据jwt生成

基本使用
  1. 引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  2. 创建UserDetailsService实现类

    @Service
    public class SecurityUserDetailsService implements UserDetailsService {
    
        @Autowired
        private UserLoginMapper userLoginMapper;
        @Autowired
        private UserIdentityMapper userIdentityMapper;
    
        //该类从数据库中获取用户完整数据,并封装成UserDetail返回
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            com.group4.class5.modules.user.entity.User user = userLoginMapper.selectByUserName(username);
    
            //判断对象是否为空,是空则用户不存在
            if(user == null){
                throw new UsernameNotFoundException("用户名不存在");
            }
    
            String pwdUser = new BCryptPasswordEncoder().encode(user.getUserPasswd());
            List<UserIdentity> userIdentities = userIdentityMapper.selectByUserId(user.getId());
    
            List<String> authorities = userIdentities.stream()
                    .map(UserIdentity::getUserIdentity)
                    .collect(Collectors.toList());
    
            //将userId添加到username中,和username和userId用,分隔
            return User.builder()
                    .username(user.getUserName() + "," + user.getId())
                    .password(pwdUser)
                    .authorities(authorities.toArray(new String[0])) // 这里一定要确保用.authorities,如果用.role,会给默认前缀ROLE_
                    .build();
    
        }
    }
    

    在这个类中,我们要实现UserDetailsService接口,重写loadUserByUsername方法,该方法固定返回UserDetails对象,该对象被包装为Authentication对象交给下一个拦截器处理

    我们要做的就是在数据库中根据传入的username参数获取用户完整信息,将账号、密码、角色传入后即可

  3. 配置密码编码器和接口授权

    也就是springsecurity的配置类

    @Configuration
    @EnableWebSecurity  //开启
    @EnableMethodSecurity
    public class SecurityConfig {
    
        //定义加密规则
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        //把 AuthenticationManager 暴露出来,需要在登入的controller中
        //作用是拿前端账密走UserDetailsService
        @Bean
        public AuthenticationManager authenticationManager(
                AuthenticationConfiguration config) throws Exception {
            return config.getAuthenticationManager();
        }
    
        //1和2都是给springsecurity框架看的,我们本身不需要调用
        //1. 验签用
        @Bean
        public JwtDecoder jwtDecoder(@Value("${jwt.secret}") String secret) {
            byte[] bytes = secret.getBytes(StandardCharsets.UTF_8);
            byte[] keyBytes = Arrays.copyOf(bytes, 32);          // 只留前32字节,因为btyes可能超过32字节,且如果太长,JWT算法会自动升级
            SecretKey key = new SecretKeySpec(keyBytes, "HMACSHA256");  //算法升级会导致生成token时和检验token时算法不一样
            return NimbusJwtDecoder.withSecretKey(key).build();
        }
    
        //2. JWT解析成Authentication,默认只解析username和身份列表
        @Bean
        public JwtAuthenticationConverter jwtAuthenticationConverter() {
            JwtAuthenticationConverter conv = new JwtAuthenticationConverter();
            conv.setPrincipalClaimName("sub");  // username 放在 sub 里
            conv.setJwtGrantedAuthoritiesConverter(jwt -> {
                List<String> roles = jwt.getClaimAsStringList("roles");
                return roles == null ? List.of()
                        : roles.stream()
                        .map(r -> new SimpleGrantedAuthority(r))    //将jwt中role列表解析出来
                        .collect(Collectors.toList());
            });
            return conv;
        }
    
    
        /* ====================  ① 登录专用链  ==================== */
        @Bean
        @Order(1)
        public SecurityFilterChain loginChain(HttpSecurity http) throws Exception {
            http
                    .securityMatcher("/api/login")          // 仅匹配登录接口
                    .csrf(csrf -> csrf.disable())
                    .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .authorizeHttpRequests(a -> a.anyRequest().permitAll()); // 直接放行
            return http.build();
        }
    
        //官方 ObjectMapper Bean,Spring Boot 已自动配置好
        private static final ObjectMapper MAPPER = new ObjectMapper();
    
        /* ====================  ② 其它接口链(JWT) ==================== */
        @Bean
        @Order(2)
        public SecurityFilterChain apiChain(HttpSecurity http,
                                            JwtDecoder decoder,
                                            JwtAuthenticationConverter converter) throws Exception {
            http
                    .csrf(csrf -> csrf.disable())   //禁用 CSRF(前后端分离项目必须禁用)
                    .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))   //禁用HttpSession,防止使用Cookie登录,强制用JWT
                    .authorizeHttpRequests(a -> a   //放行静态资源、登录接口等
                            .requestMatchers("/doc.html",
                                    "/webjars/**",
                                    "/v3/api-docs/**",
                                    "/swagger-ui/**").permitAll()
                            .requestMatchers("/api/user/**").hasAuthority("user")   //这个也要从hasRole改为hasAuthority,否则也会用加了前缀的来对比
                            .requestMatchers("/api/topic/**").hasAuthority("user")  //用户模块、话题模块、裁判模块
                            .requestMatchers("/api/judge/**").hasAuthority("user")
    //                        .requestMatchers("/api/judge/**").hasAnyAuthority("user","admin")
    //                        .requestMatchers("/api/admin/**").hasAuthority("admin")
                            .requestMatchers("/api/login").permitAll()  //登入模块
                            .requestMatchers("/api/admin/**").permitAll()
                            .requestMatchers("/api/register").permitAll()   //注册模块
                            .anyRequest().authenticated())
                    .oauth2ResourceServer(o -> o    //开启 JWT 验证,所有请求必须带合法 token
                            .jwt(j -> j.decoder(decoder)
                                    .jwtAuthenticationConverter(converter))
                            .authenticationEntryPoint((req, resp, ex) -> {
                                resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                                resp.setContentType("application/json");
    
                                Result result = Result.fail(401, "token 无效或过期");
                                String json = MAPPER.writeValueAsString(result);
                                resp.getWriter().write(json);
                            }));
            return http.build();
        }
    }
    
  4. 登入Controller中

    @RestController
    @RequestMapping("/api")
    @RequiredArgsConstructor    //给final字段添加构造方法,通过构造方法注入
    @CrossOrigin
    @Tag(name = "登入模块", description = "登入")
    public class UserLoginController {
    
        private final AuthenticationManager authenticationManager;
        private final JwtUtil jwtUtil;
    
        @Autowired
        private UserLoginMapper userLoginMapper;
    
        /**
         * 无状态登录接口
         * POST /api/login
         */
        @PostMapping("/login")
        @Operation(summary = "登入接口")
        public Result login(@RequestBody UserLoginDTO dto) {
            try{
                //手动调用AuthenticationManager,会走UserDetailsService和账密验证
                Authentication auth = authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword())
                );
    
                //从Authentication中拆分身份列表和用户名
                List<String> roles = auth.getAuthorities()
                        .stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.toList());
    
                //根据用户名和身份列表生成JWT,这些数据可在前端直接被解密得到,因此不用密码生成JWT
                //前端请求头中Authorization必须以Bearer 开头
                //这里在生成token时还传入UserId,后续请求就可以拿到id,方便操作
                String jwt = "Bearer " + jwtUtil.generateToken(auth.getName(), roles);
    
                return Result.ok(jwt);
            }
    
            catch (Exception e){
    //            e.printStackTrace(); //打印完整异常
                return Result.fail(401,"认证失败: " + e.getMessage());
    
            }
        }
    
    
        /**
         * 注册用户register,只需要账号和密码
         * @param dto
         * @return
         */
        @PostMapping("/register")
        @Operation(summary = "注册新用户",description = "只需要账号和密码")
        @Transactional
        public Result<Integer> register(@RequestBody UserLoginDTO dto){
            if(userLoginMapper.selectByUserName(dto.getUsername()) != null){
                return Result.fail(501, "用户已存在");
            }
    
            if(userLoginMapper.register(dto) == 1){
                User user = userLoginMapper.selectByUserName(dto.getUsername());
                userLoginMapper.setIdentity(user.getId(),DefaultIdentity);
                return Result.ok(1);
            }
    
            return Result.fail(501, "注册失败");
        }
    
    }
    
//注册用户到数据库时,要先加密
String secret = passwordEncoder.encode(userDto.getPassword());
  1. 添加一个拦截器,对进入的请求先拦截,将SecurityContextHolder中的用户对象处理

    之前在UserDetailsService中,我们把userid和username统一作为username来生成token

    因此上下文中的用户对象的username是id和name的字符拼接

    我们要手动处理,将id分离出来

    //针对所有带token的请求,先通过token中的username查询一次数据库,拿到userid后存入到自定义的ThreadLcoal中
    @Component
    public class LoginInterceptor implements HandlerInterceptor {
    
        @Autowired
        private UserInfoMapper userInfoMapper;
    
        // 目标方法执行前
        @Override
        public boolean preHandle(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler) throws Exception {
            //拦截器在过滤器之后
            //springsecurity已经确保了只有拥有token的请求才能进入
    
            //直接从上下文中拿到username 和 身份列表
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    
            String usernameAndUserId = auth.getName();
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
    
    //        //去数据库根据username拿到userid
    //        Integer userId = userInfoMapper.getUserIdByUsername(username);
            //从上下文中拿到userid
            //usernameAndUserId = username + "," + userId;
            String username = "";
            String userId = "";
            if (usernameAndUserId.contains(",")) {
                String[] usernameAndUserIdArray = usernameAndUserId.split(",", 2); // 限制分割数量
                username = usernameAndUserIdArray[0];
                userId = usernameAndUserIdArray[1];
            } else {
                // 如果不包含逗号,说明只有用户名
                username = usernameAndUserId;
                // 从数据库获取用户ID(需要注入userInfoMapper)
                userId = userInfoMapper.getUserIdByUsername(username).toString();
            }
    
    
            //将userid存到上下文中
    
            UsernamePasswordAuthenticationToken authNew =
                    new UsernamePasswordAuthenticationToken(username, null, authorities);
            authNew.setDetails(userId);   // 把userid 挂 details
    
    
            //将新的Authentication放到上下文中
            SecurityContextHolder.getContext().setAuthentication(authNew);
            return true;
        }
    
    }
    
    @Configuration
    @RequiredArgsConstructor
    public class WebConfig implements WebMvcConfigurer {
    
        private final LoginInterceptor loginInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //注册拦截器
            registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/**")            // 设置这个拦截器拦截的api
    
            //不需要放行,直接拦截所有请求,因为能走到拦截器的都已经携带了token,但是swagger还是要放行的
                 .excludePathPatterns(
                            "/doc.html",
                            "/webjars/**",
                            "/v3/api-docs/**",
                            "/swagger-ui/**",
                            "/api/login",
                            "/api/admin/**",
                            "/api/register"
                    );
    
    //                .excludePathPatterns(              // 放行清单
    //                        "/user/login",
    //                        "/user/register",
    //                        "/error",
    //                        "/swagger-ui/**",
    //                        "/webjars/**",
    //                        "/**/*.html", "/**/*.css", "/**/*.js");
        }
    
    }
    
注解
@EnableWebSecurity  //开启
@EnableMethodSecurity	//允许方法上配置权限

@PreAuthorize("hasRole('ADMIN')")   // 只有 ADMIN 角色可访问

综合概括(讲明流程)

关于页面,首先,我们在引入了springsecurity依赖后,就会自带登入页面,但作为前后端分离页面,我们无需理会这个页面,自定义前端页面后,在springsecurity配置类中指定登入登出的请求路径,前端根据这个路径发请求即可实现登入登出时受到springsecurity的保护

springsecurity给我们提供了很多过滤器来确保安全。

下面这种是我们自己手动创建Jwt过滤器,不方便,直接看正确使用

在用户开始登入时提交了用户名和密码,请求会被自定义的Jwt过滤器拦截并调用UserDetailsService中的方法(我们要自己编写),方法中根据传来的用户名到数据库中查询用户的完整数据,包括用户名、密码、身份列表,将这三部分封装成UserDetail对象后就会进入到下一个过滤器,它会将我们从数据库中拿到的密码和前端传入的密码进行匹配,相同则放行,并将UserDetails构建为Authentication对象(这是接口,实现类为UsernamePasswordAuthenticationToken),然后会将这个对象放到SecurityContextHolder(底层时ThreadLocal,只对那一次请求有效)中。一定程度上,这就算结束了登入

后续请求到来,并不会再被先前的过滤器拦截,而会走我们自定义的Jwt的那个过滤器,我们需要先从请求中拿到token,然后根据token判断请求是否有效(springsecurity内部包含了验证token的功能),有效则又去调用UserDetailsService生成UserDetail对象,然后我们又封装成Authentication放到上下文中就完成了(所以UserDetailsService不仅仅登入时要用,他很重要,因此也要我们自己编写)

简而言之:我们只需要自己编写UserDetailsService、Jwt过滤器、security配置类,为了方便还要写JwtUtil工具类便于调用

正确使用:

前端做自己页面,完全不用管springsecurity,只是security配置类中定义了登入登出的接口,前端向这两个接口发请求就好了

登入时,第一次请求,不带token,由于security配置类中直接放行了登入登出接口,所以请求直接到达我们的controller。在controller中,我们手动调用springsecurity的AuthenticationManager,AuthenticationManager会把前端传入的账密交给UserDetailsService,后者从数据库中根据账号(用户名)获取用户完整信息并包装成UserDetail对象,再用UserDetail对象和前端传来的密码对比,对比成功生成Authentication对象并返回给controller,controller再将Authentication对象拆分,将用户名(auth.getName())和身份列表(auth.getAuthorities())包装到JWT的token中,最后将token返回给前端。

后续请求,带token,只会走JWT过滤链路,请求路径不再被security配置类直接放行,因此会进入到过滤器链中,security框架先从请求中拿到token,后根据token进行解析拿到用户名和身份列表,将这两部分封装成Authentication并放到SercurityContextHolder(底层是ThreadLocal)中。后续业务中可以随时从里面拿到Authentication来快捷拿到usernam。后续controller我们直接写业务就好了,不用再管springsecurity。

简而言之:我们只需要自己编写UserDetailsService(定义怎么从数据库拿用户完整信息)、security配置类(放行接口、暴露bean让controller调用)、JwtUtil中的生成token方法,然后注意在登入的Controller中要手动调用AuthenticationManager和JwtUtil

从ThreadLocal中拿用户信息

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

System.out.println(auth.getName());     //用户名
System.out.println(auth.getPrincipal());    //用户实例
System.out.println(auth.getAuthorities());  //用户身份列表

Actuator

Actuator 是 Spring Boot 提供的一个核心功能模块,主要用于监控和管理应用程序,无需开发者手动编写大量监控代码

通过暴露 HTTP 或 JMX 端点(Endpoints),让开发者或运维人员能实时获取应用健康状态、配置信息、性能指标等数据,是微服务架构中运维监控的关键组件

暴露HTTP让我们可以通过浏览器访问页面,查看监控

JMX 端点是供 JMX 客户端(如 JConsole、VisualVM)访问的 “内部接口”,可通过 JMX 连接工具查看

端点是 Actuator 暴露监控信息的 “入口”,每个端点对应一类特定功能

Spring Boot 内置了多个常用端点,部分端点默认开启,部分需手动配置

端点 ID 作用说明 默认状态(Spring Boot 2.x)
/actuator/health 展示应用健康状态(如 UP/DOWN),可扩展检查项(如数据库、Redis) 开启(公开,仅基础信息)
/actuator/info 展示应用自定义信息(如版本号、作者,需配置 info 前缀参数) 开启(公开)
/actuator/metrics 展示应用所有 metrics 指标(如 JVM 内存、请求数),支持按指标名筛选 开启(需认证 / 配置暴露)
/actuator/env 展示应用所有环境变量(如配置文件、系统变量、环境变量) 开启(需认证 / 配置暴露)
/actuator/refresh 刷新 @ConfigurationProperties 注解的配置(配合 Spring Cloud Config 使用) 关闭(需手动开启)
/actuator/shutdown 优雅关闭应用(需谨慎使用) 关闭(需手动开启)

使用:

  1. 引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    
  2. 配置yml(配置端点信息)

    # Actuator 配置
    management:
      endpoints:
        web:
          exposure:
            include: health,info,metrics,env  # 暴露指定 HTTP 端点(用逗号分隔,* 表示所有)
            exclude: shutdown                # 排除不需要暴露的端点
        enabled-by-default: true             # 默认开启所有内置端点(个别端点如 shutdown 仍需单独开启)
      endpoint:
        shutdown:
          enabled: true                      # 手动开启 shutdown 端点(谨慎使用)
        health:
          show-details: always               # 显示健康状态的详细信息(默认仅在认证后显示)
    
    
  3. 查看监控

    如:访问健康状态:http://localhost:8080/actuator/health

Thymeleaf

是在服务器里把 HTML 模板填好数据再传给浏览器”的 Java 模板引擎

但在当下主流的前后端分离结构中已经用不上了

部署

通过maven打包,先clean,再package,在项目target包中得到jar包

拿到jar包通过java -jar即可运行

但只是java -jar来运行的话,既不能后台运行,也不能实现开机自启

我们通常还是要通过docker来自定义镜像,运行成容器后,就可以当作应用来自启动了

SpringCloud

基本概念

Spring Cloud = 分布式微服务全家桶

它不是一门新技术,而是把一堆开源组件封装成Spring 风格启动器,让 Java 开发者像搭积木一样解决注册、配置、熔断、限流、网关、链路追踪等问题

而开源组件大多需要安装部署,就好比mysql、redis这些,可别想着引入依赖就能用了

服务拆分搭建

将springboot转为springcloud,引入了分布式的概念,提高项目的

而第一步就是服务拆分和搭建,需要先确定四种模块各自有什么用

  1. 父模块

    在pom中继承spring-boot-starter-parent

    以及引入其他的依赖

    用于管理所有子模块的依赖的版本

    被子模块依赖

    <!-- 子模块中 -->
    <parent>
        <groupId>com.demo</groupId>
        <artifactId>sc-parent</artifactId>
        <version>1.0.0</version>
    </parent>
    
    <artifactId>sc-common</artifactId>
    <packaging>jar</packaging>
    
    
    <!-- 父模块中 -->
    <!-- 1. 继承 Spring Boot 版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    
    <groupId>com.demo</groupId>
    <artifactId>sc-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>
    
    <!-- 2. 统一版本号 -->
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <spring-cloud.version>2023.0.1</spring-cloud.version>
        <spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
        <mybatis-spring-boot.version>3.0.3</mybatis-spring-boot.version>
    </properties>
    
    <!-- 3. 只管理版本,不实际引入 -->
    <dependencyManagement>
        <dependencies>
            <!-- Spring Cloud 官方 BOM -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    
            <!-- Spring Cloud Alibaba BOM -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    
            <!-- 其他第三方 BOM 示例 -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    
  2. 公共模块

    业务模块共用的内容

    被业务模块依赖

    包括:

    1. 自定义 POJO / DTO / VO(跨服务传输)
    2. 统一返回类 R、分页封装 PageResult
    3. 全局异常 + 错误码枚举
    4. 工具类(DateUtil、JsonUtil、BeanUtil)
    5. 基础配置类(Jackson、MyBatis-Plus、Swagger、Validation)
    6. 微服务通用依赖(spring-boot-starter-validation、commons-lang3、hutool、lombok)

    不放:

    1. 具体业务实体 / Mapper / Service
    2. 数据库连接池(除非所有服务都共用)
    3. 启动类、controller、定时任务(带业务含义)
    4. 与外部中间件强相关的配置(Kafka、ES、MQ)——建议拆到 starter
  3. 网关模块

    作为所有请求的入口

    作用:

    1. 路由服务
    2. 过滤(限流、鉴权、灰度、日志、熔断)
    3. 负载均衡(Ribbon/LoadBalancer)
    4. 协议转换(HTTP⇄HTTP、HTTP⇄WebSocket、HTTP⇄gRPC)

    流程:

    1. 请求统一进入这个模块
    2. 过滤
    3. 去nacos中匹配对应的模块
    <dependencies>
        <!-- 网关核心 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- 注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 负载均衡 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    </dependencies>
    
    
    @SpringBootApplication
    @EnableDiscoveryClient   // 向 Nacos 注册
    public class GatewayApp {
        public static void main(String[] args) {
            SpringApplication.run(GatewayApp.class, args);
        }
    }
    
    
    server:
      port: 80
    
    spring:
      application:
        name: sc-gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          discovery:
            locator:
              enabled: true                 # 开启动态路由:/serviceId/**
          routes:                          # 也可手动精确路由
            - id: user-route
              uri: lb://user-service        # lb:// 代表负载均衡
              predicates:
                - Path=/api/user/**         # 浏览器访问 /api/user/xxx
              filters:
                - StripPrefix=1             # 转发前去掉第1段路径
    
    

    具体路由匹配机制(路由断言)就在yml中体现,后续Gateway模块再讲

  4. 业务模块

    根据业务类型分离成不同模块,各自存放Controller、Service等

注册中心

由于我们根据业务类型切分成了多个模块,各个模块其实都可理解为一个springboot项目,而各个模块间暂时没有任何联系,并且网关作为请求的入口,也无法将请求交给对应的模块。因此需要注册中心

作用:把各个业务模块的地址记录下来,通过业务模块的名字对应地址,避免需要分发请求和服务调用时硬编码

主要用在网关和模块通信

Eruka

Eureka 是 Netflix 开源、Spring Cloud 集成的服务注册与发现中心

但Netflix已停止迭代

Nacos

Dynamic Naming and Configuration Service
注册中心(AP 架构) + 配置中心(热刷新) + 可视化控制台,阿里开源,已取代 Eureka 成为 Spring Cloud Alibaba 官方推荐组件

概念
使用
  1. 安装

    通过docker安装

    默认8848端口,默认账号密码都是nacos

    docker pull nacos/nacos-server:v2.5.1
    
    

    启动容器

    # 提前建挂载目录
    mkdir -p /home/nacos/{conf,logs,data}
    
    docker run -d --name nacos \
      -p 8848:8848 -p 9848:9848 -p 9849:9849 \
      -e MODE=standalone \
      -e NACOS_AUTH_ENABLE=false \
      -v /home/nacos/conf:/home/nacos/conf \
      -v /home/nacos/logs:/home/nacos/logs \
      -v /home/nacos/data:/home/nacos/data \
      --restart=always \
      nacos/nacos-server:v2.5.1
    

    安装了nacos就访问8848

    虚拟机安装docker、docker概念都在运维模块,记得防火墙、selinux等

  2. 模块中引入依赖

    以下针对业务模块中配置nacos,将业务模块注册到nacos中

    网关中使用nacos上面有写,模块通信(服务调用)使用nacos后面会说

    <!-- Spring Boot 3.2 父版本已在父 pom 声明 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!-- 支持配置热刷新 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
  3. 启动类加注解

    @SpringBootApplication
    @EnableDiscoveryClient   // 开启注册
    public class ProviderApp {
        public static void main(String[] args) {
            SpringApplication.run(ProviderApp.class, args);
        }
    }
    
  4. yml配置

    spring:
      application:
        name: demo-provider       # 注册到 Nacos 的服务名
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
            namespace: public     # 可改成自定义命名空间
          config:
            server-addr: 127.0.0.1:8848
            file-extension: yaml  # 对应 dataId = demo-provider.yaml
            group: DEFAULT_GROUP
            namespace: public
            refresh-enabled: true # 开启动态刷新
    management:
      endpoints:
        web:
          exposure:
            include: "*"          # 暴露刷新端点
    
    
配置中心

就是将yml写在nacos中,各个模块引入nacos的配置实现一次定义,多模块使用

步骤:

  1. 在nacos网页创建yml配置并发布

  2. 引入配置中心依赖

    <!-- Nacos配置中心依赖 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        <version>2.2.7.RELEASE</version> <!-- 与Spring Cloud版本匹配 -->
    </dependency>
    
    
  3. 配置bootstrap.yml配置文件

    bootstrap优先级比application高,会先被读取,因此要创建bootstrap并在其中配置nacos配置中心

    # bootstrap.yml
    spring:
      application:
        name: order-service  # 服务名,与Nacos中Data ID的前缀对应
      cloud:
        nacos:
          config:
            server-addr: localhost:8848  # Nacos服务端地址
            file-extension: yml  # 配置文件格式(与Nacos中一致)
            group: DEFAULT_GROUP  # 配置分组(与Nacos中一致)
            namespace:  # 命名空间ID(可选,用于隔离环境,如dev/test/prod)
      profiles:
        active: dev  # 激活的环境,与Data ID中的"dev"对应
    
    

    bootstrap就配置nacos配置中心就好,然后模块额外的配置还是写在applictaion中

网关

Gateway,请求分发

之前写了网关模块怎么搭建,需要引入nacos,然后在yml中编写路由断言就可以将请求分发给对应的服务模块

yml通用模板:

当请求同时满足 路径匹配 /api/user/** 并且 请求方法是 GET 时,触发当前路由规则,将请求转发到 service-id 对应的服务

spring:
  cloud:
    gateway:
      routes:
        - id: xxx                      # 路由规则的id,根据业务,如user-service-route
          uri: lb://service-id         # lb=负载均衡
          order: 1                     # 数字越小越优先;默认 0
          predicates:                  # ★ 断言列表:OR 关系
            - Path=/api/user/**        # 第 1 个断言
            - Method=GET               # 第 2 个断言(与前面 AND)
          filters:                     # 命中后执行的过滤器链
            - StripPrefix=1			  # 默认过滤器,也可在后面加自定义过滤器

简而言之,predicates断言命中的请求就发送给uri对应的模块

关键字 作用 参数示例 备注
Path 路径匹配 - Path=/foo/**,/bar/** Ant 风格;多个 OR
Method HTTP 方法 - Method=GET,POST 逗号分隔 OR
Host 域名 - Host=**.example.com 支持通配符
Header 请求头 - Header=X-Request-Id,\d+ 正则匹配
Query 查询参数 - Query=pageSize,^[1-9]\d*$ 可只写 key
Cookie Cookie 值 - Cookie=mycookie,[a-z]+ 正则
After 时间之后 - After=2025-07-01T00:00:00+08:00[Asia/Shanghai] ISO-8601

模块通信

即服务调用,例如用户模块可能要调用商品模块中的方法,由于我们进行了模块切分,不能直接调用

服务调用必须要有nacos这种注册中心作为基础

OpenFeign

基于HTTP协议

由Feign升级而来

  • Feign:是 Netflix 开发的一个声明式 REST 客户端,用于简化 HTTP 调用,但已停止维护
  • OpenFeign:是 Spring Cloud 在 Feign 基础上进行的增强,支持 Spring MVC 注解(如 @GetMapping@PostMapping 等),并与 Spring Cloud 生态无缝集成,目前是主流选择

使用:

不需要引入额外依赖,确保有nacos即可

提供者像写springboot中的Controller一样写接口,提供给消费者访问

消费者模块需要定义Feign客户端,然后通过Feign客户端直接调用提供者的接口即可

消费者需要引入

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Feign客户端和提供者的接口对应好:

package com.example.consumer.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

// name: 服务提供者的服务名称(必须与提供者的 spring.application.name 一致)
@FeignClient(name = "user-service")
public interface UserFeignClient {

    // 方法声明必须与提供者的接口完全一致(路径、参数、返回值)
    @GetMapping("/user/{userId}")
    String getUserInfo(@PathVariable Long userId);
}

Dubbo

基于TCP协议

基于RPC框架

RPC框架在后续网络模块讲到了

简单来说:在一台计算机上的程序可以像调用本地函数一样,去调用另一台计算机上的函数或方法

Dubbo实现服务调用,主要需要一个中间模块,在这个模块中定义服务调用间需要用到的方法的接口

中间模块定义接口即可

package com.example.dubbo.api;

//用户服务接口
public interface UserService {
	//根据用户ID获取用户名
    String getUsername(Long userId);
    
	//获取用户信息
    String getUserInfo(Long userId);
}

中间模块需要被服务提供者和服务消费者共同依赖

服务提供者

针对中间模块的接口提供实现

package com.example.dubbo.provider.service;

import com.example.dubbo.api.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Component;

// DubboService注解:标记这是一个Dubbo服务实现
@DubboService(version = "1.0.0")
@Component
public class UserServiceImpl implements UserService {

    @Override
    public String getUsername(Long userId) {
        // 实际业务中可能从数据库查询
        return "User_" + userId;
    }

    @Override
    public String getUserInfo(Long userId) {
        return "UserInfo{id=" + userId + ", name='" + getUsername(userId) + "', status='active'}";
    }
}

启动类添加@EnableDubbo

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.example</groupId>
        <artifactId>dubbo-springcloud-demo</artifactId>
        <version>1.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>provider</artifactId>
    <name>Service Provider</name>

    <dependencies>
        <!-- 公共接口模块 -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>api</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- Spring Boot  Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- Spring Cloud Alibaba Nacos 服务发现 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- Dubbo Spring Boot Starter -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>

        <!-- Dubbo Nacos 注册中心 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-nacos</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

server:
  port: 8081 # 服务端口

spring:
  application:
    name: user-service-provider # 服务名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos注册中心地址

dubbo:
  application:
    name: ${spring.application.name}
  registry:
    address: nacos://${spring.cloud.nacos.discovery.server-addr} # 注册中心地址
  protocol:
    name: dubbo # 协议名称
    port: -1 # 端口-1表示随机分配
  scan:
    base-packages: com.example.dubbo.provider.service # 扫描Dubbo服务实现的包路径

服务消费者

启动类要加Dubbo注解

pom基本和服务提供者一致

@RestController
public class OrderController {

    // DubboReference注解:引用远程服务
    @DubboReference(version = "1.0.0")
    private UserService userService;

    @GetMapping("/order/{userId}")
    public String createOrder(@PathVariable Long userId) {
        // 调用远程服务
        String username = userService.getUsername(userId);
        String userInfo = userService.getUserInfo(userId);
        
        return "创建订单成功!用户信息:" + username + ",详细信息:" + userInfo;
    }
}

server:
  port: 8082 # 消费者端口

spring:
  application:
    name: order-service-consumer # 消费者服务名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos注册中心地址

dubbo:
  application:
    name: ${spring.application.name}
  registry:
    address: nacos://${spring.cloud.nacos.discovery.server-addr} # 注册中心地址
  consumer:
    timeout: 3000 # 调用超时时间

用到的注解:

  • @DubboService:标记服务实现类,用于暴露服务(提供者)
  • @DubboReference:标记需要引用的服务接口,用于注入远程服务代理(消费者)
  • @EnableDubbo:开启 Dubbo 注解支持(二者都要)

总结:提供者和消费者结构上很像,需要的依赖乃至yml都很像,且某个业务的提供者也可能是其他业务的消费者,所以通常老说,依赖和yml直接共用即可

总结

OpenFeign和Dubbo都起到了服务调用的作用,前者基于HTTP协议,后者基于TCP协议的RPC框架

  • OpenFeign 是 “简单通用” 的选择,适合大多数轻量级场景和跨语言交互
  • Dubbo 是 “高性能 + 强治理” 的选择,适合 Java 为主的复杂微服务架构

分布式锁

单个boot项目时,我们可使用synchronized等作为锁,但分布式环境下需要一个全局共享的锁服务

synchronized是 JVM 级别的锁,仅在单个 JVM 进程内有效

Redis Setnx

setnx是Redis的基础命令,当key不存在时才创建,key存在时不修改(set if not exits)

setnx命令可以用set + nx参数代替

SET key value NX [EX seconds | PX milliseconds]

通过setnx做分布式锁,即只有成功执行了setnx命令,创建了对应key的线程才意味着拿到了锁

需要考虑的东西很多:

  1. 拿什么作为value,key值决定是哪一把锁,value确定是谁在拿这把锁

    因此最好要用线程信息作为value

  2. 过期时间

    必须设置过期时间(redis中数据基本都要设置),避免服务器宕机导致锁一直没被释放

  3. 自动续期

    由于有过期时间,需要避免业务还未完成,锁就自动过期释放了

    需要自己手动创建守护线程,其实就是自己创建一个线程池,线程池不断刷新过期时间

    而redission采用看门狗机制,底层也是开个守护线程

    String lockKey = "resource:lock";
    // 生成唯一value作为当前实例的"锁凭证"
    String lockValue = UUID.randomUUID().toString();
    
    // 1. 尝试获取锁(原子操作)
    boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
    
    if (locked) {
        // 2. 启动守护线程池,负责续期
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        // 每隔10秒执行一次续期
        scheduler.scheduleAtFixedRate(() -> {
            // 3. 关键:续期前验证value是否匹配(确保自己仍持有锁)
            // setIfPresent:只有key存在且value匹配时,才更新过期时间
            Boolean renewed = redisTemplate.opsForValue()
                .setIfPresent(lockKey, lockValue, 30, TimeUnit.SECONDS);
            
            if (!renewed) {
                // 续期失败(锁已被释放),终止定时任务
                scheduler.shutdown();
            }
        }, 10, 10, TimeUnit.SECONDS);
    
        try {
            // 4. 执行业务逻辑(可能耗时超过30秒)
            doBusiness();
        } finally {
            // 5. 释放锁(原子操作,防止误删别人的锁)
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                            "return redis.call('del', KEYS[1]) " +
                            "else return 0 end";
            redisTemplate.execute(
                new DefaultRedisScript<>(script, Integer.class),
                Collections.singletonList(lockKey),
                lockValue
            );
            // 6. 关闭续期线程池
            scheduler.shutdown();
        }
    }
    
  4. 还锁原子性

    创建锁时只用了setnx命令,它本身就是原子性的

    还锁时需要先判断是不是自己的锁,再删除key,这里两步必须同成功同失败,确保原子性,因此要用到lua脚本

Redission
  • Redisson 是一个基于 Redis 的高级封装客户端
  • 底层使用了 SETNX 命令,但不仅仅是它

相对于setnx,redission帮我们解决了上面说到的四种问题,我们只用像使用sychronized一样拿锁还锁即可

具体使用方法如下:

  1. 引入依赖

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.23.3</version> <!-- 版本号可根据实际情况调整 -->
    </dependency>
    
    
  2. yml中正常配置redis

  3. 使用redission

    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class DistributedService {
    
        @Autowired
        private RedissonClient redissonClient;  // Redisson客户端,自动注入
    
        public void doDistributedTask() {
            // 1. 获取锁对象(锁的key)
            RLock lock = redissonClient.getLock("resource:lock");
            
            try {
                // 2. 加锁(类似synchronized的用法)
                // 参数说明:waitTime=10秒(最多等待10秒获取锁),leaseTime=-1(使用看门狗自动续期)
                lock.lock(10,  TimeUnit.SECONDS, -1, TimeUnit.SECONDS);
                
                // 3. 执行业务逻辑(即使耗时超过初始锁时间,看门狗会自动续期)
                System.out.println("执行分布式任务...");
                Thread.sleep(50000);  // 模拟耗时操作(50秒)
                
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 4. 释放锁(必须在finally中执行,确保锁一定会释放)
                if (lock.isHeldByCurrentThread()) {  // 检查当前线程是否持有锁
                    lock.unlock();
                }
            }
        }
    }
    
    

redission相关API:

  1. 获取锁:redissonClient.getLock(“lockKey”)
  2. 加锁方式:
    • lock():无限等待获取锁。拿到锁后默认30秒过期,自动续期
    • lock(long waitTime, TimeUnit unit):最多等待 waitTime,超时没拿到锁则放弃
    • lock(long waitTime, TimeUnit waitUnit, long leaseTime, TimeUnit leaseUnit):完整参数版(leaseTime=-1 启用自动续期)若leaseTime不是-1,则不自动续期,到指定时间就释放锁
  3. 释放锁:lock.unlock()必须在finally中释放,避免异常导致锁泄漏
  4. 判断持有状态:
    • lock.isHeldByCurrentThread():检查当前线程是否持有锁(避免释放别人的锁)
    • lock.isLocked():检查锁是否被任何线程持有

分布式事务

Seata

阿里开源的分布式事务解决方案

seata是独立应用,我们要安装好,并注册到nacos中

概念

三个核心角色

  • TC (Transaction Coordinator): 事务协调者,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
  • TM (Transaction Manager): 事务管理器,控制全局事务的开启、提交或回滚
  • RM (Resource Manager): 资源管理器,管理分支事务(本地事务),与 TC 通信以注册分支事务并汇报状态

事务模式

  • AT 模式:自动补偿模式,非侵入式,通过代理数据源实现自动提交 / 回滚(推荐使用)

    自动补偿就是自动回滚

  • TCC 模式:Try-Confirm-Cancel,需要手动实现三个阶段的逻辑

  • SAGA 模式:长事务解决方案,通过状态机实现复杂事务

  • XA 模式:基于 XA 协议的强一致性方案,性能较低

全局事务 ID (XID):在分布式事务中唯一标识一个全局事务,贯穿整个事务过程

工作流程说明

  1. 当执行@GlobalTransactional标注的方法时,TM 会向 TC 申请创建全局事务,生成 XID
  2. XID 通过微服务调用传递到各个分支服务
  3. 每个分支服务的 RM 会将本地事务注册到 TC,作为全局事务的分支事务
  4. 若所有分支事务执行成功,TC 协调所有分支事务提交
  5. 若任何一个分支事务失败,TC 协调所有分支事务回滚

实现事务,重点就在于回滚,回滚就需要用到代理数据源

Seata 的 AT 模式通过代理数据源(DataSourceProxy)实现对 SQL 的拦截和解析,从而自动生成回滚日志(undo_log):

  • 代理数据源会 “监听” 所有数据库操作,记录数据修改前后的状态(保存到undo_log表)
  • 当需要回滚时,Seata 根据undo_log中的记录,自动执行反向 SQL(如插入→删除、更新→恢复原值)
安装
  1. 安装seata

  2. 修改seata配置文件

    seata/conf/registry.conf

    # 注册中心配置(让Seata服务端注册到Nacos)
    registry:
      type: nacos
      nacos:
        server-addr: 127.0.0.1:8848  # 你的Nacos地址
        group: SEATA_GROUP           # 固定分组,客户端会用到
        application: seata-server    # Seata服务名,固定值
    
    # 配置中心:保持默认的file模式(单节点无需Nacos管理配置)
    config:
      type: file  # 用本地file.conf管理配置,不依赖Nacos
    
    

    这里采用seata单节点模式,也可以用seata集群,这时候就需要将seata的配置统一交给nacos配置中心管理了

  3. 修改seata/conf/file.conf

    service {
      # 事务组映射:key是客户端的tx-service-group,value是集群名(单节点固定为default)
      vgroupMapping.my_test_tx_group = "default"
      # 单节点Seata的地址(默认8091端口)
      default.grouplist = "127.0.0.1:8091"
    }
    
    

    事务组映射即,让一个事务组知道要去哪个seata寻求服务,单节点下两行解决

  4. 启动seata

使用
  1. 引入依赖

    <!-- Seata核心依赖 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!-- Seata数据源代理 -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
    </dependency>
    
    
  2. 配置yml

    spring:
      cloud:
        alibaba:
          seata:
            tx-service-group: my_test_tx_group  # 事务组名称(自定义,保持所有服务一致)
    
    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848  # Nacos地址(与Seata服务端一致)
          group: SEATA_GROUP           # 与Seata服务端配置一致
      service:
        vgroup-mapping:
          my_test_tx_group: default    # 事务组映射(固定default即可)
    
    
  3. 手动创建undo表

    CREATE TABLE IF NOT EXISTS `undo_log` (
      `branch_id` bigint NOT NULL COMMENT '分支事务ID',
      `xid` varchar(100) NOT NULL COMMENT '全局事务ID',
      `context` varchar(128) NOT NULL COMMENT '上下文信息',
      `rollback_info` longblob NOT NULL COMMENT '回滚信息',
      `log_status` int NOT NULL COMMENT '日志状态:0-正常,1-已清理',
      `log_created` datetime NOT NULL COMMENT '创建时间',
      `log_modified` datetime NOT NULL COMMENT '修改时间',
      PRIMARY KEY (`branch_id`),
      KEY `idx_xid` (`xid`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Seata回滚日志表';
    
    
  4. 配置代理数据源

    @Configuration
    public class DataSourceProxyConfig {
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSource dataSource() {
            return new DruidDataSource();
        }
    
        @Bean
        public DataSourceProxy dataSourceProxy(DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    
        @Bean
        public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSourceProxy);
            return sqlSessionFactoryBean.getObject();
        }
    }
    
    
  5. 使用注解

    @Service
    public class OrderService {
    
        @Autowired
        private OrderMapper orderMapper;
        
        @Autowired
        private AccountFeignClient accountFeignClient;  // 远程调用账户服务
        
        @Autowired
        private StorageFeignClient storageFeignClient;  // 远程调用库存服务
    
        // 全局事务入口,TM开启全局事务
        @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
        public void createOrder(Order order) {
            // 1. 创建订单(本地事务)
            orderMapper.create(order);
            
            // 2. 扣减库存(远程调用,分支事务)
            storageFeignClient.decrease(order.getProductId(), order.getCount());
            
            // 3. 扣减余额(远程调用,分支事务)
            accountFeignClient.decrease(order.getUserId(), order.getMoney());
            
            // 4. 修改订单状态(本地事务)
            orderMapper.updateStatus(order.getId(), "SUCCESS");
        }
    }
    
    

分布式缓存

Redis集群

在数据库/Redis中会讲

简单来说,一个redis不够看,就多搞几个redis做成集群,减轻单个redis压力,提高容量,提高安全性

分布式Trace平台

分布式追踪平台

简单来说,它可以给 “跨服务请求” 打一个统一的 “标记”,记录请求在每个服务中的流转路径、耗时、状态,最终形成完整的 “调用链路”

在springboot单体应用中,一个请求对应一个线程,且出现问题时,我们可以通过日志快速定位到问题所在

但在分布式项目中,一个请求经过了多个模块,出问题时我们很难定位

作用即:解决微服务跨服务请求的 “问题定位” 和 “性能分析”

关键术语:

以下也是调用链路的相关数据

  • Trace(追踪):

    • 完整的跨服务请求链路,比如 “用户下单” 从发起请求到返回结果的整个过程
    • 每个 Trace 有一个唯一的 Trace ID(全局唯一标识)
  • Span(跨度):

    • 请求在单个服务中的一段执行逻辑,是追踪的最小单位
    • 每个 Span 有唯一的 Span ID,同时会记录它的 “父 Span ID”(用来关联上一个服务的 Span)
  • Annotation(注解)

    Span 中的关键时间点标记,如:

    • cs(Client Send):客户端发送请求的时间(如订单服务调用支付服务的开始时间)
    • sr(Server Receive):服务端接收请求的时间(如支付服务收到订单服务请求的时间)
    • ss(Server Send):服务端返回响应的时间(如支付服务处理完返回结果的时间)
    • cr(Client Receive):客户端接收响应的时间(如订单服务收到支付服务结果的时间)
  • Span Context(跨度上下文):携带 Trace ID、Span ID 等信息,在服务调用时传递(比如通过 HTTP 头、RPC 元数据),确保跨服务的 Span 能关联到同一个 Trace

Zipkin
概念

Zipkin 是 Twitter 开源的分布式追踪平台,专门用来收集、存储、分析和展示分布式系统的调用链路数据

是实现 “分布式追踪” 的主流工具,开箱即用

基于java开发,使用的是 Dropwizard 框架(一个轻量级的 Java Web 框架)

关键作用:

在于用户UI界面的使用

  • 链路详情查询:输入 Trace ID,查看完整的调用链路,每个 Span 的耗时、注解、异常信息
  • 服务依赖图:自动生成服务间的依赖关系图,比如 “订单服务依赖支付服务和库存服务”,“支付服务依赖 Redis 和 MySQL”,直观看到服务间的调用关系
  • 耗时分析:按服务、按接口统计平均耗时、P95/P99 耗时(比如 “支付服务的 /pay 接口 P95 耗时 1.5 秒”),快速定位慢接口
  • 错误统计:统计每个服务、每个接口的错误率,比如 “库存服务的 /deduct 接口错误率 10%”

四大组件:

  • Collector(收集器):接收来自微服务的追踪数据(比如 Span 信息),并将数据标准化后存储到数据库
  • Storage(存储):负责存储追踪数据,支持多种存储方案(默认是内存存储,仅用于测试;生产环境常用 Elasticsearch、MySQL、Cassandra)
  • API(查询接口):提供 HTTP API,供前端 UI 查询存储的追踪数据
  • UI(用户界面):可视化界面,通过网页展示追踪链路、耗时分析、错误统计等
使用

zipkin的使用很方便,我们只需要配置好zipkin

配置好以后,zipkin会自动根据进入的请求生成调用链路数据

  1. 引入依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
    
    
  2. 配置yml

    spring:
      zipkin:
        base-url: http://localhost:9411  # Zipkin 服务器的地址(默认端口 9411)
      sleuth:
        sampler:
          probability: 1.0  
          # 采样率(1.0 表示 100% 采样,测试用;生产环境可设 0.1 即 10% 采样,减少性能开销)
    
    
  3. 启动zipkin

    # 下载并启动(默认端口 9411,内存存储)
    curl -sSL https://zipkin.io/quickstart.sh | bash -s
    java -jar zipkin.jar
    
    

    启动后,访问 http://localhost:9411 即可打开 Zipkin 的管理界面

流量控制

Sentinel

Sentinel 是阿里巴巴开源的一款面向分布式服务架构的流量控制组件,主要解决微服务场景下的限流熔断降级、系统负载保护等问题,保障服务的稳定性

核心功能
  1. 流量控制(Flow Control)

    对接口或服务的调用流量进行限制,避免因流量突增(如秒杀、峰值流量)导致服务过载

  2. 熔断降级(Circuit Breaking)

    当依赖的服务出现异常(如响应超时、错误率高)时,自动 “熔断” 对该服务的调用,避免故障扩散,保护当前服务

  3. 系统负载保护(System Protection)

    从系统整体维度出发,保护服务不被整体过载压垮(如 CPU 使用率过高、负载过高)

  4. 热点参数限流(Hot Param Flow Control)

    对接口中频繁访问的 “热点参数”(如高频 ID、高频用户)进行单独限流,避免个别参数值占用过多资源

核心概念
  1. 资源(Resource):需要保护的对象,如接口方法、服务调用等。可以通过代码注解(@SentinelResource)或埋点方式定义
  2. 规则(Rule):限流、熔断等策略的配置,可通过代码、配置文件、控制台动态配置
  3. 插槽(Slot):Sentinel 的核心骨架,通过 “插槽链”(Slot Chain)实现各种功能的串联。每个插槽负责特定的逻辑(如限流、降级、统计等),例如:
    • NodeSelectorSlot:负责构建资源的调用路径
    • FlowSlot:执行流量控制逻辑
    • DegradeSlot:执行熔断降级逻辑
  4. 控制台(Dashboard):可视化管理界面,用于配置规则、监控服务指标等
使用
  1. 引入依赖

    <!-- Sentinel 核心依赖 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    

    如果我们引入了spring-boot-starter-web,加上sentinel依赖则可以实现对controller的自动埋点

    自动埋点就让我们可以对某一个接口快速实现限流等操作

  2. 启动控制台

    sentinel和nacos以及消息队列等不太一样,后者需要安装下载应用,前者网络上提供jar包

    因为sentinel也是个springboot项目

    我们只需要运行jar包即可

    启动后,sentinel默认账号密码均为sentinel

    在控制台中,我们即可查看项目、接口,并配置规则

    如果我们要像nacos那样让sentinel可以自启动、后台运行等,就需要把jar包自定义成镜像并运行容器

    步骤如下:

    • 将jar包和Dockerfile放在一个文件夹下

    • 编写Dockerfile

      # 1. 基础镜像:使用开源的 OpenJDK 8(轻量级,适合运行 Spring Boot 应用)
      FROM openjdk:8-jre-slim
      
      # 2. 维护者信息(可选)
      LABEL maintainer="your-name <your-email>"
      
      # 3. 创建目录,用于存放 Sentinel JAR 包
      RUN mkdir -p /opt/sentinel
      
      # 4. 将本地的 Sentinel JAR 包复制到镜像的 /opt/sentinel 目录
      COPY sentinel-dashboard-1.8.6.jar /opt/sentinel/
      
      # 5. 暴露 Sentinel 控制台端口(默认 8080,若修改启动端口需同步暴露)
      EXPOSE 8080
      
      # 6. 容器启动命令:后台运行 Sentinel JAR(-d 表示后台,java -jar 启动应用)
      # 可添加参数修改端口(如 --server.port=8888)、账号密码(如 --sentinel.dashboard.auth.username=admin)
      CMD ["java", "-jar", "/opt/sentinel/sentinel-dashboard-1.8.6.jar", "--server.port=8080"]
      
    • 构建镜像

      # 构建镜像:-t 给镜像命名(sentinel-dashboard:1.8.6),.表示基于当前目录的 Dockerfile 构建
      docker build -t sentinel-dashboard:1.8.6 .
      
    • 运行容器

      # 启动容器:
      # -d:后台运行容器
      # -p 8080:8080:端口映射(主机端口:容器端口)
      # --name sentinel:给容器命名
      # --restart=always:配置容器开机自启(关键:实现自启动)
      docker run -d -p 8080:8080 --name sentinel --restart=always sentinel-dashboard:1.8.6
      
  3. yml连接控制台

    spring:
      application:
        name: order-service  # 应用名称,将显示在控制台中
      cloud:
        sentinel:
          transport:
            dashboard: localhost:8080  # 控制台地址
            port: 8719  # 本地客户端与控制台通信的端口(默认 8719,若被占用会自动+1)
    
    
  4. 手动定义资源

    也就是埋点

    @SentinelResource(value = "getUserResource", blockHandler = "handleBlock")
    @RequestMapping("/getuser")
    public String getUser() {
        return "User";
    }
    public String handleBlock(BlockException blockException) {
        return "被限流了";
    }
    
    
  5. 配置规则

    /**
     * Sentinel 规则配置类:集中管理限流、熔断等规则
     */
    @Configuration // 配置类,Spring 启动时自动加载
    public class SentinelRuleConfig {
    
        /**
         * 初始化限流规则
         * @PostConstruct 注解:在当前类被 Spring 实例化后自动执行该方法
         */
        @PostConstruct
        public void initFlowRules() {
            // 1. 创建规则列表(可同时配置多个资源的规则)
            List<FlowRule> flowRules = new ArrayList<>();
    
            // 2. 为第一个资源配置限流规则(例如:自动埋点的 Controller 接口)
            FlowRule orderApiRule = new FlowRule();
            // 资源名:对应 Controller 接口的路径(自动埋点的资源名)
            orderApiRule.setResource("/createOrder");
            // 限流阈值类型:QPS(每秒请求数)
            orderApiRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
            // 阈值:每秒最多允许 5 个请求
            orderApiRule.setCount(5);
            // 添加到规则列表
            flowRules.add(orderApiRule);
    
            // 3. 为第二个资源配置限流规则(例如:手动埋点的 Service 方法)
            FlowRule queryOrderRule = new FlowRule();
            // 资源名:对应 @SentinelResource 注解的 value 属性
            queryOrderRule.setResource("queryOrder");
            // 限流阈值类型:并发线程数(限制同时处理的线程数)
            queryOrderRule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
            // 阈值:最多允许 3 个线程同时执行
            queryOrderRule.setCount(3);
            flowRules.add(queryOrderRule);
    
            // 4. 将规则加载到 Sentinel 中
            FlowRuleManager.loadRules(flowRules);
            System.out.println("Sentinel 限流规则初始化完成");
        }
    }
    
    
Hystrix

Sentinel 更适合需要精细化流量控制、高扩展性的场景,而 Hystrix 更侧重熔断降级和线程隔离

Zookeeper

ZooKeeper 是一个分布式协调服务,由 Apache 基金会开发,主要用于解决分布式系统中常见的一致性、同步、配置管理等问题

ZooKeeper 是分布式系统的基础组件,许多框架(如 Hadoop、Kafka、Dubbo)都依赖它实现协调功能

但它并非必需品,关于一致性、同步等,我们完全可以同分布式锁redission实现,nacos也提供了配置中心的功能

消息队列

消息队列(Message Queue,简称 MQ)是一种分布式系统中的通信组件,核心作用是 “暂存消息并异步传递”,解决不同服务间的耦合、峰值削峰和异步通信问题

  • 解耦服务依赖
  • 削峰填谷
  • 异步通信

主要角色:

  • 生产者
  • 消费者
  • 队列
  • 消息
特性 RabbitMQ Kafka RocketMQ
核心优势 轻量、灵活,支持多种消息模式(如延迟队列、死信队列) 高吞吐、高持久化,适合大数据日志、流处理 阿里开源,兼顾吞吐和可靠性,适合业务消息(订单、支付)
典型场景 业务解耦(如订单状态同步)、延迟任务(如订单超时取消) 日志收集(如用户行为日志)、实时流处理(如实时推荐) 电商核心业务(下单、支付、库存)、分布式事务
吞吐能力 中(万级 / 秒) 高(十万级 / 秒) 高(十万级 / 秒)
学习成本 中等(需理解交换机、路由键等概念) 中等(需理解分区、副本机制) 较低(API 设计贴近业务,文档丰富)
生态适配 支持多语言(Java、Python、Go 等) 适配大数据生态(Spark、Flink) 适配 Spring Cloud Alibaba 生态
RabbitMQ
基本概念

RabbitMQ 是一款基于 AMQP(Advanced Message Queuing Protocol)协议的开源消息队列,以其轻量、灵活、支持多种消息模式著称,尤其适合复杂业务场景下的服务解耦和异步通信

引入了新概念

概念 作用
生产者 发送方,需考虑发送失败
消费者 接收方
队列(Queue) 存储消息,需考虑溢出、持久化
交换机(Exchange) 接收生产者发送的消息,根据 “路由规则” 转发到对应的队列;本身不存消息
绑定(Binding) 交换机和队列之间绑定
路由键(Routing Key) 消息的 “地址标识”,交换机根据它和绑定规则决定消息发往哪个队列
虚拟主机(VHost) 隔离不同业务的消息空间(类似数据库的 Schema),提高安全性

队列、交换机、绑定、路由键、虚拟主机等都可以通过可视化管理界面定义

消息模式

  1. 简单模式(一对一)

    场景:单个生产者发送消息给单个消费者

    流程:生产者 → 队列 → 消费者

  2. 工作队列模式(一对多竞争)

    场景:多个消费者共同处理一个队列的消息,实现负载均衡(如秒杀请求分布式处理)

    特点:一条消息只会被一个消费者处理,避免重复消费

  3. 发布 / 订阅模式(广播)

    场景:一条消息需要被多个消费者接收(如订单创建后,通知库存、物流、积分服务)

    原理:使用 Fanout 类型交换机,消息会转发给所有绑定到该交换机的队列,每个消费者监听一个队列

  4. 路由模式(按规则转发)

    场景:消息需要按特定规则分发

    原理:使用 Direct 类型交换机,消息的路由键需与绑定的路由键完全匹配才会转发

    既可以实现消息由交换机只发给某一个队列,也可以实现上面的广播

  5. 主题模式(模糊匹配)

    场景:更灵活的规则匹配

    原理:使用 Topic 类型交换机,支持通配符(* 匹配一个单词,# 匹配多个单词),例如 user.# 匹配 user.loginuser.register

使用
  1. 安装部署

    docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
    
    • 访问 http://localhost:15672(默认账号密码 guest/guest)进入管理界面
  2. 引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
  3. 配置yml

    spring:
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
        virtual-host: /
    
  4. 定义交换机、队列及绑定关系

    第一种方法通过配置类定义

    @Configuration
    public class RabbitConfig {
        // 声明交换机
        @Bean
        public TopicExchange orderExchange() {
            return ExchangeBuilder.topicExchange("order.exchange").durable(true).build();
        }
    
        // 声明队列
        @Bean
        public Queue stockQueue() {
            return QueueBuilder.durable("stock.queue").build();
        }
    
        // 绑定交换机和队列(路由键:order.stock)
        @Bean
        public Binding stockBinding(TopicExchange orderExchange, Queue stockQueue) {
            return BindingBuilder.bind(stockQueue).to(orderExchange).with("order.stock");
        }
    }
    

    第二种通过注解定义

    // 消费者类中直接声明(发送消息时自动创建交换机/队列)
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "stock.queue", durable = "true"), // 队列
        exchange = @Exchange(value = "order.exchange", type = ExchangeTypes.TOPIC), // 交换机
        key = "order.stock" // 路由键
    ))
    public void handleStock(Long orderId) {
        // 处理业务
    }
    
  5. 发送消息

    @Service
    public class OrderService {
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        public void createOrder(Long orderId) {
            // 发送消息(交换机、路由键、消息体)
            rabbitTemplate.convertAndSend("order.exchange", "order.stock", orderId);
        }
    }
    
  6. 消费消息

    @Service
    public class StockService {
        @RabbitListener(queues = "stock.queue")
        public void handleStock(Long orderId) {
            // 处理库存扣减逻辑
            System.out.println("扣减订单:" + orderId + " 的库存");
        }
    }
    
Kafaka

Kafka 是 “海量数据场景下的消息传递工具”,核心价值是 “高效传输和持久化”,适合需要处理大量日志、实时流数据的场景。如果你的业务是 “高吞吐、大数据量”,选 Kafka;如果是 “灵活路由、复杂业务逻辑”,选 RabbitMQ 更合适

说白了kafaka性能好,rabbitmq更灵活

热部署

不重启进程就让新代码生效

Spring Boot 热部署最常用 devtools,监控到文件变动后利用自定义 ClassLoader 快速重启容器;线上用 Arthas redefine 做紧急修复,真正不停机改代码

但devtools只能实现代码热重启,无法刷新配置的修改

DevTools 监测到 classpath 有变动后,会重新启动整个 Spring 容器

  • Spring Boot + DevTools 搞定“代码热重启”
  • Spring Cloud Config + Bus + @RefreshScope 实现“配置热刷新”

devtools使用

  1. 引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>   <!-- 防止传到其他模块 -->
    </dependency>
    
  2. 配置idea

    Settings → Build → Compiler:勾选 Build project automatically
    Help → Actions → Registry:打开 compiler.automake.allow.when.app.running
    启动配置里把 On “Update” action 和 On frame deactivation 都选 Update classes and resources

JMX

Java 自带的“远程仪表盘”,能看线程、内存、调参,还能主动触发“热”操作

JMX(Java Management Extensions)是 Java 自带的“监控 + 管理”标准,核心思想是:
把 JVM 内部或应用内部的“指标”、“可调参数”、“可操作命令”统统抽象成“MBean(托管 Bean)”,
然后通过 JMX Agent(即 JVM 内置的 MBeanServer) 暴露出去,
任何 JMX 客户端(jconsole、jvisualvm、Prometheus-jmx-exporter、自写 Java 代码)都能远程或本地连接,实时读取/修改这些 MBean,从而完成“监控 + 配置热更新 + 诊断”三位一体的工作

JMX是JVM层面的,任何java程序都能用

Actuator也可以实现监控,但它是springboot提供的,只在spring生态有效

如果我们使用Actuator,一定程度上就没必要使用JMX了

Lua脚本

概念

用于确保原子性

让多条代码同成功同失败,和事务很像对吧

二者功能确实重合,只是lua脚本更轻量,适合无法使用事务或者想提高性能的场合(例如redis和nginx,他们都没有事务)

而我们之前用到lua脚本的地方是redis种set nx作为分布式锁

常用于:

Redis 原子脚本 → 分布式锁/库存/排行榜
Nginx/OpenResty → 网关、限流、灰度、WAF
游戏热更新 → 把逻辑写到 Lua,C++ 引擎不重启重载

基本语法
  1. 注释

    -- 单行
    --[[ 多行 ]]
    
  2. 唯一数据类型

    Table既是数组又是Map

    local arr = {"apple", "banana"}     -- 下标从 1 开始!
    local map = {name="Jack", age=18}
    print(arr[1])   --> apple
    print(map.name) --> Jack
    
  3. 循环

    -- 数值型
    for i = 1, 10, 2 do print(i) end   -- 1 3 5 7 9
    -- 泛型(pairs 遍历 table)
    for k,v in pairs(map) do print(k,v) end
    
  4. 条件

    if age > 18 then
        print("adult")
    elseif age == 18 then
        print("just 18")
    else
        print("young")
    end
    
  5. 函数

    local function add(a,b) return a+b end
    print(add(3,5))  --> 8
    -- 可变参
    local function sum(...)
        local s=0
        for _,v in ipairs{...} do s=s+v end
        return s
    end
    
  6. 字符串拼接

    用 …

    local key = "user:" .. 12345
    
  7. pcall

    异常try-catch

    local ok, err = pcall(function() error("oops") end)
    if not ok then print(err) end
    
具体使用

解决并发超卖问题

lua脚本放在/resources/lua/*.lua

  1. 定义lua脚本

    -- stock.lua
    -- 参数:KEYS[1]库存key  ARGV[1]扣减数量
    local stock = tonumber(redis.call("get", KEYS[1]) or 0)
    local deduct = tonumber(ARGV[1])
    if stock < deduct then
        return -1          -- 库存不足
    end
    redis.call("decrby", KEYS[1], deduct)
    return stock - deduct  -- 返回剩余
    
  2. 定义读取lua脚本的工具类

    public final class LuaHolder {
        public static String get(String fileName) {
            try (InputStream in = new ClassPathResource("lua/" + fileName).getInputStream()) {
                return StreamUtils.copyToString(in, StandardCharsets.UTF_8);
            } catch (IOException e) {
                throw new RuntimeException("加载 Lua 失败: " + fileName, e);
            }
        }
    }
    
  3. 调用lua脚本

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    public Long deductStock(String key, int num) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LuaHolder.get("stock.lua")); // 拿到字符串
        script.setResultType(Long.class);
    
        // execute方法接收字符串,我们可以直接传字符串,字符串里面写完整的lua脚本
        return stringRedisTemplate.execute(
                script,		// 这里传入DefaultRedisScript,底层依旧是字符串
                Collections.singletonList(key),
                String.valueOf(num));
    }
    

设计模式

Java后端详解-设计模式

Logo

「智能机器人开发者大赛」官方平台,致力于为开发者和参赛选手提供赛事技术指导、行业标准解读及团队实战案例解析;聚焦智能机器人开发全栈技术闭环,助力开发者攻克技术瓶颈,促进软硬件集成、场景应用及商业化落地的深度研讨。 加入智能机器人开发者社区iRobot Developer,与全球极客并肩突破技术边界,定义机器人开发的未来范式!

更多推荐