实战总结:SpringBoot 原理 + Maven 高级

本文是对《14-1 后端Web进阶(SpringBoot原理)》和《14-2 后端Web进阶(Maven高级)》两份讲义的总结。
这一天是后端开发的最后一个篇章,偏向底层原理与工程化,目标:理解 SpringBoot 为什么”开箱即用”,掌握大型项目模块化管理。


上篇:SpringBoot 原理

一、配置优先级(5 种配置方式)

1. 三种配置文件优先级

SpringBoot 支持 propertiesymlyaml 三种格式,同时存在时优先级(从高到低):

1
2
3
1. application.properties   ← 最高
2. application.yml
3. application.yaml ← 最低

建议:项目内统一使用一种格式,yml 是主流。

2. 五种配置方式总览

除配置文件外,SpringBoot 还支持两种外部配置:

配置方式 格式 示例
命令行参数 --key=value --server.port=10010
Java 系统属性 -Dkey=value -Dserver.port=9000
properties 文件 key=value server.port=8081
yml 文件 缩进式 server:\n port: 8082
yaml 文件 缩进式 同 yml

3. 优先级排名(从高到低)

1
命令行参数 > Java系统属性 > properties > yml > yaml

4. 打包后指定参数

1
2
3
4
# 同时设置系统属性和命令行参数
java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010

# 最终生效的是命令行参数 --server.port=10010

5. 注意事项

  • SpringBoot 打包需要 spring-boot-maven-plugin 插件(官网骨架自动添加)。
  • IDEA 中通过 Modify options → Add VM options / Program arguments 设置。

二、Bean 的管理

1. Bean 的作用域(@Scope)

作用域 说明 创建时机
singleton 单例(默认),容器内同名称 bean 只有一个 容器启动时
prototype 每次使用都创建新实例 第一次获取时
request 每个请求范围创建新实例(web 环境)
session 每个会话范围创建新实例(web 环境)
application 每个应用范围创建新实例(web 环境)

用法

1
2
3
@Scope("prototype") // 改为非单例
@RestController
public class DeptController { ... }

重点提醒

  • 默认 singleton 的 bean 在容器启动时创建,可用 @Lazy 延迟到第一次使用时创建。
  • 实际开发中绝大部分 bean 是单例,不需要配置 scope
  • prototype 的 bean 每次使用都会创建新实例。

2. 第三方 Bean 的管理(@Bean)

场景

第三方依赖中的类(如阿里云 OSS 工具类),无法在源码上加 @Component,需要用 @Bean 注解声明。

方式一:直接写在启动类(不推荐)

1
2
3
4
5
6
7
@SpringBootApplication
public class Application {
@Bean
public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties props) {
return new AliyunOSSOperator(props);
}
}

方式二:配置类(推荐)

1
2
3
4
5
6
7
@Configuration
public class OSSConfig {
@Bean
public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties props) {
return new AliyunOSSOperator(props);
}
}

注意事项

  • @Bean 默认 bean 名称 = 方法名,可用 name/value 属性自定义。
  • 第三方 bean 需要依赖其他 bean 时,直接在方法形参中声明,Spring 自动按类型装配。

三、SpringBoot 原理剖析

1. SpringBoot 的两大核心

功能 解决的问题 原理
起步依赖 依赖配置繁琐 Maven 依赖传递
自动配置 bean 声明和配置繁琐 @Import + 配置文件 + @Conditional

2. 起步依赖原理

1
2
3
4
5
引入 spring-boot-starter-web

Maven 依赖传递特性

自动引入 json、web、webmvc、tomcat 等所有 web 开发常见依赖

本质就是 Maven 的依赖传递,把 web 开发所需的常见依赖都预先集成好。

3. 自动配置原理(核心!)

① 问题引入

引入第三方依赖后,第三方包中的 @Component不会被扫描到,因为 @SpringBootApplication 只扫描启动类所在包及子包。

1
2
启动类包:com.itheima   ← 能扫描
第三方包:com.example ← 扫描不到

② 四种解决方案对比

方案 写法 缺点
@ComponentScan @ComponentScan({"com.itheima","com.example"}) 繁琐、性能低
@Import 普通类 @Import(TokenParser.class) 需要知道类名
@Import 配置类 @Import(HeaderConfig.class) 需要知道配置类
@Import ImportSelector @Import(MyImportSelector.class) 需要写实现类
@EnableXxxxx(最优) 第三方提供,封装了 @Import

SpringBoot 采用的是 方案④/⑤ 的思路。

③ 源码跟踪:@SpringBootApplication

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
├── @SpringBootConfiguration → 声明启动类是配置类
├── @ComponentScan → 包扫描(启动类所在包及子包)
└── @EnableAutoConfiguration → 自动配置核心!
└── @Import(AutoConfigurationImportSelector.class)
└── selectImports()
└── getAutoConfigurationEntry()
└── getCandidateConfigurations()
└── 读取 META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports

④ 自动配置执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SpringBoot 启动

@SpringBootApplication 中的 @EnableAutoConfiguration

@Import 导入 AutoConfigurationImportSelector

selectImports() 方法读取所有依赖 jar 包中的
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件

获取文件中定义的所有自动配置类的全限定名

通过 @Import 将这些配置类加载到 IOC 容器

配置类中 @Bean 标识的方法被调用,返回值注册为 bean

@Conditional 条件装配:满足条件才真正注册

⑤ 关键文件位置

1
2
任意 SpringBoot 起步依赖 jar 包内:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件内容示例(每行一个配置类全限定名):

1
2
3
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
...

4. @Conditional 条件装配

自动配置类中声明 bean 时,常加 @Conditional 系列注解,满足条件才注册

注解 判断条件
@ConditionalOnClass 环境中存在指定字节码文件时才注册
@ConditionalOnMissingBean 环境中不存在指定类型/名称的 bean 时才注册
@ConditionalOnProperty 配置文件中存在指定属性和值时才注册

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class HeaderConfig {

@Bean
@ConditionalOnClass(name = "io.jsonwebtoken.Jwts")
// 项目中存在 Jwts 类时才注册
public HeaderParser headerParser() {
return new HeaderParser();
}

@Bean
@ConditionalOnMissingBean
// 容器中没有 HeaderParser 类型的 bean 时才注册
public HeaderParser headerParser2() {
return new HeaderParser();
}

@Bean
@ConditionalOnProperty(name = "name", havingValue = "itheima")
// application.yml 中有 name: itheima 时才注册
public HeaderParser headerParser3() {
return new HeaderParser();
}
}

这就是为什么 imports 文件中定义了上百个配置类,但并不是所有 bean 都会被注册——只有条件满足的才会真正生效。

5. 自动配置原理总结图

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────┐
│ @SpringBootApplication │
│ ├─ @SpringBootConfiguration (启动类=配置类) │
│ ├─ @ComponentScan (扫描启动类包及子包) │
│ └─ @EnableAutoConfiguration ★自动配置核心 │
│ └─ @Import(AutoConfigurationImportSelector) │
│ └─ 读取 imports 文件 │
│ └─ 加载所有自动配置类 │
│ └─ @Conditional 条件装配 │
│ └─ 满足条件 → 注册到 IOC 容器 │
└─────────────────────────────────────────────────────────────┘

四、自定义 Starter

1. 为什么需要自定义 Starter

第三方技术(如阿里云 OSS)官方未提供 starter 时,每个项目都要重复配置:引入依赖 + 写配置类 + 写工具类。封装成 starter 后,引入一个依赖即可使用

2. 命名规范

来源 命名规则 示例
SpringBoot 官方 spring-boot-starter-xxxx spring-boot-starter-web
第三方组织 xxxx-spring-boot-starter aliyun-oss-spring-boot-starter

3. 模块结构(两个模块)

1
2
3
4
5
6
7
8
9
10
aliyun-oss-spring-boot-starter          (依赖管理)
└── 依赖 aliyun-oss-spring-boot-autoconfigure

aliyun-oss-spring-boot-autoconfigure (自动配置)
├── AliyunOSSAutoConfiguration (自动配置类)
├── AliyunOSSProperties (配置属性类)
├── AliyunOSSOperator (工具类)
└── resources/META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── 内容:com.aliyun.oss.AliyunOSSAutoConfiguration

4. 实现步骤

① 创建 starter 模块(仅 pom.xml)

1
2
3
4
5
6
7
8
9
10
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>

<dependencies>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

② 创建 autoconfigure 模块

配置属性类(去掉 @Component):

1
2
3
4
5
6
@Data
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
}

工具类(去掉 @Component@Autowired,改为构造器注入):

1
2
3
4
5
6
7
8
public class AliyunOSSOperator {
private final AliyunOSSProperties aliyunOSSProperties;

public AliyunOSSOperator(AliyunOSSProperties aliyunOSSProperties) {
this.aliyunOSSProperties = aliyunOSSProperties;
}
// ... 业务方法
}

自动配置类(核心):

1
2
3
4
5
6
7
8
9
@Configuration
@EnableConfigurationProperties(AliyunOSSProperties.class)
public class AliyunOSSAutoConfiguration {

@Bean
public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties props) {
return new AliyunOSSOperator(props);
}
}

③ 创建 imports 配置文件

文件路径:src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件内容:

1
com.aliyun.oss.AliyunOSSAutoConfiguration

5. 使用方式

1
2
3
4
5
6
<!-- 引入起步依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
1
2
3
4
5
# application.yml 配置属性
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucketName: java422-web-ai
1
2
3
// 直接注入使用
@Autowired
private AliyunOSSOperator aliyunOSSOperator;

下篇:Maven 高级

五、分模块设计与开发

1. 为什么分模块

不分模块的问题

  • 项目庞大,难以管理和维护
  • 通用组件难以复用(业务代码混在一起,依赖会引入全部代码)

分模块的好处

  • 方便项目管理维护、扩展
  • 方便模块间相互调用、资源共享

2. 拆分策略

策略 示例
按功能模块拆分 公共组件、商品模块、搜索模块、订单模块
按层拆分 公共组件、实体类、控制层、业务层、数据访问层
功能 + 层混合 综合以上两种

3. 案例:tlias 项目拆分

1
2
3
tlias-web-management   (主业务模块,依赖 pojo 和 utils)
├── tlias-pojo (实体类:Result、PageBean、Emp、Dept...)
└── tlias-utils (工具类:JwtUtils、AliyunOSSOperator...)

拆分步骤(以 pojo 为例):

  1. 创建 Maven 模块 tlias-pojo
  2. 创建包 com.itheima.pojo(与原项目包名一致)
  3. 复制实体类到新模块
  4. 在新模块 pom.xml 中引入所需依赖(lombok、spring-boot-starter)
  5. 删除原项目的 pojo 包
  6. 在原项目 pom.xml 中引入 tlias-pojo 坐标

注意:实际开发是先设计模块再编码,不会先开发完再拆分。


六、继承与聚合

1. 继承

① 作用

简化依赖配置、统一管理依赖。子工程继承父工程后,自动继承父工程的依赖。

② 多重继承(SpringBoot 项目特殊处理)

由于 Maven 不支持多继承,而 SpringBoot 项目必须继承 spring-boot-starter-parent,所以采用多重继承

1
2
3
4
5
spring-boot-starter-parent
↑ 继承
tlias-parent (自定义父工程)
↑ 继承
tlias-pojo / tlias-utils / tlias-web-management

③ 父工程配置

1
2
3
4
5
6
7
8
9
10
11
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.8</version>
<relativePath/>
</parent>

<groupId>com.itheima</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging> <!-- 父工程必须是 pom -->

④ 子工程配置

1
2
3
4
5
6
7
8
9
<parent>
<groupId>com.itheima</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../tlias-parent/pom.xml</relativePath>
</parent>

<artifactId>tlias-utils</artifactId>
<!-- groupId 可省略,自动继承父工程 -->

2. 版本锁定(dependencyManagement)

① 场景

部分依赖不是所有模块都用到(如 jwt 只有 web 模块用),不能直接放父工程的 <dependencies>。但又要保证多模块使用同一依赖时版本一致。

② 父工程配置

1
2
3
4
5
6
7
8
9
10
<!-- 只锁定版本,不会真正引入 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
</dependencyManagement>

③ 子工程配置

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<!-- 不需要写 version,由父工程统一管理 -->
</dependency>
</dependencies>

④ 面试题:<dependencies> vs <dependencyManagement>

对比项 <dependencies> <dependencyManagement>
作用 直接依赖 统一管理版本
子工程是否自动继承 是(直接引入) 否(需要自己引入,但无需版本号)
父工程是否引入该依赖

3. 属性配置(properties)

集中管理版本号,修改时只需改一处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<properties>
<lombok.version>1.18.34</lombok.version>
<jwt.version>0.9.1</jwt.version>
<aliyun.oss.version>3.17.4</aliyun.oss.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version> <!-- 引用属性 -->
</dependency>
</dependencies>
</dependencyManagement>

这就是为什么 SpringBoot 项目中很多依赖都不需要写 <version>——父工程 spring-boot-starter-parent 已经用 dependencyManagement 统一管理了。

4. 聚合

① 作用

将多个模块组织成一个整体,一键构建(清理、编译、测试、打包、安装),无需按依赖关系手动一个个构建。

② 配置

在聚合工程(通常是父工程)的 pom.xml 中:

1
2
3
4
5
<modules>
<module>../tlias-pojo</module>
<module>../tlias-utils</module>
<module>../tlias-web-management</module>
</modules>

③ 效果

tlias-parent 上执行 package,所有聚合的子模块都会自动执行 package。

5. 继承与聚合对比

对比项 继承 聚合
作用 简化依赖配置、统一管理依赖 快速构建项目
配置位置 子工程配置 <parent> 聚合工程配置 <modules>
感知关系 父工程无法感知哪些子模块继承了自己 聚合工程能感知参与聚合的模块
打包方式 都是 pom 都是 pom
共同点 都是设计型模块,无实际业务内容 同左

实际开发中,父工程通常同时是聚合工程,两种关系制作在同一个 pom.xml 中。

6. Maven 三种打包方式

打包方式 用途
jar 普通模块(默认),SpringBoot 项目基本是 jar(内嵌 tomcat)
war 需部署到外部 tomcat 的 web 程序
pom 父工程或聚合工程,不写代码,仅做依赖管理

七、私服

1. 私服是什么

  • 架设在公司局域网内部的仓库服务,是一种特殊的远程仓库。
  • 用来代理位于外部的中央仓库,解决团队内部资源共享问题。
  • 一个公司/项目只需一台私服(无需自己搭建,会使用即可)。

2. 依赖查找顺序

1
2
3
1. 本地仓库     (没有就找私服)
2. 私服仓库 (没有就找中央仓库)
3. 中央仓库 (下载后存到私服和本地仓库)

3. 私服仓库类型

仓库 存储内容
RELEASE 自己开发的发布版本(稳定)
SNAPSHOT 自己开发的快照版本(开发中)
Central 从中央仓库下载的第三方依赖

4. 项目版本说明

版本类型 特点
RELEASE(发布版本) 功能稳定、可发行,存入 RELEASE 仓库
SNAPSHOT(快照版本) 功能不稳定、开发中,存入 SNAPSHOT 仓库

5. 资源上传与下载(三步配置 + 一条指令)

① 配置私服访问用户名/密码(settings.xml 的 servers)

1
2
3
4
5
6
7
8
9
10
<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin</password>
</server>
<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin</password>
</server>

② 配置私服下载地址(settings.xml 的 mirrors)

1
2
3
4
5
<mirror>
<id>maven-public</id>
<mirrorOf>*</mirrorOf>
<url>http://localhost:8081/repository/maven-public/</url>
</mirror>

③ 配置项目上传地址(项目 pom.xml 的 distributionManagement)

1
2
3
4
5
6
7
8
9
10
11
12
<distributionManagement>
<!-- release 版本上传地址 -->
<repository>
<id>maven-releases</id>
<url>http://localhost:8081/repository/maven-releases/</url>
</repository>
<!-- snapshot 版本上传地址 -->
<snapshotRepository>
<id>maven-snapshots</id>
<url>http://localhost:8081/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>

④ 执行 deploy 指令

1
mvn deploy

在聚合工程(tlias-parent)上执行 deploy,所有子模块都会上传到私服。

6. 完整流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─── A 团队开发 tlias-utils ───┐
│ 1. mvn deploy │
│ 2. 上传到私服 SNAPSHOT 仓库 │
└──────────────────────────────┘

┌─── 私服(局域网内) ─────────┐
│ maven-snapshots 仓库 │
│ 存有 tlias-utils-x.x.jar │
└──────────────────────────────┘

┌─── B 团队使用 tlias-utils ───┐
│ 1. pom.xml 引入坐标 │
│ 2. 本地仓库没有 → 私服下载 │
│ 3. 直接使用工具类 │
└──────────────────────────────┘

速查篇:高频考点与易错点

一、SpringBoot 原理速查

1. 配置优先级口诀

1
2
命令行参数 > 系统属性 > properties > yml > yaml
-- -D 文件 文件 文件

2. @SpringBootApplication 三大注解

1
2
3
4
5
@SpringBootApplication
├─ @SpringBootConfiguration → 启动类是配置类
├─ @ComponentScan → 包扫描(启动类包及子包)
└─ @EnableAutoConfiguration → 自动配置核心
└─ @Import(AutoConfigurationImportSelector)

3. 自动配置关键文件

1
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

4. @Conditional 三兄弟

注解 判断什么
@ConditionalOnClass 类路径有没有这个类
@ConditionalOnMissingBean 容器里有没有这个 bean
@ConditionalOnProperty 配置文件有没有这个属性值

二、Maven 高级速查

1. 父工程必要配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<packaging>pom</packaging>

<!-- 聚合 -->
<modules>
<module>子模块相对路径</module>
</modules>

<!-- 版本锁定 -->
<dependencyManagement>
<dependencies>...</dependencies>
</dependencyManagement>

<!-- 属性 -->
<properties>
<xxx.version>x.x.x</xxx.version>
</properties>

<!-- 上传私服 -->
<distributionManagement>
<repository>...</repository>
<snapshotRepository>...</snapshotRepository>
</distributionManagement>

2. settings.xml 三处配置

1
2
3
<servers>      <!-- 私服账号密码 -->
<mirrors> <!-- 私服下载镜像 -->
<profiles> <!-- 仓库地址配置 -->

3. Maven 生命周期对应操作

指令 作用
clean 清理 target
compile 编译
test 测试
package 打包
install 安装到本地仓库
deploy 上传到私服

三、易错点提醒

  1. 配置优先级不是固定的:5 种方式同时存在时,按 命令行 > 系统属性 > properties > yml > yaml 生效。

  2. prototype bean 不会在启动时创建:而是在第一次 getBean() 时创建,每次都新建。

  3. @Bean 方法形参就是依赖注入:第三方 bean 需要其他 bean 时,直接在方法参数中声明,Spring 自动装配。

  4. @ComponentScan 扫描范围有限:只扫启动类所在包及子包,第三方包扫不到,所以才需要 @Import 机制。

  5. imports 文件路径要准确:必须是 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,路径错一点都不会加载。

  6. @ConditionalOnMissingBean 的妙用:自定义配置覆盖默认配置就是靠它——当用户自己定义了 bean,SpringBoot 默认的就不注册。

  7. 自定义 starter 必须两个模块:starter(依赖管理)+ autoconfigure(自动配置),缺一不可。

  8. autoconfigure 模块中不能再用 @Component:因为包扫描扫不到,必须用 @Configuration + @Bean + imports 文件。

  9. 父工程 packaging 必须是 pom:否则无法被子工程继承。

  10. dependencyManagement 不会真正引入依赖:子工程还需要自己在 <dependencies> 中声明,只是不需要写 version。

  11. relativePath:子工程配置父工程时,<relativePath>../tlias-parent/pom.xml</relativePath> 指定父 pom 相对位置;不指定则从本地/远程仓库查找。

  12. 聚合工程能感知子模块,继承不能感知:聚合是父知道子,继承是子知道父。

  13. deploy 前 settings.xml 必须配好 servers 的 id:id 要和 pom.xml 中 distributionManagement 的 repository id 一致(如 maven-releases)。

  14. SNAPSHOT vs RELEASE:版本号带 -SNAPSHOT 上传到 snapshots 仓库,不带则上传到 releases 仓库。


一图回顾全天知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
┌─────────────────── SpringBoot 原理 ───────────────────┐
│ │
│ 配置优先级 (5种) │
│ 命令行 > 系统属性 > properties > yml > yaml │
│ │
│ Bean 管理 │
│ ├─ 作用域: singleton(默认) / prototype / ... │
│ └─ 第三方Bean: @Bean + @Configuration │
│ │
│ 自动配置原理 │
│ @SpringBootApplication │
│ └─ @EnableAutoConfiguration │
│ └─ @Import(AutoConfigurationImportSelector) │
│ └─ 读取 imports 文件 │
│ └─ @Conditional 条件装配 │
│ │
│ 自定义 Starter │
│ ├─ starter 模块 (依赖管理) │
│ └─ autoconfigure 模块 │
│ ├─ @Configuration + @Bean │
│ └─ imports 文件 │
└───────────────────────────────────────────────────────┘

┌─────────────────── Maven 高级 ────────────────────────┐
│ │
│ 分模块设计 │
│ 按功能 / 按层 / 混合 │
│ │
│ 继承 (简化配置) │
│ ├─ 父工程 packaging=pom │
│ ├─ dependencyManagement (锁版本不引入) │
│ └─ properties (集中管理版本号) │
│ │
│ 聚合 (一键构建) │
│ └─ <modules> 子模块列表 │
│ │
│ 私服 (团队共享) │
│ ├─ settings.xml: servers/mirrors/profiles │
│ ├─ pom.xml: distributionManagement │
│ └─ mvn deploy │
└───────────────────────────────────────────────────────┘

附:复习自测清单

SpringBoot 原理

  • 能说出 5 种配置方式及其优先级
  • 能说出 Bean 的 5 种作用域及默认值
  • 能用 @Bean + @Configuration 管理第三方 bean
  • 能画出 @SpringBootApplication 的三大注解及其作用
  • 能说出自动配置的完整流程(imports 文件 → @Import → @Conditional)
  • 能说出三种 @Conditional 子注解的作用
  • 能独立实现一个自定义 starter

Maven 高级

  • 能说出分模块设计的两种策略
  • 能配置父工程继承关系(多重继承)
  • 能区分 <dependencies><dependencyManagement>
  • 能用 properties 集中管理版本号
  • 能配置聚合关系并一键构建
  • 能说出继承与聚合的相同点和不同点
  • 能配置私服的三步配置并执行 deploy
  • 能区分 RELEASE 和 SNAPSHOT 版本

实战总结:ElementPlus 组件库

本文是对《15-前端Web实战(Vue工程化+ElementPlus)》讲义中 ElementPlus 部分 的总结,结合实战项目 vue-project03-ElementPlus 的代码。
这一天的目标:掌握 ElementPlus 组件库的引入配置,学会从官网拷贝常用组件(表格/分页/对话框/表单)并改造使用。
学习方式:不背 API,掌握”官网找 → 复制 → 改”的流程


一、ElementPlus 介绍

  • Element Plus:饿了么前端团队提供的基于 Vue3 的网站组件库。
  • 官网:https://element-plus.org/zh-CN/
  • 作用:提供按钮、表格、表单、对话框等现成组件,快速构建好看的后台页面。
  • 学习理念(后台开发者视角):学会从官网拷贝组件代码 → 粘贴到项目 → 做修改即可,不要求死记。

二、快速入门

1. 安装组件库

1
npm install element-plus@2.4.4 --save

实战中 package.json 里是 "element-plus": "^2.4.4",已安装好。

2. main.js 配置(关键)

实战项目 main.js 完整代码:

1
2
3
4
5
6
7
8
9
import './assets/main.css'
import ElementPlus from 'element-plus' // 引入组件库
import 'element-plus/dist/index.css' // 引入样式(必须)
// 中文
import zhCn from 'element-plus/es/locale/lang/zh-cn' // 引入中文语言包
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).use(ElementPlus, { locale: zhCn }).mount('#app')

3. 讲义 vs 实战:locale 引入路径差异

来源 引入路径
讲义写法 import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
实战写法 import zhCn from 'element-plus/es/locale/lang/zh-cn'

⚠️ 两种路径都能实现中文化,实战用的是 es 目录下的源码版本。如果一种路径报错,换成另一种试试。

4. 两种 main.js 写法对比

1
2
3
4
5
6
7
// 写法一:讲义风格(分步)
const app = createApp(App)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')

// 写法二:实战风格(链式)
createApp(App).use(ElementPlus, { locale: zhCn }).mount('#app')

两者等价,链式写法更简洁,但要注意 mount 必须放在最后。

5. 使用流程(通用套路)

1
2
3
4
5
1. 官网找组件 → 复制 <template> 代码
2. 复制 <script> 中的数据和函数
3. 复制 <style> 样式(如有)
4. 在 App.vue 中 import 并使用
5. 按需求改属性、改数据、改文案

三、常见组件实战(基于 vue-project03)

以下代码均来自实战项目 src/views/Element-plus-demo.vue

1. Button 按钮

1
2
3
<el-button plain @click="dialogVisible = true">打开Dialog 对话框</el-button>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false">Confirm</el-button>
属性 作用
type 颜色风格:primary / success / info / warning / danger
plain 朴素按钮(浅色背景)

2. Table 表格

1
2
3
4
5
<el-table :data="tableData" stripe style="width: 100%" border="true">
<el-table-column prop="date" align="center" label="Date" width="180" />
<el-table-column prop="name" label="Name" align="center" width="180" />
<el-table-column prop="address" align="center" label="Address" />
</el-table>
1
2
3
4
5
const tableData = [
{ date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
{ date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
// ...
]
属性 写在哪 作用
data <el-table> 表格数据源(数组)
stripe <el-table> 斑马纹(隔行变色)
border <el-table> 边框
prop <el-table-column> 该列绑定 data 中的字段名
label <el-table-column> 列标题
width <el-table-column> 列宽度
align <el-table-column> 单元格对齐:left/center/right

自定义列内容(用作用域插槽 #default="scope",讲义案例中用到):

1
2
3
4
5
6
7
8
9
10
11
<el-table-column label="头像" width="120" align="center">
<template #default="scope">
<img :src="scope.row.image" width="50">
</template>
</el-table-column>

<el-table-column label="性别" align="center">
<template #default="scope">
{{ scope.row.gender == 1 ? '男' : '女' }}
</template>
</el-table-column>

📌 scope.row 表示当前行的数据对象,用 scope.row.字段名 取值。

3. Pagination 分页

1
2
3
4
5
6
7
8
9
10
11
12
<el-pagination
v-model:current-page="currentPage4"
v-model:page-size="pageSize4"
:page-sizes="[10, 20, 30, 40, 60, 100]"
:size="size"
:disabled="disabled"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="400"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
1
2
3
4
5
6
7
8
const currentPage4 = ref(1)       // 当前页
const pageSize4 = ref(100) // 每页条数
const size = ref('default') // 分页大小尺寸
const background = ref(false) // 是否带背景色
const disabled = ref(false) // 是否禁用

const handleSizeChange = (val) => { console.log(`${val} items per page`) }
const handleCurrentChange = (val) => { console.log(`current page: ${val}`) }
属性/事件 说明
v-model:current-page 当前页码(双向绑定)
v-model:page-size 每页条数(双向绑定)
page-sizes 可选每页条数下拉
size 尺寸:large/default/small
background 按钮带背景色
disabled 禁用分页
layout 工具条布局,可选值:totalsizesprevpagernextjumper
total 数据总条数
@size-change pageSize 改变时触发
@current-change currentPage 改变时触发

4. Dialog 对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<el-button plain @click="dialogVisible = true">打开Dialog 对话框</el-button>

<el-dialog
v-model="dialogVisible"
title="收货地址"
width="60%"
:before-close="handleClose"
>
<!-- 对话框内容:可以放表格、表单等任意内容 -->
<el-table :data="tableData" stripe style="width: 100%" border="true">
<el-table-column prop="date" align="center" label="Date" width="180" />
<el-table-column prop="name" label="Name" align="center" width="180" />
<el-table-column prop="address" align="center" label="Address" />
</el-table>

<!-- 底部按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false">Confirm</el-button>
</div>
</template>
</el-dialog>
1
const dialogVisible = ref(false)   // 控制显隐
属性/插槽 说明
v-model 绑定 boolean 变量,控制显示/隐藏
title 对话框标题
width 宽度(可用百分比 60% 或像素 500px
before-close 关闭前回调(如需关闭确认)
#footer 底部插槽,放操作按钮

⚠️ 关键点:Dialog 的显示与隐藏通过 v-model 绑定的 boolean 值控制。对话框里可以放任意内容(表格、表单等)。

5. Form 表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="formInline.name" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="formInline.gender" placeholder="请选择" clearable>
<el-option label="男" value="1" />
<el-option label="女" value="2" />
</el-select>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
v-model="formInline.date"
type="date"
placeholder="请选择"
clearable
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">Query</el-button>
</el-form-item>
</el-form>
1
2
3
4
5
6
7
8
9
import { reactive } from 'vue'
const formInline = reactive({
name: '',
gender: '',
date: '',
})
const onSubmit = () => {
console.log(formInline)
}
属性 作用
:inline="true" 行内表单(一行排列)
:model 绑定表单数据对象
clearable 输入框/选择器可一键清空
value-format 日期选择器返回值格式(如 YYYY-MM-DD

📌 实战细节el-date-pickervalue-format="YYYY-MM-DD" 后,formInline.date 拿到的就是 "2026-08-07" 这样的字符串,而不是 Date 对象,方便传给后端。


四、关键点:ref vs reactive(实战踩坑)

实战代码中表单数据用了 reactive,而讲义用的是 ref,这是值得注意的差异

1. 两种写法对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 写法一:ref(讲义风格)
const formInline = ref({
user: '', region: '', date: ''
})
// 访问需 .value
console.log(formInline.value.user)
// 重置需重新赋 .value
formInline.value = { user: '', region: '', date: '' }

// 写法二:reactive(实战风格)
import { reactive } from 'vue'
const formInline = reactive({
name: '', gender: '', date: ''
})
// 直接访问属性,不用 .value
console.log(formInline.name)
// 重置需逐个赋值或 Object.assign
Object.assign(formInline, { name: '', gender: '', date: '' })

2. ref vs reactive 对比表

对比项 ref reactive
适用类型 任意值(基本类型 + 对象) 仅对象/数组
访问方式 JS 中需 .value,模板自动解包 直接访问属性
模板中使用 {{ name }} {{ form.name }}
整体替换 x.value = {...} ❌ 不能整体替换(会丢失响应式)
解构 ❌ 解构会失去响应式 ❌ 解构会失去响应式(需 toRefs
推荐场景 单个值、需要整体替换 表单对象、一组相关数据

⚠️ 易错点

  • reactive 对象不能直接整体赋值formInline = {...} 会丢响应式),要用 Object.assign(formInline, {...}) 或逐个属性赋值。
  • ref 对象在 JS 中必须用 .value模板里不用(自动解包)。

3. 模板中 v-model 绑定的差异

1
2
3
4
5
<!-- ref 写法:v-model 绑定 .属性 -->
<el-input v-model="formInline.user" />

<!-- reactive 写法:v-model 直接绑定 .属性 -->
<el-input v-model="formInline.name" />

模板里两者写法一样(都是 formInline.属性),区别在 JS 里访问和重置。


五、App.vue 引入组件

实战 App.vue

1
2
3
4
5
6
7
8
9
10
<script setup>
import Elementdemo from './views/Element-plus-demo.vue'
</script>

<template>
<Elementdemo></Elementdemo>
</template>

<style scoped>
</style>

📌 .vue 文件是默认导出import 时不加 {},名字可自定义(这里把 Element-plus-demo 改成了 Elementdemo,避免连字符影响变量名)。


六、常见问题 & 易混淆点

问题 答案
ElementPlus 样式没生效? 检查 main.js 是否 import 'element-plus/dist/index.css'
组件显示英文怎么中文化? main.jsapp.use(ElementPlus, { locale: zhCn })
locale 路径报错怎么办? 换路径:element-plus/es/locale/lang/zh-cnelement-plus/dist/locale/zh-cn.mjs
Dialog 怎么控制显示隐藏? v-model 绑定一个 boolean 的 ref
Dialog 底部按钮怎么加? <template #footer> 插槽
表格某列要自定义内容(图片/判断)? <template #default="scope"> + scope.row.字段
表格隔行变色? <el-table stripe>
日期选择器返回 Date 对象怎么转字符串? value-format="YYYY-MM-DD"
refreactive 选哪个? 单个值用 ref;表单对象用 reactive(访问不用 .value 更方便)
reactive 对象能整体替换吗? ❌ 不能,会丢响应式;用 Object.assign(obj, {...})
表单 :inline 和默认区别? inline 是一行排列;默认是每个表单项一行
el-pagination 怎么知道页码变了? 监听 @current-change@size-change 事件

七、组件属性速查表

Table 表格

标签 属性 作用
el-table data 数据源
el-table stripe 斑马纹
el-table border 边框
el-table-column prop 绑定字段
el-table-column label 列标题
el-table-column width 列宽
el-table-column align 对齐方式

Pagination 分页

属性/事件 作用
v-model:current-page 当前页
v-model:page-size 每页条数
page-sizes 可选条数数组
size 尺寸
background 背景色
disabled 禁用
layout 布局组合
total 总条数
@size-change 条数变化事件
@current-change 页码变化事件

Dialog 对话框

属性/插槽 作用
v-model 显隐控制(boolean)
title 标题
width 宽度
before-close 关闭前回调
#footer 底部按钮插槽

Form 表单

标签 属性 作用
el-form :inline 行内表单
el-form :model 数据对象
el-form-item label 标签文字
el-input v-model 双向绑定
el-input clearable 可清空
el-select v-model 双向绑定
el-option label / value 显示文字 / 实际值
el-date-picker type 类型(date/datetime 等)
el-date-picker value-format 返回值格式

八、常用命令

1
2
3
4
5
6
7
# 安装 ElementPlus
npm install element-plus@2.4.4 --save

# 项目运行
npm run dev # 启动开发服务器
npm run build # 构建生产版本
npm run preview # 预览构建结果

九、实战项目文件清单(vue-project03-ElementPlus)

1
2
3
4
5
6
7
8
9
10
11
vue-project03-ElementPlus/
├── index.html
├── package.json # 依赖:element-plus ^2.4.4, vue ^3.5.40
├── vite.config.js
└── src/
├── main.js # ★ ElementPlus 引入 + 中文化配置
├── App.vue # ★ 引入 Element-plus-demo.vue
├── views/
│ └── Element-plus-demo.vue # ★ 核心实战:表格/分页/对话框/表单
├── components/ # 脚手架自带组件(未使用)
└── assets/ # 样式文件

📌 复习建议

  1. ElementPlus 不用背 API,记住”官网找 → 复制 → 改”的流程,组件属性速查表用来快速定位。
  2. 重点掌握 4 大组件:Table(含插槽自定义列)、Pagination(含事件)、Dialog(v-model 显隐)、Form(含各种输入控件)
  3. ref vs reactive 是 Vue 的核心易错点,理解两者在访问方式整体替换上的差异。
  4. 动手试试:把表格数据改成从 axios 请求加载;给表单加一个”重置”按钮;改 Dialog 的 width 和 title。
  5. 后续可结合 day18 的”侧边栏 + 动态组件”,做一个左侧菜单切换不同 ElementPlus 页面的完整后台布局。

实战总结:Vue 工程化

本文是对《15-前端Web实战(Vue工程化+ElementPlus)》讲义中 Vue 工程化部分 的总结,并补充了自行了解的”侧边栏 + 动态组件加载”实现。
这一天是前端开发从”脚本式引入 Vue”迈向”工程化开发”的转折点,目标:掌握 Vue 脚手架项目结构、组合式 API 写法,理解单文件组件开发流程。


上篇:Vue 工程化

一、为什么需要前端工程化

1. 传统开发模式的问题

之前在 HTML 中直接用 <script src="vue.js"> 引入 Vue / Axios,存在三大痛点:

问题 说明
不规范 每次开发从零开始,无统一标准
难复用 多个页面中的组件共用性差
难维护 js、图片等资源无规范化存储目录

2. 工程化的 4 个特点

1
2
3
4
模块化  →  将 js/css 做成可复用模块
组件化 → 将 UI + 样式 + 行为封装成组件
规范化 → 统一目录结构与编码规范
自动化 → 构建/测试/部署自动完成

一句话:前端工程化就是把开发所需的工具、技术、流程、经验进行规范化和标准化,从而统一规范、提效、降难、提复用。


二、环境准备

1. create-vue 脚手架介绍

  • create-vue 是 Vue 官方最新的脚手架工具,用于快速生成工程化 Vue 项目。
  • 提供功能:统一目录结构、本地调试、热部署、单元测试、集成打包上线。
  • 依赖环境:必须先安装 NodeJS

2. NodeJS 安装要点

步骤 关键点
安装目录 没有中文、没有空格(如 D:\develop\NodeJS
验证安装 命令行执行 node -v
配置全局路径 npm config set prefix "D:\develop\NodeJS"用管理员身份运行)
切换淘宝镜像 npm config set registry https://registry.npmmirror.com

⚠️ 重点提醒prefix 后的路径必须是 NodeJS 的安装目录,配置错误会导致全局包安装位置错乱。

3. npm 介绍

  • npm = Node Package Manager,NodeJS 的软件包管理器。
  • 开发中需要的依赖通过 npm install xxx 从远程仓库下载到本地。

三、Vue 项目创建与结构

1. 创建命令

1
npm create vue@3.3.4

执行后的交互选项(默认值基本都是 No,按需开启):

选项 含义 默认
Project name 项目名称 vue-project
Add TypeScript 是否加入 TS No
Add JSX Support 是否加入 JSX 支持 No
Add Vue Router 是否添加路由(单页应用) No
Add Pinia 是否添加状态管理 No
Add Vitest 是否添加单元测试 No
Add E2E testing 是否添加端到端测试 No
Add ESLint 是否添加代码质量检查 No

2. 项目结构

1
2
3
4
5
6
7
8
9
10
vue-project/
├── index.html # 页面入口,包含 #app 挂载点
├── package.json # 项目依赖和脚本
├── vite.config.js # Vite 配置(插件、别名等)
└── src/ # ← 核心开发目录
├── main.js # 应用入口:创建并挂载根组件
├── App.vue # 根组件
├── views/ # 页面级组件
├── components/ # 可复用组件
└── assets/ # 静态资源(CSS、图片)

📌 重点:以后操作最多的就是 src 目录,前端代码都在这里写。

3. 启动项目

方式 命令/操作
命令行 npm run dev
VSCode 图形化 点击 NPM 脚本中 dev 后的运行按钮

启动后访问:http://localhost:5173


四、Vue 项目开发流程(SFC)

1. 什么是 SFC

SFC(Single-File Components,单文件组件):Vue 将一个组件的逻辑 (JS)、模板 (HTML)、样式 (CSS) 封装在同一个 *.vue 文件里。

2. .vue 文件三段式结构

1
2
3
4
5
6
7
8
9
10
11
<script setup>
// 1. 逻辑:导入、响应式数据、函数、钩子
</script>

<template>
<!-- 2. 模板:HTML 结构 + Vue 指令 -->
</template>

<style scoped>
/* 3. 样式:scoped 表示只作用于当前组件 */
</style>

五、API 风格(重点对比)

Vue 组件有两种写法:组合式 API选项式 API

1. 组合式 API(Vue3 推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { ref, onMounted } from 'vue';

const count = ref(0); // 响应式变量

function increment() { // 普通函数
count.value++;
}

onMounted(() => { // 钩子函数
console.log('Vue Mounted....');
})
</script>

<template>
<input type="button" @click="increment"> Count : {{ count }}
</template>

2. 选项式 API(Vue2 风格)

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
data() { // 数据
return { count: 0 }
},
methods: { // 方法
increment: function() { this.count++ }
},
mounted() { // 钩子
console.log('vue mounted.....');
}
}
</script>

3. 两种风格对比

对比项 组合式 API 选项式 API
标识 <script setup> <script> + export default
数据定义 const x = ref(值) data() { return {...} }
方法定义 普通函数 function fn() {} 写在 methods 选项里
钩子函数 onMounted(() => {}) mounted() {}
this 指向 没有 this(undefined) 指向当前组件实例
访问响应式值 count.value this.count
推荐度 ✅ Vue3 主推 兼容 Vue2 老项目

⚠️ 易混淆:组合式 API 中没有 this!访问 ref 变量必须用 .value(模板里除外,模板自动解包)。


六、案例:UserList.vue(组合式 API + Axios)

1. 安装 axios

1
npm install axios

2. views/UserList.vue 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

// 1. 声明响应式数据
const userList = ref([]) // 员工列表
const name = ref('')
const gender = ref('')
const job = ref('')

// 2. 声明查询函数
const search = () => {
axios.get(`https://web-server.itheima.net/emps/list?name=${name.value}&gender=${gender.value}&job=${job.value}`)
.then(res => {
userList.value = res.data.data
})
}

// 3. 钩子:页面加载完毕自动查询
onMounted(() => {
search()
})
</script>

<template>
<div id="center">
姓名: <input type="text" v-model="name">
性别:
<select v-model="gender">
<option value="1"></option>
<option value="2"></option>
</select>
职位:
<select v-model="job">
<option value="1">班主任</option>
<option value="2">讲师</option>
<option value="3">其他</option>
</select>
<input class="btn" type="button" value="查询" @click="search">
</div>

<table>
<tr>
<th>序号</th><th>姓名</th><th>头像</th><th>性别</th>
<th>职位</th><th>入职时间</th><th>更新时间</th>
</tr>
<tr v-for="(user, index) in userList" :key="user.id">
<td>{{ index + 1 }}</td>
<td>{{ user.name }}</td>
<td><img :src="user.image"></td>
<td>
<span v-if="user.gender == 1"></span>
<span v-else-if="user.gender == 2"></span>
<span v-else>其他</span>
</td>
<td>
<span v-show="user.job == 1">班主任</span>
<span v-show="user.job == 2">讲师</span>
<span v-show="user.job != 1 && user.job != 2">其他</span>
</td>
<td>{{ user.entrydate }}</td>
<td>{{ user.updatetime }}</td>
</tr>
</table>
</template>

<style scoped>
table, th, td { border: 1px solid #000; border-collapse: collapse; line-height: 50px; text-align: center; }
#center, table { width: 60%; margin: auto; }
#center { margin-bottom: 20px; }
img { width: 50px; }
input, select { width: 17%; padding: 10px; margin-right: 30px; border: 1px solid #ccc; border-radius: 4px; }
.btn { background-color: #ccc; }
</style>

3. App.vue 引入组件

1
2
3
4
5
6
7
<script setup>
import UserList from './views/UserList.vue'
</script>

<template>
<UserList></UserList>
</template>

4. 数据驱动流程

1
2
3
4
5
6
7
声明数据 (ref)

onMounted 自动调用 search()

axios 异步请求 → 赋值 userList.value

Vue 响应式更新 → v-for 重新渲染表格

扩展篇:侧边栏 + 动态组件加载(自行了解)

这部分是讲义之外,基于”侧边栏菜单切换不同页面”需求自己研究的实现方式。

一、需求场景

很多后台管理系统都有这种布局:

1
2
3
4
5
6
┌──────────┬──────────────────────┐
│ 侧边栏 │ 内容区 │
│ ─主页 │ │
│ ─热门 │ (点击菜单切换内容) │
│ ─我的 │ │
└──────────┴──────────────────────┘

核心难点:点击左侧菜单,右侧内容区切换成不同组件,但整个页面不刷新。


二、动态组件 <component :is="...">

1. 是什么

<component> 是 Vue 内置的特殊组件,不是 HTML 原生标签。它通过 :is 属性决定渲染哪个组件。

2. 基本写法

1
<component :is="当前要显示的组件" />
  • :is 后面跟的是组件对象(不是字符串)。
  • :is 的值变化时,右侧渲染的组件自动切换。

3. 配合菜单数据使用

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref } from 'vue'
import HomeView from './views/HomeView.vue'
import HotView from './views/HotView.vue'
import MineView from './views/MineView.vue'

// 菜单数据:name 是显示文字,component 是组件对象
const menus = [
{ name: '主页', component: HomeView },
{ name: '热门', component: HotView },
{ name: '我的', component: MineView },
]

const activeIndex = ref(0) // 当前选中菜单的索引
1
2
<!-- 右侧动态渲染选中菜单对应的组件 -->
<component :is="menus[activeIndex].component" />

📌 component 是自定义字段名,可改成 compview 等,但模板中要保持一致。


三、侧边栏布局实现

1. App.vue 完整结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<script setup>
import { ref } from 'vue'
import HomeView from './views/HomeView.vue'
import HotView from './views/HotView.vue'
import MineView from './views/MineView.vue'

const menus = [
{ name: '主页', component: HomeView },
{ name: '热门', component: HotView },
{ name: '我的', component: MineView },
]

const activeIndex = ref(0)
</script>

<template>
<div class="layout">
<!-- 侧边栏 -->
<aside class="sidebar">
<ul>
<li
v-for="(item, index) in menus"
:key="item.name"
:class="{ active: index === activeIndex }"
@click="activeIndex = index"
>
{{ item.name }}
</li>
</ul>
</aside>

<!-- 内容区:动态组件 -->
<main class="content">
<component :is="menus[activeIndex].component" />
</main>
</div>
</template>

<style scoped>
.layout {
display: flex; /* 侧边栏 + 内容区水平排列 */
}

.sidebar {
width: 180px; /* 侧边栏固定宽度 */
background: #f5f5f5;
}

.sidebar li {
padding: 15px;
cursor: pointer;
list-style: none;
}

.sidebar li.active {
background: #409eff; /* 选中项高亮 */
color: #fff;
}

.content {
flex: 1; /* 占据剩余空间 */
padding: 20px;
}
</style>

2. 关键语法点拆解

语法 作用 本例
v-for="(item, index) in menus" 循环渲染菜单 生成每个 <li>
:key="item.name" 列表项唯一标识(优化性能) 用 name 作 key
:class="{ active: index === activeIndex }" 动态绑定 class 选中项加 active 类
@click="activeIndex = index" 点击修改索引 切换高亮 + 切换组件
<component :is="..."> 动态组件 渲染对应页面

四、数据驱动流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
数据定义
menus (菜单数组) + activeIndex (选中索引, ref(0))

模板渲染
v-for 生成菜单 <li>
:class 根据 activeIndex 高亮当前项
<component :is> 渲染 menus[0].component

用户点击菜单
@click="activeIndex = index"

Vue 响应式触发
activeIndex 变化 → :class 重新计算 → 高亮切换
→ :is 重新计算 → 组件切换

视图更新
右侧内容区换成新组件

五、进阶:keep-alive 缓存组件状态

问题:默认情况下,<component :is> 切换组件时,旧组件会被销毁,新组件重新创建。如果组件内有输入数据(如表单),切换回来会丢失。

解决:用 <keep-alive> 包裹动态组件,缓存不活动的组件实例。

1
2
3
4
5
<main class="content">
<keep-alive>
<component :is="menus[activeIndex].component" />
</keep-alive>
</main>
对比 不用 keep-alive 用 keep-alive
切换时 销毁旧组件 → 创建新组件 缓存旧组件 → 复用
状态保留 ❌ 丢失(如表单输入) ✅ 保留
适用场景 切换频繁、状态不重要 需保留状态(如表单、滚动位置)

六、如何新增菜单项

1
2
3
4
5
1. 在 src/views/ 下新建 NewView.vue
2. 在 App.vue 中 import NewView from './views/NewView.vue'
3. 在 menus 数组中追加:
{ name: '新菜单', component: NewView }
4. 无需改其他代码,菜单自动出现

常见问题 & 易混淆点

问题 答案
import 何时用 {} 具名导出{}(如 import { ref } from 'vue');默认导出不用(如 import App from './App.vue'
.vue 文件是默认还是具名导出? 默认导出
<component> 是 HTML 原生标签吗? ❌ 不是,是 Vue 内置特殊组件
:is 后面跟字符串还是组件对象? 组件对象(导入的变量)
组合式 API 里有 this 吗? ❌ 没有,this 是 undefined
ref 变量在 JS 中怎么访问值? .value(如 count.value);模板中自动解包
scoped 的作用? 样式只作用于当前组件,不污染全局
侧边栏宽度在哪改? .sidebarwidth 属性
内容区如何占满剩余宽度? flex: 1
动态组件切换后状态丢失? <keep-alive> 包裹
v-ifv-show 区别? v-if 销毁/创建 DOM;v-show 只切换 display

常用命令速查

1
2
3
4
5
6
7
8
9
10
11
12
13
# 环境配置
npm config set prefix "NodeJS安装目录" # 配置全局安装路径
npm config set registry https://registry.npmmirror.com # 淘宝镜像

# 项目创建与运行
npm create vue@3.3.4 # 创建 Vue 项目
npm install # 安装项目依赖
npm run dev # 启动开发服务器
npm run build # 构建生产版本
npm run preview # 预览构建结果

# 依赖安装
npm install axios # 安装 axios

Vue 核心模板语法速查表

语法 作用 示例
{{ 表达式 }} 文本插值 {{ user.name }}
v-model 双向数据绑定 <input v-model="name">
v-for 列表循环 v-for="(item, index) in menus"
:key 列表项唯一标识 :key="item.id"
:class 动态绑定类名 :class="{ active: isActive }"
:src 动态绑定属性 <img :src="user.image">
@click 监听点击 @click="activeIndex = index"
v-if / v-else-if / v-else 条件渲染(销毁/创建) v-if="user.gender == 1"
v-show 条件显示(display 切换) v-show="user.job == 1"
<component :is> 动态组件 <component :is="menus[i].component" />

📌 复习建议

  1. 对照本文,理解 Vue 工程化项目每个文件的作用(main.js / App.vue / views/)。
  2. 组合式 API 是后续开发主力,重点掌握 refonMounted<script setup> 写法。
  3. 侧边栏 + 动态组件是后台管理系统的经典布局,建议手敲一遍加深理解。
  4. 动手试试:新增一个菜单项、改侧边栏宽度、用 keep-alive 缓存组件状态。

1. 今天做了什么

(外卖day6的内容)

  1. HttpClient的使用
  2. 微信小程序开发入门
    1. 项目目录结构
    2. 获取头像与昵称
    3. 获取授权码
  3. 微信登录
    1. 流程:小程序获取授权码->携带授权码向服务端发起请求->服务端使用授权码向微信的接口发起请求,获取openid(微信用户唯一标识)->查询若为新用户则1创建->创建jwt令牌,与用户基本信息一起返回给小程序
    2. 设置拦截器,对除登录与获取营业状态的其他来自用户端的请求进行令牌校验

2. 核心知识点

  1. HttpClient的使用
  2. 登录流程

3. 深度思考

4. 踩过的坑

  1. 从json字符串中解析出信息:
1
2
JSONObject jsonObject = JSON.parseObject(resultJson);//将json字符串转换为json对象  
String openid = jsonObject.getString("openid");//从json对象中获取openid

5. 遗留问题 / 想不通

前几天陆陆续续写了苍穹外卖的员工管理,菜品,分类,套餐管理等,未坚持每日学习,未写复盘

1. 今天做了什么

(day5的内容)

  1. Redis入门
    1. 安装
    2. Redis里的数据类型:字符串,哈希,集合,有序集合,列表
    3. Redis操作的命令
    4. java程序中操作Redis数据库:使用Spring Data Redis
      1. 配置
      2. 使用
  2. 店铺相关接口 , 使用Redis存储
    1. 设置店铺营业状态
    2. 获取店铺状态(用户端与管理端)
  3. 接口分组展示的配置

2. 核心知识点

Redis的基础使用

3. 深度思考

  1. 为什么使用Redis
    1. Redis将数据存在内存中,访问效率更高,可用于存方高频使用的数据
    2. 店铺状态只有一个值,使用Redis需要为它单独创建一个一行一列的表,而使用Redis这种键值对的方式就很方便

4. 踩过的坑

  1. 获取店铺状态有用户端与管理端,必须在使用@RestController(“userShopController”)注解是对其命名进行区分,否则会因为Bean对象名冲突报错

5. 遗留问题 / 想不通

  1. 通过@Bean注解在配置类中配置的Bean,使用范围是?
    1. 只有该配置类在springboot扫描范围内,才会加入IOC容器管理
    2. 只有被IOC容器管理的类,其非静态属性,才可以使用@Autowired注入Bean
  2. redisTemplate.setKeySerializer(new StringRedisSerializer());的作用
    1. 对key进行序列化
    2. java操作Redis时,使用java字符串作为key,所以为Key指定字符串序列化器,可以确保java字符串存进Redis还是该字符串
    3. 若不指定序列化器,使用默认序列化器,存进Redis的key与java字符串会不一致
    4. 也可以为Value设置序列化器
      // 设置 Value 的序列化器(通常用 Jackson,存 JSON 格式,方便其他语言读取)
      redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
1
2
3
4
5
6
7
@Bean  
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());// 设置key序列化器
return redisTemplate;
}

1. 今天做了什么

(外卖day07,day08)

  1. 购物车相关接口
  2. 用户下单相关接口

2. 核心知识点

  1. 订单表与订单明细表的设计
    1. 订单表存储订单相关的信息
    2. 订单明细表存储订单中的菜品,套餐的单价,数量等数据,一条菜品或套餐对应一条数据
    3. 二者是一对多的
  2. 冗余字段
    1. 数据表中某些字段可以通过其他字段在其他表中查询到
    2. 为什么设计冗余字段
      1. 在插入数据时将冗余字段从其他表查出来,一并插入其中
      2. 以后查询的时候可以直接从表中查到,不用进行多表联查

3. 深度思考

4. 踩过的坑

  1. 多表操作一定要开启事务

5. 遗留问题 / 想不通

1. 今天做了什么

外卖day7的一部分

  1. 缓存菜品
  2. 缓存套餐

2. 核心知识点

  1. 两种实现缓存的方式
    1. 使用Redis自己实现缓存
      1. 根据分类id查询菜品,将分类id作为key,将查询到的集合作为value
      2. 查询时先查缓存是否存在,存在直接返回,不存在则查询后存入缓存返回
      3. 一旦管理端发生新增,修改,删除,起售停售,都要对缓存进行清理
    2. 使用Spring Cache实现缓存
      1. 完全使用注解实现,很方便
      2. 在启动类使用注解进行开启
      3. 在相应查询接口上添加注解,实现查询时先查缓存是否存在,存在直接返回,不存在则查询后存入缓存返回
      4. 在管理端新增,修改,删除操作添加清除缓存的注解
      5. Spring Cache底层可以用多种方式实现,这里使用Redis,添加了Redis的依赖,可以自动识别

3. 深度思考

  1. 为什么要做缓存
    1. 在业务执行过程中,数据库查询操作从磁盘读取数据,比较耗时
    2. 用户使用量比较大时加载较慢,影响使用体验
    3. 在Redis中做缓存,从内存中读取速度很快

4. 踩过的坑

  1. redisTemplate.delete()不支持通配符,redisTemplate.keys()支持,先查询keys,再删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ApiOperation("修改菜品")  
@PutMapping
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}", dishDTO);
dishService.update(dishDTO);

//更新菜品后,清除缓存
clearCache("dish_*");
return Result.success();
}


private void clearCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
  1. Spring Cache的@CacheEvict注解,清除所有setmealcache的缓存:
1
@CacheEvict(cacheNames = "setmealcache", allEntries = true)

5. 遗留问题 / 想不通

day2

计划

  1. 学完IO流

笔记

1.缓冲流

字节缓冲流
  1. 缓冲流默认缓冲区大小为8192字节(8KB)
  2. 使用方式:在基本流外包装一层缓冲流
  3. 关闭缓冲流时,底层基本流也会自动关闭
  4. 性能对比
    1. 缓冲流逐字节读取复制:35ms(内部缓冲区已优化磁盘I/O)
    2. 缓冲流配合 byte[] 数组读取复制:4ms
  5. 缓冲流与缓冲数组同时使用是不必要的
    1. 缓冲流内部已有 8KB 缓冲区,逐字节读取时已通过内部缓冲区大幅减少磁盘 I/O,这是性能提升的核心
    2. 在缓冲流之上再使用 byte[] 数组,相当于多了一层冗余的缓冲层级,增加了内存占用和代码复杂度
    3. 实际开发中,单独使用缓冲流即可,或直接使用基本流配合自己的缓冲区,没必要两层都用
字符缓冲流
  1. BufferedReader 特有方法:readLine(),读取一行文本,遇到文件末尾返回 null
  2. BufferedWriter 特有方法:newLine(),用于多平台换行(Windows: \r\n, Linux: \n)
  3. 字符缓冲流也可以指定编码格式

2.转换流

  1. InputStreamReader:将字节输入流转换为字符输入流
  2. OutputStreamWriter:将字节输出流转换为字符输出流
  3. 可以指定编码格式,解决乱码问题
  4. 常与缓冲流配合使用,形成:BufferedReader -> InputStreamReader -> FileInputStream
  5. Java 11+ FileReader/FileWriter 已支持指定编码,但转换流仍有用武之地
    1. 处理非文件来源的字节流(如网络流、内存流)
    2. 需要精确控制编码转换场景

3.序列化流与反序列化流

  1. 对象序列化:将对象写入文件,需要实现 Serializable 接口
  2. 对象反序列化:从文件读取对象
  3. serialVersionUID:序列化版本号,确保反序列化时类版本一致
    1. 如果类结构发生变化(如新增字段),版本号不匹配会抛出 InvalidClassException
    2. 建议显式声明版本号,避免编译器自动生成导致的兼容性问题
  4. transient 关键字:修饰的成员变量不参与序列化,反序列化时为默认值
  5. 序列化集合对象:将整个集合写入文件,便于批量操作

4.压缩流与解压缩流

ZipEntry
  1. 表示压缩包里的每个文件或目录
  2. 压缩流与解压流中有一个当前条目的概念,就是你最近获取到的或创建的ZipEntry对象
  3. 所以必须要及时关闭当前条目
解压缩
  1. 使用 ZipInputStream 读取压缩包
  2. getNextEntry() 获取下一个 ZipEntry
  3. 判断是目录还是文件,分别处理
  4. 读取完成后调用 closeEntry() 关闭当前条目
压缩
  1. 使用 ZipOutputStream 写入压缩包
  2. putNextEntry() 创建新的 ZipEntry
  3. 递归处理目录结构,保持原有的层级关系
    1. 遍历目录下所有文件和子目录
    2. 文件则直接写入压缩条目
    3. 目录则递归调用,传入更新后的路径名
  4. 每个文件写入完成后调用 closeEntry()

5.打印流

  1. PrintStream:字节打印流,System.out 就是 PrintStream 的实例
  2. PrintWriter:字符打印流
  3. 特点
    1. 可以直接打印各种类型的数据(int、double、String、对象等)
    2. 可以设置自动刷新(autoFlush),调用 println() 时自动刷新缓冲区,换行。

6.URL网络读取

  1. 使用 URLURLConnection 建立网络连接
  2. 通过 getInputStream() 获取输入流
  3. 使用 InputStreamReader 按字符读取网页内容
  4. 可以配合第三方工具包(如 Hutool)简化文件写入操作

7.工具包

  1. commons-io
  2. hutool
  3. 两个都是第三方工具包,提供了很多包装好的工具,导入即可直接使用。

计划

网络编程,反射与动态代理

总结

1.网络编程

1.InetAddress
  1. 表示IP地址对象
  2. 获取方式
    • InetAddress.getByName("IP地址或主机名"):既可以传IP也可以传主机名
    • InetAddress.getLocalHost():获取本地主机
  3. 常用方法
    • getHostAddress():获取IP地址字符串
    • getHostName():获取主机名,会尝试反向DNS解析,如果失败返回IP字符串本身
    • getCanonicalHostName():获取完全限定域名(FQDN),进行更彻底的反向解析
  4. 注意事项
    • 通过IP获取InetAddress时,getHostName()可能返回IP(反向解析失败)
    • getByName()支持IPv4和IPv6地址
    • 如果参数是合法IP格式,不会发起DNS查询,直接构造对象
2.UDP通信
  1. 特点:无连接、不可靠、速度快
  2. 使用 DatagramSocketDatagramPacket
  3. 发送端
    • 创建 DatagramSocket
    • 封装 DatagramPacket(数据、目标IP、端口)
    • 调用 send() 发送
  4. 接收端
    • 创建 DatagramSocket(绑定端口)
    • 创建空的 DatagramPacket 用于接收
    • 调用 receive() 接收(阻塞式)
    • 通过 getData()getLength() 获取数据
  5. 组播:使用多播地址 224.0.0.1,接收端需调用 joinGroup() 加入组播
3.TCP通信
  1. 特点:面向连接、可靠、速度较慢
  2. 三次握手建立连接,四次挥手断开连接
  3. 服务器端
    • 创建 ServerSocket(绑定端口)
    • 调用 accept() 等待客户端连接(阻塞式)
    • 获取 Socket 对象进行通信
  4. 客户端
    • 创建 Socket(指定服务器IP和端口)
    • 连接成功后获取流进行通信
  5. 多次发送与反馈
    • 客户端循环发送数据,服务器循环接收
    • 添加反馈机制:服务器收到数据后返回确认信息
    • 使用 exit 关键字退出循环
  6. 关键问题与注意事项
    • 消息边界:TCP是流式传输,必须定义应用层协议(如换行符分隔),否则消息会粘连
    • readLine()阻塞:是阻塞方法,没数据会一直等,直到收到换行符、连接关闭或超时
    • read()返回值处理:返回int,流结束返回-1,强制转换为char会导致无法检测流结束(-1转为'\uffff'
    • flush():发送数据后需调用确保数据立即发出,避免缓冲延迟
    • socket.shutdownOutput():关闭输出流但保持连接,接收端会读到EOF;socket.close()完全关闭连接
    • setSoTimeout():设置读取超时,防止永久阻塞
4.TCP文件传输
  1. 客户端
    • 使用 FileInputStream 读取文件
    • 通过 BufferedInputStream 提高效率
    • 写入 OutputStream 发送到服务器
    • 发送完毕后调用 socket.shutdownOutput() 关闭输出流(告知服务器发送结束)
    • 读取服务器反馈信息
  2. 服务器端
    • 使用 BufferedInputStream 接收数据
    • 生成UUID作为文件名避免重复
    • 使用 BufferedOutputStream 写入文件
    • 发送反馈信息给客户端
5.多线程服务器
  1. 使用线程池处理多个客户端连接
  2. 创建 receiveFileTask 实现 Runnable 接口
  3. 将每个客户端的 Socket 封装为任务提交到线程池
  4. 服务器主循环持续接收新连接
  5. 关闭服务器时先关闭线程池再关闭 ServerSocket

2.反射

1.Class对象获取方式
  1. Class.forName("全类名"):最常用,解耦
  2. 类名.class:编译期确定
  3. 对象.getClass():运行时获取
  4. 同一个类的多个Class对象是同一个(类加载器只加载一次)
2.反射操作构造方法
  1. clazz.newInstance():调用无参构造创建对象
  2. clazz.getConstructor(参数类型...):获取public有参构造
  3. constructor.newInstance(参数值...):调用构造创建对象
3.反射操作字段
  1. clazz.getField("字段名"):只能获取public字段(包括父类继承的)
  2. clazz.getDeclaredField("字段名"):获取本类中所有访问级别的字段(包括private),不包括父类字段
  3. field.setAccessible(true):取消访问检查,必须在获取字段后调用
  4. field.get(对象):获取字段值
  5. field.set(对象, 值):设置字段值
4.反射操作方法
  1. clazz.getMethod("方法名", 参数类型...):获取public方法
  2. method.invoke(对象, 参数值...):调用方法并返回结果
  3. 可操作私有方法(需先调用 setAccessible(true)

3.动态代理

1.概念
  1. 代理模式:通过代理对象访问真实对象,增强功能
  2. 动态代理:在运行时动态生成代理类
2.实现方式
  1. 创建接口定义需要代理的方法
  2. 创建真实对象实现接口
  3. 使用 Proxy.newProxyInstance() 生成代理对象
  4. 参数
    • ClassLoader:类加载器
    • Class[] interfaces:代理类实现的接口
    • InvocationHandler:调用处理器
3.InvocationHandler
  1. 实现 invoke(Object proxy, Method method, Object[] args) 方法
  2. proxy:代理对象
  3. method:被调用的方法
  4. args:方法参数
  5. 在方法调用前后添加增强逻辑
  6. 调用 method.invoke(真实对象, args) 执行真实方法
4.代理类的本质(底层机制)
  1. 代理类是JVM在运行时动态生成的,类名格式为 com.sun.proxy.$Proxy0
  2. 继承 java.lang.reflect.Proxy,实现指定的接口(如Star
  3. 类修饰符为 public final,不能被继承
  4. 只有一个构造方法,接收 InvocationHandler 参数
  5. 所有方法体内只有一行逻辑:转发给 InvocationHandler.invoke()
5.JDK动态代理必须基于接口的原因
  1. Java是单继承语言,代理类必须继承 Proxy
  2. 因此无法再继承其他业务父类,只能通过实现接口来定义行为
6.代理对象与真实对象的区别
对比项 真实对象(如BigStar 代理对象(如$Proxy0
继承关系 直接实现接口 继承Proxy并实现接口
方法逻辑 包含具体业务代码 全部转发给InvocationHandler
类型检查 instanceof BigStartrue instanceof BigStarfalse
本质 真正的业务对象 业务对象的”替身”或”门卫”
7.应用场景
  1. 日志记录:记录方法调用信息
  2. 性能监控:统计方法执行耗时
  3. 事务管理:数据库操作前后开启/提交/回滚事务
  4. 权限控制:检查用户是否有权执行方法
  5. 缓存控制:第一次查询存缓存,后续直接取缓存
  6. 异常封装:将具体异常统一转为业务异常
  7. 远程调用(RPC):本地调用方法,实际通过网络发给远程服务器执行
  8. AOP(面向切面编程):解耦业务逻辑与通用增强逻辑

计划

多线程

总结

1.线程创建

3种方式
  1. 继承Thread
  2. 实现Runnable
  3. 实现Callable
特点:

3可以有返回值,1,2无返回值
1因为单继承,无法继承其他类

方法:

设置/获取线程名,获取当前线程名,出让,插入,睡眠等

2.同步安全问题

原因
  1. 大多操作系统对线程采用抢占式调度
  2. 线程之间会争夺CPU执行权
  3. 多个线程对同一资源进行操作的过程中,CUP执行权随时会被夺走,被其他线程占用,从而造成安全问题
解决方案
  1. 对操作共享资源的代码采用锁,只有线程拿到锁,才能进入,执行代码,操作相应资源。未拿到锁的线程即使拿到CPU执行权也无法进入。
  2. 同步代码块
    1. 对一段代码设置锁,锁对象可以自己指定
    2. 代码执行完锁会自动释放
  3. 同步方法
    1. 非静态方法以this作为锁对象
    2. 静态方法以类名.class做为锁对象
  4. Lock锁
    1. 需要手动设置锁与释放锁
    2. 为确保锁被释放,可将释放语句写进try的finally中
注意:
  1. CPU执行权的抢夺与锁的抢夺是独立的
    1. 线程只有拿到CPU执行权才可以去抢夺锁
    2. 线程即使抢到锁,也随时可能被夺走CPU执行权,但因为锁未释放,其他线程无法进入同步代码块,只有等下次锁被释放
  2. 避免锁的嵌套,防止出现死锁,即外层锁与内层锁被不同线程拿到,全部陷入阻塞

3.等待唤醒机制

流程
  1. 生产者产出资源,消费者消耗资源
  2. 生产者
    1. 有资源:等待
    2. 无资源
      1. 生产资源
      2. 唤醒正在等待的线程(消费者)
  3. 消费者
    1. 无资源:等待
    2. 有资源:
      1. 消耗资源
      2. 唤醒正在等待的进程(生产者)
细节
  1. 等待与唤醒都需要通过同个锁对象(任意类),唤醒的时候才能唤醒所有通过该锁对象进入等待的线程
  2. 进程进入等待时,释放锁,并交出CPU执行权。在被唤醒时,继续抢夺锁,拿到锁后继续执行后续语句。
实现
  1. 自己用同步代码块synchronized实现,对操作共享资源的代码设置锁
  2. 可以使用阻塞队列,阻塞队列内时线程安全的。内部也实现了等待与唤醒。没一个放入与取出操作时一定会成功执行的,无法放入进入等待,下次唤醒会继续放入。

4.线程池

意义
  1. 降低资源消耗:复用已创建的线程,避免频繁创建和销毁线程(new Thread + start())带来的性能开销
  2. 提高响应速度:任务到达时,无需等待线程创建,可直接使用空闲线程立即执行
  3. 提高线程可管理性:线程是稀缺资源,统一分配、调优和监控,防止无限制创建导致系统崩溃(OOM)
核心参数(ThreadPoolExecutor 的7个参数)
  1. 核心线程数(corePoolSize):线程池中一直存活的线程数,即使空闲也不会被回收(除非设置了allowCoreThreadTimeOut
  2. 最大线程数(maximumPoolSize):线程池中允许的最大线程数
  3. 存活时间(keepAliveTime):非核心线程空闲超过该时间会被回收
  4. 时间单位(unit)keepAliveTime 的时间单位(如 TimeUnit.SECONDS
  5. 阻塞队列(workQueue):存放等待执行的任务(如 ArrayBlockingQueueLinkedBlockingQueue
  6. 线程工厂(threadFactory):创建线程的工厂,可自定义线程名、优先级等(默认 Executors.defaultThreadFactory()
  7. 拒绝策略(handler):当线程池和队列都满时,对新提交的任务的处理方式
执行流程(任务提交后的流转)
  1. 核心线程:当前线程数 < 核心线程数 → 创建新线程执行任务
  2. 阻塞队列:核心线程已满 → 任务放入队列等待
  3. 最大线程:队列已满 → 创建非核心线程执行任务(直到达到最大线程数)
  4. 拒绝策略:最大线程也满了 → 执行拒绝策略

    注意:流程是 核心 → 队列 → 最大,而非很多人误以为的核心 → 最大 → 队列

拒绝策略(4种内置实现)
  1. AbortPolicy(默认):直接抛出 RejectedExecutionException,阻止系统正常运行
  2. CallerRunsPolicy:由调用者线程(提交任务的线程)自己执行该任务,提供反馈机制,减缓任务提交速度
  3. DiscardPolicy:直接丢弃任务,不抛出异常(丢失任务)
  4. DiscardOldestPolicy:丢弃队列中最旧的任务,然后重试提交当前任务
提交任务的两种方式
  1. execute(Runnable command)
    • 提交无返回值的任务
    • 异常直接抛出(控制台打印),调用者无法捕获
  2. submit(Callable/Runnable task)
    • 提交有返回值(Callable)或无返回值(Runnable)的任务
    • 返回 Future 对象,可通过 future.get() 获取结果或捕获异常
    • Runnable 提交时,Future.get() 返回 null
    • Runnable + 指定结果:submit(Runnable task, T result)Future.get() 返回指定的 result
注意
  1. 线程池中的线程是复用的:底层 Worker 通过 while 循环不断从队列中取任务执行,执行完一个任务后不会销毁,而是继续取下一个任务
  2. Thread 对象与内核线程Thread 对象只是堆内存中的普通对象,真正执行能力来自操作系统内核线程(LWP)。线程池复用的是内核线程,而不是 Thread 对象本身
  3. 不能直接操作池中某个线程:线程池封装了线程的个体操作,应通过 Future 控制任务(如 future.cancel(true) 中断任务),而非直接操作线程
  4. executesubmit 底层关系submit 底层也是调用 execute,但会将 Runnable/Callable 包装成 RunnableFuture(即 FutureTask),从而支持返回值和异常捕获
  5. 线程池关闭:使用 shutdown() 优雅关闭(不再接收新任务,等待已提交任务执行完毕),或 shutdownNow() 立即停止(尝试中断正在执行的任务,返回未执行的任务列表)