feat:更新文档架构
This commit is contained in:
33
docs/Web/FastAPI/FastAPI-Docker.md
Normal file
33
docs/Web/FastAPI/FastAPI-Docker.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: FastAPI Docker离线部署
|
||||
date: 2026-01-06
|
||||
---
|
||||
|
||||
# 一、安装依赖
|
||||
  通过pip将requirements.txt中的依赖包下载到本地:
|
||||
```sh
|
||||
pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
::: tip
|
||||
`--only-binary=:all:`表示只下载预编译的二进制包
|
||||
`--platform manylinux2014_x86_64`指定目标平台为 Linux
|
||||
`-i`指定pip镜像源源
|
||||
还可以加上`--python-version 310`指定python版本
|
||||
:::
|
||||
|
||||
  执行命令后,会在./packages中下载相应的二进制包。
|
||||
|
||||
# 二、Dockfile
|
||||
```dockerfile
|
||||
FROM python:3.10-slim
|
||||
WORKDIR /app
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' > /etc/timezone
|
||||
COPY ./packages /app/packages
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir --no-index --find-links=/app/packages -r requirements.txt
|
||||
COPY . /app/
|
||||
EXPOSE 8095
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8095"]
|
||||
```
|
||||
828
docs/Web/FastAPI/FastAPI-Guide.md
Normal file
828
docs/Web/FastAPI/FastAPI-Guide.md
Normal file
@@ -0,0 +1,828 @@
|
||||
---
|
||||
title: FastAPI基础教程
|
||||
date: 2025-12-10
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  [FastAPI](https://fastapi.tiangolo.com/zh/) 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 并基于标准的 Python 类型提示。
|
||||
  安装:
|
||||
```cmd
|
||||
pip install fastapi
|
||||
pip install uvicorn[standard] # ASGI服务器
|
||||
```
|
||||
|
||||
# 二、数据层
|
||||
## 2.1 数据库ORM SQLAlchemy
|
||||
### 2.1.1 配置与连接
|
||||
```python
|
||||
# database.py - 数据库配置
|
||||
from sqlalchemy import create_engine, MetaData
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from typing import Generator
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL = f"mysql+pymysql://{os.getenv("DATABASE_URL", "root:123456@127.0.0.1:3306/test")}"
|
||||
|
||||
# 创建引擎
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
echo=True, # 显示 SQL 语句(开发环境)
|
||||
pool_size=20, # 连接池大小
|
||||
max_overflow=40, # 最大溢出连接数
|
||||
pool_pre_ping=True, # 连接前 ping
|
||||
pool_recycle=3600, # 连接回收时间(秒)
|
||||
)
|
||||
|
||||
# 创建会话工厂
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine,
|
||||
expire_on_commit=False, # 提交后不使实例过期
|
||||
)
|
||||
|
||||
# 声明基类
|
||||
Base = declarative_base()
|
||||
|
||||
# 依赖注入:获取数据库会话
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
"""
|
||||
获取数据库会话
|
||||
使用 yield 确保会话正确关闭
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
```
|
||||
|
||||
::: warning
|
||||
需要把pool_pre_ping=True打开,否则会出现超过pool_recycle时间后,数据库断线。
|
||||
:::
|
||||
|
||||
### 2.1.2 数据库基类
|
||||
```python
|
||||
from sqlalchemy import Column, BigInteger, String, DateTime, event
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
from config.auth import context_sub
|
||||
from config.database import Base
|
||||
from datetime import datetime
|
||||
|
||||
from utils.common import camel_to_snake
|
||||
from id_generator import options, generator
|
||||
|
||||
# https://github.com/yitter/IdGenerator/tree/master/Python
|
||||
options = options.IdGeneratorOptions(worker_id=23)
|
||||
idgen = generator.DefaultIdGenerator()
|
||||
idgen.set_id_generator(options)
|
||||
|
||||
|
||||
# 第二层基类:包含ID
|
||||
class IdBase(Base):
|
||||
__abstract__ = True
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
# 自动把数据库实体类名驼峰转为数据库表名下划线
|
||||
return camel_to_snake(cls.__name__)
|
||||
|
||||
|
||||
# 自动填充id
|
||||
@event.listens_for(IdBase, 'before_insert', propagate=True)
|
||||
def before_insert_listener(mapper, connection, target):
|
||||
if target.id is None:
|
||||
target.id = idgen.next_id()
|
||||
|
||||
|
||||
# 第二层基类:包含ID和审计字段
|
||||
class AuditBase(IdBase):
|
||||
__abstract__ = True
|
||||
|
||||
create_time = Column(DateTime, nullable=True, default=datetime.now)
|
||||
create_by = Column(String(255), nullable=True)
|
||||
update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now)
|
||||
update_by = Column(String(255), nullable=True)
|
||||
|
||||
|
||||
@event.listens_for(AuditBase, 'before_insert', propagate=True)
|
||||
def before_insert(mapper, connection, target):
|
||||
value = context_sub.get(None)
|
||||
if value is not None:
|
||||
target.create_by = value
|
||||
|
||||
|
||||
@event.listens_for(AuditBase, 'before_update', propagate=True)
|
||||
def before_update(mapper, connection, target):
|
||||
value = context_sub.get(None)
|
||||
if value is not None:
|
||||
target.update_by = value
|
||||
```
|
||||
|
||||
  id采用yitter雪花id。
|
||||
  审计字段中的create_time和update_time通过SQLAlchemy参数配置。
|
||||
  create_by和update_by字段为登录认证中存储的账号信息。
|
||||
|
||||
### 2.1.3 数据库模型
|
||||
```python
|
||||
from sqlalchemy import (BigInteger, Boolean, Column, Integer, DECIMAL, String, LargeBinary)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.base import AuditBase, IdBase
|
||||
|
||||
|
||||
class Blog(AuditBase):
|
||||
title = Column(String(255), nullable=False, comment="博客标题")
|
||||
top_value = Column(Integer, nullable=False, default=0, comment="置顶值 越大越靠前")
|
||||
is_great = Column(Boolean, nullable=False, default=False, comment="是否是精品")
|
||||
category_id = Column(BigInteger, nullable=False, comment="博客类别")
|
||||
summary = Column(String(255), nullable=False, comment="博客内容概要")
|
||||
content_id = Column(BigInteger, nullable=False, comment="博客内容")
|
||||
word_count = Column(Integer, nullable=False, default=0, comment="字数统计")
|
||||
read_duration = Column(DECIMAL(10, 2), nullable=False, default=0.00, comment="阅读时长")
|
||||
is_approved = Column(Boolean, nullable=False, default=False, comment="是否发布")
|
||||
|
||||
# 分类关系(多对一)
|
||||
category = relationship(
|
||||
"BlogCategory",
|
||||
# 与BlogCategory的blogs属性建立双向关系
|
||||
back_populates="blogs",
|
||||
# 只级联保存和合并操作,不级联删除(删除博客不应删除分类
|
||||
cascade="save-update, merge",
|
||||
# 明确指定连接条件
|
||||
# 如果数据库设置了外键可以省略
|
||||
primaryjoin="foreign(Blog.category_id) == BlogCategory.id"
|
||||
)
|
||||
|
||||
# 内容关系(一对一)
|
||||
content = relationship(
|
||||
"BlogContent",
|
||||
# 与BlogContent的blog属性建立双向关系
|
||||
back_populates="blog",
|
||||
# 完全级联操作:保存、合并、刷新、删除等所有操作都会级联
|
||||
cascade="all, delete-orphan",
|
||||
# 设置为False表示一对一关系,返回单个对象而不是列表
|
||||
uselist=False,
|
||||
# 确保内容只有一个父博客,与delete-orphan配合使用
|
||||
single_parent=True,
|
||||
# 明确指定连接条件
|
||||
primaryjoin="foreign(Blog.content_id) == BlogContent.id"
|
||||
)
|
||||
|
||||
# 评论(一对多)
|
||||
comments = relationship(
|
||||
"BlogComment",
|
||||
# 与BlogComment的blog属性建立双向关系
|
||||
back_populates="blog",
|
||||
# 完全级联操作:博客删除时自动删除所有评论
|
||||
cascade="all, delete-orphan",
|
||||
# 明确指定连接条件
|
||||
primaryjoin="Blog.id == foreign(BlogComment.blog_id)"
|
||||
)
|
||||
|
||||
|
||||
class BlogCategory(AuditBase):
|
||||
name = Column(String(45), nullable=False, comment="类别名称")
|
||||
|
||||
blogs = relationship(
|
||||
"Blog",
|
||||
# 与Blog的category属性建立双向关系
|
||||
back_populates="category",
|
||||
# 完全级联操作:分类删除时自动删除所有关联的博客
|
||||
# 警告:这会级联删除分类下的所有博客,包括博客的内容、访问记录和评论
|
||||
cascade="all, delete-orphan",
|
||||
# 明确指定连接条件
|
||||
primaryjoin="BlogCategory.id == foreign(Blog.category_id)"
|
||||
)
|
||||
|
||||
|
||||
class BlogContent(IdBase):
|
||||
content = Column(LargeBinary, nullable=False, comment="博客内容")
|
||||
|
||||
blog = relationship(
|
||||
"Blog",
|
||||
# 与Blog的content属性建立双向关系
|
||||
back_populates="content",
|
||||
# 设置为False表示一对一关系
|
||||
uselist=False,
|
||||
# 明确指定连接条件
|
||||
primaryjoin="BlogContent.id == foreign(Blog.content_id)"
|
||||
)
|
||||
|
||||
|
||||
class BlogComment(AuditBase):
|
||||
blog_id = Column(BigInteger, nullable=False, comment="博客ID")
|
||||
parent_id = Column(BigInteger, nullable=False, comment="父评论ID")
|
||||
name = Column(String(255), nullable=False, comment="评论人昵称")
|
||||
website = Column(String(255), nullable=True, comment="评论人网站")
|
||||
ip_address = Column(String(45), nullable=False, comment="评论人IP")
|
||||
user_agent = Column(String(255), nullable=False, comment="评论人浏览器信息")
|
||||
content = Column(String(255), nullable=False, comment="评论内容")
|
||||
is_approved = Column(Boolean, nullable=False, default=False, comment="是否通过")
|
||||
|
||||
blog = relationship(
|
||||
"Blog",
|
||||
# 与Blog的comments属性建立双向关系
|
||||
back_populates="comments",
|
||||
# 明确指定连接条件
|
||||
primaryjoin="foreign(BlogComment.blog_id) == Blog.id"
|
||||
)
|
||||
```
|
||||
|
||||
  这里没有通过在数据库建立外键,而是通过SQLAlchemy中的relationship来建立。
|
||||
  其中primaryjoin明确指定了连接条件。如果为一对一,需要设置uselist=False和single_parent=True,返回单个对象而不是列表。
|
||||
  cascade表示级联操作:
|
||||
```
|
||||
# cascade 的完整可选值列表:
|
||||
|
||||
# 基本级联操作
|
||||
"save-update" # 保存/更新时级联
|
||||
"merge" # 合并会话时级联
|
||||
"refresh-expire" # 刷新过期对象时级联
|
||||
"expunge" # 从会话中移除时级联
|
||||
"delete" # 删除时级联
|
||||
"delete-orphan" # 成为孤儿时删除
|
||||
|
||||
# 快捷组合
|
||||
"all" # 包含除 delete-orphan 外的所有操作
|
||||
"all, delete-orphan" # 包含所有操作
|
||||
"none" # 禁用所有级联(默认)
|
||||
|
||||
# 其他组合
|
||||
"save-update, merge" # 常用组合
|
||||
"save-update, merge, delete" # 包含删除
|
||||
"save-update, merge, refresh-expire"
|
||||
```
|
||||
|
||||
## 2.2 数据验证pydantic
|
||||
```python
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BlogQuery(BaseModel):
|
||||
"""博客查询参数"""
|
||||
category: Optional[str] = Field(None, description="分类名称")
|
||||
title: Optional[str] = Field(None, description="标题关键词")
|
||||
year: Optional[int] = Field(None, description="发布年份", ge=2000, le=datetime.now().year)
|
||||
|
||||
@field_validator('year')
|
||||
def validate_year(cls, v):
|
||||
if v is not None and v > datetime.now().year:
|
||||
raise ValueError('年份不能超过当前年份')
|
||||
return v
|
||||
|
||||
|
||||
class BlogBase(BaseModel):
|
||||
"""博客基础模型"""
|
||||
title: str = Field(..., min_length=1, max_length=255, description="博客标题")
|
||||
top_value: int = Field(default=0, ge=0, description="置顶值,越大越靠前", alias="topValue")
|
||||
is_great: bool = Field(default=False, description="是否是精品", alias="isGreat")
|
||||
category: str = Field(..., min_length=1, max_length=45, description="分类名称")
|
||||
content: Optional[str] = Field(None, description="博客内容")
|
||||
is_approved: bool = Field(default=False, description="是否已发布", alias="isApproved")
|
||||
|
||||
@field_validator('title')
|
||||
def title_not_empty(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('标题不能为空')
|
||||
return v.strip()
|
||||
|
||||
@field_validator('category')
|
||||
def category_not_empty(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('分类不能为空')
|
||||
return v.strip()
|
||||
|
||||
|
||||
class BlogCreate(BlogBase):
|
||||
pass
|
||||
|
||||
|
||||
class BlogUpdate(BlogBase):
|
||||
pass
|
||||
|
||||
|
||||
class BlogResponse(BlogBase):
|
||||
id: int = Field(..., description="博客ID")
|
||||
summary: Optional[str] = Field(None, description="内容摘要")
|
||||
word_count: Optional[int] = Field(None, description="字数统计", alias="wordCount")
|
||||
read_duration: Optional[float] = Field(None, description="阅读时长", alias="readDuration")
|
||||
visit_count: Optional[int] = Field(0, description="访问次数", alias="visitCount")
|
||||
create_time: datetime = Field(..., description="创建时间", alias="createTime")
|
||||
update_time: datetime = Field(..., description="更新时间", alias="updateTime")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
populate_by_name=True,
|
||||
json_encoders={
|
||||
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BlogCategoryResponse(BaseModel):
|
||||
name: str = Field(..., description="分类名称")
|
||||
count: int = Field(..., description="博客数量")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class BlogStatsResponse(BaseModel):
|
||||
blog_count: int = Field(..., description="博客总数", alias="blogCount")
|
||||
category_count: int = Field(..., description="分类总数", alias="categoryCount")
|
||||
word_count: int = Field(..., description="总字数", alias="wordCount")
|
||||
|
||||
|
||||
class BlogVisitResponse(BaseModel):
|
||||
ip: str = Field(..., description="IP地址")
|
||||
os: str = Field(..., description="操作系统")
|
||||
browser: str = Field(..., description="浏览器")
|
||||
uri: str = Field(..., description="访问路径")
|
||||
title: str = Field(None, description="博客标题")
|
||||
visit_time: datetime = Field(..., description="访问时间", alias="visitTime")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
populate_by_name=True,
|
||||
json_encoders={
|
||||
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BlogLatestResponse(BaseModel):
|
||||
id: int = Field(..., description="博客ID")
|
||||
title: str = Field(..., description="博客标题")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class BlogAdjacentResponse(BaseModel):
|
||||
id: int = Field(..., description="博客ID")
|
||||
title: str = Field(..., description="博客标题")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class BlogCommentCreate(BaseModel):
|
||||
parent_id: int = Field(default=0, ge=0, description="父评论ID,0表示顶级评论", alias="parentId")
|
||||
name: str = Field(..., min_length=1, max_length=50, description="评论人昵称")
|
||||
website: Optional[str] = Field(None, description="评论人网站")
|
||||
content: str = Field(..., min_length=1, max_length=1000, description="评论内容")
|
||||
|
||||
@field_validator('name')
|
||||
def name_not_empty(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('昵称不能为空')
|
||||
return v.strip()
|
||||
|
||||
@field_validator('content')
|
||||
def content_not_empty(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('评论内容不能为空')
|
||||
return v.strip()
|
||||
|
||||
|
||||
class BlogCommentResponse(BlogCommentCreate):
|
||||
id: int = Field(..., description="评论ID")
|
||||
ip_address: str = Field(..., description="IP地址", alias="ipAddress")
|
||||
user_agent: str = Field(..., description="浏览器信息", alias="userAgent")
|
||||
create_time: datetime = Field(..., description="创建时间", alias="createTime")
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
populate_by_name=True,
|
||||
json_encoders={
|
||||
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
  `@field_validator`为Pydantic V2的验证器装饰器。
|
||||
  `ConfigDict`为全局配置类,`from_attributes=True`表示允许从SQLAlchemy等ORM对象创建。`populate_by_name=True`表示允许通过字段的别名(alias)来赋值 `json_encoders`表示自定义特定类型的 JSON 序列化方式。
|
||||
|
||||
# 三、服务层
|
||||
## 3.1 查询操作
|
||||
```python
|
||||
def query_blog_by_id(db: Session, blog_id: int) -> BlogResponse:
|
||||
check_blog_exist(db, blog_id)
|
||||
|
||||
stmt = (select(
|
||||
Blog.id,
|
||||
Blog.title,
|
||||
Blog.top_value,
|
||||
Blog.is_great,
|
||||
BlogCategory.name.label("category"),
|
||||
Blog.summary,
|
||||
BlogContent.content,
|
||||
Blog.word_count,
|
||||
Blog.read_duration,
|
||||
func.count(BlogVisit.id).label("visitCount"),
|
||||
Blog.is_approved,
|
||||
Blog.create_time,
|
||||
Blog.update_time
|
||||
).where(Blog.id == blog_id)
|
||||
.outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
|
||||
.outerjoin(BlogContent, Blog.content_id == BlogContent.id)
|
||||
.outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id))
|
||||
|
||||
blog = db.execute(stmt).first()
|
||||
|
||||
return BlogResponse.model_validate(blog)
|
||||
```
|
||||
|
||||
  条件和分页查询
|
||||
```python
|
||||
def query_blog_by_condition(db: Session, blog_query: BlogQuery) -> List[BlogResponse]:
|
||||
stmt = get_query_blog_by_condition_stmt(blog_query)
|
||||
results = db.execute(stmt).fetchall()
|
||||
return [BlogResponse.model_validate(result) for result in results]
|
||||
|
||||
|
||||
def get_query_blog_by_condition_stmt(blog_query: BlogQuery):
|
||||
stmt = (select(
|
||||
Blog.id,
|
||||
Blog.title,
|
||||
Blog.top_value,
|
||||
Blog.is_great,
|
||||
BlogCategory.name.label("category"),
|
||||
Blog.summary,
|
||||
BlogContent.content,
|
||||
Blog.word_count,
|
||||
Blog.read_duration,
|
||||
func.count(BlogVisit.id).label("visitCount"),
|
||||
Blog.is_approved,
|
||||
Blog.create_time,
|
||||
Blog.update_time
|
||||
).where(Blog.is_approved == 1)
|
||||
.outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
|
||||
.outerjoin(BlogContent, Blog.content_id == BlogContent.id)
|
||||
.outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id)
|
||||
.group_by(Blog.id))
|
||||
|
||||
conditions = []
|
||||
|
||||
if blog_query.category:
|
||||
conditions.append(BlogCategory.name == blog_query.category)
|
||||
|
||||
if blog_query.title:
|
||||
conditions.append(Blog.title.like(f"%{blog_query.title}%"))
|
||||
|
||||
if blog_query.year:
|
||||
conditions.append(func.extract('year', Blog.create_time) == blog_query.year)
|
||||
|
||||
if conditions:
|
||||
stmt = stmt.where(and_(*conditions))
|
||||
|
||||
stmt = stmt.order_by(desc(Blog.is_great), desc(Blog.update_time))
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def query_blog_by_condition_page(db: Session, blog_query: BlogQuery,
|
||||
current_page: int = 1, page_size: int = 10) -> PageResult[BlogResponse]:
|
||||
stmt = get_query_blog_by_condition_stmt(blog_query)
|
||||
return paginate_query(db, stmt, current_page, page_size)
|
||||
|
||||
def paginate_query(db: Session, query, current_page: int = 1, page_size: int = 10) -> PageResult:
|
||||
"""通用分页查询函数"""
|
||||
# 计算总记录数
|
||||
total = db.execute(select(func.count()).select_from(query.subquery())).scalar_one_or_none() or 0
|
||||
|
||||
# 计算总页数
|
||||
total_pages = (total + page_size - 1) // page_size if page_size != 0 else 0
|
||||
|
||||
# 执行分页查询
|
||||
results = db.execute(query.offset((current_page - 1) * page_size).limit(page_size)).all()
|
||||
|
||||
# 转换为字典列表
|
||||
records = [row._asdict() if hasattr(row, "_asdict") else dict(row) for row in results]
|
||||
|
||||
return PageResult(
|
||||
current=current_page,
|
||||
size=page_size,
|
||||
total=total,
|
||||
pages=total_pages,
|
||||
records=records
|
||||
)
|
||||
```
|
||||
|
||||
## 3.2 新增操作
|
||||
```python
|
||||
def add_blog(db: Session, blog: BlogCreate) -> bool:
|
||||
word_count = get_word_count(blog.content)
|
||||
|
||||
db_blog = Blog(
|
||||
title=blog.title,
|
||||
top_value=blog.top_value,
|
||||
is_great=blog.is_great,
|
||||
category_id=add_blog_category(db, blog.category),
|
||||
summary=get_blog_summary(blog.content),
|
||||
word_count=word_count,
|
||||
read_duration=get_read_duration(word_count),
|
||||
is_approved=blog.is_approved
|
||||
)
|
||||
|
||||
# 级联新增
|
||||
db_blog.content = BlogContent(content=blog.content.encode('utf-8'))
|
||||
|
||||
db.add(db_blog)
|
||||
db.commit()
|
||||
db.refresh(db_blog)
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
  这里的`db_blog.content`可以直接赋值`BlogContent`对象实现级联新增,不用传`content_id`。
|
||||
::: warning
|
||||
这里的category和blog是多对一的关系,且需要一定的逻辑处理,不能直接赋值`BlogCategory`对象,否则每新增一条博客都会新增一个`category`。
|
||||
:::
|
||||
|
||||
## 3.3 更新操作
|
||||
```python
|
||||
def update_blog(db: Session, blog_id: int, blog: BlogUpdate) -> bool:
|
||||
db_blog = check_blog_exist(db, blog_id)
|
||||
|
||||
db_blog.title = blog.title
|
||||
db_blog.top_value = blog.topValue
|
||||
db_blog.is_great = blog.isGreat
|
||||
db_blog.category_id = update_blog_category(db, blog.category)
|
||||
|
||||
# 级联更新
|
||||
db_blog.content = BlogContent(content=blog.content.encode('utf-8'))
|
||||
|
||||
word_count = get_word_count(blog.content)
|
||||
db_blog.summary = get_blog_summary(blog.content),
|
||||
db_blog.word_count = word_count
|
||||
db_blog.read_duration = get_read_duration(word_count)
|
||||
db_blog.is_approved = blog.isApproved
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_blog)
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## 3.4 删除操作
|
||||
```python
|
||||
def delete_blog(db: Session, blog_id: int) -> bool:
|
||||
db_blog = check_blog_exist(db, blog_id)
|
||||
|
||||
# 级联删除
|
||||
db.delete(db_blog)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
# 四、路由层
|
||||
```python
|
||||
router = APIRouter(
|
||||
prefix="/blog",
|
||||
tags=["博客管理"],
|
||||
responses={404: {"description": "Not found"}}
|
||||
)
|
||||
|
||||
@router.get("/page", summary="分页查询博客", response_model=PageResult[BlogResponse])
|
||||
def query_blog_by_page(
|
||||
current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码,从1开始"),
|
||||
page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页显示数量,1-100之间"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return blog_service.query_blog_by_page(db, current_page, page_size)
|
||||
|
||||
@router.get("/condition", summary="条件查询博客", response_model=List[BlogResponse])
|
||||
def query_blog_by_condition(query: BlogQuery = Depends(), db: Session = Depends(get_db)):
|
||||
return blog_service.query_blog_by_condition(db, query)
|
||||
|
||||
@router.post("", summary="新增博客内容", response_model=bool)
|
||||
def add_blog(
|
||||
blog: BlogCreate = Body(..., description="博客创建数据"),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(verify_token)
|
||||
):
|
||||
return blog_service.add_blog(db, blog)
|
||||
|
||||
@router.put("/{blog_id}", summary="更新博客内容", response_model=bool)
|
||||
def update_blog(
|
||||
blog_id: int = Path(..., ge=1, description="博客ID"),
|
||||
blog: BlogUpdate = Body(..., description="博客更新数据"),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(verify_token)
|
||||
):
|
||||
return blog_service.update_blog(db, blog_id, blog)
|
||||
|
||||
@router.delete("/{blog_id}", summary="删除博客内容", response_model=bool)
|
||||
def delete_blog(
|
||||
blog_id: int = Path(..., ge=1, description="博客ID"),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(verify_token)
|
||||
):
|
||||
return blog_service.delete_blog(db, blog_id)
|
||||
|
||||
@router.put("/{blog_id}/comment", summary="新增博客评论", response_model=bool)
|
||||
def add_blog_comment(
|
||||
request: Request,
|
||||
blog_id: int = Path(..., ge=1, description="博客ID"),
|
||||
blog_comment: BlogCommentCreate = Body(..., description="博客评论数据"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return blog_service.add_blog_comment(db, request, blog_id, blog_comment)
|
||||
```
|
||||
|
||||
  路由装饰器参数中的`response_model`表示定义接口返回的数据模型。
|
||||
  参数注解中的`Query()`表示参数来自URL查询字符串,`Path/Query/Body`分别对应路径参数、查询参数、请求体参数。
|
||||
  `Depends()`表示依赖注入,自动解析参数或执行依赖函数。
|
||||
  `db: Session = Depends(get_db)`表示获取数据库连接,`_=Depends(verify_token)`表示验证用户身份。
|
||||
  `request: Request`可以获取到HTTP请求的完整上下文信息。
|
||||
|
||||
::: tip
|
||||
这里的`query: BlogQuery = Depends()`会从查询参数中自动实例化`BlogQuery`对象,并进行数据验证和类型转换。
|
||||
:::
|
||||
|
||||
  注册路由:
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from .blog import router as blog_router
|
||||
|
||||
def register_routers(app: FastAPI):
|
||||
app.include_router(blog_router, prefix="")
|
||||
|
||||
|
||||
# main.py
|
||||
app = FastAPI(title="Blog Service")
|
||||
register_routers(app)
|
||||
```
|
||||
|
||||
# 五、中间件
|
||||
## 5.1 全局异常处理器
|
||||
```python
|
||||
from fastapi import Request, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from config.logging import logger
|
||||
|
||||
|
||||
# 自定义异常类
|
||||
class AppException(Exception):
|
||||
def __init__(self, message: str, details=None):
|
||||
self.message = message
|
||||
self.details = details
|
||||
|
||||
|
||||
# 全局异常处理中间件
|
||||
async def global_exception_handler(request: Request, call_next):
|
||||
try:
|
||||
# 记录请求信息(可选)
|
||||
logger.info(f"请求: {request.method} {request.url}")
|
||||
if request.query_params:
|
||||
logger.info(f"查询参数: {dict(request.query_params)}")
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
# 记录响应信息(可选)
|
||||
if response.status_code >= 400:
|
||||
logger.warning(f"响应: {response.status_code}")
|
||||
|
||||
return response
|
||||
|
||||
except AppException as e:
|
||||
# 记录业务异常
|
||||
logger.error(f"业务异常: {e.message} - 详情: {e.details}")
|
||||
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"message": e.message, "details": e.details})
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
# 记录数据库异常
|
||||
logger.critical(f"数据库异常: {str(e)}")
|
||||
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"message": "数据库操作失败", "details": str(e)})
|
||||
|
||||
except Exception as e:
|
||||
# 记录未知异常(带堆栈信息)
|
||||
logger.critical(f"未知异常: {str(e)}")
|
||||
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"code": 500, "message": "服务器内部错误", "details": str(e)})
|
||||
|
||||
|
||||
def get_credentials_exception() -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
```
|
||||
|
||||
## 5.2 日志处理器
|
||||
```python
|
||||
import atexit
|
||||
import sys
|
||||
|
||||
from fluent import sender
|
||||
from loguru import logger
|
||||
|
||||
from config.setting import settings
|
||||
|
||||
FLUENTD_HOST = settings.FLUENTD_HOST
|
||||
FLUENTD_PORT = 24224
|
||||
TOPIC_TAG = 'blog-service'
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL = settings.LOG_LEVEL.upper()
|
||||
|
||||
# 日志格式
|
||||
STDOUT_FORMAT = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
||||
"<level>{message}</level>"
|
||||
)
|
||||
|
||||
FILE_FORMAT = (
|
||||
"{time:YYYY-MM-DD HH:mm:ss.SSS} | "
|
||||
"{level: <8} | "
|
||||
"{name}:{function}:{line} - {message}"
|
||||
)
|
||||
|
||||
fluent_sender = sender.FluentSender(
|
||||
tag=TOPIC_TAG,
|
||||
host=FLUENTD_HOST,
|
||||
port=FLUENTD_PORT,
|
||||
buffer_max_size=8 * 1024 * 1024,
|
||||
timeout=3.0,
|
||||
retry_timeout=60
|
||||
)
|
||||
|
||||
|
||||
def log_to_fluent(message):
|
||||
try:
|
||||
record = message.record
|
||||
|
||||
# 构建结构化日志数据
|
||||
log_data = {
|
||||
'topic': TOPIC_TAG,
|
||||
'timestamp': record['time'].timestamp(),
|
||||
'level': record['level'].name.lower(),
|
||||
'message': record['message'],
|
||||
'source': f"{record['file'].path}:{record['line']}",
|
||||
'module': record['module'],
|
||||
'function': record['function'],
|
||||
'process_id': record['process'].id,
|
||||
'thread_id': record['thread'].id,
|
||||
**record['extra']
|
||||
}
|
||||
|
||||
if not fluent_sender.emit(TOPIC_TAG, log_data):
|
||||
print(f"Fluentd 发送失败: {fluent_sender.last_error}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"日志处理异常: {str(e)}")
|
||||
|
||||
|
||||
# 移除默认处理器
|
||||
logger.remove()
|
||||
|
||||
# 添加控制台处理器
|
||||
logger.add(
|
||||
sink=sys.stdout,
|
||||
level=LOG_LEVEL,
|
||||
format=STDOUT_FORMAT,
|
||||
colorize=True,
|
||||
backtrace=True, # 显示完整异常堆栈
|
||||
diagnose=True, # 显示详细异常信息
|
||||
)
|
||||
|
||||
if settings.ENVIRONMENT == 'docker':
|
||||
logger.add(
|
||||
log_to_fluent,
|
||||
level=LOG_LEVEL, # 处理 INFO 及以上级别
|
||||
format="{message}", # 原始消息(实际使用结构化数据)
|
||||
backtrace=True, # 启用堆栈回溯
|
||||
diagnose=True # 显示诊断信息
|
||||
)
|
||||
|
||||
atexit.register(fluent_sender.close)
|
||||
|
||||
# 导出配置好的logger
|
||||
__all__ = ["logger"]
|
||||
```
|
||||
170
docs/Web/FastAPI/OAuth2.md
Normal file
170
docs/Web/FastAPI/OAuth2.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: 基于OAuth2的FastApi安全验证
|
||||
date: 2025-11-29
|
||||
---
|
||||
|
||||
# 一、基础概念
|
||||
## 1.1 OAuth2 Password Bearer 模式
|
||||
  用于**用户名+密码**登录,获取**access_token**。
|
||||
|
||||
## 1.2 FastAPI 的 OAuth2PasswordBearer
|
||||
  从请求头**Authorization**中提取Token,Token格式为 **Bearer Token**,必须是这个格式,如果不是则会提示**401 Unauthorized**错误。
|
||||
|
||||
# 二、核心流程
|
||||
## 2.1 获取Token
|
||||
```python
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="session")
|
||||
|
||||
@app.post("/session")
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
# 1. 验证用户名和密码
|
||||
# 2. 返回 Token
|
||||
return Token
|
||||
```
|
||||
|
||||
   这里的**tokenUrl="session"** 对应的是fastapi中的url路径(@app.post("/session")),用于Swagger文档中的Token认证。
|
||||
   前端必须要以FormData表单的形式传递username和password,且必须是username和password字段。
|
||||
```ts
|
||||
const data = new FormData()
|
||||
data.append('username', username)
|
||||
data.append('password', password)
|
||||
```
|
||||
   在获取Token前,一般还需要进行验证用户名和密码是否和数据库中的信息一致。
|
||||
   可以采用JWT格式封装TokenValue:
|
||||
```python
|
||||
def create_token(payload: dict, expires_delta: Optional[timedelta] = None):
|
||||
# 复制一份
|
||||
payload_copy = payload.copy()
|
||||
|
||||
# 加上有效时间
|
||||
if expires_delta:
|
||||
expire = datetime.now() + expires_delta
|
||||
else:
|
||||
expire = datetime.now() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
payload_copy.update({"exp": expire})
|
||||
|
||||
# 生成jwt Token
|
||||
return jwt.encode(payload_copy, SECRET_KEY, algorithm=ALGORITHM)
|
||||
```
|
||||
|
||||
## 2.2 访问接口
|
||||
```python
|
||||
@router.delete("/{blog_id}", summary="删除博客内容", response_model=bool)
|
||||
def delete_blog(blog_id: int, db: Session = Depends(get_db), _ = Depends(verify_token)):
|
||||
return blog_service.delete_blog(db, blog_id)
|
||||
```
|
||||
   例如访问这个删除接口,在参数中添加验证token的依赖:
|
||||
```python
|
||||
async def verify_token(token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
# 1. 检验token信息
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
# 2. 校验payload信息
|
||||
if not verify_payload(payload):
|
||||
raise get_credentials_exception()
|
||||
# 3. 校验数据库中是否存在 payload中的账户信息
|
||||
sub = get_payload_sub(payload)
|
||||
|
||||
# 4. 存储账户信息
|
||||
context_sub.set(sub)
|
||||
except jwt.exceptions.InvalidTokenError:
|
||||
raise get_credentials_exception()
|
||||
```
|
||||
   该依赖又依赖于子依赖oauth2_scheme,通过调用OAuth2PasswordBearer方法从请求头Authorization中获取token值。
|
||||
   校验Token通常包含校验格式是否正确和Token包含的账户信息是否正确。
|
||||
|
||||
# 三、参考代码
|
||||
```python
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
import jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from middleware.exceptions import get_credentials_exception
|
||||
|
||||
# 密钥和算法配置
|
||||
SECRET_KEY = "sjdi@!#3ksj2780se1283"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
|
||||
# 密码哈希上下文
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# OAuth2 方案
|
||||
# 设置默认登录接口
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="session")
|
||||
|
||||
# 请求上下文
|
||||
context_sub: ContextVar[str] = ContextVar('sub')
|
||||
|
||||
|
||||
# 获取数据库 加密密码
|
||||
def get_password_hash(password: str):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
# 验证数据库密码
|
||||
def verify_password(plain_password: str, hashed_password: str):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
# 生成payload
|
||||
def create_payload(sub: str) -> dict:
|
||||
return {
|
||||
"sub": sub
|
||||
}
|
||||
|
||||
|
||||
# 验证payload
|
||||
def verify_payload(payload: dict) -> bool:
|
||||
return "sub" in payload
|
||||
|
||||
|
||||
# 从payload中获取用户
|
||||
def get_payload_sub(payload: dict) -> str:
|
||||
return payload["sub"]
|
||||
|
||||
|
||||
# 创建token
|
||||
def create_token(payload: dict, expires_delta: Optional[timedelta] = None):
|
||||
# 复制一份
|
||||
payload_copy = payload.copy()
|
||||
|
||||
# 加上有效时间
|
||||
if expires_delta:
|
||||
expire = datetime.now() + expires_delta
|
||||
else:
|
||||
expire = datetime.now() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
payload_copy.update({"exp": expire})
|
||||
|
||||
# 生成jwt Token
|
||||
return jwt.encode(payload_copy, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
# 验证token
|
||||
async def verify_token(token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
# 1. 检验token信息
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
# 2. 校验payload信息
|
||||
if not verify_payload(payload):
|
||||
raise get_credentials_exception()
|
||||
# 3. 校验数据库中是否存在 payload中的账户信息
|
||||
sub = get_payload_sub(payload)
|
||||
|
||||
# 4. 存储账户信息
|
||||
context_sub.set(sub)
|
||||
except jwt.exceptions.InvalidTokenError:
|
||||
raise get_credentials_exception()
|
||||
```
|
||||
|
||||
# 四、注意事项
|
||||
1. 如果整个路由模块都需要Token验证,可以在APIRouter中添加依赖:
|
||||
|
||||
```python
|
||||
protected_router = APIRouter(dependencies=[Depends(verify_token)])
|
||||
```
|
||||
377
docs/Web/Flutter/DartGuide.md
Normal file
377
docs/Web/Flutter/DartGuide.md
Normal file
@@ -0,0 +1,377 @@
|
||||
---
|
||||
title: Dart基础教程
|
||||
date: 2025-12-05
|
||||
---
|
||||
|
||||
# 一、基础语法
|
||||
## 1.1 变量与常量
|
||||
```dart
|
||||
// 显式声明类型
|
||||
String name = 'Dart';
|
||||
int age = 10;
|
||||
|
||||
// 类型推断(使用 var)
|
||||
var city = 'Beijing'; // 自动推断为 String
|
||||
var count = 100; // 自动推断为 int
|
||||
|
||||
// 延迟初始化
|
||||
String? email; // 可空类型
|
||||
late String description; // 延迟初始化
|
||||
|
||||
const PI = 3.14159;
|
||||
const List<int> nums = [1, 2, 3]; // 不可修改的列表
|
||||
|
||||
final currentTime = DateTime.now(); // 运行时才知道值
|
||||
final List<int> numbers = [1, 2, 3];
|
||||
numbers.add(4); // 可以!列表内容可修改
|
||||
|
||||
dynamic value = 'Hello';
|
||||
value = 123; // 可以
|
||||
value = true; // 可以
|
||||
|
||||
// 非空类型(默认)
|
||||
String name = 'Dart';
|
||||
// name = null; // 错误!
|
||||
|
||||
// 可空类型(使用 ?)
|
||||
String? nickname;
|
||||
nickname = null; // 正确
|
||||
|
||||
// 空值断言(!)
|
||||
String? value = 'Hello';
|
||||
print(value!.length); // 确信不为null时使用
|
||||
|
||||
// 空值合并运算符(??)
|
||||
String? name;
|
||||
String displayName = name ?? 'Guest'; // name为null时使用'Guest'
|
||||
|
||||
// 条件访问(?.)
|
||||
String? text;
|
||||
print(text?.length); // text为null时返回null,不报错
|
||||
```
|
||||
|
||||
  const常量:值在编译期就确定,对象及其内容都不可变。
|
||||
  final常量:值在运行时确定,但只能赋值一次,引用不可变,但对象内容可能可变。
|
||||
|
||||
## 1.2 数据类型
|
||||
```dart
|
||||
// 单引号或双引号
|
||||
String str1 = 'Hello';
|
||||
String str2 = "World";
|
||||
|
||||
// 字符串插值
|
||||
String name = 'Dart';
|
||||
int version = 3;
|
||||
print('$name $version');
|
||||
print('${name version}');
|
||||
|
||||
// int - 整数
|
||||
int count = 10;
|
||||
int hex = 0xDEADBEEF;
|
||||
|
||||
// double - 浮点数
|
||||
double price = 19.99;
|
||||
double exponent = 1.42e5; // 科学计数法
|
||||
|
||||
// num - int 和 double 的父类
|
||||
num value1 = 10;
|
||||
num value2 = 10.5;
|
||||
|
||||
// 常用方法
|
||||
int a = 5;
|
||||
double b = 3.14159;
|
||||
print(b.toStringAsFixed(2)); // 3.14
|
||||
print(int.parse('42')); // 字符串转int
|
||||
print(double.parse('3.14')); // 字符串转double
|
||||
|
||||
bool isActive = true;
|
||||
bool isCompleted = false;
|
||||
|
||||
// 创建列表
|
||||
List<int> numbers = [1, 2, 3, 4, 5];
|
||||
var fruits = ['apple', 'banana', 'orange'];
|
||||
|
||||
// 访问元素
|
||||
print(numbers[0]); // 1
|
||||
print(numbers.length); // 5
|
||||
|
||||
// 添加元素
|
||||
numbers.add(6);
|
||||
numbers.addAll([7, 8, 9]);
|
||||
|
||||
// 删除元素
|
||||
numbers.remove(3); // 删除值为3的元素
|
||||
numbers.removeAt(0); // 删除索引0的元素
|
||||
|
||||
// 常用方法
|
||||
print(numbers.first); // 第一个元素
|
||||
print(numbers.last); // 最后一个元素
|
||||
print(numbers.isEmpty); // 是否为空
|
||||
numbers.forEach((num) => print(num)); // 遍历
|
||||
|
||||
// 不可变列表
|
||||
const fixedList = [1, 2, 3];
|
||||
|
||||
// 扩展运算符
|
||||
var list1 = [1, 2, 3];
|
||||
var list2 = [0, ...list1]; // [0, 1, 2, 3]
|
||||
|
||||
// 创建 Map
|
||||
Map<String, int> ages = {
|
||||
'Alice': 25,
|
||||
'Bob': 30,
|
||||
'Charlie': 35
|
||||
};
|
||||
|
||||
// 访问元素
|
||||
print(ages['Alice']); // 25
|
||||
|
||||
// 添加/修改元素
|
||||
ages['David'] = 28;
|
||||
ages['Alice'] = 26;
|
||||
|
||||
// 删除元素
|
||||
ages.remove('Bob');
|
||||
|
||||
// 常用方法
|
||||
print(ages.keys); // 所有键
|
||||
print(ages.values); // 所有值
|
||||
print(ages.length); // 元素个数
|
||||
print(ages.isEmpty); // 是否为空
|
||||
print(ages.containsKey('Alice')); // 是否包含键
|
||||
|
||||
// 遍历
|
||||
ages.forEach((key, value) {
|
||||
print('$key: $value');
|
||||
});
|
||||
```
|
||||
|
||||
# 二、函数
|
||||
```dart
|
||||
// 简写(箭头函数,适用于单行表达式)
|
||||
String greet2(String name) => 'Hello, $name!';
|
||||
|
||||
// 必传参数
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// 可选位置参数(用 [] 包裹,可提供默认值)
|
||||
String introduce(String name, [int? age, String city = 'Beijing']) {
|
||||
if (age != null) {
|
||||
return '$name, $age years old, from $city';
|
||||
}
|
||||
return '$name from $city';
|
||||
}
|
||||
|
||||
// 调用示例
|
||||
print(introduce('Alice')); // Alice from Beijing
|
||||
print(introduce('Bob', 25)); // Bob, 25 years old, from Beijing
|
||||
print(introduce('Charlie', 30, 'Shanghai')); // Charlie, 30 years old, from Shanghai
|
||||
|
||||
// 命名参数(用 {} 包裹)
|
||||
void createUser({
|
||||
required String name, // required 表示必传
|
||||
int age = 18, // 有默认值
|
||||
String? email // 可选,可为null
|
||||
}) {
|
||||
print('Name: $name, Age: $age, Email: $email');
|
||||
}
|
||||
|
||||
// 调用时使用参数名
|
||||
createUser(name: 'Alice');
|
||||
createUser(name: 'Bob', age: 25, email: 'bob@example.com');
|
||||
|
||||
// 命名参数的优点:顺序无关,更清晰
|
||||
void setStyle({String? color, double? size, bool? bold}) {
|
||||
// ...
|
||||
}
|
||||
setStyle(bold: true, color: 'red'); // 顺序可以任意
|
||||
```
|
||||
|
||||
# 三、类
|
||||
```dart
|
||||
class BankAccount {
|
||||
String accountNumber; // 公有属性
|
||||
double _balance; // 私有属性(以_开头)
|
||||
|
||||
BankAccount(this.accountNumber, this._balance);
|
||||
|
||||
// 公有方法
|
||||
double getBalance() {
|
||||
return _balance;
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
void _updateBalance(double amount) {
|
||||
_balance += amount;
|
||||
}
|
||||
|
||||
void deposit(double amount) {
|
||||
if (amount > 0) {
|
||||
_updateBalance(amount);
|
||||
}
|
||||
}
|
||||
|
||||
// Getter
|
||||
double get balance => _balance;
|
||||
|
||||
// Setter
|
||||
set balance(double value) {
|
||||
if (value >= 0) {
|
||||
_balance = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
var account = BankAccount('123456', 1000);
|
||||
print(account.balance); // 使用getter:1000
|
||||
account.deposit(500);
|
||||
print(account.balance); // 1500
|
||||
account.balance = 2000; // 使用setter
|
||||
```
|
||||
|
||||
# 四、任务
|
||||
```dart
|
||||
void eventLoopExample() {
|
||||
print("主线程开始");
|
||||
|
||||
// 微任务
|
||||
scheduleMicrotask(() {
|
||||
print("微任务1");
|
||||
});
|
||||
|
||||
// 异步任务
|
||||
Future.delayed(Duration(seconds: 0), () {
|
||||
print("异步任务1");
|
||||
});
|
||||
|
||||
// 再次添加微任务
|
||||
scheduleMicrotask(() {
|
||||
print("微任务2");
|
||||
});
|
||||
|
||||
print("主线程结束");
|
||||
}
|
||||
|
||||
//输出结果:
|
||||
主线程开始
|
||||
主线程结束
|
||||
微任务1
|
||||
微任务2
|
||||
异步任务1
|
||||
```
|
||||
|
||||
  在 Dart 的事件循环中,执行顺序如下:
|
||||
1. 同步代码:首先执行所有同步代码。
|
||||
2. 微任务队列:然后依次执行微任务队列中的所有微任务。
|
||||
3. 事件队列:最后执行事件队列中的异步任务。
|
||||
|
||||
# 五、异步编程
|
||||
```dart
|
||||
// 1. Future() 构造函数
|
||||
Future<int> calculateSum(int a, int b) {
|
||||
return Future(() {
|
||||
return a + b;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Future.value() - 立即完成
|
||||
Future<String> immediateFuture = Future.value("立即结果");
|
||||
|
||||
// 3. Future.error() - 立即失败
|
||||
Future<void> errorFuture = Future.error("错误信息");
|
||||
|
||||
// 4. Future.delayed() - 延迟执行
|
||||
Future<String> delayedFuture = Future.delayed(
|
||||
Duration(seconds: 3),
|
||||
() => "延迟结果"
|
||||
);
|
||||
|
||||
// 5. Future.sync() - 同步执行
|
||||
Future<int> syncFuture = Future.sync(() => 42);
|
||||
|
||||
// 从网络请求
|
||||
Future<http.Response> fetchUserData() async {
|
||||
return await http.get(Uri.parse('https://api.example.com/user'));
|
||||
}
|
||||
|
||||
// 文件操作
|
||||
Future<File> writeToFile(String content) async {
|
||||
final file = File('data.txt');
|
||||
return await file.writeAsString(content);
|
||||
}
|
||||
```
|
||||
|
||||
  其他用法:
|
||||
```dart
|
||||
Future<void> processData() async {
|
||||
try {
|
||||
print("开始获取数据...");
|
||||
|
||||
// 等待第一个 Future
|
||||
final data1 = await fetchDataFromSource1();
|
||||
print("数据1: $data1");
|
||||
|
||||
// 等待第二个 Future
|
||||
final data2 = await fetchDataFromSource2();
|
||||
print("数据2: $data2");
|
||||
|
||||
// 处理结果
|
||||
final result = await processCombinedData(data1, data2);
|
||||
print("最终结果: $result");
|
||||
|
||||
} catch (e) {
|
||||
print("处理过程中出错: $e");
|
||||
} finally {
|
||||
print("清理资源");
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Future.wait() - 等待所有完成
|
||||
Future<void> fetchAllData() async {
|
||||
final futures = [
|
||||
fetchUserData(),
|
||||
fetchProductData(),
|
||||
fetchOrderData(),
|
||||
];
|
||||
|
||||
try {
|
||||
final results = await Future.wait(futures);
|
||||
print("所有数据获取完成: $results");
|
||||
} catch (e) {
|
||||
print("部分请求失败: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Future.any() - 第一个完成
|
||||
Future<String> getFastestResponse() {
|
||||
return Future.any([
|
||||
fetchFromServer1(),
|
||||
fetchFromServer2(),
|
||||
fetchFromServer3(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Future.forEach() - 顺序执行
|
||||
Future<void> processItems(List<String> items) async {
|
||||
await Future.forEach(items, (item) async {
|
||||
await processItem(item);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 链式操作
|
||||
Future<String> complexOperation() {
|
||||
return authenticateUser()
|
||||
.then((token) => fetchUserProfile(token))
|
||||
.then((profile) => updateProfile(profile))
|
||||
.then((updated) => saveToDatabase(updated))
|
||||
.then((savedId) => "操作完成,ID: $savedId")
|
||||
.catchError((error) {
|
||||
// 统一错误处理
|
||||
print("链式操作失败: $error");
|
||||
return "默认值";
|
||||
});
|
||||
}
|
||||
```
|
||||
248
docs/Web/Flutter/FlutterGuide.md
Normal file
248
docs/Web/Flutter/FlutterGuide.md
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
title: Flutter基础教程
|
||||
date: 2025-12-05
|
||||
---
|
||||
|
||||
# 应用核心Widget
|
||||
```dart
|
||||
// Flutter 应用的入口,配置主题、路由、国际化等。
|
||||
MaterialApp(
|
||||
home: HomePage(), // 必需:首页
|
||||
theme: ThemeData.light(), // 主题
|
||||
routes: {'/details': (c) => DetailsPage()}, // 路由
|
||||
debugShowCheckedModeBanner: false, // 隐藏调试条
|
||||
)
|
||||
|
||||
// 页面骨架,包含顶部栏、内容区、悬浮按钮等标准组件。
|
||||
Scaffold(
|
||||
appBar: AppBar(title: Text('标题')), // 顶部栏
|
||||
body: Center(child: Text('内容')), // 主体
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {}, // 悬浮按钮
|
||||
),
|
||||
drawer: Drawer(child: Text('侧边栏')), // 抽屉
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: [], // 底部导航
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
# 布局Widget
|
||||
```dart
|
||||
Container(
|
||||
width: 100,
|
||||
height: 50,
|
||||
margin: EdgeInsets.all(10), // 外边距
|
||||
padding: EdgeInsets.all(20), // 内边距
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue, // 背景色
|
||||
borderRadius: BorderRadius.circular(10), // 圆角
|
||||
border: Border.all(color: Colors.black), // 边框
|
||||
),
|
||||
child: Text('内容'),
|
||||
)
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主轴对齐
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // 交叉轴对齐
|
||||
children: [Text('左'),Text('中'),Text('右')],
|
||||
)
|
||||
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: Container(color: Colors.red)), // 扩展
|
||||
Flexible(child: Container(color: Colors.blue)), // 灵活
|
||||
],
|
||||
)
|
||||
|
||||
// 层叠布局,用于重叠显示 Widget,配合 Positioned 定位。
|
||||
Stack(
|
||||
children: [
|
||||
Container(color: Colors.red, width: 200, height: 200), // 底层
|
||||
Positioned( // 绝对定位
|
||||
top: 20,
|
||||
left: 20,
|
||||
child: Text('重叠内容'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Text('左'),
|
||||
Spacer(), // 自动占据剩余空间
|
||||
Text('右'),
|
||||
],
|
||||
)
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2, // 权重
|
||||
child: Container(color: Colors.red),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(color: Colors.blue),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
# 列表和网格
|
||||
```dart
|
||||
ListView(
|
||||
children: List.generate(20, (i) => ListTile(
|
||||
title: Text('项目 $i'), // 列表项
|
||||
)),
|
||||
)
|
||||
|
||||
// 懒加载版本(推荐)
|
||||
ListView.builder(
|
||||
itemCount: 1000,
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
title: Text('项目 $index'),
|
||||
),
|
||||
)
|
||||
|
||||
GridView.count(
|
||||
crossAxisCount: 2, // 每行数量
|
||||
children: List.generate(20, (i) => Container(
|
||||
color: Colors.blue,
|
||||
child: Center(child: Text('$i')),
|
||||
)),
|
||||
)
|
||||
```
|
||||
|
||||
# 展示 Widget
|
||||
```dart
|
||||
Text(
|
||||
'Hello Flutter',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
|
||||
Image.network(
|
||||
'https://example.com/image.jpg',
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.cover, // 填充方式
|
||||
loadingBuilder: (c, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return CircularProgressIndicator();
|
||||
},
|
||||
)
|
||||
|
||||
Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.red,
|
||||
size: 30,
|
||||
)
|
||||
```
|
||||
|
||||
# 对话框Widget
|
||||
```dart
|
||||
// 提示对话框,showDialog 显示,需要 context。
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('提示'),
|
||||
content: Text('确定删除吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
// 底部弹出表单
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
height: 200,
|
||||
child: ListView(
|
||||
children: [],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// 底部轻提示,不打断用户操作
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('操作成功'),
|
||||
action: SnackBarAction(
|
||||
label: '撤销',
|
||||
onPressed: () {},
|
||||
),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
# 导航和路由
|
||||
```dart
|
||||
Navigator.push( // 跳转
|
||||
context,
|
||||
MaterialPageRoute(builder: (c) => DetailsPage()),
|
||||
);
|
||||
|
||||
Navigator.pop(context); // 返回
|
||||
Navigator.pushNamed(context, '/details'); // 命名路由
|
||||
|
||||
// 页面切换器
|
||||
PageView(
|
||||
children: [
|
||||
Page1(),
|
||||
Page2(),
|
||||
Page3(),
|
||||
],
|
||||
controller: PageController(),
|
||||
onPageChanged: (index) => print('切换到 $index'),
|
||||
)
|
||||
```
|
||||
|
||||
# 手势Widget
|
||||
```dart
|
||||
GestureDetector(
|
||||
onTap: () => print('点击'),
|
||||
onDoubleTap: () => print('双击'),
|
||||
onLongPress: () => print('长按'),
|
||||
onPanUpdate: (details) => print('拖拽: ${details.delta}'),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.blue,
|
||||
),
|
||||
)
|
||||
|
||||
InkWell(
|
||||
onTap: () => print('点击'),
|
||||
splashColor: Colors.blue.withOpacity(0.3), // 水波纹颜色
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text('可点击区域'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
# 其他Widget
|
||||
```dart
|
||||
// 控制 Widget 显示/隐藏,比条件渲染性能更好。
|
||||
Visibility(
|
||||
visible: isVisible, // 是否显示
|
||||
child: Text('内容'),
|
||||
maintainSize: true, // 保持占位
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
)
|
||||
```
|
||||
29
docs/Web/Flutter/FlutterInstall.md
Normal file
29
docs/Web/Flutter/FlutterInstall.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Flutter简介与安装
|
||||
date: 2026-05-02
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  Flutter 是 Google 推出的跨平台 UI 开发框架,使用 Dart 语言,通过自带的渲染引擎直接绘制界面,可一套代码同时构建 iOS、Android、Web 及 Windows/macOS/Linux 桌面应用,具备原生级性能、热重载和高 UI 一致性,适合追求多端统一与体验效率的项目开发。
|
||||
|
||||
# 二、安装
|
||||
## 2.1 下载SDK
|
||||
  打开[官网](https://docs.flutter.dev/install/archive),选择SDK版本下载,然后解压到相应目录。
|
||||
|
||||
## 2.2 环境变量配置
|
||||
1. 在系统变量->path中添加:'/path/to/flutter/bin'
|
||||
2. 在系统变量中添加: 'PUB_HOSTED_URL'->'https://pub.flutter-io.cn'
|
||||
3. 在系统变量中添加: 'FLUTTER_STORAGE_BASE_URL'->'https://storage.flutter-io.cn'
|
||||
4. 在系统变量中添加: 'FLUTTER_GIT_URL'->'https://gitee.com/mirrors/Flutter.git'
|
||||
|
||||
## 2.3 校验安装
|
||||
  输入`flutter --version`和`flutter doctor`查看是否有错误输出。
|
||||
::: tip
|
||||
Windows开发需要安装Visual Studio的'C++的桌面开发'。
|
||||
Android开发需要安装Android SDK。
|
||||
Web开发需要安装Chrome浏览器。
|
||||
:::
|
||||
|
||||
## 三、Android Studio
|
||||
1. 安装Flutter和Dart插件
|
||||
2. 在Settings->Languages->Dart中选择Dart路径:`/path/to/flutter/bin/cache/dart-sdk`
|
||||
77
docs/Web/Flutter/Practice.md
Normal file
77
docs/Web/Flutter/Practice.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: 实战技巧
|
||||
date: 2025-12-03
|
||||
---
|
||||
|
||||
# 安卓签名
|
||||
  每次安装/升级软件必须使用同一个签名,否则会将本地数据全部清空。
|
||||
## 1. 生成密钥库文件
|
||||
  使用keytool命令生成
|
||||
```cmd
|
||||
keytool -genkey -v -keystore android/app/my-release-key.keystore -alias my-key -keyalg RSA -keysize 2048 -validity 10000
|
||||
```
|
||||
|
||||
  根据提示输入相应信息。
|
||||
  my.keystore为自定义名称。生成后的文件位于android/app/文件夹内
|
||||
|
||||
## 2. 配置key.properties
|
||||
  在android文件夹内新建key.properties文件,并配置信息
|
||||
```properties
|
||||
# 密钥库文件的密码
|
||||
storePassword=12345678
|
||||
# 密钥本身的密码
|
||||
keyPassword=12345678
|
||||
# 密钥的别名,在密钥库中标识具体的密钥
|
||||
keyAlias=my-key
|
||||
# 密钥库文件相对于本配置文件的路径
|
||||
storeFile=my-release-key.keystore
|
||||
```
|
||||
|
||||
## 3. 配置build.gradle.kts
|
||||
```kts
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
// 从根路径加载密钥属性
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
```
|
||||
|
||||
  在android块内新增签名配置
|
||||
```kts
|
||||
android {
|
||||
// 签名配置
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
storeFile = file(keystoreProperties.getProperty("storeFile"))
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
release {
|
||||
signingConfig = if (keystorePropertiesFile.exists()) {
|
||||
// 使用发布签名
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
// 使用默认签名
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  打包为release包时即可生效签名。
|
||||
|
||||
::: danger
|
||||
禁止将签名文件上传到Git仓库中。
|
||||
:::
|
||||
10
docs/Web/Flutter/index.md
Normal file
10
docs/Web/Flutter/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
layout: doc
|
||||
title: Flutter
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { routers } from '../.vitepress/theme/router'
|
||||
</script>
|
||||
|
||||
<MenuList :routers=routers[3] />
|
||||
62
docs/Web/MySQL/Flyway.md
Normal file
62
docs/Web/MySQL/Flyway.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Flyway简单使用
|
||||
date: 2025-11-27
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  Flyway 是一个开源的数据库版本控制工具,它极大地简化了数据库的迁移和版本管理。它的核心思想是像**用 Git 管理代码一样来管理数据库的结构**。
|
||||
|
||||
# 二、原理
|
||||
  Flyway 通过在数据库中创建一个名为 flyway_schema_history的特殊表来工作
|
||||
| 列名 | 含义 |
|
||||
| - | - |
|
||||
| installed_rank | 执行顺序 |
|
||||
| version | 脚本的版本号 |
|
||||
| description | 脚本的描述 |
|
||||
| type | 脚本类型(通常是 SQL) |
|
||||
| script | 脚本文件名 |
|
||||
| checksum | 脚本文件的校验和(用于检测篡改) |
|
||||
| installed_by | 执行人 |
|
||||
| installed_on | 执行时间 |
|
||||
| execution_time | 执行耗时(毫秒) |
|
||||
| success | 是否成功 |
|
||||
|
||||
  工作流程:
|
||||
1. 应用启动时,Flyway 会检查配置的数据库路径。
|
||||
2. 检查目标数据库中的 flyway_schema_history表。
|
||||
3. 将数据库路径下的迁移脚本与 flyway_schema_history表中的记录进行对比。
|
||||
4. 按照版本号顺序执行那些尚未执行的迁移脚本。
|
||||
5. 执行成功后,将记录插入 flyway_schema_history表。
|
||||
|
||||
## 2.1 校验和计算
|
||||
  Flyway 使用 CRC32 算法 计算 SQL 脚本文件的校验和(Checksum)。Javs使用32位有符号整数存储,因此有时会得到负数。可以通过`mvn flyway:info`查看每个脚本的校验和。
|
||||
|
||||
# 三、与SpringBoot集成
|
||||
## 3.1 添加依赖
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 3.2 配置数据源
|
||||
```yml
|
||||
spring:
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true # 如果数据库非空,且无 flyway_schema_history 表,则先创建基线版本
|
||||
```
|
||||
  `baseline-on-migrate: true`,当数据库已经存在数据,但是没有flyway_schema_history表时,Flyway会插入一条基线数据,并标记为1.0版本,则后续的迁移脚本只会执行比1.0版本更高的数据库文件。
|
||||
  **如果设置为false,如果数据库已经存在数据时,Flyway会报错。**
|
||||
  因此需要避免这种情况的存在,在**初始发布应用时,要保证数据库为空**。
|
||||
|
||||
## 3.3 创建数据库脚本
|
||||
  在项目的资源目录 src/main/resources下创建文件夹 db/migration。
|
||||
  Flyway 的 SQL 脚本文件名有严格的命名规则:`V<Version>__<Description>.sql`
|
||||
  例如V1.0.0_001__20251027.sql,表示v1.0.0版本的第一个sql,日期为2025年10月27日。
|
||||
  **创建了迁移脚本,一旦应用,就不可修改,否则会导致校验错误。如果确实要修改,请再创建一个脚本。**
|
||||
|
||||
## 3.4 启动程序
|
||||
  启动程序后。Flyway会自动在数据库中创建flyway_schema_history表,然后扫描db/migration目录下的所有脚本,按顺序执行sql文件,并记录到flyway_schema_history表中。
|
||||
525
docs/Web/MySQL/MyBatis.md
Normal file
525
docs/Web/MySQL/MyBatis.md
Normal file
@@ -0,0 +1,525 @@
|
||||
---
|
||||
title: MyBatis简介和使用
|
||||
date: 2025-11-27
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  [MyBatis](https://mybatis.org/mybatis-3/)是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
|
||||
|
||||
# 二、安装
|
||||
### 2.1 引入依赖
|
||||
  在`pom.xml`文件中,引入依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>x.x.x</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
  可以在Github中查看[MyBatis](https://github.com/mybatis/mybatis-3)最新版本号。
|
||||
|
||||
### 2.2 配置文件
|
||||
  在`resource`文件夹中新建mybatis-config.xml文件和mapper->BlogMapper.xml映射文件:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd">
|
||||
<configuration>
|
||||
<!-- 环境配置 -->
|
||||
<environments default="development">
|
||||
<!-- 环境名称 -->
|
||||
<environment id="development">
|
||||
<!-- 事务管理器配置 -->
|
||||
<transactionManager type="JDBC"/>
|
||||
<!-- 数据源配置 -->
|
||||
<dataSource type="POOLED">
|
||||
<!-- JDBC驱动名称 -->
|
||||
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
|
||||
<!-- 数据库地址 -->
|
||||
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_learn?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT"/>
|
||||
<!-- 数据库用户名 -->
|
||||
<property name="username" value="root"/>
|
||||
<!-- 数据库密码-->
|
||||
<property name="password" value="123456"/>
|
||||
</dataSource>
|
||||
</environment>
|
||||
</environments>
|
||||
|
||||
<!-- 映射器 -->
|
||||
<mappers>
|
||||
<!-- mapper文件 -->
|
||||
<mapper resource="mapper/BlogMapper.xml"/>
|
||||
</mappers>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
  注:如果使用MySql数据库,需要增加MySql驱动依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.12</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 2.3 定义映射语句
|
||||
  新建dao->BlogDao.java:
|
||||
|
||||
```java
|
||||
public interface BlogDao {
|
||||
Blog selectBlog(@Param("id") Integer id);
|
||||
}
|
||||
```
|
||||
|
||||
  BlogMapper.xml:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.example.mybatislearn.dao.BlogDao">
|
||||
<select id="selectBlog" resultType="com.example.mybatislearn.entity.Blog">
|
||||
select * from Blog where author_id = #{id}
|
||||
</select>
|
||||
</mapper>
|
||||
```
|
||||
|
||||
### 2.4 执行SqlSession
|
||||
```java
|
||||
// 配置文件路径
|
||||
String resource = "mybatis-config.xml";
|
||||
try {
|
||||
// 读取配置文件
|
||||
InputStream inputStream = Resources.getResourceAsStream(resource);
|
||||
// 构建SqlSession工厂
|
||||
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
|
||||
// 获取SqlSession
|
||||
SqlSession session = sqlSessionFactory.openSession();
|
||||
// 获取映射文件
|
||||
BlogDao blogDao = session.getMapper(BlogDao.class);
|
||||
// 执行已映射的SQL语句
|
||||
Blog blog = blogDao.selectBlog(101);
|
||||
System.out.println(blog);
|
||||
// 关闭SqlSession
|
||||
session.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
```
|
||||
|
||||
  每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。
|
||||
  SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。
|
||||
  而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
|
||||
|
||||
### 2.5 作用域和生命周期
|
||||
1. SqlSessionFactoryBuilder
|
||||
|
||||
  这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
|
||||
|
||||
2. SqlSessionFactory
|
||||
|
||||
  SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
|
||||
|
||||
3. SqlSession
|
||||
|
||||
  每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。
|
||||
|
||||
# 三、注入SpringBoot框架
|
||||
## 3.1 引入依赖
|
||||
  将之前MyBatis的依赖替换成MyBatis的SpringBoot Starter:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 3.2 配置文件
|
||||
  在Resource文件夹下新建application.yml:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/mybatis_learn?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
mybatis:
|
||||
# mapper文件路径
|
||||
mapper-locations: classpath*:mapper/*Mapper.xml
|
||||
configuration:
|
||||
# 开启日志
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
```
|
||||
|
||||
  注:这里的url的写法和XML文件中的写法不一致。
|
||||
|
||||
# 3.3 定义映射语句
|
||||
  在原映射接口文件BlogDao.java中添加@Mapper注解
|
||||
|
||||
## 3.4 实现原理
|
||||
  引入mybatis-spring-boot-starter模块之后,其可以:
|
||||
|
||||
1. **自动检测DataSource**
|
||||
2. **使用SqlSessionFactoryBean注册SqlSessionFactory 实例,并设置DataSource数据源**
|
||||
3. **基于SqlSessionFactory自动注册SqlSessionTemplate实例**
|
||||
4. **自动扫描@Mapper注解类,并通过SqlSessionTemplate注册到Spring Context中**
|
||||
|
||||
  每次执行@Mapper映射文件中的接口时,都会自动开启一个SqlSession并在执行结束时关闭。
|
||||
|
||||
## 3.5 执行映射语句
|
||||
```java
|
||||
Blog blog = blogDao.selectBlog(101);
|
||||
System.out.println(blog);
|
||||
```
|
||||
|
||||
  相较之前的写法,节省了大量的配置工作。
|
||||
|
||||
# 四、高级特性
|
||||
## 4.1 动态参数
|
||||
```xml
|
||||
#{}是参数占位符的标记,它可以防止SQL注入,当使用#{}时,MyBatis会自动处理参数的数据类型,
|
||||
如果参数是字符串,它会给传入的值加上引号,这样可以有效地防止SQL注入攻击。
|
||||
|
||||
${}则是直接将参数值嵌入SQL语句中。当使用${}时,传入的参数会直接显示在SQL中,
|
||||
MyBatis不会对参数进行任何类型转换或加引号处理。一般用在动态表名、列名或数据库名称中。
|
||||
```
|
||||
|
||||
## 4.2 SQL片段
|
||||
  可以用来定义可重复的SQL代码片段:
|
||||
|
||||
```xml
|
||||
<sql id="userColumns">
|
||||
id, username, email, phone
|
||||
</sql>
|
||||
|
||||
<select id="findAllUsers" resultType="User">
|
||||
SELECT <include refid="userColumns" /> FROM users
|
||||
</select>
|
||||
```
|
||||
|
||||
## 4.3 批量操作
|
||||
  推荐使用集合方式批量操作:
|
||||
|
||||
```java
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
Integer insertUsers(@Param("list") List<User> userList);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```xml
|
||||
<insert id="insertUsers">
|
||||
INSERT INTO user (username, password)
|
||||
VALUES
|
||||
<foreach collection ="list" item="item" separator =",">
|
||||
(#{item.username}, #{item.password})
|
||||
</foreach>
|
||||
</insert>
|
||||
```
|
||||
|
||||
## 4.4 结果映射
|
||||
  复杂结果缓存
|
||||
|
||||
```xml
|
||||
<!-- 非常复杂的结果映射 -->
|
||||
<resultMap id="detailedBlogResultMap" type="Blog">
|
||||
<!-- 一般不需要 -->
|
||||
<constructor>
|
||||
<idArg column="blog_id" javaType="int"/>
|
||||
</constructor>
|
||||
<result property="title" column="blog_title"/>
|
||||
<!-- 复杂对象 1对1 -->
|
||||
<association property="author" javaType="Author">
|
||||
<id property="id" column="author_id"/>
|
||||
<result property="username" column="author_username"/>
|
||||
<result property="password" column="author_password"/>
|
||||
<result property="email" column="author_email"/>
|
||||
<result property="bio" column="author_bio"/>
|
||||
<result property="favouriteSection" column="author_favourite_section"/>
|
||||
</association>
|
||||
<!-- 列表 1对多-->
|
||||
<collection property="posts" ofType="Post">
|
||||
<id property="id" column="post_id"/>
|
||||
<result property="subject" column="post_subject"/>
|
||||
<association property="author" javaType="Author"/>
|
||||
<collection property="comments" ofType="Comment">
|
||||
<id property="id" column="comment_id"/>
|
||||
</collection>
|
||||
<collection property="tags" ofType="Tag" >
|
||||
<id property="id" column="tag_id"/>
|
||||
</collection>
|
||||
<discriminator javaType="int" column="draft">
|
||||
<case value="1" resultType="DraftPost"/>
|
||||
</discriminator>
|
||||
</collection>
|
||||
</resultMap>
|
||||
```
|
||||
|
||||
  其中`<collection>`也可以使用嵌套查询:
|
||||
|
||||
```xml
|
||||
<collection property="posts" ofType="Post" select="queryPost"/>
|
||||
|
||||
<resultMap id="postResultMap" type="Post">
|
||||
<id property="id" column="post_id"/>
|
||||
<result property="subject" column="post_subject"/>
|
||||
<association property="author" javaType="Author"/>
|
||||
<collection property="comments" ofType="Comment">
|
||||
<id property="id" column="comment_id"/>
|
||||
</collection>
|
||||
<collection property="tags" ofType="Tag" >
|
||||
<id property="id" column="tag_id"/>
|
||||
</collection>
|
||||
<discriminator javaType="int" column="draft">
|
||||
<case value="1" resultType="DraftPost"/>
|
||||
</discriminator>
|
||||
</resultMap>
|
||||
|
||||
<select id="queryPost" resultMap="postResultMap">
|
||||
|
||||
</select>
|
||||
|
||||
```
|
||||
|
||||
  如果需要传递参数,可以在`<collection>`添加`column`属性:
|
||||
|
||||
```xml
|
||||
<!-- 单个参数 -->
|
||||
<collection property="posts" column="name" ofType="Post" select="queryPost"/>
|
||||
<!-- 多个参数 -->
|
||||
<collection property="posts" column="{param1=param_1, param2=param_2}" ofType="Post" select="queryPost"/>
|
||||
```
|
||||
|
||||
  注:建立在非列表数据时使用嵌套查询,否则每查到一个数据都会进行一次子查询操作。
|
||||
|
||||
## 4.5 一二级缓存
|
||||
  默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:<cache/>
|
||||
|
||||
+ 映射语句文件中的所有 select 语句的结果将会被缓存。
|
||||
+ 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
|
||||
+ 一级缓存和二级缓存区别在于一级缓存只针对一次SqlSession,二级缓存针对全局范围。
|
||||
|
||||
## 4.6 动态SQL
|
||||
+ if :是/否
|
||||
|
||||
```xml
|
||||
<select id="findActiveBlogWithTitleLike" resultType="Blog">
|
||||
SELECT * FROM BLOG
|
||||
WHERE state = 'ACTIVE'
|
||||
<if test="title != null">
|
||||
AND title like #{title}
|
||||
</if>
|
||||
</select>
|
||||
```
|
||||
|
||||
+ choose、when、otherwise:选择其中一个
|
||||
|
||||
```xml
|
||||
<select id="findActiveBlogLike" resultType="Blog">
|
||||
SELECT * FROM BLOG WHERE state = 'ACTIVE'
|
||||
<choose>
|
||||
<when test="title != null">
|
||||
AND title like #{title}
|
||||
</when>
|
||||
<when test="author != null and author.name != null">
|
||||
AND author_name like #{author.name}
|
||||
</when>
|
||||
<otherwise>
|
||||
AND featured = 1
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
```
|
||||
|
||||
+ where、set:解决SQL语法问题
|
||||
|
||||
```xml
|
||||
<select id="findActiveBlogLike" resultType="Blog">
|
||||
SELECT * FROM BLOG
|
||||
<where>
|
||||
<if test="state != null">
|
||||
state = #{state}
|
||||
</if>
|
||||
<if test="title != null">
|
||||
AND title like #{title}
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<update id="updateAuthorIfNecessary">
|
||||
update Author
|
||||
<set>
|
||||
<if test="username != null">username=#{username},</if>
|
||||
<if test="password != null">password=#{password},</if>
|
||||
</set>
|
||||
where id=#{id}
|
||||
</update>
|
||||
```
|
||||
|
||||
  where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
|
||||
|
||||
# 五、自定义类型处理器
|
||||
  MyBatis 在预处理语句(PreparedStatement)中设置参数时,会从 Java 类型(javaType)转换为 JDBC 类型(jdbcType);而从结果集中取出值时,会将 JDBC 类型转换为 Java 类型。这个转换工作就是由 TypeHandler来完成的。
|
||||
  需要创建一个类来实现 org.apache.ibatis.type.TypeHandler接口,或者继承 org.apache.ibatis.type.BaseTypeHandler类实现自定义类型处理器。
|
||||
  需要实现的方法:
|
||||
```java
|
||||
/**
|
||||
* 将Java对象设置到PreparedStatement中(Java类型 → JDBC类型)
|
||||
* @param ps PreparedStatement对象
|
||||
* @param i 参数位置(从1开始)
|
||||
* @param parameter 要设置的Java对象(非空)
|
||||
* @param jdbcType JDBC类型
|
||||
*/
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
|
||||
// 实现转换逻辑:T → JDBC类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列名从ResultSet中获取值(JDBC类型 → Java类型)
|
||||
* @param rs ResultSet对象
|
||||
* @param columnName 列名
|
||||
* @return 转换后的Java对象
|
||||
*/
|
||||
@Override
|
||||
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
// 实现转换逻辑:JDBC类型 → T
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列索引从ResultSet中获取值
|
||||
* @param rs ResultSet对象
|
||||
* @param columnIndex 列索引(从1开始)
|
||||
* @return 转换后的Java对象
|
||||
*/
|
||||
@Override
|
||||
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
// 实现转换逻辑:JDBC类型 → T
|
||||
}
|
||||
|
||||
/**
|
||||
* 从CallableStatement中获取值(用于存储过程)
|
||||
* @param cs CallableStatement对象
|
||||
* @param columnIndex 列索引
|
||||
* @return 转换后的Java对象
|
||||
*/
|
||||
@Override
|
||||
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
// 实现转换逻辑:JDBC类型 → T
|
||||
}
|
||||
```
|
||||
|
||||
  例如自定义一个处理 MEDIUMBLOB 字段与 Base64 字符串的转换:
|
||||
```java
|
||||
/**
|
||||
* 处理 MEDIUMBLOB 字段与 Base64 字符串的转换
|
||||
*/
|
||||
@MappedJdbcTypes(JdbcType.BLOB)
|
||||
@MappedTypes(String.class)
|
||||
public class BlobToBase64TypeHandler extends BaseTypeHandler<String> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
|
||||
// Base64字符串 -> 数据库的byte[]
|
||||
if (parameter != null && !parameter.trim().isEmpty()) {
|
||||
byte[] bytes = Base64.getDecoder().decode(parameter);
|
||||
ps.setBytes(i, bytes);
|
||||
} else {
|
||||
ps.setBytes(i, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
// 数据库的byte[] -> Base64字符串
|
||||
byte[] bytes = rs.getBytes(columnName);
|
||||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
byte[] bytes = rs.getBytes(columnIndex);
|
||||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
byte[] bytes = cs.getBytes(columnIndex);
|
||||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  例如自定义一个处理敏感字段(如密码、手机号)的自动加解密:
|
||||
```java
|
||||
public class EncryptTypeHandler extends BaseTypeHandler<String> {
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
|
||||
ps.setString(i, AESUtil.encrypt(parameter)); // 自定义加密方法
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String encrypted = rs.getString(columnName);
|
||||
return AESUtil.decrypt(encrypted); // 自定义解密方法
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  然后在实体类字段上加上 @TableField(typeHandler = EncryptTypeHandler.class):
|
||||
```java
|
||||
@TableName("user")
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
// 这个字段在数据库里存的是加密后的字符串
|
||||
@TableField(typeHandler = EncryptTypeHandler.class)
|
||||
private String phone; // 例如 "13800138000" → 存为 "U2FsdGVkX1+oO1W..."
|
||||
|
||||
@TableField(typeHandler = EncryptTypeHandler.class)
|
||||
private String idCard; // 身份证号
|
||||
|
||||
// 普通字段,不加密
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
  如果是在XML文件中查询,需要指定TypeHandler:
|
||||
```xml
|
||||
<resultMap id="UserResultMap" type="com.example.entity.User">
|
||||
<id column="id" property="id" />
|
||||
<result column="name" property="name" />
|
||||
<!-- 关键:phone 字段使用 EncryptTypeHandler -->
|
||||
<result column="phone" property="phone" typeHandler="com.example.handler.EncryptTypeHandler"/>
|
||||
<!-- idCard 字段也用同一个处理器 -->
|
||||
<result column="id_card" property="idCard" typeHandler="com.example.handler.EncryptTypeHandler"/>
|
||||
<result column="email" property="email" />
|
||||
</resultMap>
|
||||
```
|
||||
|
||||
  MyBatisPlus中定义了一些常用的类型处理器,例如:JacksonTypeHandler:
|
||||
```java
|
||||
@TableField(value = "fault_type", typeHandler = JacksonTypeHandler.class)
|
||||
```
|
||||
|
||||
  在XML文件中,将typeHandler设置为`com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler`。
|
||||
|
||||
# 六、MyBatis/MyBatis Plus常见问题
|
||||
1. 更新值为null的字段时会失效
|
||||
|
||||
  需要更改字段的更新策略:
|
||||
```java
|
||||
@TableField(value = "file_name", updateStrategy = FieldStrategy.ALWAYS)
|
||||
```
|
||||
|
||||
2. 自动填充出现失效
|
||||
|
||||
  strictInsert/UpdateFill的默认填充策略是:如果实体类中需要自动填充的字段已经有值了,那么当前值就不会进行填充,或者你想给一个字段填充null值也是不可以的。可以替换为setFieldValByName,将其直接覆盖。(一般是更新的时候需要替换,新增默认就是没有值的)
|
||||
520
docs/Web/MySQL/MySQL-Knowledge.md
Normal file
520
docs/Web/MySQL/MySQL-Knowledge.md
Normal file
@@ -0,0 +1,520 @@
|
||||
---
|
||||
title: MySQL知识点
|
||||
date: 2025-11-26
|
||||
---
|
||||
|
||||
# 一、基础知识
|
||||
## 1.1 数据类型
|
||||
### 1.1.1 汇总
|
||||
|类型|存储空间|范围|适用场景|
|
||||
| :-------------------------------------------------------------------------------------------------: | :-----------------: | :--------------------------: | :-----------------------: |
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">TINYINT</span>|1字节|-128 \~ 127|状态码、年龄、布尔值|
|
||||
|SMALLINT|2字节|-32,768 \~ 32,767|小范围计数、年份||
|
||||
|MEDIUMINT|3字节|-8,388,608 \~ 8,388,607|中型ID、访问量统计|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">INT/INTEGER</span>|4字节|-2\^31 \~ 2\^31-1|用户ID、订单号(常用)|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">BIGINT</span>|8字节|-2\^63 \~ 2\^63-1|分布式ID、大数据量计数|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">DECIMAL(M,D)</span>|变长(M+2字节)||金融金额、精确计算|精确小数,M\=总位数,D\=小数位|
|
||||
|FLOAT|4字节||科学测量、非精确计算|
|
||||
|DOUBLE|8字节||地理坐标、高精度计算|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">CHAR(M)</span>|255字符||固定长度编码、MD5哈希|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">VARCHAR(M)</span>|65,535字节||用户名、地址等变长数据|
|
||||
|TINYTEXT|255字节||短标题、简介|
|
||||
|TEXT|65,535字节||文章内容、评论|
|
||||
|MEDIUMTEXT|16MB (2\^24-1)||博客文章、产品描述|
|
||||
|LONGTEXT|4GB (2\^32-1)||电子书、大型文档|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">DATE</span>|||纯日期|
|
||||
|TIME(fsp)|||可指定微秒精度(0-6)|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">DATETIME(fsp)</span>|||高精度时间记录|
|
||||
|TIMESTAMP(fsp)|||自动时区转换,4字节存储|
|
||||
|ENUM|||性别、状态等有限选项|`gender ENUM('M','F','O')`|
|
||||
|SET|||用户兴趣、文章标签|`tags SET('red','green','blue')`|
|
||||
|TINYBLOB|255字节||微小二进制对象|
|
||||
|BLOB|65KB||标准二进制对象|
|
||||
|MEDIUMBLOB|16MB||中等二进制对象|
|
||||
|LONGBLOB|4GB||超大二进制对象|
|
||||
|
||||
### 1.1.2 法则
|
||||
  **整数选择**:优先INT,大数量用BIGINT,布尔值用TINYINT(1)或BIT(1)
|
||||
  **小数选择**:金融金额必须用DECIMAL,非精确测量可用FLOAT/DOUBLE
|
||||
  **字符串选择**:定长编码用CHAR,变长文本用VARCHAR,大文本用TEXT系列
|
||||
  **时间选择**:日期用DATE,精确时间用DATETIME,自动更新用TIMESTAMP
|
||||
  **特殊场景**:多选项用SET,结构化数据用JSON
|
||||
|
||||
## 1.2 常用函数
|
||||
### 1.2.1 字符串函数
|
||||
1. CONCAT(str1, str2, ...):连接字符串
|
||||
```sql
|
||||
SELECT CONCAT('Hello', ' ', 'World');
|
||||
```
|
||||
2. LENGTH(str):返回字符串的长度(字节数)
|
||||
```sql
|
||||
SELECT LENGTH('Hello');
|
||||
```
|
||||
3. SUBSTRING(str, pos, len):从字符串 str 的 pos 位置开始,截取长度为 len 的子字符串。
|
||||
```sql
|
||||
SELECT SUBSTRING('Hello World', 7, 5); -- 返回 'World'
|
||||
```
|
||||
|
||||
### 1.2.2 日期/时间函数
|
||||
1. NOW():返回当前日期和时间(yyyy-MM-DD HH:mm:ss)。
|
||||
2. CURDATE():返回当前日期,不带时间部分(yyyy-MM-DD)。
|
||||
3. DATE_FORMAT(date, format):按照指定的格式返回日期值。
|
||||
```sql
|
||||
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');
|
||||
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d');
|
||||
```
|
||||
4. DATEDIFF(date1, date2):返回两个日期之间的天数差。
|
||||
```sql
|
||||
SELECT DATEDIFF('2024-12-31', '2024-01-01'); -- 返回364
|
||||
```
|
||||
5. YEAR(date) / MONTH(date) / DAY(date):分别返回日期的年、月、日部分。
|
||||
6. TIME(date):从日期或时间值中提取时间部分。
|
||||
7. TIMESTAMPDIFF(unit, datetime1, datetime2):返回两个日期或时间的差值,单位可以是 SECOND, MINUTE, HOUR, DAY, MONTH, YEAR 等。
|
||||
8. STR_TO_DATE(str, format):根据给定的格式将字符串转换为日期。
|
||||
```sql
|
||||
SELECT STR_TO_DATE('01-09-2024', '%d-%m-%Y'); -- 返回 '2024-09-01'
|
||||
```
|
||||
|
||||
### 1.2.3 数值函数
|
||||
1. ROUND(x, d):将数值 x 四舍五入到 d 位小数。
|
||||
```sql
|
||||
SELECT ROUND(123.4567, 2); -- 返回 123.46
|
||||
```
|
||||
2. FLOOR(x) / CEIL(x):返回小于或等于 x 的最大整数(向下取整)或大于或等于 x 的最小整数(向上取整)。
|
||||
```sql
|
||||
SELECT FLOOR(2.9); -- 返回 2
|
||||
SELECT CEIL(2.1); -- 返回 3
|
||||
```
|
||||
|
||||
### 1.2.4 聚合函数
|
||||
1. COUNT(expression):返回某列中的记录数。
|
||||
2. SUM(expression):返回某列中数值的总和。
|
||||
3. AVG(expression):返回某列中数值的平均值。
|
||||
4. MAX(expression) / MIN(expression):返回某列的最大值或最小值。
|
||||
|
||||
### 1.2.5 控制流函数
|
||||
1. IF(condition, true_value, false_value):如果 condition 为真,返回 true_value,否则返回 false_value。
|
||||
```sql
|
||||
SELECT IF(1 > 0, 'Yes', 'No'); -- 返回 'Yes'
|
||||
```
|
||||
2. CASE:用于条件判断,类似于多路选择。
|
||||
```sql
|
||||
SELECT
|
||||
CASE
|
||||
WHEN salary > 10000 THEN 'High'
|
||||
WHEN salary BETWEEN 5000 AND 10000 THEN 'Medium'
|
||||
ELSE 'Low'
|
||||
END AS salary_range
|
||||
FROM employees;
|
||||
```
|
||||
|
||||
# 二、索引
|
||||
  索引是对数据库表中的一列或多列值进行**排序**的一种结构,使用索引可以快速访问数据库表中的特定信息。
|
||||
  **索引相当于图书上的目录,可以根据目录上的页码快速找到所需的内容,提高性能(查询速度)**
|
||||
|
||||
## 2.1 优缺点
|
||||
优点:
|
||||
* **查询速度起飞 (主要目的)** :通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。
|
||||
* **保证数据唯一性**:通过创建**唯一索引 (Unique Index)** ,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户ID、邮箱等。**主键本身就是一种唯一索引**。
|
||||
* **加速排序和分组**:如果查询中的 ORDER BY 或 GROUP BY 子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作,从而提升性能。
|
||||
|
||||
缺点:
|
||||
* **创建和维护耗时**:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行**增、删、改 (DML操作)** 时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会**降低这些 DML 操作的执行效率**。
|
||||
* **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。
|
||||
* **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。
|
||||
|
||||
## 2.2 适用场景
|
||||
适用场景
|
||||
* **频繁作为查询条件的字段**应该创建索引
|
||||
* **查询中排序的字段**创建索引将大大提高排序的速度(索引就是排序加快速查找)
|
||||
* **查询中统计或者分组的字段**
|
||||
|
||||
不适用场景
|
||||
* **频繁更新的字段不适合创建索引**,因为每次更新不单单是更新记录,还会更新索引,保存索引文件
|
||||
* **表记录太少,不需要创建索引**;
|
||||
* **数据重复且分布平均的字段**,因此为经常查询的和经常排序的字段建立索引。注意某些数据包含大量重复数据,因此他建立索引就没有太大的效果,例如性别字段,只有男女,不适合建立索引。
|
||||
|
||||
## 2.3 数据结构
|
||||
  在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构。
|
||||

|
||||
  例如要查找id=75的用户:`SELECT * FROM users WHERE id = 75`
|
||||
  查找步骤:
|
||||
1. 从根节点开始,75在50~100之间,走中间分支
|
||||
2. 到达叶子节点,找到id=75的数据位置
|
||||
3. 根据位置直接获取完整的用户数据
|
||||
|
||||
  整个过程只需要3次IO操作,而全表扫描可能需要很多次。
|
||||
|
||||
|特点|优势|实际效果|
|
||||
| :--------------: | :------------: | :--------------------: |
|
||||
|多路平衡|树的高度很低|减少磁盘访问次数|
|
||||
|叶子节点连接|支持范围查询|ORDER BY、分页查询快|
|
||||
|只在叶子存数据|内部节点小|更多索引数据放入内存|
|
||||
|
||||
## 2.4 索引类型
|
||||
1. 主键索引
|
||||
主键索引是最特殊的索引。
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(50),
|
||||
email VARCHAR(100)
|
||||
);
|
||||
|
||||
SELECT * FROM users WHERE id = 12345;
|
||||
```
|
||||
|
||||
2. 唯一索引
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_email ON users(email);
|
||||
```
|
||||
  插入重复的邮箱会报错
|
||||
|
||||
3. 普通索引
|
||||
```sql
|
||||
CREATE INDEX idx_name ON users(name);
|
||||
|
||||
SELECT * FROM users WHERE name = "张三";
|
||||
```
|
||||
|
||||
4. 复合索引
|
||||
多个字段组合的索引
|
||||
```sql
|
||||
CREATE INDEX idx_name_age_city ON users(name, age, city);
|
||||
|
||||
SELECT * FROM users WHERE name = '张三';
|
||||
SELECT * FROM users WHERE name = '张三' AND age = 25;
|
||||
SELECT * FROM users WHERE name = '张三' AND age = 25 AND city = '北京';
|
||||
SELECT * FROM users WHERE age = 25; -- 无法使用索引
|
||||
SELECT * FROM users WHERE city = '北京'; -- 无法使用索引
|
||||
```
|
||||
  复合索引的使用最左前缀原则。
|
||||
  `CREATE INDEX idx_name_age_city ON users(name, age, city);`相当于创建了3个索引:`users(name),users(name, age),users(name, age, city)`。
|
||||
|
||||
## 2.5 索引设计
|
||||
* 为WHERE条件添加索引
|
||||
```sql
|
||||
SELECT * FROM orders WHERE user_id = 123;
|
||||
SELECT * FROM orders WHERE status = 'paid';
|
||||
SELECT * FROM orders WHERE create_time > '2024-01-01';
|
||||
|
||||
CREATE INDEX idx_user_id ON orders(user_id);
|
||||
CREATE INDEX idx_status ON orders(status);
|
||||
CREATE INDEX idx_create_time ON orders(create_time);
|
||||
```
|
||||
|
||||
* 为ORDER BY字段添加索引
|
||||
```sql
|
||||
SELECT * FROM articles ORDER BY create_time DESC LIMIT 10;
|
||||
|
||||
CREATE INDEX idx_create_time ON articles(create_time);
|
||||
```
|
||||
|
||||
* 复合索引的顺序很关键
|
||||
```sql
|
||||
SELECT * FROM users WHERE city = '北京' AND age > 25 ORDER BY create_time;
|
||||
|
||||
-- 索引字段顺序应该是:过滤性强的字段在前
|
||||
CREATE INDEX idx_city_age_create_time ON users(city, age, create_time);
|
||||
```
|
||||
|
||||
* 限制每张表上的索引数量,建议单张表索引不超过 5 个。
|
||||
* 被频繁更新的字段应该慎重建立索引,虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
|
||||
* 尽可能的考虑建立联合索引而不是单列索引。因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
|
||||
|
||||
## 2.6 索引优化
|
||||
1. 监控慢查询
|
||||
```sql
|
||||
-- 开启慢查询日志
|
||||
SET GLOBAL slow_query_log = 'ON';
|
||||
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录下来
|
||||
|
||||
-- 查看慢查询
|
||||
SHOW VARIABLES LIKE 'slow_query_log_file';
|
||||
```
|
||||
|
||||
2. 合理使用前缀索引
|
||||
```sql
|
||||
-- 对于很长的字符串字段,使用前缀索引
|
||||
CREATE INDEX idx_title_prefix ON articles(title(20)); -- 只索引前20个字符
|
||||
```
|
||||
|
||||
## 三、事务
|
||||
  事务是数据库操作的基本单位,它是一组原子性的 SQL 语句,或者说是一个独立的工作单元。事务内的所有操作**要么全部成功,要么全部失败**。
|
||||
|
||||
## 3.1 特性
|
||||
* **原子性**(`Atomicity`):原子性确保事务中的所有操作要么全部完成,要么全部不完成。如果事务执行过程中发生错误,所有已执行的操作都会回滚。
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
INSERT INTO orders (user_id, amount) VALUES (1, 100);
|
||||
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1;
|
||||
-- 如果任何一步失败,整个事务都会回滚
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
* **一致性**(`Consistency`):一致性确保数据库从一个一致的状态转换到另一个一致的状态。事务执行前后,数据库的完整性约束不会被破坏。
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
-- 确保账户余额不会出现负数
|
||||
UPDATE accounts SET balance = balance - 100
|
||||
WHERE id = 1 AND balance >= 100;
|
||||
UPDATE accounts SET balance = balance + 100
|
||||
WHERE id = 2;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
* **隔离性**(`Isolation`):隔离性确保并发执行的事务之间不会相互影响。每个事务都感觉不到其他事务的存在。
|
||||
```sql
|
||||
-- 事务1
|
||||
START TRANSACTION;
|
||||
SELECT balance FROM accounts WHERE id = 1;
|
||||
-- 其他事务的修改不会影响这个查询结果
|
||||
COMMIT;
|
||||
|
||||
-- 事务2
|
||||
START TRANSACTION;
|
||||
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
* **持久性**(`Durability`):持久性确保一旦事务提交,其所做的修改就会永久保存到数据库中。
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
INSERT INTO logs (message) VALUES ('重要操作');
|
||||
COMMIT;
|
||||
-- 提交后,数据已经持久化到磁盘
|
||||
```
|
||||
|
||||
## 3.2 隔离级别
|
||||
### 3.2.1 概念
|
||||
* **脏读 (Dirty Read):** 事务A读取了事务B**尚未提交**的修改数据。如果事务B最终回滚,事务A读到的就是无效的"脏"数据。
|
||||
* **不可重复读 (Non-Repeatable Read):** 在同一个事务A中,**两次读取同一行数据**,得到的结果不同。这是因为在两次读取之间,该行数据被另一个提交了的事务B**修改**了。
|
||||
* **幻读 (Phantom Read):** 在同一个事务A中,**两次执行相同的查询**(通常是范围查询 `SELECT ... WHERE ...`),得到的结果集行数不同(出现了新的"幻影"行或原有行消失了)。这是因为在两次查询之间,另一个提交了的事务B**插入**了满足查询条件的新行或**删除**了原有的行。
|
||||
|
||||
### 3.2.2 类型
|
||||
1. 读未提交 (READ UNCOMMITTED)
|
||||
* 特点: 这是最低的隔离级别,允许读取尚未提交的数据变更。
|
||||
* 允许的问题:
|
||||
* **脏读:** 事务可以读取其他事务尚未提交的修改。
|
||||
* **不可重复读:** 可能发生。
|
||||
* **幻读:** 可能发生。
|
||||
* 并发性: **最高**。因为它几乎不加锁(或者锁持有时间非常短),事务之间等待最少。
|
||||
* 数据一致性: **最差**。读取的数据可能是临时的、无效的(如果其他事务回滚)或中间状态。
|
||||
* 使用场景: 非常罕见,通常仅在对数据准确性要求极低、需要极高吞吐量且能容忍脏数据的统计类场景(如实时大屏粗略计数)中考虑。**强烈不建议在要求数据准确性的业务中使用。**
|
||||
|
||||
2. 读已提交 (READ COMMITTED)
|
||||
* 特点: 允许读取并发事务已经提交的数据,这是许多数据库(如 Oracle, PostgreSQL)的默认隔离级别(但不是 MySQL 的默认)。
|
||||
* 允许的问题:
|
||||
* **脏读: ❌ 避免。** 事务只能读取其他事务**已经提交**的修改。
|
||||
* **不可重复读: ✔️ 可能发生。** 同一事务内多次读取同一行,结果可能不同(如果其他已提交事务修改了该行)。
|
||||
* **幻读: ✔️ 可能发生。** 同一事务内多次执行相同范围查询,结果集行数可能不同(如果其他已提交事务插入/删除了满足条件的行)。
|
||||
* 并发性: **较高**。避免了脏读带来的最基础问题,锁的持有时间通常比 `REPEATABLE READ` 短(行锁在语句执行后可能更快释放)。
|
||||
* 数据一致性: **较好**。保证了读取的数据是已提交的、有效的。但同一事务内的多次读取结果可能不一致。
|
||||
* 使用场景: 适用于大多数不需要在同一个事务内保证多次读取数据绝对一致的场景。例如,一个展示数据的列表页,每次查询都是独立的快照。
|
||||
|
||||
3. **可重复读 (REPEATABLE READ)**
|
||||
* 特点: 确保在同一事务中多次读取同一数据得到相同的结果。这是 **MySQL 的默认事务隔离级别**。通过 **MVCC (多版本并发控制)** 实现。
|
||||
* 允许的问题:
|
||||
* **脏读: ❌ 避免。**
|
||||
* **不可重复读: ❌ 避免。** 在同一事务内,**多次读取同一行**数据的结果保证是一致的(即使其他事务已提交修改)。MVCC 通过为事务提供**一致性视图 (Consistent Read View)** 来实现,该视图基于事务开始时的快照。
|
||||
* **幻读: ⚠️ 理论可能,但 MySQL InnoDB 很大程度上避免。** 这是关键点!标准的 SQL 定义中,`REPEATABLE READ` 允许幻读。但是,**MySQL 的 InnoDB 存储引擎通过 MVCC 和 Next-Key Locking(临键锁)的组合,在绝大多数情况下避免了幻读**。对于**快照读**(普通 `SELECT` 语句),MVCC 保证看到的是事务开始时的快照,因此不会看到新插入的行。对于**当前读**(`SELECT ... FOR UPDATE`, `SELECT ... LOCK IN SHARE MODE`, `UPDATE`, `DELETE`),Next-Key Locking 会锁定扫描到的索引范围,阻止其他事务在该范围内插入,从而避免幻读。
|
||||
* 并发性: **中等**。比 `READ COMMITTED` 稍低,因为锁(特别是 Next-Key Locks)可能持有更长时间,覆盖更大的范围(索引区间)。
|
||||
* 数据一致性: **好**。保证了事务内读取数据的稳定性(同一行可重复读),并通过机制有效防止了幻读,满足大多数应用的需求。
|
||||
* **使用场景:** MySQL 的默认选择,适用于绝大多数需要保证事务内数据读取一致性的场景,如订单处理、账户管理等。是兼顾一致性和并发性的良好平衡点。
|
||||
4. 串行化 (SERIALIZABLE)
|
||||
* 特点: 最高的隔离级别。它通过强制事务**串行执行**来实现,所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。
|
||||
* 允许的问题:
|
||||
* **脏读: ❌ 避免。**
|
||||
* **不可重复读: ❌ 避免。**
|
||||
* **幻读: ❌ 避免。**
|
||||
* **实现方式:** 简单理解,它会在读取的数据上自动加共享锁(`SELECT` 默认变成 `SELECT ... LOCK IN SHARE MODE`),在写入的数据上加排他锁。这些锁会持有到事务结束。这导致事务之间几乎完全串行化,读写相互阻塞非常严重。
|
||||
* 并发性: **最低**。性能开销巨大,吞吐量急剧下降。
|
||||
* 数据一致性: **最好**。完全保证事务的隔离性,不会出现任何并发问题。
|
||||
* 使用场景: 仅在对数据一致性要求极高,且完全不能接受任何并发副作用(如金融核心系统的某些极端操作),并且能承受极低并发性能的情况下使用。**实践中很少使用。**
|
||||
|
||||
## 3.3 MySQL锁
|
||||
* **表级锁:** MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
|
||||
* **行级锁:** MySQL 中锁定粒度最小的一种锁,是 **针对索引字段加的锁** ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
|
||||
* **共享锁(S 锁)** :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
|
||||
* **排他锁(X 锁)** :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
|
||||
|
||||
|SQL语句|默认锁类型|说明|
|
||||
| :-----------------------------: | :------------: | :----------------------------: |
|
||||
|SELECT|无锁(MVCC)|使用快照读取,不阻塞其他事务|
|
||||
|SELECT ... FOR UPDATE|排他锁(X)|阻塞其他事务修改这些行|
|
||||
|SELECT ... LOCK IN SHARE MODE|共享锁(S)|允许其他事务读取但阻塞修改|
|
||||
|INSERT|排他锁(X)|自动获取|
|
||||
|UPDATE|排他锁(X)|自动获取|
|
||||
|DELETE|排他锁(X)|自动获取|
|
||||
|
||||
# 四、日志
|
||||
  MySQL 的三大核心日志系统是保证数据一致性、实现故障恢复和提供复制功能的关键组件。这三大日志分别是:二进制日志(binlog)、错误日志(error log) 和 重做日志(redo log)。
|
||||
## 4.1 二进制日志(Binary Log,简称 binlog)
|
||||
### 4.1.1 概念
|
||||
  二进制日志是 MySQL 记录所有修改数据或可能修改数据的语句(或数据变更)的日志文件。它记录了数据库执行的更改操作(如 INSERT、UPDATE、DELETE 等 DML 操作,以及 CREATE、ALTER、DROP 等 DDL 操作),但**不记录 SELECT 这类不修改数据的查询操作**。
|
||||
|
||||
### 4.1.2 作用
|
||||
1. 主从复制(Replication):在主从架构中,主库将 binlog 发送给从库,从库通过读取并重放 binlog 来保持与主库的数据同步。
|
||||
2. 数据恢复(Point-in-Time Recovery):通过备份 + binlog 可以恢复到某个具体时间点。
|
||||
3. 审计:可以追踪数据库的所有变更操作。
|
||||
|
||||
## 4.2 重做日志(Redo Log)
|
||||
### 4.2.1 概念
|
||||
  重做日志是 InnoDB 存储引擎特有的日志,它记录的是 “物理级别” 上的页修改信息,主要用于 崩溃恢复(Crash Recovery)。
|
||||
  InnoDB 使用 WAL(Write-Ahead Logging,预写日志)技术,即 事务提交时,先写 redo log,再写磁盘数据页,从而保证即使系统崩溃,也能通过 redo log 恢复数据。
|
||||
|
||||
### 4.2.2 作用
|
||||
1. 实现事务的持久性(Durability):确保事务提交后,即使发生宕机,数据也不会丢失。
|
||||
2. 提高写入性能:数据不是每次修改都直接写磁盘,而是先写 redo log(顺序写,速度快),后续再异步刷盘。
|
||||
3. 支持 crash-safe:MySQL 宕机重启后,可通过 redo log 恢复未刷盘的数据。
|
||||
|
||||
## 4.3 回滚日志(Undo Log)
|
||||
### 4.3.1 概念
|
||||
  回滚日志也是 InnoDB 引擎特有 的日志,它记录的是 数据被修改前的原始值
|
||||
|
||||
### 4.3.2 作用
|
||||
1. 支持事务回滚:如果事务执行失败或调用了 ROLLBACK,可以通过 undo log 将数据恢复到修改之前的状态。
|
||||
2. 实现 MVCC(多版本并发控制):在读已提交(RC)、可重复读(RR)隔离级别下,undo log 用于提供历史版本数据,使得不同事务能看到不同的数据快照,而不需要加锁。
|
||||
|
||||
## 4.4 和事务、锁之间的关系
|
||||
  例如,当执行一条SQL语句时,事务、锁、日志等之间的关系:
|
||||
1. 发起一个事务
|
||||
2. 执行一系列的DML操作(如 INSERT/UPDATE/DELETE)
|
||||
  **涉及 锁:对操作的数据行或表加锁,防止其他事务干扰。**
|
||||
  **涉及 隔离级别:决定其他事务是否能“看到”你未提交的数据。**
|
||||
  **涉及 undo log:如果事务回滚,可以根据 undo log 恢复旧值。**
|
||||
3. 事务提交(commit) 或 回滚(rollback)
|
||||
  **redo log:保证即使宕机,已提交事务的修改也不丢失(持久性)。**
|
||||
  **binlog:记录数据变更,用于主从复制与时间点恢复。**
|
||||
  **undo log:用于实现事务回滚、MVCC。**
|
||||
4. **背后有日志系统默默记录一切,锁系统保障并发安全,隔离级别定义了“你能看到啥”。**
|
||||
|
||||
### 4.4.1 相互关系
|
||||
1. 事务是锁的使用者,锁是事务实现隔离性的手段。当一个事务对某行数据执行 UPDATE/DELETE 操作时,为了防止其他事务同时修改相同数据,InnoDB 会自动对该行或索引加 排他锁(X锁)。如果事务只是读取数据,根据隔离级别,可能会加 共享锁(S锁) 或使用 MVCC(不加锁)。
|
||||
2. 隔离级别定义了事务之间的可见性规则,是事务“隔离性”的具体体现。事务的隔离性是通过锁 + MVCC(多版本并发控制,依赖 undo log)+ 隔离级别共同实现的。
|
||||
3. 日志是事务实现 持久性、崩溃恢复、回滚 等能力的基石。redo log(重做日志)保障事务的持久性(Durability),事务提交时,先将数据页的变更记录到 redo log(顺序写,高性能),随后再异步刷盘到磁盘数据页。即使系统崩溃,重启后也能通过 redo log 恢复已提交但尚未刷盘的数据。undo log(回滚日志)支持事务回滚 和 MVCC。事务修改数据前,会先把原始数据保存到 undo log,如果事务回滚,可以用它恢复旧值。同时,undo log 也是 MVCC(多版本控制)实现的基础,用于提供历史版本数据。binlog(二进制日志)用于主从复制、时间点恢复。虽然 binlog 是 Server 层的日志,不属于 InnoDB 事务引擎的一部分,但在事务提交时,binlog 与 redo log 通过两阶段提交(2PC)保证一致性。
|
||||
4. 隔离级别决定了锁的粒度和行为,锁是隔离级别的底层实现机制之一。在 READ UNCOMMITTED 下,一般不加锁(脏读允许)。在 READ COMMITTED 和 REPEATABLE READ 下,InnoDB 通过 行锁 + MVCC 实现隔离,不一定非得阻塞其他事务。在 SERIALIZABLE 下,会自动为读操作也加上共享锁,相当于所有操作串行执行,隔离性最强,但并发性能最低。
|
||||
5. 锁保障并发安全,日志保障操作可恢复。两者从不同维度确保数据库的正确性。锁:是在运行时控制谁可以访问哪些数据,是 并发控制 的手段。日志:是在磁盘上记录操作过程,是 故障恢复 & 事务一致性 的手段。
|
||||
6. 隔离级别定义了事务间数据的可见性,而日志(尤其是 undo log 和 binlog)为这种“可见性”提供了实现基础。undo log 是实现 MVCC(多版本并发控制) 的基础,而 MVCC 是 REPEATABLE READ 等隔离级别的关键。binlog 虽不直接影响隔离性,但它记录了事务的变更历史,是构建主从环境、实现数据恢复的基础。
|
||||
|
||||
# 五、优化
|
||||
## 5.1 Explain 执行计划
|
||||
  `EXPLAIN`是 MySQL 自带的一个诊断工具,它可以模拟 MySQL 查询优化器的执行过程,对`SELECT`语句(在 MySQL 8.0 及以上版本,也支持对`UPDATE`、`DELETE`等语句使用)进行分析,并输出该语句的执行计划。
|
||||
### 5.1.1 基本用法
|
||||
  在select语句前面加上EXPLAIN关键字即可,例如:`EXPLAIN SELECT * FROM users WHERE age > 25;
|
||||
`
|
||||
|
||||
### 5.1.2 输出列说明
|
||||
| 列名 | 说明 | 示例 |
|
||||
| :-: | :-: | :-: |
|
||||
| id | 查询标识符 | 相同 id 表示同组查询,执行顺序从上到下;不同 id 值越大优先级越高 |
|
||||
| select_type | 查询类型 | SIMPLE(无子查询)、PRIMARY(外层查询)、SUBQUERY(子查询) |
|
||||
| table | 访问的表名 | |
|
||||
| partitions | 匹配的分区 | |
|
||||
| **type** | 访问类型 | system > const > eq_ref > ref > range > index > ALL(性能核心指标,从优到劣排序) |
|
||||
| possible_keys | 可能使用的索引 | |
|
||||
| **key** | 实际使用的索引 | |
|
||||
| key_len | 索引使用的字节数 | |
|
||||
| ref | 索引匹配的列或常量 | |
|
||||
| **rows** | 预估扫描行数 | 越小越好 |
|
||||
| filtered | 存储引擎返回数据后在 server 层过滤的比例 | |
|
||||
| **Extra** | 额外执行信息 | |
|
||||
|
||||
### 5.1.3 重要指标:Type
|
||||
| 类型 | 描述 | 性能 | 示例 |
|
||||
| - | - | - | - |
|
||||
| system | 系统表,仅一行 | 最优 | MyISAM 引擎的空表 |
|
||||
| const | 主键/唯一索引的常量查询 | 极优 | WHERE id = 1 |
|
||||
| eq_ref | JOIN 时主键/唯一索引关联 | 优 | JOIN ... ON t1.pk = t2.pk |
|
||||
| ref | 非唯一索引的等值查询 | 良 | WHERE index_col = 10 |
|
||||
| fulltext | 全文索引 | 中 | MATCH(...) AGAINST(...) |
|
||||
| ref_or_null | ref + NULL 值搜索 | 中 | WHERE col = 10 OR col IS NULL |
|
||||
| index_merge | 索引合并优化 | 中 | 多个索引条件组合 |
|
||||
| unique_subquery | 唯一索引子查询 | 中 | value IN (SELECT pk FROM ...) |
|
||||
| index_subquery | 非唯一索引子查询 | 中下 | value IN (SELECT index_col FROM ...) |
|
||||
| range |索引范围扫描 | 中下 | WHERE id > 10 |
|
||||
| index | 全索引扫描 | 差 | SELECT indexed_col FROM table |
|
||||
| ALL | 全表扫描 | 最差 | 无索引查询|
|
||||
|
||||
### 5.1.4 重要指标:Extra
|
||||
| 值 | 含义 | 优化建议 |
|
||||
| - | - | - |
|
||||
| Using index | 覆盖索引(无需回表) | 优,保持 |
|
||||
| Using where | Server 层过滤数据 | 检查索引使用 |
|
||||
| Using temporary | 使用临时表 | 优化 GROUP BY/ORDER BY |
|
||||
| Using filesort | 额外排序操作 | 为排序字段加索引 |
|
||||
| Select tables optimized away | 使用聚合函数优化 | 优 |
|
||||
| Using index condition | 索引条件下推(ICP) | MySQL 5.6+ 优化特性 |
|
||||
| Using join buffer | 使用连接缓冲区 | 增大 join_buffer_size |
|
||||
| Impossible WHERE | WHERE 条件永不成立 | 查询逻辑错误 |
|
||||
| Distinct | 优化 DISTINCT 操作 | |
|
||||
|
||||
# 六、执行过程
|
||||

|
||||
  MySQL架构分为两层,Service层和存储引擎层。
|
||||
  Server 层负责建立连接、分析和执行 SQL。存储引擎层负责数据的存储和提取。
|
||||
1. 连接器:建立连接、管理连接、校验用户身份。
|
||||
2. 查询缓存(MySQL8.0已删除)
|
||||
3. 解析SQL,通过解析器对SQL查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段和语句类型等。
|
||||
4. 执行SQL:分为预处理阶段、优化阶段和执行阶段。
|
||||
|
||||
# 七、视图
|
||||
## 7.1 定义
|
||||
  视图(View)是 MySQL 中的一种虚拟表,它的数据来源于一个或多个实际的表(基表),**其结构和数据是通过 SQL 查询语句定义的**。简单来说,视图就像是一个 “查询窗口”,你看到的是基表的数据,但**视图本身并不存储实际数据,每次访问视图时都会执行对应的查询语句**。
|
||||
|
||||
## 7.2 作用
|
||||
1. 简化复杂查询:将多表关联、聚合等复杂的 SQL 逻辑封装到视图中,后续使用时只需查询视图即可。
|
||||
2. 数据安全:可以只暴露基表中的部分列或部分行给用户,隐藏敏感数据。
|
||||
3. 数据一致性:如果业务逻辑发生变化,只需修改视图的定义,而无需修改所有使用该逻辑的查询。
|
||||
|
||||
## 7.3 使用
|
||||
  创建视图
|
||||
```sql
|
||||
-- 创建视图v_student_basic
|
||||
CREATE VIEW v_student_basic
|
||||
AS
|
||||
SELECT name, age, gender FROM student;
|
||||
```
|
||||
|
||||
  使用视图和正常使用数据表一致:
|
||||
```sql
|
||||
-- 查询视图(和查询普通表的方式完全一样)
|
||||
SELECT * FROM v_student_basic;
|
||||
```
|
||||
|
||||
# 八、事件
|
||||
## 8.1 定义
|
||||
  MySQL 事件(Event)也被称为事件调度器(Event Scheduler),是 `MySQL 中一种定时执行的数据库对象`,可以理解为数据库层面的 “定时任务” 或 “计划任务”。它能根据你设定的时间规则(一次性执行、周期性执行),自动触发并执行指定的 SQL 逻辑(如数据清理、统计报表生成、数据同步等)。
|
||||
|
||||
## 8.2 特点
|
||||
1. 定时执行:支持一次性执行(如某个具体时间点)和周期性执行(如每天凌晨 3 点、每小时执行一次)。
|
||||
2. 自动运行:依赖 MySQL 的事件调度器线程,只要调度器开启,事件就会按规则自动触发。
|
||||
3. 与存储过程结合:事件的执行逻辑可以是单条 SQL,也可以是复杂的存储过程(推荐用存储过程封装复杂逻辑)。
|
||||
|
||||
## 8.3 作用
|
||||
- 数据清理:定期删除过期数据(如删除 30 天前的日志表数据)。
|
||||
- 数据统计:定时生成业务统计报表(如每天凌晨统计前一天的订单数据)。
|
||||
- 数据同步:定期将 A 表的数据同步到 B 表。
|
||||
- 定时备份:定期执行数据库备份脚本(配合存储过程)。
|
||||
|
||||
## 8.4 使用
|
||||
  开启事件调度器:
|
||||
```sql
|
||||
-- 查看事件调度器状态(ON表示开启,OFF表示关闭)
|
||||
SHOW VARIABLES LIKE 'event_scheduler';
|
||||
|
||||
-- 临时开启(MySQL重启后会恢复为默认状态)
|
||||
SET GLOBAL event_scheduler = ON;
|
||||
-- 永久开启(需要修改my.cnf/my.ini配置文件,添加以下内容,然后重启MySQL)
|
||||
event_scheduler = ON # 放在[mysqld]节点下
|
||||
```
|
||||
|
||||
  每天凌晨 1 点执行的事件,删除日志表中 7 天前的过期数据:
|
||||
```sql
|
||||
-- 创建周期性事件,每天凌晨1点执行,无结束时间
|
||||
CREATE EVENT IF NOT EXISTS event_clear_log
|
||||
ON SCHEDULE EVERY 1 DAY STARTS '2025-12-22 01:00:00'
|
||||
COMMENT '每天凌晨1点删除7天前的日志'
|
||||
DO
|
||||
DELETE FROM log WHERE create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);
|
||||
```
|
||||
41
docs/Web/MySQL/SQL-Advance.md
Normal file
41
docs/Web/MySQL/SQL-Advance.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: SQL高阶用法
|
||||
date: 2025-12-21
|
||||
---
|
||||
|
||||
# 一、WITH
|
||||
## 1.1 定义
|
||||
  SQL 中的WITH子句也被称为**公用表表达式**(CTE,Common Table Expression),它的作用是**在执行主查询之前,先定义一个临时的结果集,这个结果集可以在后续的查询中被多次引用**,就像一个临时表一样。它能让复杂的 SQL 查询变得更清晰、更易读,还能简化嵌套查询的逻辑。
|
||||
|
||||
## 1.2 使用
|
||||
```sql
|
||||
-- 定义第一个CTE:数学高分学生
|
||||
WITH math_high_score AS (
|
||||
SELECT name, score FROM student_score WHERE subject = '数学' AND score > 85
|
||||
),
|
||||
-- 定义第二个CTE:语文高分学生
|
||||
chinese_high_score AS (
|
||||
SELECT name, score FROM student_score WHERE subject = '语文' AND score > 85
|
||||
)
|
||||
-- 主查询:查询既在数学高分又在语文高分的学生
|
||||
SELECT m.name
|
||||
FROM math_high_score m
|
||||
JOIN chinese_high_score c ON m.name = c.name;
|
||||
```
|
||||
|
||||
  递归 CTE:
|
||||
```sql
|
||||
WITH recursive dept_hierarchy AS (
|
||||
-- 锚点成员:查询顶级部门(parent_id为NULL)
|
||||
SELECT dept_id, dept_name, parent_id, 1 AS level
|
||||
FROM department
|
||||
WHERE parent_id IS NULL
|
||||
UNION ALL
|
||||
-- 递归成员:查询子部门,关联自身的dept_id和parent_id
|
||||
SELECT d.dept_id, d.dept_name, d.parent_id, dh.level + 1 AS level
|
||||
FROM department d
|
||||
JOIN dept_hierarchy dh ON d.parent_id = dh.dept_id
|
||||
)
|
||||
-- 主查询:获取所有部门的层级
|
||||
SELECT * FROM dept_hierarchy;
|
||||
```
|
||||
79
docs/Web/Others/AList.md
Normal file
79
docs/Web/Others/AList.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: AList简介和使用
|
||||
date: 2025-12-17
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  [AList](https://alistgo.com/zh/)是一个支持多种存储,支持网页浏览和 WebDAV 的文件列表程序,由 gin 和 Solidjs 驱动。
|
||||
|
||||
# 二、安装
|
||||
## 2.1 Docker安装
|
||||
```yml
|
||||
services:
|
||||
alist:
|
||||
image: xhofe/alist:v3.41.0
|
||||
container_name: alist
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/opt/alist/data
|
||||
- /srv:/srv
|
||||
ports:
|
||||
- "5244:5244"
|
||||
environment:
|
||||
- PUID=0
|
||||
- PGID=0
|
||||
```
|
||||
|
||||
  这里的srv为宿主机的系统服务数据目录,添加该映射,可以让Alist能够访问宿主机/srv目录下的本地文件。
|
||||
|
||||
# 三、挂载
|
||||
## 3.1 挂载本地存储
|
||||
  打开控制台,选择存储->添加->本地存储,设置挂载路径(自定义,如`/local`),选择根文件夹路径为`/srv`,点击添加。
|
||||
  这里的`/srv`文件夹为`docker`里面的文件夹路径,一般只需要更改`docker compose`文件里面的`/srv:/srv`前面的`srv`路径为宿主机路径即可,后面的`/srv`路径不要更改。
|
||||
  可以在宿主机的`/srv`目录中添加文件,查看AList的主页是否可以正确显示。
|
||||
|
||||
# 四、API
|
||||
  在线调试地址:[Alist-Public](https://alist-public.apifox.cn/)
|
||||
|
||||
## 4.1 登录
|
||||
```JavaScript
|
||||
const username = ''
|
||||
const password = ''
|
||||
|
||||
// 1. 登录获取token
|
||||
const login = await axios.post(`${AListUrl}/api/auth/login`, {
|
||||
username,
|
||||
password
|
||||
})
|
||||
|
||||
const token = login.data.data.token
|
||||
```
|
||||
|
||||
## 4.2 上传文件
|
||||
```JavaScript
|
||||
const data = new FormData()
|
||||
data.append('file', options.file)
|
||||
|
||||
const filePath = `${AListPath}/${options.file.name}`
|
||||
|
||||
await axios({
|
||||
method: 'put',
|
||||
url: `${AListUrl}/api/fs/form`,
|
||||
headers: {
|
||||
Authorization: token,
|
||||
'File-Path': filePath
|
||||
},
|
||||
data
|
||||
})
|
||||
```
|
||||
|
||||
## 4.3 下载文件
|
||||
  在AList的文件路径中加上前缀`/p/`。例如文件路径为`/local/Test/test.mp4`,则下载路径为`/p/local/Test/test.mp4`。
|
||||
::: tip
|
||||
以上为相对路径,使用时加上相应的ip和端口号。
|
||||
:::
|
||||
|
||||
::: warning
|
||||
如果出现无法访问的情况,需要在设置->全局里面,将`签名所有`关闭。
|
||||
签名所有:Alist是否将为这个存储下的所有文件请求生成带签名的临时链接,如果开启则需要带上校验码签名,否则不需要。
|
||||
:::
|
||||
100
docs/Web/Others/Browser-Process.md
Normal file
100
docs/Web/Others/Browser-Process.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: 浏览器渲染全流程解析
|
||||
date: 2025-12-13
|
||||
---
|
||||
|
||||
# 一、网络请求阶段
|
||||
1. DNS 解析
|
||||
|
||||
  浏览器先解析输入的网址对应的 IP 地址,先检查本地 DNS 缓存,若没有则向本地 DNS 服务器发起查询。最终通过 DNS 系统获取服务器的 IP 地址。
|
||||
|
||||
2. 建立网络连接(TCP/IP 握手)
|
||||
|
||||
  通过三次握手建立客户端(浏览器)与服务器的 TCP 连接。若网址使用 HTTPS,还需完成 TLS/SSL 握手,确保数据加密传输。
|
||||
|
||||
3. 发起 HTTP/HTTPS 请求
|
||||
|
||||
  浏览器向服务器发送请求报文,包含:
|
||||
- **请求行**:方法(GET)、URL、协议版本(如 HTTP/1.1)。
|
||||
- **请求头**:包含浏览器信息(User-Agent)、缓存策略(Cache-Control)等。
|
||||
- **若为 POST 请求**:还包含请求体数据。
|
||||
|
||||
4. 服务器处理请求
|
||||
|
||||
  服务器接收到请求后,根据 URL 路由到对应的后端服务(如 Node.js、Java 等)。
|
||||
- **若为 Vue3 项目**:
|
||||
* 若为单页应用(SPA),服务器通常返回一个通用的 `index.html` 页面(前端路由由 Vue Router 处理)。
|
||||
* 若涉及 API 请求(如获取数据),服务器返回 JSON 格式的数据(如通过 Express、Spring Boot 等接口)。
|
||||
|
||||
5. 返回响应结果
|
||||
|
||||
  服务器返回响应报文,包含:
|
||||
- **状态行**:状态码(如 200 OK、404 Not Found)。
|
||||
- **响应头**:包含内容类型(Content-Type: text/html)、缓存控制(Cache-Control: max-age=3600)等。
|
||||
- **响应体**:
|
||||
* 若为 `index.html`:返回 HTML 文本(包含 Vue3 项目的入口脚本引用,如 `<script src="/static/js/app.js"></script>`)。
|
||||
* 若为 API 数据:返回 JSON 格式的数据(如用户信息、列表数据等)。
|
||||
|
||||
|
||||
# 二、解析与执行阶段
|
||||
1. 解析 HTML 并构建 DOM 树
|
||||
|
||||
  浏览器逐行解析 HTML 内容,构建 **DOM 树**(Document Object Model)。
|
||||
- 遇到 `<link rel="stylesheet">` 标签时,异步加载 CSS 文件并构建 **CSSOM 树**(CSS Object Model)。
|
||||
- 遇到 `<script>` 标签时:
|
||||
* 若为 **同步脚本**(`<script src="app.js">`):暂停 HTML 解析,下载并执行脚本。
|
||||
* 若为 **异步脚本**(`<script async src="app.js">`):异步下载脚本,不阻塞 HTML 解析。
|
||||
|
||||
2. 加载 Vue3 相关资源
|
||||
|
||||
  浏览器下载并执行 Vue3 的核心库(`vue.runtime.global.js`)、项目打包后的 JS 文件(如 `app.js`)和 CSS 文件(如 `app.css`)。
|
||||
::: tip
|
||||
- `app.js`:包含 Vue3 应用的入口逻辑(如 `createApp(App).mount('#app')`)、组件定义、路由配置等。
|
||||
- `app.css`:包含全局样式或组件样式(通过 `<style>` 标签在组件中定义的样式会被编译为内联样式)。
|
||||
:::
|
||||
|
||||
3. 执行 Vue3 应用初始化
|
||||
|
||||
  当浏览器执行到 Vue3 入口脚本时,会触发以下流程:
|
||||
- **创建应用实例**:通过 `createApp(App)` 创建 Vue 应用实例。
|
||||
- **配置插件和路由**:
|
||||
* 安装路由插件 `vue-router`:`app.use(router)`。
|
||||
* 安装状态管理插件(如 `pinia`):`app.use(pinia)`。
|
||||
- **挂载应用到 DOM**:通过 `app.mount('#app')` 将 Vue 组件渲染到 HTML 中的指定容器(如 `<div id="app"></div>`)。
|
||||
|
||||
4. Vue3 组件渲染流程
|
||||
|
||||
  **初始化组件**:从根组件(`App.vue`)开始,递归解析组件树,初始化响应式数据。
|
||||
- **模板编译**:
|
||||
* 若使用单文件组件(`.vue`),模板(`<template>`)会被编译为渲染函数(`render`)。
|
||||
- **响应式依赖收集**:Vue3 通过 `Proxy` 和 `Reflect` 实现响应式,当组件访问数据(如 `message`)时,自动建立依赖关系。
|
||||
- **首次渲染(挂载阶段)**:
|
||||
* 根据渲染函数生成 **虚拟 DOM(VNode)**。
|
||||
* 通过 `patch` 函数将虚拟 DOM 渲染为真实 DOM,并插入到页面中。
|
||||
* 例如,根组件渲染后,`#app` 容器内会生成对应的 HTML 结构。
|
||||
|
||||
5. 处理异步操作(如数据请求)
|
||||
|
||||
  在组件的 `setup` 函数或生命周期钩子(如 `onMounted`)中,通过 `axios` 或 Vue3 内置的 `useFetch` 发起 API 请求:
|
||||
- **数据更新与重新渲染**:当响应数据返回后,更新响应式数据(如 `data.value`),Vue3 检测到变化后,重新生成虚拟 DOM 并与真实 DOM 对比,执行 **差异化更新(Diff 算法)**,只更新变化的部分。
|
||||
|
||||
# 三、页面渲染与交互阶段
|
||||
1. 合成图层并绘制页面
|
||||
|
||||
  浏览器将 DOM 树和 CSSOM 树合并为 **渲染树(Render Tree)**,计算每个元素的布局(Layout)和样式(Style)。通过 **图层合成(Composite)** 将不同层级的元素绘制到页面上,最终显示完整的 UI。
|
||||
|
||||
2. 用户交互与组件更新
|
||||
|
||||
- 当用户触发事件(如点击按钮、输入表单)时:
|
||||
* Vue3 通过 `v-on` 指令绑定事件处理函数(如 `@click="handleClick"`)。
|
||||
* 事件处理函数中修改响应式数据,触发组件重新渲染。
|
||||
- 点击按钮时,`count` 的值更新,Vue3 自动重新渲染按钮文本。
|
||||
|
||||
# 四、总结
|
||||
::: tip
|
||||
输入网址 → DNS解析 → TCP连接 → 服务器返回index.html → 解析HTML加载Vue3脚本 →
|
||||
执行createApp初始化 → 挂载组件到DOM → 首次渲染(虚拟DOM转真实DOM) →
|
||||
异步请求数据 → 数据更新触发重新渲染 → 页面最终呈现 → 用户交互驱动组件更新
|
||||
:::
|
||||
|
||||
  通过以上流程,浏览器从输入网址到完成 Vue3 应用的渲染,充分利用了前端框架的响应式机制和虚拟 DOM 技术,实现高效的页面更新与交互。
|
||||
313
docs/Web/Others/ElUpload.md
Normal file
313
docs/Web/Others/ElUpload.md
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: Element Plus 上传器
|
||||
date: 2026-01-29
|
||||
---
|
||||
|
||||
# 一、上传图片
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
v-model:file-list="imageFileList"
|
||||
list-type="picture-card"
|
||||
:on-preview="handlePreview"
|
||||
:limit="3"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="httpRequest"
|
||||
:on-exceed="handleExceed"
|
||||
:before-remove="beforeRemove"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
请上传小于10M的图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<el-image :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
|
||||
import { getFileExtension, getFileHashName, validateImage } from '@/utils/file.ts'
|
||||
import { uploadFileApi } from '@/apis/file.ts'
|
||||
|
||||
const imageFileList = ref<UploadUserFile[]>([])
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const baseUrl = ''
|
||||
const apiImageList = [] as string[]
|
||||
|
||||
onMounted(async () => {
|
||||
imageFileList.value = []
|
||||
|
||||
// 根据后端传来的List数据将信息添加文件列表中
|
||||
apiImageList.forEach((item, index) => {
|
||||
imageFileList.value.push({
|
||||
name: index.toString(),
|
||||
url: `${baseUrl}${item}`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
|
||||
const maxFileMax = 1024 * 1024 * 10
|
||||
const fileType = rawFile.type
|
||||
const fileName = rawFile.name
|
||||
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||
|
||||
if (!validateImage(fileType, fileExtension)) {
|
||||
ElMessage.error('请上传图片')
|
||||
return false
|
||||
}
|
||||
|
||||
if (rawFile.size > maxFileMax) {
|
||||
ElMessage.error('请上传10M以内的文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '文件上传中 请等待',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
// 重命名文件
|
||||
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
|
||||
|
||||
const data = new FormData()
|
||||
data.append('file', options.file)
|
||||
data.append('filePath', filePath)
|
||||
|
||||
// 调用后端接口上传文件
|
||||
await uploadFileApi(data)
|
||||
apiImageList.push(filePath)
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('上传失败 请联系管理员')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多上传3张图片')
|
||||
}
|
||||
|
||||
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
|
||||
const index = imageFileList.value.findIndex((item) => item.uid === uploadFile.uid)
|
||||
apiImageList.splice(index, 1)
|
||||
}
|
||||
|
||||
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||
dialogImageUrl.value = uploadFile.url!
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const beforeRemove: UploadProps['beforeRemove'] = () => {
|
||||
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
# 二、上传视频
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
v-model:file-list="videoFileList"
|
||||
:limit="1"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="httpRequest"
|
||||
:on-exceed="handleExceed"
|
||||
:before-remove="beforeRemove"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
请上传小于200M的视频
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
|
||||
import { getFileExtension, getFileHashName, validateVideo } from '@/utils/file.ts'
|
||||
import { uploadFileApi } from '@/apis/file.ts'
|
||||
|
||||
const videoFileList = ref<UploadUserFile[]>([])
|
||||
|
||||
let baseUrl = ''
|
||||
const apiVideoUrl = ''
|
||||
|
||||
onMounted(async () => {
|
||||
videoFileList.value = []
|
||||
if (apiVideoUrl) {
|
||||
videoFileList.value.push({
|
||||
name: 'video',
|
||||
url: `${baseUrl}p/${apiVideoUrl}`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
|
||||
const maxFileMax = 1024 * 1024 * 200
|
||||
const fileType = rawFile.type
|
||||
const fileName = rawFile.name
|
||||
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||
|
||||
if (!validateVideo(fileType, fileExtension)) {
|
||||
ElMessage.error('请上传视频')
|
||||
return false
|
||||
}
|
||||
|
||||
if (rawFile.size > maxFileMax) {
|
||||
ElMessage.error('请上传200M以内的文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '文件上传中 请等待',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
|
||||
|
||||
const data = new FormData()
|
||||
data.append('file', options.file)
|
||||
data.append('filePath', filePath)
|
||||
|
||||
await uploadFileApi(data)
|
||||
baseUrl = filePath
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('上传失败 请联系管理员')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多上传1个视频')
|
||||
}
|
||||
|
||||
const beforeRemove: UploadProps['beforeRemove'] = () => {
|
||||
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
|
||||
}
|
||||
|
||||
const handleRemove: UploadProps['onRemove'] = () => {
|
||||
baseUrl = ''
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
# 三、工具类
|
||||
```ts
|
||||
import { hashSHA256 } from 'vue3-common/utils/cryptoUtil'
|
||||
|
||||
export const getFileHashName = (name: string) => {
|
||||
return hashSHA256(name)
|
||||
}
|
||||
|
||||
export const getFileExtension = (name: string) => {
|
||||
return name.substring(name.lastIndexOf('.') + 1).toLowerCase()
|
||||
}
|
||||
|
||||
export const validateExcel = (fileType: string, fileExtension: string): boolean => {
|
||||
// 常见的 Excel MIME 类型
|
||||
const excelMimeTypes = [
|
||||
'application/vnd.ms-excel', // .xls
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm
|
||||
'text/csv' // CSV
|
||||
]
|
||||
|
||||
// 常见的 Excel 文件扩展名
|
||||
const excelExtensions = ['xls', 'xlsx', 'xlsm', 'csv']
|
||||
|
||||
return excelMimeTypes.includes(fileType) || excelExtensions.includes(fileExtension.toLowerCase())
|
||||
}
|
||||
|
||||
export const validateImage = (fileType: string, fileExtension: string): boolean => {
|
||||
// 常见的图片 MIME 类型
|
||||
const imageMimeTypes = [
|
||||
'image/jpeg', // JPEG
|
||||
'image/jpg', // JPG
|
||||
'image/png', // PNG
|
||||
'image/gif', // GIF
|
||||
'image/bmp', // BMP
|
||||
'image/webp', // WebP
|
||||
'image/svg+xml' // SVG
|
||||
]
|
||||
|
||||
// 常见的图片文件扩展名
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
|
||||
|
||||
return imageMimeTypes.includes(fileType) || imageExtensions.includes(fileExtension.toLowerCase())
|
||||
}
|
||||
|
||||
export const validateVideo = (fileType: string, fileExtension: string): boolean => {
|
||||
// 常见的视频 MIME 类型
|
||||
const videoMimeTypes = [
|
||||
'video/mp4', // MP4
|
||||
'video/mpeg', // MPEG
|
||||
'video/quicktime', // MOV
|
||||
'video/x-msvideo', // AVI
|
||||
'video/x-matroska', // MKV
|
||||
'video/webm', // WebM
|
||||
'video/3gpp', // 3GP
|
||||
'video/3gpp2', // 3G2
|
||||
'video/x-flv', // FLV
|
||||
'video/mp2t', // TS (MPEG Transport Stream)
|
||||
'application/x-mpegURL', // M3U8 (HLS)
|
||||
'video/H264', // H264
|
||||
'video/H265' // H265/HEVC
|
||||
]
|
||||
|
||||
// 常见的视频文件扩展名
|
||||
const videoExtensions = [
|
||||
'mp4',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'mov',
|
||||
'avi',
|
||||
'mkv',
|
||||
'webm',
|
||||
'3gp',
|
||||
'3g2',
|
||||
'flv',
|
||||
'ts',
|
||||
'm3u8',
|
||||
'h264',
|
||||
'h265',
|
||||
'hevc'
|
||||
]
|
||||
|
||||
return videoMimeTypes.includes(fileType) || videoExtensions.includes(fileExtension.toLowerCase())
|
||||
}
|
||||
```
|
||||
82
docs/Web/Others/InfluxDB.md
Normal file
82
docs/Web/Others/InfluxDB.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: InfluxDB简介和使用
|
||||
date: 2026-05-11
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  InfluxDB 是一个开源的时序数据库(Time Series Database, TSDB),由 InfluxData 公司开发,专门用于存储、查询和处理时间序列数据(按时间顺序记录的数据点)。它在监控、物联网(IoT)、实时分析等场景中广泛应用,是时序数据领域的流行解决方案之一。
|
||||
  InfluxDB 3.0 是一次底层的彻底重构(内核代号 IOx),核心代码从 Go 改写为 Rust,并从自研的 TSM 引擎转向基于 Apache Arrow 生态的开放架构。
|
||||
|
||||
# 二、特点
|
||||
1. Rust 全新重写:更快、更省内存、更稳
|
||||
2. 支持无限标签基数:不再怕设备 ID、用户 ID 这类高基数场景
|
||||
3. 原生支持标准 SQL:不用只学 InfluxQL,普通 SQL 直接查
|
||||
4. 底层基于 Arrow + Parquet:列式存储、压缩率高、查询快
|
||||
5. 原生对接对象存储:支持 S3/OSS,冷热数据自动分层,可存海量历史数据
|
||||
|
||||
# 三、概念
|
||||
1. 库 Database:相当于 MySQL 的库,隔离业务数据
|
||||
2. 测量 Measurement:相当于 MySQL 的表(比如 cpu、温度传感器)
|
||||
3. 标签 Tag:维度、索引字段(字符串,如设备 id、地区,用来筛选分组)
|
||||
4. 字段 Field:实际指标值(数值,如温度、CPU 使用率,不做索引)
|
||||
5. 时间 Time:自带时间戳,时序数据唯一主键、按时间排序
|
||||
|
||||
# 四、安装
|
||||
## 4.1 Docker安装
|
||||
```yml
|
||||
services:
|
||||
influxdb3-core:
|
||||
image: influxdb:3.9.1-core
|
||||
container_name: influxdb3-core
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- influx-network
|
||||
ports:
|
||||
- "8181:8181"
|
||||
volumes:
|
||||
- ./influxdb3/data:/var/lib/influxdb3/data
|
||||
- ./influxdb3/plugins:/var/lib/influxdb3/plugins
|
||||
user: root
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
command: >
|
||||
influxdb3 serve
|
||||
--node-id=node0
|
||||
--object-store=file
|
||||
--data-dir=/var/lib/influxdb3/data
|
||||
--plugin-dir=/var/lib/influxdb3/plugins
|
||||
|
||||
influxdb3-explorer:
|
||||
image: influxdata/influxdb3-ui:1.6.3
|
||||
container_name: influxdb3-explorer
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- influx-network
|
||||
ports:
|
||||
- "8600:80"
|
||||
- "8888:8888"
|
||||
volumes:
|
||||
- ./config:/app-root/config:ro
|
||||
- ./db:/db:rw
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
command: --mode=admin
|
||||
|
||||
networks:
|
||||
influx-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
## 4.2 token生成
|
||||
  进入influxdb3-core容器,输入命令获取token:
|
||||
```bash
|
||||
influxdb3 create token --admin
|
||||
```
|
||||
|
||||
## 4.3 可视化页面
|
||||
  打开ip:8600(具体端口看docker的配置),选择`Configure->Servers`,点击`Connect Your First Server`,输入InfluxDB服务器信息进行配置。
|
||||
  在`Query Data->Data Explorer`中查看数据。
|
||||
|
||||
# 五、使用
|
||||
## 5.1 SpringBoot使用
|
||||
1766
docs/Web/Others/JavaScript-Guide.md
Normal file
1766
docs/Web/Others/JavaScript-Guide.md
Normal file
File diff suppressed because it is too large
Load Diff
943
docs/Web/Others/Netword-Program.md
Normal file
943
docs/Web/Others/Netword-Program.md
Normal file
@@ -0,0 +1,943 @@
|
||||
---
|
||||
title: 网络编程简介
|
||||
date: 2025-12-15
|
||||
---
|
||||
|
||||
# 一、TCP
|
||||
## 1.1 定义
|
||||
  TCP(Transmission Control Protocol,传输控制协议)是互联网核心的**面向连接、可靠、字节流**的传输层协议,工作在 OSI 模型的传输层(TCP/IP 模型的传输层),基于 IP 协议提供端到端的可靠数据传输服务。它是 HTTP、HTTPS、WebSocket、MQTT 等应用层协议的底层依赖,核心目标是解决 IP 协议 “无连接、不可靠、无顺序” 的缺陷,确保数据在不可靠的网络中准确、完整、有序地传输。
|
||||
|
||||
## 1.2 特性
|
||||
### 1.2.1 面向连接
|
||||
  通信前必须完成「三次握手」建立连接,通信后通过「四次挥手」释放连接:
|
||||
- 三次握手:客户端发 SYN → 服务器回 SYN+ACK → 客户端发 ACK(确保双方收发能力正常);
|
||||
- 四次挥手:客户端发 FIN → 服务器回 ACK → 服务器发 FIN → 客户端回 ACK(确保数据传输完毕)。
|
||||
|
||||
### 1.2.2 可靠传输
|
||||
- 序号与确认号:每个字节都有序号,接收方收到后回复确认号,未收到则发送方重传;
|
||||
- 超时重传:发送方未在规定时间收到确认,自动重传数据;
|
||||
- 流量控制:通过滑动窗口机制,防止发送方发送过快导致接收方缓冲区溢出;
|
||||
- 拥塞控制:通过慢启动、拥塞避免等算法,适应网络带宽变化。
|
||||
|
||||
### 1.2.3 面向字节流
|
||||
  TCP 将应用层数据视为连续的字节流,不保留应用层数据的边界(与 UDP 的 “数据报” 模式不同):
|
||||
- 发送方:应用层数据被拆分为 TCP 报文段(Segment)发送,拆分规则由 TCP 协议决定(如 MSS 限制)。
|
||||
- 接收方:将收到的报文段按顺序重组为完整的字节流,再交给应用层,确保数据顺序与发送时一致。
|
||||
|
||||
### 1.2.4 有序传输
|
||||
  TCP 报文段头部包含 “序号(Sequence Number)” 和 “确认号(Acknowledgment Number)”:
|
||||
- 序号(SN):标识发送方当前发送的字节流位置(如序号为 100 表示当前报文段的第一个字节是整个字节流的第 100 字节)。
|
||||
- 确认号(ACK):标识接收方期望下次接收的字节流位置(如确认号为 200 表示已正确接收前 199 字节,下次需从 200 字节开始接收)。
|
||||
- 接收方通过序号排序报文段,丢弃重复报文,确保按发送顺序交付数据。
|
||||
|
||||
### 1.2.5 全双工通信
|
||||
  TCP 连接是双向的,双方可同时发送和接收数据,无需等待对方结束发送:
|
||||
- 每个方向都有独立的发送缓冲区和接收缓冲区,以及独立的滑动窗口用于流量控制。
|
||||
- 示例:客户端发送数据的同时,服务器可同步向客户端返回响应,无需等待客户端发送完毕。
|
||||
|
||||
## 1.3 优缺点
|
||||
  优点:
|
||||
1. 可靠、有序、无丢包
|
||||
2. 支持流量 / 拥塞控制
|
||||
3. 适用于大数据传输
|
||||
|
||||
  缺点:
|
||||
1. 连接建立 / 释放开销大
|
||||
2. 头部开销大(20-60 字节)
|
||||
3. 不适合实时性要求极高的场景(如直播低延迟)
|
||||
|
||||
## 1.4 Python实现
|
||||
  TCP服务器:
|
||||
```python
|
||||
import socket
|
||||
import signal
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
from model.mqtt import UNKNOWN_MESSAGE
|
||||
from processor.tcp_processor import TcpMessageProcessor
|
||||
from config.logger_config import logger
|
||||
|
||||
|
||||
class TCPServer:
|
||||
def __init__(self):
|
||||
load_dotenv()
|
||||
|
||||
self.host = os.getenv("TCP_HOST", 'localhost')
|
||||
self.port = int(os.getenv("TCP_PORT", 9100))
|
||||
|
||||
self.max_workers = 10
|
||||
self.timeout = 300
|
||||
|
||||
self.server_socket = None
|
||||
self.running = False
|
||||
self.thread_pool = ThreadPoolExecutor(max_workers=self.max_workers)
|
||||
self.message_processor = TcpMessageProcessor()
|
||||
|
||||
# 已经连接的客户端
|
||||
self.clients = {}
|
||||
self.client_lock = threading.Lock()
|
||||
|
||||
self.server_thread = None
|
||||
|
||||
signal.signal(signal.SIGTERM, self._handle_signal)
|
||||
signal.signal(signal.SIGINT, self._handle_signal)
|
||||
|
||||
def _handle_signal(self, signum, frame):
|
||||
"""处理终止信号,触发优雅关闭"""
|
||||
logger.info(f"收到信号 {signum},准备关闭服务器...")
|
||||
self.running = False
|
||||
|
||||
def _start_loop(self):
|
||||
"""启动服务器"""
|
||||
try:
|
||||
# 创建 TCP/IP socket AF_INET: IPv4地址族 SOCK_STREAM: TCP协议(面向连接)
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
# 设置SO_REUSEADDR选项,允许重用地址和端口,避免服务端重启时出现 “地址已被占用” 的错误
|
||||
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
# 绑定到指定主机和端口 0.0.0.0 表示所有主机
|
||||
self.server_socket.bind((self.host, self.port))
|
||||
# 设置最大等待连接数
|
||||
self.server_socket.listen(5)
|
||||
# 设置socket超时时间(1.0秒)
|
||||
self.server_socket.settimeout(1.0)
|
||||
self.running = True
|
||||
|
||||
logger.info(f"TCP服务器启动,监听 {self.host}:{self.port} "
|
||||
f"(最大线程: {self.max_workers}, 超时: {self.timeout}s)")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# accept() 会阻塞直到有客户端连接
|
||||
# client_socket: 与客户端通信的新socket client_address: 客户端地址(ip, port)元组
|
||||
client_socket, client_address = self.server_socket.accept()
|
||||
# 获取客户端IP
|
||||
client_ip = client_address[0]
|
||||
# 设置客户端socket超时
|
||||
# 防止客户端长时间不发送数据
|
||||
client_socket.settimeout(self.timeout)
|
||||
logger.info(f"新连接: {client_address}")
|
||||
|
||||
# 存储客户端
|
||||
with self.client_lock:
|
||||
self.clients[client_ip] = client_socket
|
||||
|
||||
# 提交到线程池处理
|
||||
# 提交到线程池是为了让服务器能同时服务多个客户端,而不让一个慢客户端阻塞所有其他客户端
|
||||
self.thread_pool.submit(self.handle_client, client_socket, client_ip)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
logger.error(f"接受连接失败: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"TCP服务器启动失败: {str(e)}")
|
||||
finally:
|
||||
self.stop()
|
||||
|
||||
def start(self):
|
||||
self.server_thread = threading.Thread(target=self._start_loop, daemon=True)
|
||||
self.server_thread.start()
|
||||
|
||||
def stop(self):
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.running = False
|
||||
logger.info("开始关闭服务器...")
|
||||
|
||||
# 移除客户端
|
||||
with self.client_lock:
|
||||
for client_socket in self.clients.values():
|
||||
try:
|
||||
client_socket.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭客户端连接失败: {str(e)}")
|
||||
self.clients.clear()
|
||||
|
||||
# 关闭线程池
|
||||
self.thread_pool.shutdown(wait=True)
|
||||
logger.info("所有客户端处理线程已结束")
|
||||
|
||||
# 关闭连接
|
||||
if self.server_socket:
|
||||
self.server_socket.close()
|
||||
logger.info(f"服务器已关闭({self.host}:{self.port})")
|
||||
|
||||
def handle_client(self, client_socket, client_ip):
|
||||
"""处理客户端连接"""
|
||||
try:
|
||||
while True:
|
||||
data = client_socket.recv(1024)
|
||||
if not data:
|
||||
logger.info(f"客户端 {client_ip} 主动断开连接")
|
||||
break
|
||||
|
||||
message = data.decode('utf-8').strip()
|
||||
logger.info(f"收到 {client_ip} 的消息: {message}")
|
||||
|
||||
# 放到消息处理器里面处理
|
||||
response = self.message_processor.process(message)
|
||||
|
||||
# 回复消息
|
||||
if response != UNKNOWN_MESSAGE:
|
||||
client_socket.sendall(response.encode('utf-8'))
|
||||
logger.info(f"回复 {client_ip}: {response}")
|
||||
except socket.timeout:
|
||||
logger.warning(f"客户端 {client_ip} 超时未活动")
|
||||
except Exception as e:
|
||||
logger.error(f"处理 {client_ip} 出错: {str(e)}")
|
||||
finally:
|
||||
# 异常情况下关闭连接
|
||||
with self.client_lock:
|
||||
if client_ip in self.clients:
|
||||
del self.clients[client_ip]
|
||||
|
||||
try:
|
||||
client_socket.close()
|
||||
logger.info(f"客户端 {client_ip} 连接已关闭")
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭 {client_ip} 连接失败: {str(e)}")
|
||||
|
||||
def send_to_client(self, client_ip, message):
|
||||
# 先获取客户端连接(加锁保护)
|
||||
with self.client_lock:
|
||||
client_socket = self.clients.get(client_ip)
|
||||
if not client_socket:
|
||||
logger.warning(f"客户端 {client_ip} 不存在或已断开连接")
|
||||
return False
|
||||
|
||||
# 发送消息
|
||||
try:
|
||||
client_socket.sendall(message.encode('utf-8'))
|
||||
logger.info(f"主动发送消息给 {client_ip}: {message}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"向 {client_ip} 发送消息失败: {str(e)}")
|
||||
# 发送失败时移除无效连接
|
||||
with self.client_lock:
|
||||
if client_ip in self.clients:
|
||||
del self.clients[client_ip]
|
||||
return False
|
||||
|
||||
|
||||
tcp_server = TCPServer()
|
||||
```
|
||||
|
||||
  main.py:
|
||||
```python
|
||||
from endpoint.tcp_server import tcp_server
|
||||
from config.logger_config import logger
|
||||
import time
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
tcp_server.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("收到终止信号,开始关闭程序...")
|
||||
finally:
|
||||
tcp_server.stop()
|
||||
logger.info("程序已退出")
|
||||
except Exception as e:
|
||||
logger.critical(f"程序启动失败: {str(e)}", exc_info=True)
|
||||
exit(1)
|
||||
```
|
||||
|
||||
  启动tcp_server后,通过while循环防止主线程退出,从而让后台的TCP服务器线程能继续运行。
|
||||
  在TCPServer中,通过while循环持续接收tcp客户端的连接,每当有一个客户端连接时,会提交到线程池中去处理,通过自定义消息处理器,将处理完的结果返回给客户端。
|
||||
  TCP消息处理器:
|
||||
::: code-group
|
||||
```python [抽象消息处理器]
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class AbstractDeviceMessageParser(ABC):
|
||||
"""设备消息解析器基类"""
|
||||
|
||||
@abstractmethod
|
||||
def check(self, message):
|
||||
"""判断当前解析器是否能处理该消息"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse(self, message):
|
||||
"""解析消息内容"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def response(self, parsed_data):
|
||||
"""根据解析后的数据生成响应"""
|
||||
pass
|
||||
```
|
||||
|
||||
```python [示例消息处理器]
|
||||
from datetime import datetime
|
||||
|
||||
from model.band import BandData
|
||||
from model.mqtt import MqttTopic, MqttData, UNKNOWN_MESSAGE
|
||||
from parser.abstract_parsers import AbstractDeviceMessageParser
|
||||
import re
|
||||
|
||||
from utils.index import str_length_to_4hex
|
||||
from config.logger_config import logger
|
||||
from endpoint.mqtt_client import mqtt_client
|
||||
|
||||
|
||||
class JuweiBandParser(AbstractDeviceMessageParser):
|
||||
"""聚伟手环消息解析器"""
|
||||
|
||||
def __init__(self):
|
||||
# 聚伟手环消息格式: [MNYD*设备ID*内容长度*内容]
|
||||
self.pattern = r'MNYD'
|
||||
self.vendor = "聚伟手环"
|
||||
self.tag = "MNYD"
|
||||
self.device_id = ""
|
||||
|
||||
def check(self, message):
|
||||
"""检查是否为聚伟手环的消息格式"""
|
||||
return re.search(self.pattern, message) is not None
|
||||
|
||||
def parse(self, message):
|
||||
"""解析聚伟手环消息"""
|
||||
try:
|
||||
parts = message.strip("[]").split("*")
|
||||
|
||||
self.device_id = parts[1]
|
||||
content = parts[3]
|
||||
|
||||
return {
|
||||
'vendor': self.vendor,
|
||||
'device_id': self.device_id,
|
||||
'content': content,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"解析{self.vendor}消息出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def publish_vital_data(self, item, value):
|
||||
mqtt_client.publish(
|
||||
topic=MqttTopic.JuWei_Band_Post.value,
|
||||
payload=MqttData(
|
||||
deviceIp="",
|
||||
deviceId=self.device_id,
|
||||
payload=BandData(item=item, value=value).model_dump_json(),
|
||||
).model_dump_json())
|
||||
|
||||
def response(self, parsed_data):
|
||||
"""生成聚伟手环的响应消息"""
|
||||
if not parsed_data:
|
||||
return UNKNOWN_MESSAGE
|
||||
|
||||
# 处理不同命令
|
||||
content = parsed_data['content']
|
||||
|
||||
parts = content.split(",", 1)
|
||||
content_tag = parts[0] if len(parts) > 0 else ""
|
||||
content_value = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
replay = UNKNOWN_MESSAGE
|
||||
|
||||
logger.info(f"解析{content_tag}消息")
|
||||
|
||||
match content_tag:
|
||||
# PING消息 [MNYD*334588000000156*0004*PING]
|
||||
case "PING":
|
||||
replay = "PING,1"
|
||||
# 日期,步数,翻滚次数,电量百分数,里程数(km)
|
||||
# [MNYD*334588000000156*0014*KA,120414,50,100,100,100.12]
|
||||
case "KA":
|
||||
replay = content_tag
|
||||
# 位置数据上报
|
||||
# case "UD":
|
||||
# replay = ""
|
||||
# 报警数据上报
|
||||
# [MNYD*334588000000156*00CD*AL,180916,064153,A,22.570512,N,113.8623267,
|
||||
# E,0.00,154.8,0.0,11,100,100,0,0,00100018,7,0,460,1,9529,
|
||||
# 21809,155,9529,21242,132,9529,21405,131,9529,63554,131,9529,
|
||||
# 63555,130,9529,63556,118,9529,21869,116,0,12.4]
|
||||
case "AL":
|
||||
replay = content_tag
|
||||
# 获取服务器端时间
|
||||
# [MNYD*YYYYYYYYYYYYYYY*LEN*LGZONE]
|
||||
case "LGZONE":
|
||||
now = datetime.now()
|
||||
current_date = now.date().strftime("%Y-%m-%d")
|
||||
current_time = now.time().strftime("%H:%M:%S")
|
||||
replay = f"{content_tag},+8,{current_time},{current_date}"
|
||||
# 请求位置数据 TODO
|
||||
case "WG":
|
||||
replay = content_tag
|
||||
# 请求电话本设置信息 TODO
|
||||
case "PHLQ":
|
||||
replay = "PHL"
|
||||
# 请求SOS设置信息 TODO
|
||||
case "SOS":
|
||||
replay = content_tag
|
||||
# 终端心率上传
|
||||
case "heart":
|
||||
self.publish_vital_data(content_tag, content_value)
|
||||
replay = content_tag
|
||||
# 上传体温数据 [MNYD*334588000000156*0009*temp,36.2]
|
||||
case "temp":
|
||||
self.publish_vital_data(content_tag, content_value)
|
||||
replay = content_tag
|
||||
# 上传血压数据 [MNYD*334588000000156*000C*blood,150,70]
|
||||
case "blood":
|
||||
self.publish_vital_data(content_tag, content_value)
|
||||
replay = content_tag
|
||||
# 上传血氧数据 [MNYD*334588000000156*0009*oxygen,97]
|
||||
case "oxygen":
|
||||
self.publish_vital_data(content_tag, content_value)
|
||||
replay = content_tag
|
||||
# 上传睡眠数据报告
|
||||
case "SLEEPRPT":
|
||||
replay = "SLEEP"
|
||||
case _:
|
||||
logger.info("不需要回复")
|
||||
return UNKNOWN_MESSAGE
|
||||
|
||||
return f"[{self.tag}*{parsed_data['device_id']}*{str_length_to_4hex(replay)}*{replay}]"
|
||||
```
|
||||
|
||||
```python [消息处理器]
|
||||
from model.mqtt import UNKNOWN_MESSAGE
|
||||
from parser.juwei_band_parser import JuweiBandParser
|
||||
from config.logger_config import logger
|
||||
|
||||
class TcpMessageProcessor:
|
||||
"""消息处理器,负责将消息路由到正确的设备解析器"""
|
||||
|
||||
def __init__(self):
|
||||
# 注册所有支持的设备解析器
|
||||
self.parsers = [JuweiBandParser()]
|
||||
|
||||
def process(self, message):
|
||||
"""处理消息,返回响应"""
|
||||
# 尝试找到能处理该消息的解析器
|
||||
for parser in self.parsers:
|
||||
if parser.check(message):
|
||||
parsed_data = parser.parse(message)
|
||||
if parsed_data:
|
||||
logger.info(f"处理{parsed_data['vendor']}消息: {message}")
|
||||
return parser.response(parsed_data)
|
||||
|
||||
# 没有找到合适的解析器
|
||||
logger.warning(f"未识别的消息格式: {message}")
|
||||
return UNKNOWN_MESSAGE
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
# 二、HTTP
|
||||
## 2.1 定义
|
||||
  HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网的核心协议之一,用于**客户端(如浏览器、App)与服务器之间的通信**,是万维网(WWW)数据交换的基础。它定义了请求 / 响应的格式、传输规则和状态码等核心机制,支持从简单文本到复杂多媒体(图片、视频、文件)的传输,也是现代 Web 应用的底层通信标准。
|
||||
|
||||
## 2.2 特性
|
||||
### 2.2.1 请求 - 响应模式
|
||||
  通信由客户端主动发起请求,服务器接收后处理并返回响应,不存在服务器主动向客户端推送数据的情况(HTTP/2 引入 Server Push 扩展,可主动推送关联资源)。
|
||||
  一次完整通信流程:**客户端建立连接 → 发送请求 → 服务器处理 → 返回响应 → 连接关闭**(HTTP/1.1 默认开启长连接 Keep-Alive)。
|
||||
|
||||
### 2.2.2 无状态
|
||||
  **服务器不会保存客户端的会话状态**(如登录状态、浏览记录),每次请求都是独立的,服务器无法通过协议本身识别连续请求是否来自同一客户端。通过 Cookie、Session、Token(如 JWT)等机制补充状态管理。
|
||||
|
||||
## 2.3 版本
|
||||
### 2.3.1 HTTP/1.0(1996 年)
|
||||
- 基础版本,支持 GET、POST、HEAD 三种请求方法。
|
||||
- 每次请求都需要建立新的 TCP 连接(短连接),连接建立和关闭的开销大,性能较低。
|
||||
- 不支持长连接、管线化请求,仅支持简单的文本传输。
|
||||
|
||||
### 2.3.2 HTTP/1.1(1999 年)
|
||||
- 默认开启 长连接(Keep-Alive):同一 TCP 连接可处理多个请求,减少连接开销。
|
||||
- 支持 管线化请求:客户端可连续发送多个请求,无需等待前一个响应返回(部分浏览器未完全支持)。
|
||||
- 新增请求方法:PUT、DELETE、OPTIONS、TRACE、CONNECT。
|
||||
- 支持 chunked 编码(分块传输)、缓存控制(Cache-Control)、内容协商等核心功能。
|
||||
|
||||
### 2.3.3 HTTP/2(2015 年)
|
||||
  基于 SPDY 协议优化,核心目标是提升性能:
|
||||
- 二进制帧传输:将请求 / 响应数据拆分为二进制帧,而非 HTTP/1.x 的文本格式,解析效率更高。
|
||||
- 多路复用:同一 TCP 连接中可并发处理多个请求(通过帧的 Stream ID 区分),解决 HTTP/1.1 的 “队头阻塞” 问题。
|
||||
- 服务器推送(Server Push):服务器可主动向客户端推送关联资源(如 HTML 引用的 CSS/JS),减少客户端请求次数。
|
||||
- 头部压缩(HPACK):对请求头和响应头进行压缩,减少传输体积(HTTP/1.x 头部重复传输开销大)。
|
||||
|
||||
### 2.3.4 HTTP/3(2022 年)
|
||||
  基于 SPDY 协议优化,核心目标是提升性能:
|
||||
- 解决 TCP 队头阻塞:UDP 无连接特性,单个流的阻塞不影响其他流。
|
||||
- 更快的连接建立:QUIC 集成 TLS 1.3,减少握手次数(1-RTT 甚至 0-RTT 建立连接)。
|
||||
- 更好的移动网络支持:支持连接迁移(如手机切换 WiFi/4G 时,连接不中断)。
|
||||
|
||||
## 2.4 组成
|
||||
### 2.4.1 请求消息(Request)
|
||||
  客户端向服务器发送的请求格式,由 请求行、请求头、空行、请求体 四部分组成:
|
||||
```http
|
||||
GET /api/courses/1 HTTP/1.1 # 请求行
|
||||
Host: lms.example.com # 请求头(键值对形式)
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
User-Agent: Mozilla/5.0 (Chrome/120.0.0.0)
|
||||
Content-Type: application/json
|
||||
|
||||
{"username": "admin", "password": "123456"} # 请求体(可选,POST/PUT 等方法常用)
|
||||
```
|
||||
|
||||
#### 请求行
|
||||
- 请求方法:表示请求的操作类型(常用方法如下表)。
|
||||
- 请求 URI:指定服务器上的资源路径。
|
||||
- 协议版本:如 HTTP/1.1、HTTP/2。
|
||||
|
||||
#### 请求头
|
||||
  描述请求的附加信息,常用字段:
|
||||
- Host:目标服务器域名(如 lms.example.com),HTTP/1.1 必选字段。
|
||||
- User-Agent:客户端身份标识(如浏览器版本、App 名称)。
|
||||
- Accept:客户端可接收的响应数据格式(如 application/json、text/html)。
|
||||
- Content-Type:请求体的数据格式(如 application/json、multipart/form-data(文件上传))。
|
||||
- Authorization:身份认证信息(如 Token、Basic Auth)。
|
||||
- Cookie:客户端存储的会话信息(如登录态 Cookie)。
|
||||
- Cache-Control:缓存控制策略(如 no-cache 表示不使用缓存)。
|
||||
|
||||
#### 请求体
|
||||
  可选部分,仅在需要向服务器提交数据时使用(如 POST 提交表单、PUT 更新资源),数据格式由 Content-Type 指定:
|
||||
- 表单数据:application/x-www-form-urlencoded(如 username=admin&password=123)。
|
||||
- JSON 数据:application/json(如 {"key": "value"})。
|
||||
- 文件上传:multipart/form-data(如 LMS 系统的作业文件上传)。
|
||||
- 纯文本:text/plain。
|
||||
|
||||
### 2.4.2 响应消息(Response)
|
||||
  服务器向客户端返回的响应格式,由 状态行、响应头、空行、响应体 四部分组成:
|
||||
```http
|
||||
HTTP/1.1 200 OK # 状态行
|
||||
Server: Nginx
|
||||
Content-Type: application/json
|
||||
Content-Length: 128
|
||||
Set-Cookie: sessionId=abc123; Path=/
|
||||
|
||||
{"code": 200, "message": "success", "data": {"id": 1, "name": "Vue3 实战课程"}} # 响应体
|
||||
```
|
||||
|
||||
#### 状态行
|
||||
- 协议版本:如 HTTP/1.1。
|
||||
- 状态码:表示请求处理结果。
|
||||
- 状态短语:状态码的文字描述(如 OK、Not Found)。
|
||||
|
||||
#### HTTP状态码
|
||||
| 分类 | 状态码范围 | 含义 | 常用码 |
|
||||
|------|------------|-----------------------|-------------------------|
|
||||
| 1xx | 100-199 | 信息性响应(临时响应)| 100 Continue(预检通过)|
|
||||
| 2xx | 200-299 | 成功响应 | 200 OK(成功)、201 Created(资源创建成功)、204 No Content(成功无响应体)|
|
||||
| 3xx | 300-399 | 重定向 | 301 永久重定向、302 临时重定向、304 Not Modified(缓存有效)|
|
||||
| 4xx | 400-499 | 客户端错误 | 400 Bad Request(请求参数错误)、401 Unauthorized(未认证)、403 Forbidden(权限不足)、404 Not Found(资源不存在)、405 Method Not Allowed(请求方法不支持)|
|
||||
| 5xx | 500-599 | 服务器错误 | 500 Internal Server Error(服务器内部错误)、502 Bad Gateway(网关错误)、503 Service Unavailable(服务不可用)、504 Gateway Timeout(网关超时)|
|
||||
|
||||
#### 响应头
|
||||
  描述响应的附加信息,常用字段:
|
||||
- Server:服务器软件标识(如 Nginx、Tomcat)。
|
||||
- Content-Type:响应体的数据格式(如 application/json、text/html)。
|
||||
- Content-Length:响应体的字节大小。
|
||||
- Set-Cookie:服务器向客户端设置 Cookie(如登录态、会话 ID)。
|
||||
- Cache-Control:缓存控制策略(如 max-age=3600 表示缓存 1 小时)。
|
||||
- Access-Control-Allow-Origin:跨域资源共享(CORS)配置(如 * 表示允许所有域名跨域)。
|
||||
- Location:重定向目标地址(3xx 状态码必选)。
|
||||
|
||||
#### 响应体
|
||||
  服务器返回的核心数据,数据格式由 Content-Type 指定,常见格式:
|
||||
- JSON:前后端分离项目首选(如 {"code": 200, "data": [...]})。
|
||||
- HTML:传统 Web 页面(如静态网页、JSP 页面)。
|
||||
- 图片 / 视频:二进制流(如 image/jpeg、video/mp4)。
|
||||
- 纯文本:text/plain。
|
||||
|
||||
# 三、WebSocket
|
||||
## 3.1 定义
|
||||
  WebSocket 是一种**全双工、双向、持久化的网络通信协议**(属于应用层协议),由 HTML5 规范定义,专门解决 HTTP 协议无法实现服务器主动向客户端推送数据的问题。
|
||||
  它的核心特点是:**一旦客户端与服务器建立连接,双方就可以在这个连接上实时、双向地发送数据**,无需像 HTTP 那样每次通信都由客户端发起请求,非常适合实时通信场景(如聊天、直播弹幕、实时数据监控、在线协作等)。
|
||||
|
||||
| 特性 | HTTP | WebSocket |
|
||||
|---------------------|-------------------------------|-------------------------------|
|
||||
| 通信方向 | 单向(客户端请求→服务器响应)| 全双工(双方可同时发数据)|
|
||||
| 连接类型 | 短连接 / 长连接(需重复请求) | 持久连接(一次建立,持续通信) |
|
||||
| 数据传输效率 | 每次请求带大量头部信息,效率低 | 连接建立后仅传输数据,开销小 |
|
||||
| 服务器主动推送 | 不支持(HTTP/2 的 Server Push 仅能推送资源,非实时数据) | 原生支持,可主动向客户端发数据 |
|
||||
| 协议标识 | http:// / https:// | ws:// / wss://(加密版)|
|
||||
|
||||
## 3.2 特性
|
||||
### 3.2.1 握手过程
|
||||
1. 客户端发送 HTTP 请求,请求头包含 Upgrade: websocket 和 Connection: Upgrade(表示要升级为 WebSocket 协议);
|
||||
2. 服务器响应 101 Switching Protocols,握手成功,连接转为 WebSocket 持久连接;
|
||||
3. 后续通信不再使用 HTTP 格式,而是 WebSocket 帧格式(二进制 / 文本)。
|
||||
|
||||
  请求头:
|
||||
```http
|
||||
GET /chat HTTP/1.1
|
||||
Host: example.com
|
||||
Upgrade: websocket # 核心:请求升级为WebSocket协议
|
||||
Connection: Upgrade # 核心:表示连接要升级
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # 客户端生成的随机密钥,用于验证
|
||||
Sec-WebSocket-Version: 13 # 指定WebSocket版本(主流为13)
|
||||
Origin: https://example.com # 跨域验证
|
||||
```
|
||||
|
||||
  响应头:
|
||||
```http
|
||||
HTTP/1.1 101 Switching Protocols
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 服务器加密后的密钥,客户端验证
|
||||
```
|
||||
|
||||
### 3.2.2 全双工通信
|
||||
  此时 HTTP 连接已升级为 WebSocket 连接,双方可以通过这个连接实时、双向地发送数据。数据传输采用帧(Frame) 格式,支持文本数据和二进制数据(如图片、视频流)。
|
||||
|
||||
### 3.2.3 帧格式
|
||||
  WebSocket 数据以帧为单位传输,帧头包含操作码(文本帧 0x01、二进制帧 0x02、关闭帧 0x08 等)、掩码(客户端发送数据需掩码,服务器无需);
|
||||
|
||||
### 3.2.4 无同源限制
|
||||
  WebSocket 不遵循同源策略(但服务器可通过 Origin 头限制跨域);
|
||||
|
||||
### 3.2.5 心跳机制
|
||||
  通过 Ping/Pong 帧维持连接(避免网络设备断开空闲连接,如 LMS 系统需定期发送 Ping 帧,服务器回复 Pong 帧)。
|
||||
|
||||
# 四、MQTT
|
||||
## 4.1 定义
|
||||
  MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一种**轻量级、低带宽、低功耗的发布 / 订阅(Publish/Subscribe)模式物联网(IoT)通信协议**,由 IBM 于 1999 年设计,核心目标是解决受限设备(如传感器、嵌入式设备)和低带宽、不稳定网络环境下的高效数据传输问题。
|
||||
|
||||
## 4.2 架构
|
||||
- 发布者(Publisher):发送消息的设备 / 服务;
|
||||
- 订阅者(Subscriber):接收消息的设备 / 服务;
|
||||
- broker(代理服务器):核心中间件,接收发布者的消息,根据「主题(Topic)」转发给订阅者(如 EMQ X、Mosquitto、RabbitMQ 支持 MQTT 插件);
|
||||
- 主题(Topic):消息的分类标识。
|
||||
|
||||
## 4.3 特性
|
||||
### 4.3.1 QoS(服务质量)等级
|
||||
- QoS 0(最多一次):消息发送一次,不保证送达;
|
||||
- QoS 1(至少一次):消息至少送达一次,可能重复;
|
||||
- QoS 2(恰好一次):消息仅送达一次,最可靠。
|
||||
|
||||
### 4.3.2 轻量级
|
||||
  头部开销极小(固定头部仅 2 字节),消息体支持二进制 / 文本,适合低带宽场景
|
||||
|
||||
### 4.3.3 保留消息(Retained Message)
|
||||
  broker 保存某个主题的最后一条消息,新订阅者订阅后立即收到该消息
|
||||
|
||||
### 4.3.4 遗嘱消息(Will Message)
|
||||
  客户端异常断开时,broker 自动发送预设消息
|
||||
|
||||
### 4.3.5 清洁会话(Clean Session)
|
||||
  客户端断开连接后,broker 是否保留订阅信息和未送达消息
|
||||
|
||||
## 4.4 组成
|
||||
  MQTT 消息由 固定头部(Fixed Header)、可变头部(Variable Header)、负载(Payload) 三部分组成:
|
||||
| 部分 | 作用 |
|
||||
|--------------|----------------------------------------------------------------------|
|
||||
| 固定头部 | 必选,2 字节起,包含消息类型(如发布、订阅)、QoS 等级、是否保留消息等标识。 |
|
||||
| 可变头部 | 可选,仅部分消息类型(如发布、订阅)需要,包含主题名、消息 ID 等信息。 |
|
||||
| 负载 | 可选,消息的实际内容(如 JSON 字符串、二进制数据),例如 {"temperature": 25}。 |
|
||||
|
||||
# 五、.Net Core实现
|
||||
## 5.1 SuperSocket
|
||||
  [SuperSocket](https://www.supersocket.net/)是一个轻量级, 跨平台而且可扩展的 .Net/Mono Socket 服务器程序框架。可以轻松构建TCP、UDP、WebSocket服务器。
|
||||
|
||||
## 5.2 安装依赖
|
||||
  NuGut安装SuperSocket、SuperSocket.WebSocket和SuperSocket.WebSocket.Server 2.0及以上版本。
|
||||
|
||||
## 5.3 配置文件
|
||||
  appsettings.json
|
||||
```json
|
||||
{
|
||||
"serverOptions": {
|
||||
"TcpServer": {
|
||||
"name": "TcpServer",
|
||||
"listeners": [
|
||||
{
|
||||
"ip": "Any",
|
||||
"port": 4040
|
||||
}
|
||||
]
|
||||
},
|
||||
"WebSocketServer": {
|
||||
"name": "WebSocket",
|
||||
"listeners": [
|
||||
{
|
||||
"ip": "Any",
|
||||
"port": 5050
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Mqtt": {
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 1883,
|
||||
"ClientId": "MqttClient",
|
||||
"Topics": [
|
||||
"test/topic1",
|
||||
"test/topic2"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5.3 主程序
|
||||
  program.cs
|
||||
```cs
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton<IPackageHandler<ProtocolFrame>, TcpPackageHandler>();
|
||||
services.AddSingleton<WebSocketMessageHandler>();
|
||||
|
||||
// 1. 注入 MQTT 配置
|
||||
services.Configure<MqttSettings>(context.Configuration.GetSection("Mqtt"));
|
||||
// 2. 注册 MQTT 客户端
|
||||
services.AddSingleton<IMqttClient>(serviceProvider => new MqttClientFactory().CreateMqttClient());
|
||||
// 3. 注册 MQTT 后台服务
|
||||
services.AddHostedService<MqttService>();
|
||||
})
|
||||
.AsMultipleServerHostBuilder()
|
||||
.AddServer<TcpService, ProtocolFrame, BinaryPipelineFilter>(builder =>
|
||||
{
|
||||
builder.ConfigureServerOptions((ctx, config) => config.GetSection("TcpServer"));
|
||||
})
|
||||
.AddWebSocketServer(builder =>
|
||||
{
|
||||
builder
|
||||
.UseWebSocketMessageHandler(async (session, package) =>
|
||||
{
|
||||
using var scope = session.Server.ServiceProvider.CreateScope();
|
||||
var handler = scope.ServiceProvider.GetRequiredService<WebSocketMessageHandler>();
|
||||
|
||||
await handler.HandleAsync(session, package);
|
||||
})
|
||||
.ConfigureServerOptions((ctx, config) => config.GetSection("WebSocketServer"));
|
||||
})
|
||||
.ConfigureLogging(logging => logging.ClearProviders())
|
||||
.UseNLog()
|
||||
.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
```
|
||||
|
||||
  通过.AsMultipleServerHostBuilder()可以构造多服务器实例。
|
||||
|
||||
## 5.4 TCP服务器
|
||||
### 5.4.1 协议数据
|
||||
```cs
|
||||
/// <summary>
|
||||
/// 协议数据结构 示例:65 6D 00 05 00 01 68 65 6C 6C 6F
|
||||
/// </summary>
|
||||
/// <param name="Magic">帧头标识</param>
|
||||
/// <param name="Length">数据长度</param>
|
||||
/// <param name="Type">数据类型</param>
|
||||
/// <param name="Payload">数据内容</param>
|
||||
///
|
||||
record ProtocolFrame(
|
||||
ushort Magic,
|
||||
ushort Length,
|
||||
ushort Type,
|
||||
byte[] Payload
|
||||
);
|
||||
```
|
||||
|
||||
  可以根据实际情况自定义消息格式。
|
||||
|
||||
### 5.4.2 协议解析
|
||||
```cs
|
||||
class BinaryPipelineFilter : FixedHeaderPipelineFilter<ProtocolFrame>
|
||||
{
|
||||
/// <summary>
|
||||
/// 固定头长度
|
||||
/// </summary>
|
||||
public BinaryPipelineFilter() : base(6)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从包头中解析出 Body 长度
|
||||
/// </summary>
|
||||
/// <param name="buffer">字节流</param>
|
||||
/// <returns>Body 长度</returns>
|
||||
protected override int GetBodyLengthFromHeader(ref ReadOnlySequence<byte> buffer)
|
||||
{
|
||||
var reader = new SequenceReader<byte>(buffer);
|
||||
|
||||
// 读取前2字节 → magic
|
||||
reader.TryReadBigEndian(out ushort magic);
|
||||
|
||||
// 再读2字节 → Length
|
||||
reader.TryReadBigEndian(out ushort length);
|
||||
|
||||
// 再读2字节 → Type
|
||||
reader.TryReadBigEndian(out ushort type);
|
||||
|
||||
// 校验帧头
|
||||
if (magic != 0x656D)
|
||||
{
|
||||
throw new Exception("非法帧头");
|
||||
}
|
||||
|
||||
// 限制长度
|
||||
if (length == 0 || length > 8192)
|
||||
{
|
||||
throw new Exception("非法长度");
|
||||
}
|
||||
|
||||
// 返回 Payload 长度
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把完整字节包 → 转换成 ProtocolFrame
|
||||
/// </summary>
|
||||
/// <param name="buffer">字节流</param>
|
||||
/// <returns>ProtocolFrame</returns>
|
||||
protected override ProtocolFrame DecodePackage(ref ReadOnlySequence<byte> buffer)
|
||||
{
|
||||
var reader = new SequenceReader<byte>(buffer);
|
||||
|
||||
// 读取前2字节 → magic
|
||||
reader.TryReadBigEndian(out ushort magic);
|
||||
|
||||
// 再读2字节 → Length
|
||||
reader.TryReadBigEndian(out ushort length);
|
||||
|
||||
// 再读2字节 → Type
|
||||
reader.TryReadBigEndian(out ushort type);
|
||||
|
||||
var payload = buffer.Slice(6, length).ToArray();
|
||||
|
||||
// 构造ProtocolFrame
|
||||
return new ProtocolFrame(magic, length, type, payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  根据协议数据ProtocolFrame来解包。
|
||||
|
||||
### 5.4.3 消息处理
|
||||
```cs
|
||||
class TcpPackageHandler(ILogger<TcpPackageHandler> logger) : IPackageHandler<ProtocolFrame>
|
||||
{
|
||||
private readonly ILogger<TcpPackageHandler> _logger = logger;
|
||||
|
||||
public async ValueTask Handle(IAppSession session, ProtocolFrame package, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"Magic={package.Magic:X4}, Type={package.Type}, Len={package.Length}");
|
||||
await session.SendAsync(package.Payload, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4.4 TCPService
|
||||
```cs
|
||||
class TcpService(IServiceProvider serviceProvider, IOptions<ServerOptions> serverOptions) : SuperSocketService<ProtocolFrame>(serviceProvider, serverOptions)
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
## 5.5 Websocket服务器
|
||||
### 5.5.1 消息处理
|
||||
```cs
|
||||
class WebSocketMessageHandler(ILogger<WebSocketMessageHandler> logger)
|
||||
{
|
||||
private readonly ILogger<WebSocketMessageHandler> _logger = logger;
|
||||
|
||||
public async ValueTask HandleAsync(WebSocketSession session, WebSocketPackage package)
|
||||
{
|
||||
_logger.LogInformation($"[WebSocket] {package.Message}");
|
||||
await session.SendAsync("ok");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5.6 MQTT服务器
|
||||
### 5.6 MqttService
|
||||
```cs
|
||||
class MqttService(IMqttClient mqttClient, IOptions<MqttSettings> settings, ILogger<TcpPackageHandler> logger) : BackgroundService
|
||||
{
|
||||
private readonly IMqttClient _mqttClient = mqttClient;
|
||||
private readonly MqttSettings _settings = settings.Value;
|
||||
private readonly ILogger<TcpPackageHandler> _logger = logger;
|
||||
private MqttClientOptions? _mqttOptions;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// 构建连接配置
|
||||
_mqttOptions = new MqttClientOptionsBuilder()
|
||||
.WithClientId($"{_settings.ClientId}_{Guid.NewGuid():N}")
|
||||
.WithTcpServer(_settings.Host, _settings.Port)
|
||||
.WithCleanSession()
|
||||
.Build();
|
||||
|
||||
_mqttClient.ConnectedAsync += OnConnectedAsync;
|
||||
_mqttClient.DisconnectedAsync += OnDisconnectedAsync;
|
||||
_mqttClient.ApplicationMessageReceivedAsync += HandleMessage;
|
||||
|
||||
_logger.LogInformation("[MQTT] 正在连接到服务器 {Host}:{Port}...", _settings.Host, _settings.Port);
|
||||
|
||||
await _mqttClient.ConnectAsync(_mqttOptions, stoppingToken);
|
||||
|
||||
// 等待程序停止
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||
}
|
||||
|
||||
public async Task OnConnectedAsync(MqttClientConnectedEventArgs arg)
|
||||
{
|
||||
_logger.LogInformation("[MQTT] 已成功连接到服务器");
|
||||
|
||||
// 连接成功后订阅主题
|
||||
foreach (var topic in _settings.Topics)
|
||||
{
|
||||
await _mqttClient.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce);
|
||||
_logger.LogInformation($"[MQTT] 已订阅主题:{topic}");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task OnDisconnectedAsync(MqttClientDisconnectedEventArgs arg)
|
||||
{
|
||||
_logger.LogError("[MQTT] 连接失败,原因:{Reason}", arg.Reason);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task HandleMessage(MqttApplicationMessageReceivedEventArgs arg)
|
||||
{
|
||||
var topic = arg.ApplicationMessage.Topic;
|
||||
var payload = Encoding.UTF8.GetString(arg.ApplicationMessage.Payload);
|
||||
|
||||
_logger.LogInformation($"\n[MQTT] 收到消息\n主题:{topic}\n内容:{payload}\n");
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool> PublishAsync(string topic, string payload, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(topic)
|
||||
.WithPayload(payload)
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
.Build();
|
||||
|
||||
await _mqttClient.PublishAsync(message, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[MQTT] 发布消息到主题 {Topic} 失败", topic);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await _mqttClient.DisconnectAsync();
|
||||
await base.StopAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  需要实现后台服务接口一直运行。
|
||||
95
docs/Web/Others/Package.md
Normal file
95
docs/Web/Others/Package.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: 前端包管理器
|
||||
date: 2025-12-13
|
||||
---
|
||||
|
||||
# npm
|
||||
  2010年随Node.js发布的npm,首次为JavaScript引入了标准的包管理系统。其核心创新是**package.json文件**,通过语义化版本规范定义了依赖声明标准。这一设计解决了手动管理依赖时的版本混乱问题,为模块化开发提供了基础设施。
|
||||
|
||||
## 第一阶段:嵌套依赖架构
|
||||
  早期版本采用树状嵌套的依赖结构。每个包将其依赖安装在自身的node_modules目录中,形成深层嵌套。这种设计保证了依赖隔离,但导致了几个严重问题:
|
||||
- 路径深度问题:依赖树深度可达十几层,触发Windows 260字符路径限制
|
||||
- 空间极度浪费:相同包在不同层级重复安装,项目体积指数级膨胀
|
||||
- 安装性能低下:大量递归操作导致I/O效率极低
|
||||
|
||||
## 第二阶段:扁平化依赖重构
|
||||
  2015年发布的npm v3引入扁平化依赖安装策略。该方案将**可共享的依赖提升到顶层node_modules**,减少嵌套深度。
|
||||
  主要解决方案为:
|
||||
- 依赖提升算法:将兼容版本的依赖包提升至顶层
|
||||
- 冲突处理:版本冲突时,低版本保持嵌套结构
|
||||
- 确定性牺牲:提升顺序影响最终结构
|
||||
|
||||
  但同时也引入了新问题:
|
||||
- 幻影依赖:未声明但被提升的包意外可用
|
||||
- 依赖不确定性:相同package.json产生不同目录结构
|
||||
- 模块解析复杂化:Node.js模块解析算法与扁平结构不匹配
|
||||
|
||||
# yarn
|
||||
  2016年发布的Yarn针对npm核心痛点提出了系统性解决方案:
|
||||
- 确定性版本锁定:yarn.lock文件记录精确版本和依赖树
|
||||
- 并行下载优化:多线程并发下载提升网络利用率
|
||||
- 离线缓存机制:全局缓存支持离线安装
|
||||
- 完整性校验:checksum验证确保包完整性
|
||||
|
||||
  Yarn的成功迫使npm进行重大改进。npm v5版本借鉴了Yarn的核心设计,增加了package-lock.json和缓存优化,形成了技术竞争的良性循环。
|
||||
|
||||
# pnpm
|
||||
  2017年发布的pnpm从存储层面重新设计了包管理模型。其核心是**基于内容寻址的全局存储与符号链接架构**。
|
||||
  实现原理:
|
||||
- 内容可寻址存储:包按内容哈希存储,全局唯一
|
||||
- 硬链接复用:相同包文件在所有项目间共享
|
||||
- 符号链接树:通过软链接构建符合Node.js解析规则的依赖树
|
||||
- 严格依赖隔离:每个包只能访问其声明的依赖
|
||||
|
||||
  npm/yarn的node_modules目录:
|
||||
```cmd
|
||||
项目A: node_modules/lodash@4.17.21
|
||||
项目B: node_modules/lodash@4.17.21 # 重复存储
|
||||
项目C: node_modules/lodash@4.17.21 # 重复存储
|
||||
```
|
||||
|
||||
  pnpm的node_modules目录:
|
||||
```cmd
|
||||
全局存储: .pnpm-store/lodash@4.17.21
|
||||
项目A: 硬链接 → 全局存储
|
||||
项目B: 硬链接 → 全局存储
|
||||
项目C: 硬链接 → 全局存储
|
||||
```
|
||||
|
||||
  npm/Yarn采用扁平化结构,包可访问非声明依赖,pnpm采用符号链接嵌套结构,包仅可访问声明依赖。
|
||||
  pnpm的符号链接架构在Monorepo场景中表现优异。跨包依赖通过本地文件系统链接实现,避免了重复安装。Workspaces功能通过优化符号链接策略,确保开发环境与生产环境一致性。
|
||||
|
||||
## 简单使用
|
||||
```cmd
|
||||
# 通过 npm 安装
|
||||
npm install -g pnpm
|
||||
|
||||
# 检查版本
|
||||
pnpm --version
|
||||
|
||||
# 安装所有依赖(根据 package.json)
|
||||
pnpm install
|
||||
|
||||
# 安装生产依赖
|
||||
pnpm add <package-name>
|
||||
|
||||
# 安装开发依赖
|
||||
pnpm add -D <package-name>
|
||||
|
||||
# 移除依赖
|
||||
pnpm remove <package-name>
|
||||
|
||||
# 查看所有配置
|
||||
pnpm config list
|
||||
|
||||
# 设置存储路径
|
||||
pnpm config set store-dir ~/.pnpm-store
|
||||
|
||||
# 使用淘宝镜像
|
||||
pnpm config set registry https://registry.npmmirror.com/
|
||||
```
|
||||
|
||||
::: tip
|
||||
npm确立了基本范式,Yarn解决了确定性问题,pnpm重构了存储模型
|
||||
:::
|
||||
|
||||
220
docs/Web/Others/RustFS.md
Normal file
220
docs/Web/Others/RustFS.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
title: RustFS简介和使用
|
||||
date: 2025-12-09
|
||||
---
|
||||
|
||||
# 简介
|
||||
  [RustFS](https://rustfs.com.cn/) 是一个基于 Rust 构建的高性能分布式对象存储系统。
|
||||
  具体以下特点:
|
||||
|
||||
- 高性能:基于 Rust 构建,确保极致的速度和资源效率。
|
||||
- 分布式架构:可扩展且容错的设计,适用于大规模部署。
|
||||
- S3 兼容性:与现有的 S3 兼容应用和工具无缝集成。
|
||||
- 数据湖支持:专为高吞吐量的大数据和 AI 工作负载优化。
|
||||
- 完全开源:采用 Apache 2.0 许可证,鼓励社区贡献和商业使用。
|
||||
- 简单易用:设计简洁,易于部署和管理。
|
||||
|
||||
  S3 标准通常指 Amazon S3 的 API 接口规范,现已成为对象存储领域事实上的行业标准接口协议。它定义了对象存储的核心操作方式和数据模型。
|
||||
- 对象:基本存储单元,包含数据、元数据和唯一标识符
|
||||
- 桶:对象的逻辑容器,类似文件夹
|
||||
- 扁平结构:无传统目录层级,通过键名(Key)定位
|
||||
|
||||
# 安装部署
|
||||
  通过docker部署:
|
||||
```yml
|
||||
services:
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.0-alpha.65
|
||||
container_name: rustfs
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./logs:/logs
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- RUSTFS_ACCESS_KEY=admin
|
||||
- RUSTFS_SECRET_KEY=19940822Cxx
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
  其中9000是API端口,9001是控制台端口。
|
||||
  默认的登录账号密码为:rustfsadmin / rustfsadmin。
|
||||
|
||||
# API调用
|
||||
  登录控制台,获取access_key和secret_access。
|
||||
|
||||
## Python
|
||||
1. 安装AWS官方SDK依赖库
|
||||
```cmd
|
||||
pip install boto3
|
||||
```
|
||||
|
||||
2. 配置与连接
|
||||
```python
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
|
||||
from config.setting import settings
|
||||
|
||||
url = 'ip:port'
|
||||
access_key = ''
|
||||
secret_access = ''
|
||||
|
||||
s3 = boto3.client('s3',
|
||||
endpoint_url=url,
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_access,
|
||||
config=Config(signature_version='s3v4'),
|
||||
region_name='cn-east-1'
|
||||
)
|
||||
```
|
||||
|
||||
3. 上传文件
|
||||
```python
|
||||
async def upload_file(md5: str, file: UploadFile = File(...)) -> str:
|
||||
file_ext = file.filename.split('.')[-1]
|
||||
unique_filename = f"{md5}.{file_ext}"
|
||||
|
||||
file_content = await file.read()
|
||||
|
||||
# 上传到S3
|
||||
s3.put_object(
|
||||
Bucket=BUCKET,
|
||||
Key=unique_filename,
|
||||
Body=file_content,
|
||||
ContentType=file.content_type
|
||||
)
|
||||
|
||||
# 返回文件url
|
||||
return f"{BUCKET}/{unique_filename}"
|
||||
```
|
||||
|
||||
## Flutter
|
||||
1. 安装依赖包
|
||||
```yaml
|
||||
dependencies:
|
||||
minio: ^3.5.8
|
||||
file_picker: ^10.3.3
|
||||
```
|
||||
|
||||
2. 工具类
|
||||
```dart
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:minio/io.dart';
|
||||
import 'package:minio/minio.dart';
|
||||
|
||||
import 'file_utils.dart';
|
||||
|
||||
class MinIOHelper {
|
||||
static final MinIOHelper _instance = MinIOHelper._internal();
|
||||
|
||||
factory MinIOHelper() => _instance;
|
||||
|
||||
final String ip = 'ip';
|
||||
final int port = 9100;
|
||||
final String fileUrl = 'http://ip:port';
|
||||
final String accessKey = '';
|
||||
final String secretKey = '';
|
||||
|
||||
MinIOHelper._internal() {
|
||||
_minio = Minio(
|
||||
endPoint: ip,
|
||||
port: port,
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
useSSL: false,
|
||||
);
|
||||
}
|
||||
|
||||
late Minio _minio;
|
||||
|
||||
Future<String> uploadFile({
|
||||
required PlatformFile file,
|
||||
required String bucketName,
|
||||
Function(double)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
if (isImageFile(file.name)) {
|
||||
// 压缩图片
|
||||
final compressedFile = await compressImage(File(file.path!));
|
||||
|
||||
String hashName = await generateMD5HashName(compressedFile.path);
|
||||
String fileName = '$hashName${getFileExtension(file.name)}';
|
||||
|
||||
await _minio.fPutObject(bucketName, fileName, compressedFile.path);
|
||||
await compressedFile.delete();
|
||||
|
||||
return fileName;
|
||||
} else {
|
||||
String hashName = await generateMD5HashName(file.path!);
|
||||
String fileName = '$hashName${getFileExtension(file.name)}';
|
||||
|
||||
await _minio.fPutObject(bucketName, fileName, file.path!);
|
||||
|
||||
return fileName;
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('文件上传失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  文件工具类:
|
||||
```dart
|
||||
import 'dart:io';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
|
||||
import 'log_utils.dart';
|
||||
|
||||
String getFileExtension(String fileName) {
|
||||
if (fileName.contains('.')) {
|
||||
return '.${fileName.split('.').last.toLowerCase()}';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<String> generateMD5HashName(String filePath) async {
|
||||
final file = File(filePath);
|
||||
final bytes = await file.readAsBytes();
|
||||
final hash = md5.convert(bytes);
|
||||
return hash.toString();
|
||||
}
|
||||
|
||||
// 图片压缩方法
|
||||
Future<File> compressImage(File file) async {
|
||||
try {
|
||||
// 获取压缩后的文件路径
|
||||
final result = await FlutterImageCompress.compressAndGetFile(
|
||||
file.absolute.path,
|
||||
'${file.parent.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
quality: 70,
|
||||
format: CompressFormat.jpeg,
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
throw Exception('图片压缩失败');
|
||||
}
|
||||
|
||||
return File(result.path);
|
||||
} catch (e) {
|
||||
logger.e('图片压缩失败,使用原文件: $e');
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为图片文件
|
||||
bool isImageFile(String fileName) {
|
||||
final imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
|
||||
final extension = fileName.toLowerCase().substring(fileName.lastIndexOf('.'));
|
||||
return imageExtensions.contains(extension);
|
||||
}
|
||||
```
|
||||
243
docs/Web/Others/VideoCompressor.md
Normal file
243
docs/Web/Others/VideoCompressor.md
Normal file
@@ -0,0 +1,243 @@
|
||||
---
|
||||
title: 视频压缩
|
||||
date: 2026-01-29
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  视频码率(Bit Rate),指的是视频文件在单位时间内(通常是每秒)所包含的数据量。单位通常是 Mbps(兆比特每秒)或 Kbps(千比特每秒)。
|
||||
  码率越高:视频中包含的细节信息越多,画面越清晰、色彩越丰富、动态画面(如快速运动、爆炸等)越流畅,不容易出现马赛克或模糊。但代价是文件体积越大,传输所需的网络带宽也越高。码率越低:视频文件更小,传输更快,但在复杂画面中容易产生压缩瑕疵(如马赛克、模糊、色带等)。
|
||||
  如果不压缩,原始视频数据量极其庞大,一部2小时的未压缩1080p电影可能占用数TB的存储空间,普通硬盘无法承受,家庭宽带或移动网络无法实时传输如此巨大的数据量,视频播放会不断卡顿。
|
||||
  如果服务器的带宽为10M,适合 720p,勉强支持低码率 1080p。
|
||||
|
||||
# 二、引入依赖
|
||||
```xml
|
||||
<!-- Jave 2 视频处理库 -->
|
||||
<dependency>
|
||||
<groupId>ws.schild</groupId>
|
||||
<artifactId>jave-all-deps</artifactId>
|
||||
<version>3.5.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
# 三、工具类
|
||||
```java
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import ws.schild.jave.Encoder;
|
||||
import ws.schild.jave.MultimediaObject;
|
||||
import ws.schild.jave.encode.AudioAttributes;
|
||||
import ws.schild.jave.encode.EncodingAttributes;
|
||||
import ws.schild.jave.encode.VideoAttributes;
|
||||
import ws.schild.jave.info.VideoSize;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 视频压缩工具类
|
||||
* 提供将视频压缩到720p的静态方法
|
||||
*/
|
||||
@Slf4j
|
||||
public class VideoCompressor {
|
||||
|
||||
// 支持的视频格式
|
||||
private static final String[] SUPPORTED_FORMATS = {
|
||||
".mp4", ".avi", ".mov", ".mkv", ".flv",
|
||||
".wmv", ".webm", ".mpeg", ".mpg", ".3gp"
|
||||
};
|
||||
|
||||
// 压缩参数常量
|
||||
private static final int TARGET_WIDTH = 1280;
|
||||
private static final int TARGET_HEIGHT = 720;
|
||||
private static final int VIDEO_BITRATE = 1500000; // 1.5 Mbps
|
||||
private static final int AUDIO_BITRATE = 128000; // 128 kbps
|
||||
private static final String VIDEO_CODEC = "libx264";
|
||||
private static final String AUDIO_CODEC = "aac";
|
||||
private static final String OUTPUT_FORMAT = "mp4";
|
||||
private static final String OUTPUT_SUFFIX = "_720p.mp4";
|
||||
|
||||
private static final int MAX_BITRATE = 2500; // 2.5 Mbps
|
||||
private static final int MIN_BITRATE = 1000; // 1 Mbps
|
||||
|
||||
/**
|
||||
* 压缩视频到720p,统一输出为MP4格式
|
||||
*
|
||||
* @param inputFile 输入视频文件
|
||||
* @return 压缩后的视频文件,如果不需要压缩则返回原文件
|
||||
* @throws Exception 压缩过程中可能出现的异常
|
||||
*/
|
||||
public static File compressTo720p(File inputFile) throws Exception {
|
||||
if (!isSupportedVideoFile(inputFile)) {
|
||||
log.info("文件格式不支持或文件不存在,直接返回源文件");
|
||||
return inputFile;
|
||||
}
|
||||
|
||||
// 判断是否需要压缩
|
||||
if (!needCompression(inputFile)) {
|
||||
return inputFile;
|
||||
}
|
||||
|
||||
File outputFile = createOutputFile(inputFile);
|
||||
Encoder encoder = new Encoder();
|
||||
|
||||
// 配置编码参数
|
||||
EncodingAttributes encodingAttributes = createEncodingAttributes();
|
||||
|
||||
log.info("开始压缩视频: {} -> {}", inputFile.getName(), outputFile.getName());
|
||||
encoder.encode(new MultimediaObject(inputFile), outputFile, encodingAttributes);
|
||||
log.info("视频压缩完成: {}", outputFile.getName());
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能判断视频是否需要压缩
|
||||
* 基于分辨率、码率、数据密度等多维度综合判断
|
||||
*/
|
||||
public static boolean needCompression(File videoFile) throws Exception {
|
||||
MultimediaObject media = new MultimediaObject(videoFile);
|
||||
|
||||
// 获取视频基本信息
|
||||
double fileSizeMB = videoFile.length() / 1048576.0; // 转换为MB
|
||||
double duration = media.getInfo().getDuration() / 1000.0;
|
||||
int bitRateKbps = media.getInfo().getVideo().getBitRate() / 1000; // 转换为kbps
|
||||
|
||||
// 获取视频分辨率
|
||||
int width = media.getInfo().getVideo().getSize().getWidth();
|
||||
int height = media.getInfo().getVideo().getSize().getHeight();
|
||||
|
||||
log.info("视频分析: 分辨率 {}x{}, 大小 {}MB, 时长 {}秒, 码率约 {}kbps", width, height, fileSizeMB, duration, bitRateKbps);
|
||||
|
||||
// 判断逻辑:基于分辨率和码率的智能判断
|
||||
boolean needsCompression = false;
|
||||
|
||||
// 1. 如果分辨率已经<=720p,检查码率是否过高
|
||||
if (height <= TARGET_HEIGHT && width <= TARGET_WIDTH) {
|
||||
if (bitRateKbps > MAX_BITRATE) {
|
||||
log.info("视频分辨率达标但码率({}kbps)过高,需要压缩", bitRateKbps);
|
||||
needsCompression = true;
|
||||
} else {
|
||||
log.info("视频分辨率已达720p或以下,且码率合理,无需压缩");
|
||||
}
|
||||
}
|
||||
// 2. 如果分辨率>720p,但码率很低,说明已是优化过的视频,无需压缩
|
||||
else if (height > TARGET_HEIGHT && bitRateKbps < MIN_BITRATE) {
|
||||
log.info("视频虽为{}p高分辨率,但码率({}kbps)已优化,无需压缩", height, bitRateKbps);
|
||||
}
|
||||
// 3. 高分辨率+高码率,需要压缩
|
||||
else if (height > TARGET_HEIGHT && bitRateKbps >= MAX_BITRATE) {
|
||||
log.info("视频为{}p高分辨率且码率较高({}kbps),需要压缩至720p", height, bitRateKbps);
|
||||
needsCompression = true;
|
||||
}
|
||||
// 4. 其他情况(如异常数据)默认不压缩
|
||||
else {
|
||||
log.info("视频参数异常,保守处理:不进行压缩");
|
||||
}
|
||||
|
||||
return needsCompression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否为支持的视频格式
|
||||
*/
|
||||
private static boolean isSupportedVideoFile(File file) {
|
||||
if (file == null || !file.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String filename = file.getName().toLowerCase();
|
||||
for (String format : SUPPORTED_FORMATS) {
|
||||
if (filename.endsWith(format)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建输出文件对象
|
||||
*/
|
||||
private static File createOutputFile(File inputFile) {
|
||||
String baseName = getFileBaseName(inputFile.getName());
|
||||
String outputFilename = baseName + OUTPUT_SUFFIX;
|
||||
return new File(inputFile.getParent(), outputFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名(不含扩展名)
|
||||
*/
|
||||
private static String getFileBaseName(String filename) {
|
||||
int dotIndex = filename.lastIndexOf('.');
|
||||
return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建编码配置参数
|
||||
*/
|
||||
private static EncodingAttributes createEncodingAttributes() {
|
||||
// 音频配置
|
||||
AudioAttributes audio = new AudioAttributes();
|
||||
audio.setCodec(AUDIO_CODEC);
|
||||
audio.setBitRate(AUDIO_BITRATE);
|
||||
|
||||
// 视频配置
|
||||
VideoAttributes video = new VideoAttributes();
|
||||
video.setCodec(VIDEO_CODEC);
|
||||
video.setBitRate(VIDEO_BITRATE);
|
||||
video.setSize(new VideoSize(TARGET_WIDTH, TARGET_HEIGHT));
|
||||
|
||||
// 编码设置
|
||||
EncodingAttributes attributes = new EncodingAttributes();
|
||||
attributes.setOutputFormat(OUTPUT_FORMAT);
|
||||
attributes.setAudioAttributes(audio);
|
||||
attributes.setVideoAttributes(video);
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  通过分辨率和码率的双重判断,确定是否需要压缩。
|
||||
|
||||
# 四、使用
|
||||
```java
|
||||
public void uploadFile(MultipartFile file, String filePath) {
|
||||
try {
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
|
||||
File tempFile = convertMultipartFileToFile(file);
|
||||
log.info("临时文件地址:{}", tempFile.toPath());
|
||||
|
||||
log.info("开始压缩文件:{}", tempFile.getName());
|
||||
File compressFile = VideoCompressor.compressTo720p(tempFile);
|
||||
log.info("完成压缩文件:{}", tempFile.getName());
|
||||
|
||||
log.info("压缩文件地址:{}", tempFile.toPath());
|
||||
|
||||
body.add("file", new FileSystemResource(compressFile));
|
||||
|
||||
aListClient.put()
|
||||
.uri("api/fs/form")
|
||||
.header(HttpHeaders.AUTHORIZATION, token)
|
||||
.header("File-Path", filePath)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(body)
|
||||
.retrieve()
|
||||
.body(Map.class);
|
||||
|
||||
Files.deleteIfExists(compressFile.toPath());
|
||||
Files.deleteIfExists(tempFile.toPath());
|
||||
} catch (Exception e) {
|
||||
log.error("文件上传失败{}", e.getMessage());
|
||||
throw new RuntimeException("文件上传失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private File convertMultipartFileToFile(MultipartFile file) throws IOException {
|
||||
String suffix = (file.getOriginalFilename() != null && file.getOriginalFilename().contains("."))
|
||||
? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
|
||||
: ".tmp";
|
||||
|
||||
Path tempFile = Files.createTempFile(System.currentTimeMillis() + "", suffix);
|
||||
file.transferTo(tempFile);
|
||||
return tempFile.toFile();
|
||||
}
|
||||
```
|
||||
171
docs/Web/Others/Vite-Principle.md
Normal file
171
docs/Web/Others/Vite-Principle.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: 前端包管理器
|
||||
date: 2025-12-13
|
||||
---
|
||||
|
||||
# 一、Vite简介
|
||||
  Vite是新一代的前端构建工具,在尤雨溪开发Vue3.0的时候诞生。类似于Webpack+ Webpack-dev-server。
|
||||
  其主要利用浏览器**ESModule特性**导入组织代码,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。生产中利用Rollup作为打包工具,号称下一代的前端构建工具。
|
||||
|
||||
# 二、核心原理
|
||||
## 2.1 ESModule
|
||||
  ESModule是JavaScript提出的官方标准化模块系统,不同于之前的CJS,AMD,CMD等等,ESModule提供了更原生以及更动态的模块加载方案,最重要的就是它是浏览器原生支持的,也就是说我们可以**直接在浏览器中去执行import**,动态引入我们需要的模块,而不是把所有模块打包在一起。
|
||||
  例如声明一个script标签:`<script type="module" src="/src/main.js"></script>`,当浏览器解析资源时,会往当前域名发起一个GET请求`main.js`文件:
|
||||
```javascript
|
||||
// main.js
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
createApp(App).mount('#app')
|
||||
```
|
||||
  请求到了main.js文件,会检测到内部含有import引入的包,又会import引用发起HTTP请求获取模块的内容文件,如App.vue文件。
|
||||
|
||||
## 2.2 基于ESModule的Dev Server
|
||||
  Vite其核心原理是利用浏览器现在已经支持ES6的import,碰见import就会发送一个HTTP请求去加载文件,**Vite启动一个koa服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器**。
|
||||
  Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多。
|
||||
|
||||
## 2.3 基于ESModule的热更新
|
||||
  热更新主要思路是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
|
||||
  **Vite 通过 chokidar 来监听文件系统的变更,只用对发生变更的模块重新加载**,只需要精确的使相关模块与其临近的 HMR边界连接失效即可,这样HMR 更新速度就不会因为应用体积的增加而变慢而 Webpack 还要经历一次打包构建。所以 HMR 场景下,Vite 表现也要好于 Webpack。
|
||||
|
||||
## 2.4 基于Esbuild的开发环境编译
|
||||
  Esbuild是一个JavaScript Bundler 打包和压缩工具,它提供了与Webpack、Rollup等工具相似的资源打包能力。可以将JavaScript 和TypeScript代码打包分发在网页上运行。但其打包速度却是其他工具的10~100倍。
|
||||
  在开发环境中,Vite 使用 esbuild 进行快速的热重载和模块编译。
|
||||
|
||||
### 2.4.1 Esbuild高性能原理
|
||||
1. Go 语言实现:避开 JavaScript 运行时的性能瓶颈。
|
||||
2. 并行编译:利用多核 CPU 同时处理多个文件。
|
||||
3. 内存优化:在单个进程中完成解析、转换和生成,减少内存开销。
|
||||
|
||||
### 2.4.2 Esbuild关键特性
|
||||
1. 语法转换:TypeScript → JS(保留类型信息或移除)、现代 JS 语法降级(如 ?.、?? 转换)等。
|
||||
2. 压缩优化:变量重命名、死代码消除、支持 CSS 压缩(通过 esbuild-plugin-css-minify)等。
|
||||
|
||||
## 2.5 基于Rollup的生产环境打包
|
||||
  Rollup是基于ESM的JavaScript打包工具。相比于其他打包工具如Webpack,他总是能打出更小、更快的包。因为 Rollup 基于 ESM 模块,比 Webpack 和 Browserify 使用的 CommonJS模块机制更高效。Rollup的亮点在于同一个地方,一次性加载。能针对源码进行 Tree Shaking(去除那些已被定义但没被使用的代码),以及 Scope Hoisting 以减小输出文件大小提升运行性能。
|
||||
  在生产环境中,Vite 使用 Rollup 进行最终的打包和优化。
|
||||
|
||||
### 2.5.1 Rollup构建流程
|
||||
1. 解析入口文件:从 index.js 开始,递归分析所有 import 语句。
|
||||
2. 生成抽象语法树(AST):使用 acorn 解析 JS 文件为 AST。
|
||||
3. 应用插件转换:如 @rollup/plugin-typescript 处理 TypeScript。
|
||||
4. 生成依赖图:分析模块间的依赖关系。
|
||||
5. 输出优化:
|
||||
Tree-shaking:基于 ESM 静态分析,移除未使用的代码
|
||||
代码分割:识别动态导入(import())并拆分包。。
|
||||
|
||||
# 三、Mini-Vite
|
||||
## 3.1 初始化工程
|
||||
  初始化NodeJs工程,并添加Vue3依赖
|
||||
|
||||
## 3.2 Node Server
|
||||
```javascript
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const miniVueParse = require('./mini-vue-parser')
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// 获取请求的路径
|
||||
const reqPath = req.url;
|
||||
// 构建本地文件路径
|
||||
const filePath = path.join(__dirname, reqPath);
|
||||
|
||||
console.log('filePath: ', filePath)
|
||||
|
||||
|
||||
// 检查文件是否存在
|
||||
fs.access(filePath, fs.constants.R_OK, (err) => {
|
||||
if (err) {
|
||||
console.error('文件不存在:', err);
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('文件不存在');
|
||||
} else {
|
||||
// 读取文件并发送给客户端
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error('读取文件出错:', err);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end('内部服务器错误');
|
||||
} else {
|
||||
// 根据文件类型设置合适的 Content-Type
|
||||
const extname = path.extname(filePath);
|
||||
let contentType = 'text/plain';
|
||||
switch (extname) {
|
||||
case '.html':
|
||||
contentType = 'text/html';
|
||||
break;
|
||||
case '.js':
|
||||
contentType = 'text/javascript';
|
||||
break;
|
||||
case '.vue':
|
||||
contentType = 'text/javascript';
|
||||
break;
|
||||
case '.css':
|
||||
contentType = 'text/css';
|
||||
break;
|
||||
// 添加其他需要支持的文件类型
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
|
||||
if (extname === '.js') {
|
||||
data = data.replace(/from ['"]vue['"]/g, "from './node_modules/vue/dist/vue.esm-browser.js'");
|
||||
}
|
||||
|
||||
if (extname === '.vue') {
|
||||
const fileName = path.basename(filePath);
|
||||
data = miniVueParse.parse(data, fileName)
|
||||
}
|
||||
|
||||
res.end(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const port = 3000;
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`代理服务器正在监听端口 ${port}`);
|
||||
});
|
||||
```
|
||||
  在浏览器输入`http://localhost:3000/index.html`,会首先解析`<script type="module" src="main.js"></script>`,获取`main.js`,然后会把`import { createApp } from 'vue'`中的`Vue`换成替换成:`./node_modules/vue/dist/vue.esm-browser.js'`,在遇到`Vue`文件时,会利用`Vue`的模板编译器转成Javascript文件:
|
||||
  mini-vue-parser.js:
|
||||
```JavaScript
|
||||
const compilerSfc = require("@vue/compiler-sfc");
|
||||
const path = require('path');
|
||||
|
||||
function parse(vueFileString,fileName) {
|
||||
const sfc = compilerSfc.parse(
|
||||
vueFileString
|
||||
);
|
||||
const sfcTemplate = compilerSfc.compileTemplate({
|
||||
id: 'sfc',
|
||||
filename: fileName + '.template.vue',
|
||||
source: sfc.descriptor.template.content
|
||||
})
|
||||
|
||||
const sfcScript = compilerSfc.compileScript(sfc.descriptor, {
|
||||
filename: 'sfc.script.vue'
|
||||
})
|
||||
|
||||
let result = '';
|
||||
result += sfcTemplate.code.replace("export", "") + '\n';
|
||||
result += sfcScript.content.replace("export default ", "const __script = ") + '\n';
|
||||
result += `__script.render=render;\n`
|
||||
result += `export default __script;`
|
||||
result = result.replace(/from ['"]vue['"]/g, "from './node_modules/vue/dist/vue.esm-browser.js'");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parse
|
||||
}
|
||||
```
|
||||
|
||||
# 四、参考资料
|
||||
1. [深入浅出vite(核心原理 + 手撕mini-vite)](https://juejin.cn/post/7026285200766140453#heading-37)
|
||||
2. [深入理解Vite核心原理](https://juejin.cn/post/7064853960636989454)
|
||||
3. [step-vite](https://github.com/Llyonss/step-vite)
|
||||
221
docs/Web/SpringBoot/SpringAOP.md
Normal file
221
docs/Web/SpringBoot/SpringAOP.md
Normal file
@@ -0,0 +1,221 @@
|
||||
---
|
||||
title: Spring AOP简介
|
||||
date: 2025-12-25
|
||||
---
|
||||
|
||||
# 一、基本概念
|
||||
  AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,核心思想是**将与业务核心逻辑无关但又被多个模块共享的功能(如日志、事务、权限校验、性能监控)抽取出来,形成独立的 “切面”**,在不修改原有业务代码的前提下,通过 “织入” 的方式动态添加到目标方法的执行流程中。
|
||||
  Spring AOP 是 Spring 框架对 AOP 思想的实现,基于**动态代理(JDK 动态代理 + CGLIB 动态代理)**,专注于解决企业级开发中的横切关注点问题。
|
||||
| 术语 | 说明 |
|
||||
|------|------|
|
||||
| 切面(Aspect) | 封装横切逻辑的类(如日志切面、事务切面),包含通知和切点 |
|
||||
| 连接点(Joinpoint) | 程序执行过程中的某个特定点(如方法调用、异常抛出),Spring AOP 中仅支持方法级连接点 |
|
||||
| 切点(Pointcut) | 匹配连接点的规则,用于指定切面要织入到哪些方法上 |
|
||||
| 通知(Advice) | 切面的具体逻辑(如日志打印、权限校验),按执行时机分为 5 种 |
|
||||
| 目标对象(Target) | 被切面织入的目标类(业务类) |
|
||||
| 代理对象(Proxy) | Spring AOP 为目标对象创建的代理类,横切逻辑通过代理执行 |
|
||||
| 织入(Weaving) | 将切面逻辑添加到目标对象的过程,Spring AOP 是运行时织入(通过动态代理) |
|
||||
|
||||
| 通知类型 | 执行时机 |
|
||||
|----------|----------|
|
||||
| 前置通知(Before) | 目标方法执行前执行 |
|
||||
| 后置通知(After) | 目标方法执行后执行(无论是否抛出异常) |
|
||||
| 返回通知(AfterReturning) | 目标方法正常返回后执行 |
|
||||
| 异常通知(AfterThrowing) | 目标方法抛出异常后执行 |
|
||||
| 环绕通知(Around) | 包裹目标方法执行,可自定义目标方法执行前后的逻辑(功能最强) |
|
||||
|
||||
# 二、原理
|
||||
  Spring AOP 基于动态代理实现,分为两种方式:
|
||||
1. JDK 动态代理
|
||||
通过 java.lang.reflect.Proxy 生成代理类,代理类实现目标接口,调用 InvocationHandler 处理横切逻辑;基于接口,无侵入。
|
||||
|
||||
2. CGLIB 动态代理
|
||||
通过继承目标类生成子类作为代理类,重写目标方法,织入横切逻辑;基于继承,目标类不能是 final,方法不能是 final。
|
||||
|
||||
  Spring AOP 自动选择规则:如果目标类实现了接口,使用 JDK 动态代理;否则使用 CGLIB。
|
||||
|
||||
# 三、实战
|
||||
  例如存在以下业务模块:
|
||||
```java
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
// 模拟创建订单
|
||||
public String createOrder(String orderNo) {
|
||||
System.out.println("创建订单:" + orderNo);
|
||||
return "订单创建成功:" + orderNo;
|
||||
}
|
||||
|
||||
// 模拟删除订单(故意抛出异常)
|
||||
public void deleteOrder(String orderNo) {
|
||||
System.out.println("删除订单:" + orderNo);
|
||||
throw new RuntimeException("删除订单失败:订单不存在");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  AOP实现:
|
||||
```java
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
// 1. 标记为切面类
|
||||
@Aspect
|
||||
// 2. 交给Spring容器管理
|
||||
@Component
|
||||
public class LogAspect {
|
||||
|
||||
// 3. 定义切点:匹配OrderService类的所有方法
|
||||
@Pointcut("execution(* com.example.aopdemo.service.OrderService.*(..))")
|
||||
public void orderServicePointcut() {}
|
||||
|
||||
// 4. 前置通知:目标方法执行前执行
|
||||
@Before("orderServicePointcut()")
|
||||
public void beforeAdvice(JoinPoint joinPoint) {
|
||||
// 获取方法名和参数
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
System.out.println("[前置通知] 方法:" + methodName + ",参数:" + Arrays.toString(args));
|
||||
}
|
||||
|
||||
// 5. 后置通知:目标方法执行后执行(无论是否异常)
|
||||
@After("orderServicePointcut()")
|
||||
public void afterAdvice(JoinPoint joinPoint) {
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
System.out.println("[后置通知] 方法:" + methodName + " 执行完毕");
|
||||
}
|
||||
|
||||
// 6. 返回通知:目标方法正常返回后执行
|
||||
@AfterReturning(value = "orderServicePointcut()", returning = "result")
|
||||
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
System.out.println("[返回通知] 方法:" + methodName + ",返回值:" + result);
|
||||
}
|
||||
|
||||
// 7. 异常通知:目标方法抛出异常后执行
|
||||
@AfterThrowing(value = "orderServicePointcut()", throwing = "ex")
|
||||
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
System.out.println("[异常通知] 方法:" + methodName + ",异常:" + ex.getMessage());
|
||||
}
|
||||
|
||||
// 8. 环绕通知:包裹目标方法执行(功能最强)
|
||||
@Around("orderServicePointcut()")
|
||||
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
|
||||
String methodName = proceedingJoinPoint.getSignature().getName();
|
||||
Object[] args = proceedingJoinPoint.getArgs();
|
||||
|
||||
// 前置逻辑
|
||||
System.out.println("[环绕通知-前置] 方法:" + methodName + ",参数:" + Arrays.toString(args));
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
// 执行目标方法
|
||||
result = proceedingJoinPoint.proceed();
|
||||
// 返回后逻辑
|
||||
System.out.println("[环绕通知-返回] 方法:" + methodName + ",返回值:" + result);
|
||||
} catch (Throwable e) {
|
||||
// 异常逻辑
|
||||
System.out.println("[环绕通知-异常] 方法:" + methodName + ",异常:" + e.getMessage());
|
||||
throw e; // 必须抛出异常,否则上层无法捕获
|
||||
} finally {
|
||||
// 后置逻辑
|
||||
System.out.println("[环绕通知-后置] 方法:" + methodName + " 执行完毕");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  测试:
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
public class OrderServiceTest {
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
// 测试正常方法(createOrder)
|
||||
@Test
|
||||
public void testCreateOrder() {
|
||||
orderService.createOrder("ORDER_20251225");
|
||||
}
|
||||
|
||||
// 测试异常方法(deleteOrder)
|
||||
@Test
|
||||
public void testDeleteOrder() {
|
||||
try {
|
||||
orderService.deleteOrder("ORDER_9999");
|
||||
} catch (Exception e) {
|
||||
// 捕获异常,避免测试中断
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  测试结果:
|
||||
```plaintext
|
||||
[环绕通知-前置] 方法:createOrder,参数:[ORDER_20251225]
|
||||
[前置通知] 方法:createOrder,参数:[ORDER_20251225]
|
||||
创建订单:ORDER_20251225
|
||||
[环绕通知-返回] 方法:createOrder,返回值:订单创建成功:ORDER_20251225
|
||||
[返回通知] 方法:createOrder,返回值:订单创建成功:ORDER_20251225
|
||||
[后置通知] 方法:createOrder 执行完毕
|
||||
[环绕通知-后置] 方法:createOrder 执行完毕
|
||||
|
||||
|
||||
[环绕通知-前置] 方法:deleteOrder,参数:[ORDER_9999]
|
||||
[前置通知] 方法:deleteOrder,参数:[ORDER_9999]
|
||||
删除订单:ORDER_9999
|
||||
[环绕通知-异常] 方法:deleteOrder,异常:删除订单失败:订单不存在
|
||||
[异常通知] 方法:deleteOrder,异常:删除订单失败:订单不存在
|
||||
[后置通知] 方法:deleteOrder 执行完毕
|
||||
[环绕通知-后置] 方法:deleteOrder 执行完毕
|
||||
```
|
||||
|
||||
# 四、切点表达式
|
||||
| 切点表达式 | 说明 |
|
||||
|------------|------|
|
||||
| `execution(* com.example.service.*.*(..))` | 匹配 com.example.service 包下所有类的所有方法 |
|
||||
| `execution(public String com.example.service.OrderService.create*(String))` | 匹配 OrderService 类中公共、返回值为 String、方法名以 create 开头、参数为 String 的方法 |
|
||||
| `execution(* com.example..*Service.*(..))` | 匹配 com.example 包及其子包下所有以 Service 结尾的类的所有方法 |
|
||||
| `@annotation(com.example.annotation.Log)` | 匹配标注了 @Log 自定义注解的方法 |
|
||||
|
||||
  组合切点:
|
||||
```java
|
||||
// 匹配OrderService的方法 且 方法名包含"Order"
|
||||
@Pointcut("execution(* com.example.service.OrderService.*(..)) && execution(* *Order*(..))")
|
||||
public void combinePointcut() {}
|
||||
```
|
||||
|
||||
# 五、总结
|
||||
  通俗的解释,AOP本质是:找目标(通过切点规则,精准找到要 “加戏” 的方法 / 类)→ 造代理(Spring 自动给这些目标生成一个 “中间商”(代理对象))→ 加戏(在目标方法真正执行前 / 后 / 出问题时,让这个 “中间商” 帮你干额外的活)。
|
||||
|
||||
::: tip
|
||||
基于 Spring AOP 的知名框架/组件汇总
|
||||
| 框架/组件 | 核心作用 | Spring AOP 应用说明 |
|
||||
|-----------|----------|---------------------|
|
||||
| AspectJ | 提供切点表达式语法、注解解析(@Aspect/@Pointcut 等) | Spring AOP 仅复用其语法规则,底层仍为自身动态代理(未使用 AspectJ 编译期织入) |
|
||||
| Spring Core | 容器管理、动态代理创建(JDK/CGLIB) | 负责切面/目标类的生命周期管理,生成代理对象并完成织入逻辑 |
|
||||
| Spring Transaction | 声明式事务管理 | 切点:匹配 @Transactional 注解方法;<br>通知:方法执行前开启事务、执行后提交、异常时回滚 |
|
||||
| Spring Cache | 声明式缓存管理 | 切点:匹配 @Cacheable/@CacheEvict 等注解方法;<br>通知:方法执行前查缓存、执行后更新缓存 |
|
||||
| spring-boot-starter-aop | AOP 基础依赖封装 | 自动引入 AspectJ + Spring AOP 核心,开箱即用 |
|
||||
| Spring Security | 安全权限管理 | 切点:匹配 @PreAuthorize 等权限注解方法;<br>通知:方法执行前校验用户权限,无权限抛异常 |
|
||||
| MyBatis-Plus | 增强 MyBatis CRUD 操作 | 切点:匹配 Mapper 接口方法/标注注解的实体类;<br>通知:织入分页逻辑、注解(@TableName)解析等 |
|
||||
| Shiro | 权限管理框架 | 切点:匹配 @RequiresPermissions 注解方法;<br>通知:方法执行前校验用户权限 |
|
||||
| Seata | 分布式事务框架 | 切点:匹配 @GlobalTransactional 注解方法;<br>通知:织入全局事务的开启、提交、回滚逻辑 |
|
||||
| Sentinel | 流量控制/熔断降级框架 | 切点:匹配 @SentinelResource 注解方法;<br>通知:方法执行前校验流量阈值、熔断规则 |
|
||||
| Dynamic-TP | 动态线程池框架 | 切点:匹配线程池执行方法;<br>通知:织入线程池耗时、队列长度等监控逻辑 |
|
||||
| p6spy | 数据库 SQL 监控 | 切点:拦截 JDBC 操作方法;<br>通知:织入 SQL 打印、执行耗时统计逻辑 |
|
||||
| dynamic-datasource-spring-boot-starter | 动态数据源切换 | 切点:匹配 @DS 注解方法;<br>通知:方法执行前切换数据源、执行后恢复默认数据源 |
|
||||
| xxl-job | 分布式任务调度 | 切点:匹配任务执行方法;<br>通知:织入任务执行日志、监控指标采集逻辑 |
|
||||
| Spring Cloud Sleuth | 微服务链路追踪 | 切点:拦截 HTTP/RPC 调用方法;<br>通知:织入 traceId/spanId 传递、链路信息采集逻辑 |
|
||||
:::
|
||||
72
docs/Web/SpringBoot/SpringBoot-Annotation.md
Normal file
72
docs/Web/SpringBoot/SpringBoot-Annotation.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: SpingBoot注解
|
||||
date: 2025-12-30
|
||||
---
|
||||
|
||||
# 一、注解
|
||||
## 1.1 定义
|
||||
  注解(Annotation)是 JDK5.0 引入的特性,可以理解为:给代码添加的 “元数据”(描述数据的数据),就像给代码贴标签,**本身不直接影响代码执行,但可以被编译器、框架(如 SpringBoot)读取并做相应处理**。
|
||||
|
||||
## 1.2 分类
|
||||
| 类型 | 核心作用 | 典型示例 |
|
||||
|----------------|--------------------|------------------|
|
||||
| 源码注解 | 仅在源码编译阶段生效,编译后注解消失 | `@Override` |
|
||||
| 编译时注解 | 编译期生效,注解信息保留到class文件,但JVM运行时不加载 | Lombok的`@Data` |
|
||||
| 运行时注解 | 整个生命周期都存在(源码→编译→运行),可通过反射动态获取注解信息 | SpringBoot的`@RestController`、`@Service`、`@Transactional` |
|
||||
|
||||
## 1.3 示例
|
||||
```java
|
||||
import java.lang.annotation.*;
|
||||
|
||||
// 1. 注解的元注解(描述注解的注解)
|
||||
@Target(ElementType.METHOD) // 注解作用在方法上
|
||||
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可通过反射获取
|
||||
@Documented // 生成Javadoc时包含该注解
|
||||
public @interface MyAnnotation {
|
||||
// 注解的属性(类似方法,可设置默认值)
|
||||
String value() default "默认描述";
|
||||
int num() default 0;
|
||||
}
|
||||
|
||||
// 2. 使用自定义注解
|
||||
public class AnnotationTest {
|
||||
@MyAnnotation(value = "测试方法", num = 10)
|
||||
public void test() {
|
||||
System.out.println("执行测试方法");
|
||||
}
|
||||
|
||||
// 3. 通过反射读取注解
|
||||
public static void main(String[] args) throws NoSuchMethodException {
|
||||
// 获取方法对象
|
||||
java.lang.reflect.Method method = AnnotationTest.class.getMethod("test");
|
||||
// 判断方法是否有该注解
|
||||
if (method.isAnnotationPresent(MyAnnotation.class)) {
|
||||
// 获取注解实例
|
||||
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
|
||||
// 读取注解属性
|
||||
System.out.println("注解value:" + annotation.value()); // 输出:测试方法
|
||||
System.out.println("注解num:" + annotation.num()); // 输出:10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  @Target:指定注解能作用的位置(如ElementType.METHOD= 方法、ElementType.TYPE= 类 / 接口、ElementType.FIELD= 字段)。
|
||||
  @Retention:指定注解的保留阶段(RUNTIME是 SpringBoot 注解最常用的)
|
||||
  首先定义注解的@Target和@Retention信息,然后设置注解的属性,类似于方法的参数,比如这里的value和num,需要在使用的时候通过命名参数的形式传递过来。如果是RUNTIME类型的注解,可以通过反射来获取方法和注解的参数,实现自定义逻辑功能。
|
||||
|
||||
# 二、SpringBoot核心注解
|
||||
## 2.1 启动类注解
|
||||
  @SpringBootApplication为SpringBoot启动类注解,由以下三个注解组成:
|
||||
1. @Configuration:标记类为配置类(替代 XML 配置)
|
||||
2. @EnableAutoConfiguration:开启自动配置(SpringBoot 核心,自动配置 Tomcat、数据库连接等)
|
||||
3. @ComponentScan:扫描当前包及子包下的 @Component、@Service 等注解的类,纳入 Spring 容器管理
|
||||
|
||||
## 2.2 组件注册注解
|
||||
| 注解 | 作用 | 使用场景 |
|
||||
|----------------|--------------------------|------------------------------------------------------------------|
|
||||
| @Component | 通用组件注解 | 通用工具类、非业务层类 |
|
||||
| @Controller | 控制器注解 | MVC 的控制层(返回页面) |
|
||||
| @RestController| REST 控制器 | API 接口层(返回 JSON/XML,= @Controller + @ResponseBody) |
|
||||
| @Service | 服务层注解 | 业务逻辑层 |
|
||||
| @Repository | 数据访问层注解 | DAO 层 / 持久层(如 MyBatis 的 Mapper 接口) |
|
||||
877
docs/Web/SpringBoot/SpringBoot-Common.md
Normal file
877
docs/Web/SpringBoot/SpringBoot-Common.md
Normal file
@@ -0,0 +1,877 @@
|
||||
---
|
||||
title: SpringBoot Common
|
||||
date: 2025-12-15
|
||||
---
|
||||
|
||||
# 一、框架说明
|
||||
  本框架基于Spring Boot3框架二次开发,增加了依赖包管理和启动项配置等功能。
|
||||
|
||||
# 二、项目结构
|
||||
|
||||
| 项目模块 | 模块含义 | 主要功能 |
|
||||
| :-----------: | :-------------------: | :---------------------------------: |
|
||||
| starters | SpringBoot 启动项 | 主要包括web、jdbc和log启动项配置 |
|
||||
| autoconfigure | staters具体启动配置类 | 主要包括web和jdbc具体的启动项配置类 |
|
||||
| dependencies | 依赖项 | 主要包括本框架中的依赖包管理 |
|
||||
| framework | 通用配置 | 主要包括web和data的一些通用工具方法 |
|
||||
|
||||
# 三、项目说明
|
||||
## 3.1 common模块
|
||||
  该模块主要声明项目结构,包括autoconfigure、dependencies、framework、starter-parent和starters等模块。
|
||||
```xml
|
||||
<modules>
|
||||
<!-- modules表示聚合关系,即common有以下模块 -->
|
||||
<module>dependencies</module>
|
||||
<module>starters</module>
|
||||
<module>framework</module>
|
||||
<module>autoconfigure</module>
|
||||
<module>starter-parent</module>
|
||||
</modules>
|
||||
```
|
||||
|
||||
## 3.2 dependencies模块
|
||||
  该模块为其他模块的父模块,声明了一些常用依赖包及版本,通过`<dependencyManagement>`管理,只声明依赖的版本,并不会实际引入依赖。
|
||||
  后续有新增依赖时,需要先在dependencies模块声明版本,然后在相应的starter模块中增加依赖。
|
||||
```xml
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
|
||||
<spring.boot.version>3.5.0</spring.boot.version>
|
||||
<springdoc.version>2.7.0</springdoc.version>
|
||||
|
||||
<logback-more-appenders.version>1.8.8</logback-more-appenders.version>
|
||||
<fluency-fluentd.version>2.7.0</fluency-fluentd.version>
|
||||
|
||||
<mybatis-spring.version>3.0.4</mybatis-spring.version>
|
||||
<mybatis.plus.version>3.5.12</mybatis.plus.version>
|
||||
<sa.token.version>1.43.0</sa.token.version>
|
||||
<hutool.version>5.8.26</hutool.version>
|
||||
|
||||
<commons.io.version>2.19.0</commons.io.version>
|
||||
<commons.collections.version>4.4</commons.collections.version>
|
||||
<commons-lang3.version>3.14.0</commons-lang3.version>
|
||||
<commons.net.version>3.9.0</commons.net.version>
|
||||
<guava.version>31.1-jre</guava.version>
|
||||
<mapstruct.version>1.5.5.Final</mapstruct.version>
|
||||
<yitter.idgenerator.version>1.0.6</yitter.idgenerator.version>
|
||||
|
||||
<lombok.version>1.18.30</lombok.version>
|
||||
<lombok.mapstruct.version>0.2.0</lombok.mapstruct.version>
|
||||
|
||||
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
|
||||
<docker-maven-plugin.version>0.41.0</docker-maven-plugin.version>
|
||||
<native-maven-plugin.version>0.10.2</native-maven-plugin.version>
|
||||
</properties>
|
||||
|
||||
<!-- dependencyManagement部分只声明依赖的版本,并不会实际引入依赖。
|
||||
需要在具体的模块中显式声明依赖,才能让模块使用这些库 -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI + Swagger UI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 扩展 logback appender -->
|
||||
<dependency>
|
||||
<groupId>com.sndyuk</groupId>
|
||||
<artifactId>logback-more-appenders</artifactId>
|
||||
<version>${logback-more-appenders.version}</version>
|
||||
</dependency>
|
||||
<!-- Fluentd 日志搜集和转发 -->
|
||||
<dependency>
|
||||
<groupId>org.komamitsu</groupId>
|
||||
<artifactId>fluency-fluentd</artifactId>
|
||||
<version>${fluency-fluentd.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SaToken BOM -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-bom</artifactId>
|
||||
<version>${sa.token.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- hutool BOM -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-bom</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis -->
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>${mybatis-spring.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus Maven BOM -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-bom</artifactId>
|
||||
<version>${mybatis.plus.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- IO工具类 -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons.io.version}</version>
|
||||
</dependency>
|
||||
<!-- 集合工具类 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>${commons.collections.version}</version>
|
||||
</dependency>
|
||||
<!-- 字符串工具类 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
<!-- 网络工具类 -->
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>${commons.net.version}</version>
|
||||
</dependency>
|
||||
<!-- 工具类(集合、缓存、并发、IO、字符串) -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava-bom</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 实体映射工具类 -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 雪花ID生成器 -->
|
||||
<dependency>
|
||||
<groupId>com.github.yitter</groupId>
|
||||
<artifactId>yitter-idgenerator</artifactId>
|
||||
<version>${yitter.idgenerator.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- starter中的依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-logging</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-web</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-jdbc</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
<version>${docker-maven-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
<version>${native-maven-plugin.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
```
|
||||
|
||||
::: tip
|
||||
dependencyManagement部分只声明依赖的版本,并不会实际引入依赖。
|
||||
需要在具体的模块中显式声明依赖,才能让模块使用这些库
|
||||
:::
|
||||
|
||||
## 3.3 framework模块
|
||||
  该模块主要是一些通用的配置。
|
||||
|
||||
### 3.3.1 data
|
||||
  主要提供了2个数据库实体类的基类。
|
||||
::: code-group
|
||||
```java [AbstractIdEntity]
|
||||
@Getter
|
||||
@Setter
|
||||
public abstract class AbstractIdEntity {
|
||||
@TableId(value = "id", type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
}
|
||||
```
|
||||
|
||||
```java [AbstractEntity]
|
||||
@Getter
|
||||
@Setter
|
||||
public abstract class AbstractEntity extends AbstractIdEntity {
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(value = "create_by", fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### 3.3.2 web
|
||||
  主要提供了自定义异常处理类。
|
||||
::: code-group
|
||||
```java [ErrorResponse]
|
||||
/**
|
||||
* 错误响应
|
||||
*/
|
||||
public final class ErrorResponse {
|
||||
/**
|
||||
* 自定义code
|
||||
*/
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* 自定义消息
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.code = "Unspecified";
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ErrorResponse(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java [CustomException]
|
||||
/**
|
||||
* 自定义异常类
|
||||
*/
|
||||
public class CustomException extends AbstractException {
|
||||
|
||||
public CustomException(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
|
||||
public CustomException(String code, String errorMessage) {
|
||||
super(code, errorMessage);
|
||||
}
|
||||
|
||||
public CustomException(String errorMessage, Exception innerException) {
|
||||
super(errorMessage, innerException);
|
||||
}
|
||||
|
||||
public CustomException(String code, String errorMessage, Exception innerException) {
|
||||
super(code, errorMessage, innerException);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
```java [AbstractException]
|
||||
/**
|
||||
* 抽象异常类
|
||||
*/
|
||||
public abstract class AbstractException extends RuntimeException {
|
||||
protected String code;
|
||||
|
||||
public AbstractException(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
|
||||
public AbstractException(String code, String errorMessage) {
|
||||
super(errorMessage);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public AbstractException(String errorMessage, Exception innerException) {
|
||||
super(errorMessage, innerException);
|
||||
}
|
||||
|
||||
public AbstractException(String code, String errorMessage, Exception innerException) {
|
||||
super(errorMessage, innerException);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### 3.4 starters模块
|
||||
  该模块为整个项目的核心模块,在实际项目中,通过引入相应的starter模块,并配合autoconfigure模块中的功能,即可实现快速自动装配。
|
||||
  该模块的pom.xml声明的依赖会注入到实际的项目中。
|
||||
::: danger
|
||||
在SringBoot3中,自动配置的路径改成了**main->resources->META-INF->spring->org.springframework.boot.autoconfigure.AutoConfiguration.imports**路径,然后在该文件中声明需要自动执行的类。
|
||||
之前的SringBoot2,是在**main->resources->META-INF**目录下新建spring.factories文件。
|
||||
:::
|
||||
|
||||
#### 3.4.1 stater-jdbc
|
||||
  该模块主要提供数据库相关的配置功能。
|
||||
  目前实现的功能有:
|
||||
1. 配置MybatisPlus拦截器,添加乐观锁和分页插件。
|
||||
2. 自定义MybatisPlus ID生成器(雪花ID)。
|
||||
3. 配置MybatisPlus自动填充字段(create_time、create_by、update_time和update_by)。
|
||||
|
||||
::: code-group
|
||||
```java [JdbcAutoConfiguration]
|
||||
/**
|
||||
* jdbc自动配置
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(SqlSessionFactory.class)
|
||||
@Import({YitterGenerator.class, MybatisMetaObjectHandler.class})
|
||||
public class JdbcAutoConfiguration {
|
||||
/**
|
||||
* MybatisPlus拦截器
|
||||
* @return 拦截器
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 乐观锁插件
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java [YitterGenerator]
|
||||
/**
|
||||
* 雪花id生成器
|
||||
*/
|
||||
public class YitterGenerator implements IdentifierGenerator {
|
||||
|
||||
@Override
|
||||
public Number nextId(Object entity) {
|
||||
return YitIdHelper.nextId();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
```java [MybatisMetaObjectHandler]
|
||||
/**
|
||||
* Mybatis Plus自动填充
|
||||
*/
|
||||
public class MybatisMetaObjectHandler implements MetaObjectHandler {
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, Constants.CREATE_TIME_FLAG, LocalDateTime.class, LocalDateTime.now());
|
||||
this.strictInsertFill(metaObject, Constants.UPDATE_TIME_FLAG, LocalDateTime.class, LocalDateTime.now());
|
||||
|
||||
if (StpUtil.isLogin()) {
|
||||
this.strictInsertFill(metaObject, Constants.CREATE_BY_FLAG, String.class, StpUtil.getLoginIdAsString());
|
||||
this.strictInsertFill(metaObject, Constants.UPDATE_BY_FLAG, String.class, StpUtil.getLoginIdAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, Constants.UPDATE_TIME_FLAG, LocalDateTime.class, LocalDateTime.now());
|
||||
|
||||
if (StpUtil.isLogin()) {
|
||||
this.strictUpdateFill(metaObject, Constants.UPDATE_BY_FLAG, String.class, StpUtil.getLoginIdAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
  `@ConditionalOnClass(SqlSessionFactory.class)`表示只有存在SqlSessionFactory类时,这个配置类才会生效。即如果没有 MyBatis 相关依赖,这个配置类会被 Spring 完全忽略。
|
||||
  `@Import`用于导入其他配置类或组件到当前配置类中。
|
||||
|
||||
#### 3.4.2 starter-web
|
||||
  该模块主要提供web相关的配置功能。
|
||||
  目前实现的功能有:
|
||||
1. 注册审计拦截器
|
||||
2. 注册Sa-Token拦截器
|
||||
3. 注册安全拦截器
|
||||
4. 配置CORS跨越
|
||||
5. 全局异常处理器
|
||||
|
||||
::: code-group
|
||||
```java [ServerAutoConfiguration]
|
||||
@Configuration()
|
||||
@Import({DefaultExceptionAdvice.class, AuditBodyAdvice.class, CustomProperties.class})
|
||||
public class ServerAutoConfiguration implements WebMvcConfigurer {
|
||||
@Resource
|
||||
private CustomProperties customProperties;
|
||||
|
||||
/**
|
||||
* 注册拦截器 需要实现 WebMvcConfigurer 接口
|
||||
* @param registry 注册器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册安全拦截器
|
||||
// registry.addInterceptor(new SecurityInterceptor());
|
||||
// 注册审计拦截器
|
||||
registry.addInterceptor(new AuditInterceptor(customProperties.getBasePackage()));
|
||||
// 注册Sa-Token拦截器 登录校验
|
||||
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
|
||||
.excludePathPatterns("/error", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**")
|
||||
.excludePathPatterns("/files/**")
|
||||
.excludePathPatterns("/doc.html", "/webjars/**");
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置CORS跨越 需要实现 WebMvcConfigurer 接口
|
||||
* @return 过滤器
|
||||
*/
|
||||
@Bean
|
||||
@Order(-128)
|
||||
public CorsFilter corsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
// 允许所有来源
|
||||
config.addAllowedOrigin("*");
|
||||
// 允许所有请求头
|
||||
config.addAllowedHeader("*");
|
||||
// 允许所有请求方法
|
||||
config.addAllowedMethod("*");
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java [AuditInterceptor]
|
||||
public class AuditInterceptor implements HandlerInterceptor {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuditInterceptor.class);
|
||||
|
||||
private final String basePackage;
|
||||
|
||||
public AuditInterceptor(String basePackage) {
|
||||
this.basePackage = basePackage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
// 如果是Http请求
|
||||
if (handler instanceof HandlerMethod hd) {
|
||||
request.setAttribute(Constants.AUDIT_TIME_FLAG, System.currentTimeMillis());
|
||||
// 判断该请求是否需要审计
|
||||
if (WebUtils.checkIsAuditPackages(hd.getBeanType().getPackage(), basePackage)) {
|
||||
handleAuditPackageRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
// true表示继续处理请求
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
|
||||
// 这里我们不需要处理时间计算,所有的计算在 afterCompletion 中完成
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
// 获取开始时间
|
||||
Long startTime = (Long) request.getAttribute(Constants.AUDIT_TIME_FLAG);
|
||||
if (startTime != null) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.info("<AuditSummary> Request URL {} | Time Taken {} ms", request.getRequestURI(), duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理审计包请求
|
||||
* @param request 请求
|
||||
*/
|
||||
private void handleAuditPackageRequest(HttpServletRequest request) {
|
||||
if (StringUtils.isEmpty(request.getQueryString())) {
|
||||
logger.info("<AuditSummary> {} {}", request.getMethod(), request.getRequestURI());
|
||||
} else {
|
||||
logger.info("<AuditSummary> {} {}?{}", request.getMethod(), request.getRequestURI(), WebUtils.format2UTF8(request.getQueryString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java [DefaultExceptionAdvice]
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class DefaultExceptionAdvice {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionAdvice.class);
|
||||
|
||||
/**
|
||||
* 处理SaToken权限错误
|
||||
* @param e 权限异常
|
||||
* @return 错误响应
|
||||
*/
|
||||
@ExceptionHandler(SaTokenException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public ErrorResponse handleSaTokenException(SaTokenException e) {
|
||||
ErrorResponse response = new ErrorResponse(String.valueOf(e.getCode()), e.getMessage());
|
||||
logger.error("<{}> {}", response.getCode(), e.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验错误
|
||||
* @param e 校验异常
|
||||
* @return 错误响应
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ErrorResponse handleValidationException(MethodArgumentNotValidException e) {
|
||||
ErrorResponse response = new ErrorResponse(WebUtils.formatValidationException(e.getBindingResult().getFieldErrors()));
|
||||
logger.error("<{}> {}", response.getCode(), response.getMessage());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自定义异常错误
|
||||
* @param e 自定义异常
|
||||
* @return 错误响应
|
||||
*/
|
||||
@ExceptionHandler(AbstractException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ErrorResponse handleAbstractException(AbstractException e) {
|
||||
ErrorResponse response;
|
||||
if (e.getCode() == null || e.getCode().isEmpty()) {
|
||||
response = new ErrorResponse(e.getMessage());
|
||||
} else {
|
||||
response = new ErrorResponse(e.getCode(), e.getMessage());
|
||||
}
|
||||
logger.error(String.format("<%s> ", response.getCode()), e);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理默认异常错误
|
||||
* @param e 默认异常
|
||||
* @return 错误响应
|
||||
*/
|
||||
@ExceptionHandler
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ErrorResponse handleDefaultException(Exception e) {
|
||||
ErrorResponse response = new ErrorResponse(e.getMessage());
|
||||
logger.error(String.format("<%s> ", response.getCode()), e);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java [AuditBodyAdvice]
|
||||
@ControllerAdvice
|
||||
@Import({CustomProperties.class})
|
||||
public class AuditBodyAdvice implements RequestBodyAdvice, ResponseBodyAdvice<Object> {
|
||||
@Resource
|
||||
private CustomProperties customProperties;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuditBodyAdvice.class);
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// 判断是否为需要审计的包
|
||||
String auditPackages = customProperties.getBasePackage();
|
||||
return WebUtils.checkIsAuditPackages(methodParameter.getDeclaringClass().getPackage(), auditPackages)
|
||||
&& AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
|
||||
return inputMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// 入参结束
|
||||
try {
|
||||
String jsonBody = objectMapper.writeValueAsString(body);
|
||||
logger.info("<AuditRequest> {}", jsonBody);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.info("<AuditResponse> {}", body);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// 判断是否为需要审计的包
|
||||
String auditPackages = customProperties.getBasePackage();
|
||||
return WebUtils.checkIsAuditPackages(returnType.getDeclaringClass().getPackage(), auditPackages)
|
||||
&& AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
// 出参结束
|
||||
if (body != null && !(body instanceof ErrorResponse)) {
|
||||
try {
|
||||
String jsonBody = objectMapper.writeValueAsString(body);
|
||||
logger.info("<AuditResponse> {}", jsonBody);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.info("<AuditResponse> {}", body);
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
```java [CustomProperties]
|
||||
/**
|
||||
* 用于将配置文件(如 application.properties 或 application.yml)中的属性值绑定到 Java 对象
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "web-starter")
|
||||
public class CustomProperties {
|
||||
// yml中web-starter下的base-package字段值
|
||||
private String basePackage = "";
|
||||
|
||||
public String getBasePackage() {
|
||||
return basePackage;
|
||||
}
|
||||
|
||||
public void setBasePackage(String basePackage) {
|
||||
this.basePackage = basePackage;
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
  `@ConfigurationProperties`用于将配置文件(如 application.properties 或 application.yml)中的属性值绑定到 Java 对象
|
||||
|
||||
::: tip
|
||||
SpringBoot MVC总体执行顺序:
|
||||
1. 进入Tomcat容器
|
||||
2. 进入Filter过滤器
|
||||
3. 进入Servlet容器
|
||||
4. 进入Interceptor拦截器
|
||||
5. 进入Controller控制器
|
||||
6. 进入AOP
|
||||
:::
|
||||
|
||||
::: tip
|
||||
请求和响应体拦截器
|
||||
RequestBodyAdvice, ResponseBodyAdvice主要发生在Controller执行前后:
|
||||
1. preHandle():请求处理前
|
||||
2. beforeBodyRead:请求体反序列化前
|
||||
3. @RequestBody:Controller方法参数绑定
|
||||
4. Controller:请求处理
|
||||
5. beforeBodyWrite():响应体序列化之前
|
||||
6. postHandle():请求处理后
|
||||
7. 视图渲染
|
||||
8. afterCompletion():请求结束
|
||||
:::
|
||||
|
||||
::: tip
|
||||
审计拦截器 可以实现请求日志打印等功能
|
||||
HandlerInterceptor 拦截器执行顺序:
|
||||
1. preHandle():请求处理前 按注册顺序依次执行。
|
||||
2. Controller:请求处理 请求到达Controller并被处理。
|
||||
3. postHandle():请求处理后,视图渲染前 按注册顺序逆序执行。
|
||||
4. afterCompletion():视图渲染后 按注册顺序逆序执行。
|
||||
:::
|
||||
|
||||
# 四、项目发布
|
||||
|
||||
## 4.1 发布到Git中
|
||||
1. 在Gitee/Github中创建工程,需要在工程中创建一个文件夹,例如repo,后续发布的文件要放在该文件夹下。
|
||||
2. 在maven的setting.xml中添加server和repository信息
|
||||
```xml
|
||||
<servers>
|
||||
<server>
|
||||
<id>gitee</id>
|
||||
<username>Cxx0822</username>
|
||||
<password>token</password>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>gitee</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<url>https://maven.aliyun.com/repository/central</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>gitee</id>
|
||||
<url>https://gitee.com/Cxx0822/springboot2-common/raw/master/repo</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>central</id>
|
||||
<url>https://maven.aliyun.com/repository/central</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<activeProfiles>
|
||||
<activeProfile>gitee</activeProfile>
|
||||
</activeProfiles>
|
||||
```
|
||||
|
||||
  注:这里要去掉mirror的阿里云镜像。
|
||||
|
||||
3. 在工程的根目录的pom.xml中添加发布配置:
|
||||
```xml
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>gitee</id>
|
||||
<name>springboot2-common</name>
|
||||
<url>file:D:/temp/maven</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
```
|
||||
|
||||
  注:gitee不支持通过deploy发布jar包,可以先发布到本地,再将文件复制到项目文件夹下,通过git push推送。
|
||||
|
||||
4. 将本地产生的发布文件上传至git仓库中。
|
||||
5. 其他项目引用:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-web</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-jdbc</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-logging</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
# 五、项目使用
|
||||
1. 将父工程改为starter-parent模块
|
||||
```xml
|
||||
<parent>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-parent</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
```
|
||||
2. 根据需要引入starter-web、starter-jdbc和starter-logging模块
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-web</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-jdbc</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cxx</groupId>
|
||||
<artifactId>starter-logging</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
3. 打包模块时,需要添加spring-boot-maven-plugin
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
```
|
||||
142
docs/Web/SpringBoot/SpringBoot-Redis.md
Normal file
142
docs/Web/SpringBoot/SpringBoot-Redis.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: SpringBoot Redis简介和使用
|
||||
date: 2026-03-22
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  Redis是一个高性能的开源内存数据库,用作缓存、数据库和消息中间件。它以极快的读写速度(基于内存存储)支持字符串、列表、哈希等多种数据结构。Redis提供数据持久化、主从复制、哨兵模式等高可用特性,广泛应用于缓存、会话存储、排行榜、消息队列等场景。
|
||||
|
||||
# 二、安装
|
||||
1. 通过apt包管理器安装:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install redis-server
|
||||
sudo systemctl enable redis-server
|
||||
```
|
||||
|
||||
2. 配置远程和密码访问(可选)
|
||||
|
||||
  打开`/etc/redis/redis.conf`文件,更改配置:
|
||||
```conf
|
||||
bind 0.0.0.0 ::1
|
||||
protected-mode yes
|
||||
requirepass password
|
||||
```
|
||||
|
||||
  `systemctl restart redis-server`重启服务。
|
||||
|
||||
# 三、SpringBoot使用
|
||||
## 3.1 引入依赖
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 3.2 配置文件
|
||||
```yml
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password: 123456
|
||||
```
|
||||
|
||||
::: tip
|
||||
SpringBoot2中没有data层级
|
||||
:::
|
||||
|
||||
## 3.3 简单使用
|
||||
```java
|
||||
@Component
|
||||
public class RedisUtils {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate redis;
|
||||
|
||||
public void set(String key, String value, long seconds) {
|
||||
redis.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public String get(String key) {
|
||||
return redis.opsForValue().get(key);
|
||||
}
|
||||
|
||||
public void delete(String key) {
|
||||
redis.delete(key);
|
||||
}
|
||||
|
||||
public Boolean hasKey(String key) {
|
||||
return redis.hasKey(key);
|
||||
}
|
||||
|
||||
// 匹配查询
|
||||
public Set<String> scanKeys(String pattern) {
|
||||
return redis.execute((RedisCallback<Set<String>>) connection -> {
|
||||
Set<String> keys = new HashSet<>();
|
||||
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();
|
||||
|
||||
try (Cursor<byte[]> cursor = connection.keyCommands().scan(options)) {
|
||||
while (cursor.hasNext()) {
|
||||
keys.add(new String(cursor.next(), StandardCharsets.UTF_8));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Redis SCAN 执行失败,pattern=" + pattern, e);
|
||||
}
|
||||
|
||||
return keys;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip
|
||||
生产环境中使用`scan`命令批量查询`key`值,`keys`命令会阻塞`Redis`线程。
|
||||
:::
|
||||
|
||||
# 四、实战
|
||||
## 4.1 设备在线功能
|
||||
  实现一个统计设备是否实时在线的功能。
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/device")
|
||||
public class DeviceController {
|
||||
|
||||
@Resource
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
private static final String HEARTBEAT_KEY = "device:heartbeat:";
|
||||
private static final long EXPIRE_SECONDS = 40;
|
||||
|
||||
/**
|
||||
* 设备心跳接口
|
||||
* 设备每20-30秒调用一次
|
||||
* 心跳key 40秒后过期,过期即认为设备离线
|
||||
*/
|
||||
@PostMapping("/heartbeat")
|
||||
public Boolean heartbeat(@RequestParam String deviceId) {
|
||||
redisUtils.set(HEARTBEAT_KEY + deviceId, String.valueOf(System.currentTimeMillis()), EXPIRE_SECONDS);
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前在线设备列表
|
||||
*/
|
||||
@GetMapping("/online")
|
||||
public Set<String> getOnlineDevices() {
|
||||
Set<String> keys = redisUtils.scanKeys(HEARTBEAT_KEY + "*");
|
||||
Set<String> deviceIds = new HashSet<>();
|
||||
|
||||
for (String key : keys) {
|
||||
String deviceId = key.substring(HEARTBEAT_KEY.length());
|
||||
deviceIds.add(deviceId);
|
||||
}
|
||||
|
||||
return deviceIds;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  客户端每30秒调用一次`/device/heartbeat`接口。
|
||||
134
docs/Web/SpringBoot/SpringBoot-RestClient.md
Normal file
134
docs/Web/SpringBoot/SpringBoot-RestClient.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: SpringBoot RestClient简介
|
||||
date: 2026-01-06
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  RestClient 是 Spring Framework 6.1(及对应的 Spring Boot 3.2+)推出的新一代同步 HTTP 客户端,设计目标是替代传统的 RestTemplate(Spring 已标记 RestTemplate 为维护模式,不再新增功能),同时结合了 WebClient 的流畅 API 设计,又保持了 RestTemplate 的同步、简单易用的特点。
|
||||
|
||||
# 二、使用
|
||||
## 2.1 创建实例
|
||||
```java
|
||||
@Configuration
|
||||
public class RestClientConfig {
|
||||
final String baseUrl = "https://demo.com";
|
||||
|
||||
@Bean
|
||||
public RestClient restClient() {
|
||||
|
||||
return RestClient.builder()
|
||||
// 基础RURL
|
||||
.baseUrl(baseUrl)
|
||||
// 默认请求头 请求体为JSON格式
|
||||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
// 默认请求头 响应体为JSON格式
|
||||
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.2 HTTP请求
|
||||
```java
|
||||
@Service
|
||||
public class RestClientService {
|
||||
|
||||
@Resource
|
||||
private RestClient restClient;
|
||||
|
||||
// 1. GET 请求:获取单个资源
|
||||
public Post getPostById(Long id) {
|
||||
return restClient.get() // 指定 GET 方法
|
||||
.uri("/posts/{id}", id) // 请求路径(支持路径参数)
|
||||
.retrieve() // 执行请求并获取响应
|
||||
.body(Post.class); // 解析响应体为Post实体
|
||||
}
|
||||
|
||||
// 2. GET 请求:获取列表并解析为 Map
|
||||
public Map<String, Object>[] getAllPosts() {
|
||||
return restClient.get()
|
||||
.uri("/posts")
|
||||
.retrieve()
|
||||
.body(Map[].class); // 解析为 Map 数组(适合未知结构的 JSON)
|
||||
}
|
||||
|
||||
// 3. POST 请求:提交数据并获取响应
|
||||
public Boolean createPost(Post post) {
|
||||
return restClient.post() // 指定 POST 方法
|
||||
.uri("/posts")
|
||||
.body(post) // 设置请求体
|
||||
.retrieve()
|
||||
.body(Boolean.class);
|
||||
}
|
||||
|
||||
// 4. PUT 请求:更新资源
|
||||
public Boolean updatePost(Long id, Post post) {
|
||||
return restClient.put() // 指定 PUT 方法
|
||||
.uri("/posts/{id}", id)
|
||||
.body(post)
|
||||
.retrieve()
|
||||
.body(Boolean.class);
|
||||
}
|
||||
|
||||
// 5. DELETE 请求:删除资源
|
||||
public void deletePost(Long id) {
|
||||
restClient.delete() // 指定 DELETE 方法
|
||||
.uri("/posts/{id}", id)
|
||||
.retrieve(); // DELETE 请求通常无响应体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip
|
||||
在.retrieve()之前的是请求阶段,在此之后是响应阶段。
|
||||
因此在此之前的.body()是请求体,在此之后的.body()是响应体。
|
||||
:::
|
||||
|
||||
## 2.3 进阶用法
|
||||
### 2.3.1 设置超时时间
|
||||
```java
|
||||
@Bean
|
||||
public RestClient restClient() {
|
||||
// 创建请求工厂,设置超时
|
||||
ClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory() {{
|
||||
setConnectTimeout(3000); // 连接超时 3 秒
|
||||
setReadTimeout(5000); // 读取超时 5 秒
|
||||
}};
|
||||
|
||||
return RestClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.requestFactory(factory) // 设置请求工厂
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3.2 添加请求拦截器
|
||||
```java
|
||||
@Bean
|
||||
public RestClient restClient() {
|
||||
return RestClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
// 添加拦截器
|
||||
.requestInterceptor(request -> {
|
||||
request.getHeaders().add("Authorization", "Bearer your-token-here");
|
||||
})
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3.3 自定义响应处理
|
||||
```java
|
||||
public String getPostWithErrorHandling(Long id) {
|
||||
return restClient.get()
|
||||
.uri("/posts/{id}", id)
|
||||
.retrieve()
|
||||
// 自定义状态码处理
|
||||
.onStatus(status -> status.is4xxClientError(), (request, response) -> {
|
||||
throw new RuntimeException("客户端错误:" + response.getStatusCode() + ",路径:" + request.getURI());
|
||||
})
|
||||
.onStatus(status -> status.is5xxServerError(), (request, response) -> {
|
||||
throw new RuntimeException("服务端错误:" + response.getStatusCode());
|
||||
})
|
||||
.body(String.class);
|
||||
}
|
||||
```
|
||||
359
docs/Web/SpringBoot/SpringBoot-Skills.md
Normal file
359
docs/Web/SpringBoot/SpringBoot-Skills.md
Normal file
@@ -0,0 +1,359 @@
|
||||
---
|
||||
title: SpingBoot技巧
|
||||
date: 2025-12-12
|
||||
---
|
||||
|
||||
# 一、JsonView
|
||||
  JsonView 是 Jackson 提供的注解,用于控制对象序列化/反序列化时包含哪些字段。可以实现:
|
||||
- 不同接口返回不同字段
|
||||
- 敏感字段过滤
|
||||
- 前后端数据分离
|
||||
|
||||
## 1.1 添加依赖
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## 1.2 使用
|
||||
1. 定义视图接口
|
||||
```java
|
||||
// JsonView 视图定义
|
||||
public class Views {
|
||||
// 基础视图 - 所有接口都包含的字段
|
||||
public interface Basic {}
|
||||
|
||||
// 返回给前端时使用
|
||||
public interface Response extends Basic {}
|
||||
|
||||
// 接收前端数据时使用
|
||||
public interface Request extends Basic {}
|
||||
|
||||
// 管理视图 - 包含所有字段
|
||||
public interface AdminView extends ReadView {}
|
||||
}
|
||||
```
|
||||
|
||||
2. Dto对象使用JsonView
|
||||
```java
|
||||
@Data
|
||||
public class UserDTO {
|
||||
@JsonView(Views.Response.class)
|
||||
private Long id;
|
||||
|
||||
@JsonView(Views.Request.class)
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, message = "密码至少6位")
|
||||
private String password;
|
||||
|
||||
@JsonView(Views.Basic.class)
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@JsonView(Views.Basic.class)
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@JsonView(Views.Basic.class)
|
||||
private String nickname;
|
||||
|
||||
@JsonView(AdminView.class)
|
||||
private Boolean isActive;
|
||||
}
|
||||
```
|
||||
|
||||
3. Controller中使用
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping
|
||||
@JsonView(UserDTO.Response.class)
|
||||
public UserDTO createUser(@RequestBody @JsonView(UserDTO.Request.class) UserDTO userDTO) {
|
||||
return userService.createUser(userDTO);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@JsonView(UserDTO.Response.class)
|
||||
public UserDTO updateUser(@PathVariable Long id,
|
||||
@RequestBody @JsonView(UserDTO.Request.class) UserDTO userDTO) {
|
||||
return userService.updateUser(id, userDTO);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@JsonView(UserDTO.Response.class)
|
||||
public UserDTO getUser(@PathVariable Long id) {
|
||||
return userService.getUserById(id);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/admin")
|
||||
@JsonView(UserDTO.AdminView.class)
|
||||
public UserDTO getAdminUser(@PathVariable Long id) {
|
||||
return userService.getAdminUserById(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 二、MapStruct
|
||||
  MapStruct 是一个 Java 注解处理器,用于生成类型安全的 Bean 映射代码:
|
||||
- 编译时生成映射代码,无运行时性能损失
|
||||
- 类型安全
|
||||
- 支持复杂映射
|
||||
|
||||
## 2.1 添加依赖
|
||||
```xml
|
||||
<properties>
|
||||
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
## 2.2 使用
|
||||
```java
|
||||
import org.mapstruct.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
|
||||
@Mapping(target = "name", source = "userName") // 字段名不同
|
||||
@Mapping(target = "phoneNumber", source = "phone") // 字段名不同
|
||||
@Mapping(target = "description", source = "remark") // 字段名不同
|
||||
@Mapping(target = "statusDesc", ignore = true) // 需要特殊处理
|
||||
@Mapping(target = "createTime", ignore = true) // 需要格式化
|
||||
UserDTO toDTO(User user);
|
||||
|
||||
@Mapping(target = "userName", source = "name")
|
||||
@Mapping(target = "phone", source = "phoneNumber")
|
||||
@Mapping(target = "remark", source = "description")
|
||||
@Mapping(target = "password", ignore = true) // 密码不映射
|
||||
User toEntity(UserDTO dto);
|
||||
|
||||
// 列表映射
|
||||
List<UserDTO> toDTOList(List<User> users);
|
||||
|
||||
// 带默认值的映射
|
||||
@Mapping(target = "name", source = "userName")
|
||||
@Mapping(target = "phoneNumber", source = "phone")
|
||||
@Mapping(target = "description", source = "remark", defaultValue = "暂无描述")
|
||||
@Mapping(target = "statusDesc", expression = "java(convertStatus(user.getStatus()))")
|
||||
@Mapping(target = "createTime", expression = "java(formatTime(user.getCreateTime()))")
|
||||
UserDTO toDTOWithDefault(User user);
|
||||
|
||||
// 自定义转换方法
|
||||
default String convertStatus(Integer status) {
|
||||
if (status == null) return "未知";
|
||||
switch (status) {
|
||||
case 0: return "禁用";
|
||||
case 1: return "正常";
|
||||
case 2: return "锁定";
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
// 时间格式化方法
|
||||
default String formatTime(LocalDateTime time) {
|
||||
if (time == null) return "";
|
||||
return time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  需要执行`mvn compile`后检查`target/generated-sources`文件夹下是否存在生成的转换代码。
|
||||
|
||||
# 三、Bean Validation
|
||||
  Spring Boot的数据校验基于JSR-303/JSR-380规范(Bean Validation),通常使用Hibernate Validator作为实现。
|
||||
|
||||
## 3.1 添加依赖
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 3.2 常用检验注解
|
||||
  Java Bean Validation 常用注解
|
||||
|
||||
| 注解 | 适用类型 | 说明 |
|
||||
|------|----------|------|
|
||||
| `@NotNull` | 任意类型 | 值不能为 null |
|
||||
| `@NotBlank` | CharSequence | 字符串不能为 null,且必须包含至少一个非空白字符 |
|
||||
| `@NotEmpty` | CharSequence, Collection, Map, Array | 字符串/集合/数组不能为 null 且不能为空(长度/大小大于 0) |
|
||||
| `@Size(min=, max=)` | 字符串、集合、数组 | 限制长度或大小在 min 和 max 之间 |
|
||||
| `@Min(value)` | 数值类型 | 数值必须大于或等于指定值 |
|
||||
| `@Max(value)` | 数值类型 | 数值必须小于或等于指定值 |
|
||||
| `@Email` | 字符串 | 字符串必须是合法的电子邮件地址格式 |
|
||||
| `@Pattern(regexp=)` | 字符串 | 字符串必须匹配指定的正则表达式 |
|
||||
| `@Future` / `@Past` | 日期时间类型 | 日期必须在当前时间的未来 / 过去 |
|
||||
|
||||
## 3.3 使用方法
|
||||
```java
|
||||
public class UserDTO {
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 20, message = "用户名长度必须在3到20个字符之间")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 8, message = "密码长度至少为8个字符")
|
||||
private String password;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
  在Controller的方法参数前使用@Valid或@Validated注解来触发校验:
|
||||
```java
|
||||
@RestController
|
||||
public class UserController {
|
||||
@PostMapping("/users")
|
||||
public String createUser(@Valid @RequestBody UserDTO userDTO) {
|
||||
return "用户创建成功";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.4 处理校验错误
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
ex.getBindingResult().getAllErrors().forEach((error) -> {
|
||||
String fieldName = ((FieldError) error).getField();
|
||||
String errorMessage = error.getDefaultMessage();
|
||||
errors.put(fieldName, errorMessage);
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.5 自定义注解
|
||||
```java
|
||||
@Documented
|
||||
@Constraint(validatedBy = PhoneValidator.class)
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PhoneNumber {
|
||||
String message() default "手机号码格式不正确";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
```
|
||||
|
||||
  校验规则:
|
||||
```java
|
||||
public class PhoneValidator implements ConstraintValidator<PhoneNumber, String> {
|
||||
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return true; // 使用@NotBlank等注解处理空值
|
||||
}
|
||||
return PHONE_PATTERN.matcher(value).matches();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  使用自定义校验:
|
||||
```java
|
||||
public class UserDTO {
|
||||
@PhoneNumber
|
||||
private String mobile;
|
||||
}
|
||||
```
|
||||
|
||||
# 四、ConfigurationProperties注解
|
||||
  @ConfigurationProperties 是 Spring Boot 提供的核心注解,用于将配置文件(如 application.yml/application.properties)中的属性批量绑定到 Java 类的字段上,相比 @Value 注解,它更适合管理一组有层级、有前缀的配置,代码更整洁、可维护性更高。
|
||||
|
||||
## 4.1 配置文件
|
||||
```yml
|
||||
app:
|
||||
name: SpringBootDemo
|
||||
version: 1.0.0
|
||||
author:
|
||||
name: 张三
|
||||
age: 25
|
||||
servers:
|
||||
- 192.168.1.100
|
||||
- 192.168.1.101
|
||||
- 192.168.1.102
|
||||
```
|
||||
|
||||
## 4.2 配置属性类
|
||||
```java
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app")
|
||||
@Getter
|
||||
@Setter
|
||||
public class AppProperties {
|
||||
// 对应 app.name
|
||||
private String name;
|
||||
// 对应 app.version
|
||||
private String version;
|
||||
// 嵌套属性 - 对应 app.author
|
||||
private Author author;
|
||||
// 集合属性 - 对应 app.servers
|
||||
private List<String> servers;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Author {
|
||||
private String name;
|
||||
|
||||
private Integer age;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  属性类支持嵌套和集合属性。
|
||||
|
||||
## 4.3 使用
|
||||
  在Service或相应地方注入即可:
|
||||
```java
|
||||
@Autowired
|
||||
private AppProperties appProperties;
|
||||
```
|
||||
287
docs/Web/SpringBoot/SpringBoot-Start-Process.md
Normal file
287
docs/Web/SpringBoot/SpringBoot-Start-Process.md
Normal file
@@ -0,0 +1,287 @@
|
||||
---
|
||||
title: SpringBoot启动流程
|
||||
date: 2025-11-26
|
||||
---
|
||||
|
||||
# 一、启动流程
|
||||

|
||||
|
||||
# 二、 阶段详解
|
||||
## 2.1 SpringApplication初始化
|
||||
**🔹 步骤1.1 - main方法入口执行**
|
||||
- 执行位置:标注@SpringBootApplication的类中的main方法
|
||||
- 核心动作:
|
||||
1. 创建SpringApplication实例对象
|
||||
2. 设置应用的基本配置信息
|
||||
3. 准备启动所需的基础环境
|
||||
|
||||
  main方法是整个Spring Boot应用启动的唯一起点。当Java虚拟机开始执行程序时,首先会调用标注有@SpringBootApplication注解的主类中的main方法。在这个方法内部,会创建SpringApplication实例并调用其run方法,正式开启启动流程。
|
||||
  SpringApplication的构造方法会进行一些重要的初始化工作,包括推断主配置类、设置初始引导类等。推断主配置类的过程是通过分析当前线程的堆栈信息来完成的,确保能够准确找到包含main方法的那个类。
|
||||
|
||||
**🔹 步骤1.2 - 加载SpringFactories配置**
|
||||
- 配置源:META-INF/spring.factories文件
|
||||
- 加载内容:
|
||||
1. ApplicationContextInitializer(上下文初始化器)
|
||||
2. ApplicationListener(应用监听器)
|
||||
3. BeanFactoryPostProcessor(Bean工厂后处理器)
|
||||
4. AutoConfigurationImportSelector(自动配置选择器)
|
||||
|
||||
  Spring Boot使用SpringFactoriesLoader机制从类路径下的**META-INF/spring.factories**文件中加载各种扩展组件。**这种机制是Spring Boot自动配置的核心基础,它允许框架和开发者通过标准的配置文件来注册和发现各种扩展实现。**
|
||||
  配置文件中定义了多种类型的组件,包括**应用上下文初始化器、应用事件监听器、Bean工厂后处理器**等。这些组件将在后续的启动过程中按照特定的顺序被执行,共同完成应用的初始化工作。
|
||||
|
||||
**🔹 步骤1.3 - 推断Web应用类型**
|
||||
- 检测逻辑:
|
||||
1. 检查类路径是否存在Servlet相关类
|
||||
2. 检查Spring MVC相关组件
|
||||
3. 检查WebFlux相关组件
|
||||
- 推断结果
|
||||
🅰️ SERVLET:传统Web应用
|
||||
🅱️ REACTIVE:响应式Web应用
|
||||
©️ NONE:非Web应用
|
||||
|
||||
  应用类型推断是Spring Boot自动配置的重要环节。系统会根据项目的依赖情况自动判断应用类型,这直接影响后续创建的应用上下文类型和内嵌服务器的选择。
|
||||
  **推断过程主要通过检查类路径中是否存在特定的类来完成。例如,如果存在Servlet相关的类,就推断为Web应用;如果存在WebFlux相关的类,就推断为响应式Web应用。** 这种基于类路径的自动推断机制使得开发者无需手动配置应用类型,大大简化了配置工作。
|
||||
|
||||
## 2.2 环境准备与配置加载
|
||||
**🔹 步骤2.1 - 创建环境对象**
|
||||
```
|
||||
环境对象层次结构:
|
||||
Environment(环境接口)
|
||||
↓
|
||||
ConfigurableEnvironment(可配置环境)
|
||||
↓
|
||||
具体环境实现(StandardEnvironment/StandardServletEnvironment)
|
||||
├── PropertySources(属性源列表)
|
||||
├── Profiles(激活的配置文件)
|
||||
└── ConversionService(类型转换服务)
|
||||
```
|
||||
|
||||
  环境对象是Spring Boot应用运行时的配置中心,它负责管理所有的配置属性和运行环境信息。根据应用类型的不同,Spring Boot会创建相应的环境实例。
|
||||
  环境对象采用分层设计,提供了统一的属性访问接口,同时支持多种属性源的动态管理。环境对象还负责管理激活的配置文件(Profile),支持基于不同环境的配置隔离和切换。
|
||||
|
||||
**🔹 步骤2.2 - 配置属性源加载**
|
||||
📊 属性源加载优先级(从高到低):
|
||||
| 优先级 | 属性源类型 | 说明 |
|
||||
| - | - | - |
|
||||
| 1 | 命令行参数 | --spring.profiles.active=dev |
|
||||
| 2 | Java系统属性 | System.getProperties() |
|
||||
| 3 | 操作系统环境变量 | 系统级环境配置 |
|
||||
| 4 | 应用配置文件 | application-{profile}.yml/properties |
|
||||
| 5 | 默认属性 | SpringApplication.setDefaultProperties() |
|
||||
|
||||
  属性源加载遵循严格的优先级顺序,确保重要的配置能够覆盖默认配置。这种优先级设计使得配置管理更加灵活,开发者可以通过不同级别的配置来调整应用行为。
|
||||
|
||||
**🔹 步骤2.3 - Profile处理机制**
|
||||
- Profile激活方式:
|
||||
1. 通过spring.profiles.active显式指定
|
||||
2. 通过spring.profiles.include包含其他profile
|
||||
3. 默认使用default profile
|
||||
- Profile解析流程:
|
||||
1. 读取所有可用的profile配置
|
||||
2. 解析条件化配置注解
|
||||
3. 合并不同profile的配置项
|
||||
4. 处理配置覆盖和冲突解决
|
||||
|
||||
  Profile机制是Spring框架提供的环境隔离解决方案,它允许开发者为不同的运行环境定义不同的配置。Spring Boot在启动时会根据激活的Profile来加载相应的配置,并处理配置之间的覆盖关系。
|
||||
  Profile的激活可以通过多种方式实现,包括命令行参数、系统属性、配置文件等。系统支持同时激活多个Profile,并提供了灵活的配置合并策略来处理可能出现的配置冲突。
|
||||
|
||||
**🔹 步骤2.4 - 配置属性绑定**
|
||||
1. 属性值解析:从各种属性源中获取属性值
|
||||
2. 类型转换:将字符串值转换为目标类型
|
||||
3. 数据验证:基于JSR-303进行数据校验
|
||||
4. 对象绑定:将属性值设置到@ConfigurationProperties类
|
||||
|
||||
  配置属性绑定是将外部配置值注入到Java对象中的过程。Spring Boot提供了强大的属性绑定功能,支持宽松的绑定规则、类型转换、数据验证等特性。
|
||||
  绑定过程首先会从环境对象中获取属性值,然后进行类型转换,将字符串类型的配置值转换为目标属性类型。如果配置了验证规则,还会执行数据验证,确保配置值的正确性。最后,将验证通过的属性值设置到目标对象中。
|
||||
|
||||
## 2.3 应用上下文创建与初始化
|
||||
**🔹 步骤3.1 - 创建应用上下文实例**
|
||||
📋 上下文类型映射表:
|
||||
| 应用类型 | 上下文实现类 | 特点描述 |
|
||||
| - | - | - |
|
||||
| Web应用 | AnnotationConfigServletWebServerApplicationContext | 支持Servlet容器 |
|
||||
| 响应式应用 | AnnotationConfigReactiveWebServerApplicationContext | 支持响应式编程 |
|
||||
| 普通应用 | AnnotationConfigApplicationContext | 基础应用上下文 |
|
||||
|
||||
  应用上下文是Spring框架的核心容器,负责管理Bean的生命周期和依赖关系。Spring Boot会根据应用类型创建相应类型的应用上下文实例
|
||||
|
||||
**🔹 步骤3.2 - 应用上下文层次结构**
|
||||
```
|
||||
BeanFactory(Bean工厂基础接口)
|
||||
↓
|
||||
ApplicationContext(应用上下文接口)
|
||||
↓
|
||||
ConfigurableApplicationContext(可配置应用上下文)
|
||||
↓
|
||||
AbstractApplicationContext(抽象实现)
|
||||
↓
|
||||
GenericApplicationContext/AnnotationConfigApplicationContext
|
||||
```
|
||||
|
||||
  应用上下文采用层次化设计,不同层次的接口和类承担不同的职责。这种设计使得上下文的功能可以逐步扩展,同时保持代码的清晰性和可维护性。
|
||||
|
||||
**🔹 步骤3.3 - 执行应用上下文初始化器**
|
||||
- 执行时机:上下文创建后,Bean加载前
|
||||
- 主要功能:
|
||||
1. 🔧 注册自定义Bean定义
|
||||
2. 🔧 设置上下文特定属性
|
||||
3. 🔧 添加特殊的后处理器
|
||||
4. 🔧 配置环境变量覆盖
|
||||
|
||||
  应用上下文初始化器是Spring Boot提供的重要扩展点,允许开发者**在上下文正式刷新之前执行自定义的初始化逻辑。** **初始化器可以通过spring.factories文件注册,也可以通过SpringApplication的addInitializers方法添加。**
|
||||
  **初始化器的主要作用包括注册自定义的Bean定义、配置上下文特定的属性、添加特殊的后处理器等。** 通过初始化器,开发者可以深度定制应用上下文的行为,满足特殊的业务需求。
|
||||
|
||||
**🔹 步骤3.4 - 发布应用上下文事件**
|
||||
事件发布序列:
|
||||
1. ApplicationStartingEvent- 应用启动事件
|
||||
2. ApplicationEnvironmentPreparedEvent- 环境准备完成事件
|
||||
3. ApplicationContextInitializedEvent- 上下文初始化事件
|
||||
4. ApplicationPreparedEvent- 应用准备事件
|
||||
|
||||
  Spring Boot的启动过程采用事件驱动模型,**每个关键步骤都会发布相应的事件**。这种设计使得各个模块之间解耦,同时也便于开发者通过监听事件来扩展启动逻辑。
|
||||
|
||||
## 2.4 Bean定义加载与处理
|
||||
**🔹 步骤4.1 - Bean定义加载方式**
|
||||
多种Bean定义加载途径:
|
||||
1. 🔍 组件扫描:自动扫描@Component、@Service等注解
|
||||
2. 📝 @Bean方法:处理@Configuration类中的@Bean方法
|
||||
3. 📂 @Import导入:导入其他配置类
|
||||
4. 🔗 ImportSelector:动态选择导入的配置类
|
||||
|
||||
  Bean定义加载是Spring容器初始化的核心环节。Spring Boot支持多种Bean定义加载方式,每种方式都有其适用的场景和特点。
|
||||
|
||||
**🔹 步骤4.2 - 组件扫描详细过程**
|
||||
```
|
||||
组件扫描流程:
|
||||
开始扫描指定包路径
|
||||
↓
|
||||
读取包下的所有class文件
|
||||
↓
|
||||
解析类上的注解信息
|
||||
↓
|
||||
识别Spring组件注解
|
||||
↓
|
||||
注册Bean定义到容器
|
||||
↓
|
||||
完成组件扫描
|
||||
```
|
||||
|
||||
  组件扫描是Spring Boot自动配置的基础机制,它能够自动发现和注册项目中的Spring组件。扫描过程基于注解元数据,通过反射机制分析类的结构信息。
|
||||
|
||||
**🔹 步骤4.3 - Bean工厂后处理**
|
||||
| 后处理器 | 功能描述 | 执行时机 |
|
||||
| - | - | - |
|
||||
| ConfigurationClassPostProcessor | 处理@Configuration类 | Bean定义加载后 |
|
||||
| PropertySourcesPlaceholderConfigurer | 处理属性占位符 | 属性解析阶段 |
|
||||
| CustomScopeConfigurer | 注册自定义作用域 | 作用域配置阶段 |
|
||||
|
||||
  BeanFactoryPostProcessor是Spring框架的重要扩展点,允许在Bean实例化之前修改Bean定义信息。Spring Boot在启动过程中会执行多个内置的BeanFactoryPostProcessor。
|
||||
|
||||
## 2.5 Bean实例化与生命周期
|
||||
**🔹 步骤5.1 - Bean实例化策略**
|
||||
实例化顺序规则:
|
||||
1. 🥇 BeanFactoryPostProcessor- 工厂后处理器最先实例化
|
||||
2. 🥈 BeanPostProcessor- Bean后处理器其次实例化
|
||||
3. 🥉 单例Bean- 按依赖顺序实例化普通Bean
|
||||
4. 🏅 其他作用域Bean- 按需实例化
|
||||
|
||||
  Bean实例化遵循特定的顺序规则,确保依赖关系正确的Bean能够按正确的顺序创建。实例化过程采用懒加载和急切实例化相结合的策略。
|
||||
|
||||
**🔹 步骤5.2 - 依赖注入机制**
|
||||
依赖注入的三种方式:
|
||||
1. 🏗️ 构造器注入- 通过构造函数注入依赖
|
||||
2. 🛠️ Setter注入- 通过setter方法注入依赖
|
||||
3. 🎯 字段注入- 直接在字段上使用@Autowired注入
|
||||
|
||||
  依赖注入是Spring框架的核心特性,它通过自动装配机制将Bean之间的依赖关系解耦。Spring支持多种依赖注入方式,每种方式都有其适用的场景。
|
||||
|
||||
**🔹 步骤5.3 - Bean后处理流程**
|
||||
```
|
||||
Bean后处理序列:
|
||||
Bean实例化
|
||||
↓
|
||||
执行BeanPostProcessor.postProcessBeforeInitialization
|
||||
↓
|
||||
执行@PostConstruct方法
|
||||
↓
|
||||
执行InitializingBean.afterPropertiesSet
|
||||
↓
|
||||
执行自定义init方法
|
||||
↓
|
||||
执行BeanPostProcessor.postProcessAfterInitialization
|
||||
↓
|
||||
Bean完全就绪
|
||||
```
|
||||
|
||||
  BeanPostProcessor是Bean生命周期管理的重要扩展点,它允许在Bean初始化前后执行自定义逻辑。Spring Boot内置了多个BeanPostProcessor,用于处理各种注解和AOP代理。
|
||||
|
||||
**🔹 步骤5.4 - 循环依赖解决机制**
|
||||
三级缓存解决方案:
|
||||
1. 一级缓存:存放完全初始化完成的Bean
|
||||
2. 二级缓存:存放早期暴露的Bean(已实例化但未初始化)
|
||||
3. 三级缓存:存放Bean工厂,用于创建Bean的早期引用
|
||||
|
||||
  循环依赖是Spring容器需要解决的重要问题。Spring通过三级缓存机制来解决单例Bean的循环依赖问题,确保即使存在循环引用也能正确完成依赖注入。
|
||||
|
||||
## 2.6 Web服务器启动与配置
|
||||
**🔹 步骤6.1 - 内嵌服务器选择策略**
|
||||
服务器自动配置逻辑:
|
||||
1. 检查类路径中的服务器依赖
|
||||
2. 按优先级选择:Tomcat > Jetty > Undertow
|
||||
3. 根据应用类型创建对应的Web服务器工厂
|
||||
4. 配置服务器参数(端口、上下文路径等)
|
||||
|
||||
  Spring Boot支持多种内嵌服务器,包括Tomcat、Jetty和Undertow。服务器选择基于类路径中的依赖,采用特定的优先级规则。
|
||||
|
||||
**🔹 步骤6.2 - Servlet容器初始化**
|
||||
1. 🎯 创建ServletContext(Servlet上下文)
|
||||
2. 🎯 注册DispatcherServlet(前端控制器)
|
||||
3. 🎯 配置字符编码过滤器
|
||||
4. 🎯 设置会话管理配置
|
||||
5. 🎯 启用静态资源服务
|
||||
|
||||
  Servlet容器初始化是Web应用启动的关键环节。Spring Boot会自动配置Servlet容器,并注册必要的Servlet、Filter和Listener。
|
||||
|
||||
**🔹 步骤6.3 - MVC组件自动配置**
|
||||
自动配置的MVC组件:
|
||||
1. HandlerMapping- 请求映射处理器
|
||||
2. HandlerAdapter- 处理器适配器
|
||||
3. ViewResolver- 视图解析器
|
||||
4. MessageConverter- 消息转换器
|
||||
5. Interceptor- 拦截器配置
|
||||
|
||||
  Spring Boot为Spring MVC提供了完整的自动配置,包括处理器映射、视图解析、消息转换等组件。这些组件基于约定大于配置的原则,提供了合理的默认值。
|
||||
|
||||
## 2.7 启动完成与后处理
|
||||
**🔹 步骤7.1 - 启动事件发布序列**
|
||||
```
|
||||
ApplicationStartingEvent
|
||||
↓
|
||||
ApplicationEnvironmentPreparedEvent
|
||||
↓
|
||||
ApplicationContextInitializedEvent
|
||||
↓
|
||||
ApplicationPreparedEvent
|
||||
↓
|
||||
ContextRefreshedEvent
|
||||
↓
|
||||
ApplicationReadyEvent
|
||||
```
|
||||
|
||||
  当所有的Bean实例化、依赖注入和初始化回调都执行完成后,Spring容器会发布ContextRefreshedEvent事件。这个事件标志着Spring IoC容器已经完全刷新并准备就绪。
|
||||
|
||||
**🔹 步骤7.2 - 命令行运行器执行**
|
||||
运行器类型与特点:
|
||||
| 运行器接口 | 执行方法 | 参数类型 | 使用场景 |
|
||||
| - | - | - | - |
|
||||
| ApplicationRunner | run(ApplicationArguments) | 封装的应用参数 | 需要丰富参数信息时 |
|
||||
| CommandLineRunner | run(String... args) | 原始字符串参数 | 简单参数处理时 |
|
||||
|
||||
  Spring Boot会执行所有实现了ApplicationRunner或CommandLineRunner接口的Bean。
|
||||
|
||||
**🔹 步骤7.3 - 健康检查与指标收集**
|
||||
启动后的监控机制:
|
||||
1. ❤️ 健康检查:通过HealthIndicator监控应用健康状态
|
||||
2. 📊 应用指标:通过Micrometer收集运行时指标
|
||||
3. 🔔 事件监听:监控应用生命周期事件
|
||||
4. 📝 日志记录:记录启动完成状态和耗时
|
||||
77
docs/Web/SpringBoot/SpringBoot3-GraalVM.md
Normal file
77
docs/Web/SpringBoot/SpringBoot3-GraalVM.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: SpringBoot3原生镜像
|
||||
date: 2025-11-27
|
||||
---
|
||||
|
||||
# 一、原生镜像
|
||||
  传统 Java 应用基于 JVM 运行,需要加载完整的类库和 JVM 运行时环境,导致启动时间长、内存占用高。而**原生镜像技术通过提前编译(AOT)将 Java 应用直接编译为本地机器码,无需 JVM 即可运行**,具有以下核心优势:
|
||||
|
||||
1. 启动速度极快:毫秒级启动,相比传统 JVM 应用提升 10-100 倍、
|
||||
2. 内存占用低:仅包含应用运行所需的最小资源集合
|
||||
3. 部署体积小:通常为传统 JAR 包的 1/10 左右
|
||||
4. 适合云原生场景:尤其适合 Kubernetes 等容器化环境
|
||||
|
||||
# 二、安装使用
|
||||
## 2.1 基础软件
|
||||
1. GraalVM(类似于JAVA JDK)下载:[官网](https://www.graalvm.org/downloads/),下载完成后需要配置环境变量
|
||||
2. native-image安装:cmd运行:gu install native-image
|
||||
3. Visual Studio Community2022 选择:使用C++的桌面开发
|
||||
|
||||
## 2.2 结合Springboot3 项目
|
||||
1. IDEA创建Springboot3项目
|
||||
2. Project Structure选择GraalVM JDK
|
||||
3. 配置pom打包
|
||||
```xml
|
||||
<plugins>
|
||||
<!-- 将项目打包成可执行的 JAR/WAR 文件 -->
|
||||
<!-- repackage目标会替换 Maven 默认的打包行为 -->
|
||||
<!-- 使 JAR/WAR 文件包含所有依赖项,成为一个独立运行的应用 -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
<!-- 执行 AOT 编译 -->
|
||||
<goal>process-aot</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- GraalVM Native Maven 插件 -->
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
<version>0.10.2</version>
|
||||
<configuration>
|
||||
<buildArgs combine.children="append">
|
||||
<!-- 支持 http 协议 -->
|
||||
<buildArg>--enable-url-protocols=http</buildArg>
|
||||
<!-- 用于解决数据库中文乱码问题 -->
|
||||
<buildArg>-H:+AddAllCharsets</buildArg>
|
||||
</buildArgs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build-native</id>
|
||||
<goals>
|
||||
<goal>compile-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
```
|
||||
4. 选择Maven->Lifecycle->clean和package,或者输入命令:mvn -Pnative native:compile
|
||||
5. 在target目录下可以看到生成的.exe文件,打开运行即可。
|
||||
|
||||
## 三、注意事项
|
||||
  实际体验下来发现主要存在以下几个问题
|
||||
1. 编译打包的速度非常慢
|
||||
2. 目前有些第三方库并不支持(Mybatis已支持,Mybatis Plus暂不支持)
|
||||
|
||||
# 四、参考资料
|
||||
1. [使用 Spring Boot 和 GraalVM 构建原生镜像](https://springdoc.cn/spring-native-intro/)
|
||||
2. [GraalVM 原生镜像支持](https://docs.springframework.org.cn/spring-boot/reference/native-image/introducing-graalvm-native-images.html)
|
||||
65
docs/Web/SpringBoot/SpringBootBean.md
Normal file
65
docs/Web/SpringBoot/SpringBootBean.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: SpringBoot Bean简介
|
||||
date: 2025-11-29
|
||||
---
|
||||
|
||||
# 一、Bean概念
|
||||
## 1.1 定义
|
||||
  Spring bean是Spring框架在运行时管理的对象,Bean是一个由Spring IoC容器实例化、组装和管理的对象。
|
||||
  一个 Bean 可以是任何普通的 Java 对象,例如 POJO、Service、Respository、Controller 等等。将一个类声明为 Bean 的方式可以是在类级别上使用 `@Component` 注解或其派生注解(`@Service`、`@Repository`、`@Controller`等),也可以是通过配置文件进行显式的声明。
|
||||
|
||||
## 1.2 实例化
|
||||
  Spring 容器负责实例化 Bean。当应用程序启动时,Spring 容器会根据配置信息或注解扫描的结果,找到并实例化所有被标记为 Bean 的类,并将它们加入容器中。实例化的过程由 Spring 的 IoC 容器负责。
|
||||
|
||||
## 1.3 管理
|
||||
  一旦 Bean 被实例化,Spring 容器将负责管理 Bean 的生命周期和依赖关系。它会根据配置文件或注解的信息,自动解决 Bean 之间的依赖关系,确保在需要的时候正确的注入依赖。Spring 容器还会负责销毁不再需要的 Bean。
|
||||
|
||||
## 1.4 依赖注入
|
||||
  依赖注入是 Spring 框架的一个重要特性,它允许通过自动或显式配置的方式将 Bean 的依赖项注入到其它 Bean 中。依赖注入可以通过构造函数注入、Setter 方法注入或字段注入的方式实现,其中最常见的是使用 `@Autowired`注解进行注入。
|
||||
|
||||
## 1.5 作用域
|
||||
  Spring 框架提供了多种作用域(scope)来管理 Bean 的生命周期。常见的作用域包括单例(Singleton)、原型(Prototype)、会话(Session)、请求(Request)等。默认情况下,Bean 是单例的,即每个容器中只存在一个实例。但可以根据需要配置其它作用域。
|
||||
|
||||
## 1.6 自动装配
|
||||
  Spring Boot 支持自动装配(Auto - wiring),它能够根据类型或名称自动解析和注入依赖关系。通过在需要注入的字段、构造函数或 Setter 方法上使用 `@Autowired` 注解,Spring 容器会自动查找并注入对应的 Bean。
|
||||
|
||||
# 二、Bean生命周期
|
||||

|
||||
|
||||
## 2.1 实例化
|
||||
  指创建对象的过程,也就是说,当 Spring 容器启动时,它会扫描所有的配置文件,读取 Bean 定义,然后根据定义创建 Bean 对象的实例。这个过程中,Spring 容器会使用反射机制来调用 Bean 的构造函数来实例化对象。
|
||||
|
||||
## 2.2 属性赋值
|
||||
  对 Bean 的实例化对象进行属性填充;
|
||||
|
||||
## 2.3 初始化
|
||||
  指 Bean 对象被创建后,Spring 容器会根据配置文件中的定义来对其进行一系列的属性设置、依赖注入等操作,使其可以正常运行。这个过程中,Spring 容器会调用 Bean 的一些特定方法,如 set 方法、init-method 方法等来完成对象的初始化。
|
||||
|
||||
## 2.4 销毁
|
||||
  执行一些预定义的销毁方法;
|
||||
|
||||
# 三、Bean注解——@Component
|
||||
## 基本概念
|
||||
  @Component 是 Spring 框架中的一个注解,用于将类标记为 Spring 管理的组件。当一个类被 @Component 注解修饰时,Spring 会在启动时自动扫描并将该类实例化为 Bean,放入应用程序的上下文(ApplicationContext)中,以便在其他地方可以通过依赖注入使用。
|
||||
|
||||
## 主要作用
|
||||
1. 组件扫描:Spring 通过组件扫描机制自动发现并注册被 @Component 注解的类。
|
||||
2. Bean 定义:被注解的类会被视为 Spring Bean,由 Spring 容器管理其生命周期。
|
||||
3. 依赖注入:其他组件可以通过注解(如 @Autowired)注入这些 Bean。
|
||||
|
||||
## 其他衍生注解
|
||||
| 注解 | 所属层 | 继承关系 | 适用场景 |
|
||||
|------------|------------|-------------|----------|
|
||||
| @Component | 通用组件 | 基础注解 | 不适合特定层的组件(如工具类、配置类) |
|
||||
| @Repository | 数据访问层 (DAO) | 继承 @Component | 数据库操作类、ORM 映射类(如 JPA Repository) |
|
||||
| @Service | 服务层 (Service) | 继承 @Component | 业务逻辑处理(如订单处理、用户认证) |
|
||||
| @Controller | 控制层 (MVC/Web) | 继承 @Component | 处理 HTTP 请求的控制器(如 REST API、Web 页面控制器) |
|
||||
| @RestController | 控制层 (REST API) | 继承 @Controller | RESTful API 接口开发 |
|
||||
|
||||
| 注解 | 功能描述 | Spring 自动处理特性 |
|
||||
|------------------|----------------------|-------------------|
|
||||
| @Component | 标记类为 Spring Bean,通过组件扫描自动注册。<br>无特定语义,是其他组件注解的基础。 | - Bean 生命周期管理<br>- 依赖注入支持 |
|
||||
| @Repository | 标记数据访问组件(如数据库操作类),<br>自动处理数据访问异常。 | - 异常转换<br>- DAO 组件扫描 |
|
||||
| @Service | 标记业务逻辑组件,负责处理业务流程、事务管理等。<br>提供语义化,明确该类在架构中的角色。 | - 与 @Transactional 注解默认集成<br>- IDE 识别为服务层组件 |
|
||||
| @Controller | 标记 Web 控制器,处理 HTTP 请求。<br>与 Spring MVC 框架紧密集成。 | - 请求映射自动注册<br>- 支持 @RequestMapping、@GetMapping 等注解<br>- 视图解析(返回视图名称或 JSON) |
|
||||
| @RestController | @Controller + @ResponseBody 的组合,专门用于 RESTful API,直接返回 JSON/XML 等数据。 | - 所有方法默认返回 JSON/XML(无需每个方法添加 @ResponseBody) |
|
||||
110
docs/Web/SpringBoot/SpringBootStarter.md
Normal file
110
docs/Web/SpringBoot/SpringBootStarter.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: SpringBoot Starter原理
|
||||
date: 2025-11-27
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  Spring Boot Starter是一组预定义的依赖项集合,旨在简化Maven或Gradle等构建工具中的依赖管理。每个Starter都包含了实现特定功能所需的库和组件,以及相应的配置文件。开发者只需在项目中引入相应的Starter依赖,即可快速搭建起具备该功能的项目骨架。
|
||||
  Starter=依赖+自动配置+配置文件
|
||||
|
||||
# 二、实现原理
|
||||
## 2.1 传统实现
|
||||
  例如引入Spring中的jpa,则需要以下步骤:
|
||||
1. 在Maven中引入数据库依赖
|
||||
2. 在Maven中引入jpa依赖
|
||||
3. 在配置文件中配置属性
|
||||
4. 调试程序
|
||||
|
||||
  每次新建项目都需要重复此流程,操作繁琐。
|
||||
|
||||
## 2.2 自定义Starter实现
|
||||
1. 新建Maven项目,在pom.xml文件中定义需要的依赖项。
|
||||
2. 创建自动配置类 AutoConfigurationTest,添加@configuration注解,使其能够被SpringBoot自动扫描到。
|
||||
3. 添加自动装配机制,在src/main/resources/META-INF文件夹下创建spring.factories文件,添加以下配置:
|
||||
|
||||
```bash
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.example.springbootstartercustom.AutoConfigurationTest
|
||||
```
|
||||
|
||||
  注:这里文件夹和文件名一定要正确,因此SpringFactoriesLoader中就是这么定义的。
|
||||
|
||||
4. 在配置文件中自定义属性(可选)。
|
||||
5. 安装打包到maven仓库中。
|
||||
6. 其他项目通过pom.xml文件引入该starter。
|
||||
|
||||
## 2.3 Starter实现原理
|
||||
  加载依赖->扫描自动配置类->加载配置文件
|
||||
|
||||
# 三、高级特性
|
||||
## 3.1 可插拔Starter
|
||||
  所谓可插拔就是可以自行决定是否需要加载该starter的功能。例如可以通过注解的方式决定是否加载。
|
||||
|
||||
1. 定义注解
|
||||
|
||||
```java
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface EnableAutoConfigTest {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
2. 在自动配置类 AutoConfiguration中增加条件注解
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@ConditionalOnBean(annotation = EnableAutoConfigTest.class)
|
||||
public class AutoConfigurationTest {
|
||||
}
|
||||
```
|
||||
|
||||
3. 在相应位置添加@EnableAutoConfigTest注解该stater才会生效。
|
||||
|
||||
## 3.2 自定义配置文件
|
||||
  所谓自定义配置文件,就是可以在引入stater后,可以通过修改配置文件覆盖原来的配置属性,从而灵活配置stater功能。
|
||||
|
||||
1. 引用spring-boot-configuration-processor
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
2. 定义Properties配置类
|
||||
|
||||
```java
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
public class TestProperties {
|
||||
private String name = "test";
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 在自动配置类 AutoConfigurationTest中引用
|
||||
|
||||
```java
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import({TestProperties.class})
|
||||
public class AutoConfigurationTest {
|
||||
@Resource
|
||||
private TestProperties testProperties;
|
||||
}
|
||||
```
|
||||
|
||||
4. 在引入的stater工程中,修改配置文件:
|
||||
|
||||
```yaml
|
||||
test:
|
||||
name: test1
|
||||
```
|
||||
561
docs/Web/SpringBoot/SpringIOC.md
Normal file
561
docs/Web/SpringBoot/SpringIOC.md
Normal file
@@ -0,0 +1,561 @@
|
||||
---
|
||||
title: Spring IOC简介
|
||||
date: 2025-11-26
|
||||
---
|
||||
|
||||
# 一、IOC
|
||||
  Spring框架的核心是IOC(控制反转)容器,它负责管理应用程序中的对象(称为Bean)的创建、配置和组装。
|
||||
|
||||
# 二、Bean
|
||||
## 2.1 定义
|
||||
  在Spring中,Bean是指由Spring IOC容器管理的对象实例。这些对象不再由应用程序直接创建,而是由容器负责实例化、配置和组装。
|
||||
  例如@Service、@Controller就是特殊的Bean。
|
||||
|
||||
## 2.2 特点
|
||||
- 由容器管理:生命周期由Spring容器控制
|
||||
- 可配置性:通过配置元数据定义Bean的行为
|
||||
- 依赖注入:容器自动处理Bean之间的依赖关系
|
||||
- 作用域支持:支持singleton、prototype等不同作用域
|
||||
|
||||
## 2.3 创建方式
|
||||
```java
|
||||
// 1. 通过@Component注解
|
||||
@Component
|
||||
public class UserService {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 2. 通过@Bean注解(配置类中)
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new DataSource();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 通过XML配置
|
||||
<bean id="userService" class="com.example.UserService"/>
|
||||
```
|
||||
|
||||
::: tip
|
||||
@Service、@Controller这些注解是@Component的衍生注解。
|
||||
@Component注解可以通过@Autowired自动注入,一般用于自己编写的业务模块,需要配合@ComponentScan启用组件扫描(实际开发中@SpringBootApplication注解已经包含了@ComponentScan注解)。
|
||||
@Bean注解一般用于注册第三方库的类。
|
||||
:::
|
||||
|
||||
## 2.4 作用域
|
||||
  控制 Bean 在容器中的实例数量和生命周期。Spring 提供多种作用域,默认是 singleton。通过 @Scope 注解指定,如 @Scope("prototype")。
|
||||
- singleton(单例):容器中只有一个实例,所有请求共享该实例(默认值)。
|
||||
- prototype(多例):每次请求(如 getBean())都会创建新实例,容器不管理其销毁。
|
||||
- request(Web):每个 HTTP 请求创建一个实例,仅在当前请求有效。
|
||||
- session(Web):每个会话创建一个实例,在会话有效期内有效。
|
||||
|
||||
## 2.5 生命周期
|
||||
1. 实例化(Instantiation)
|
||||
容器通过构造器创建 Bean 实例(分配内存)。
|
||||
|
||||
2. 属性注入(Population)
|
||||
容器将依赖的 Bean 注入到当前 Bean 的字段或方法(如 @Autowired 标注的依赖)。
|
||||
|
||||
3. 初始化前(Post-processing before initialization)
|
||||
执行 BeanPostProcessor 的 postProcessBeforeInitialization 方法(AOP 代理生成在此阶段)。
|
||||
|
||||
4. 初始化(Initialization)
|
||||
调用 @PostConstruct 注解的方法(JSR-250 标准,推荐)。
|
||||
若实现 InitializingBean 接口,调用 afterPropertiesSet() 方法。
|
||||
执行自定义初始化方法(如 @Bean(initMethod = "init") 中指定的 init 方法)。
|
||||
|
||||
5. 初始化后(Post-processing after initialization)
|
||||
执行 BeanPostProcessor 的 postProcessAfterInitialization 方法。
|
||||
|
||||
6. 使用(In Use)
|
||||
Bean 被应用程序调用。
|
||||
|
||||
7. 销毁(Destruction)
|
||||
调用 @PreDestroy 注解的方法(JSR-250 标准,推荐)。
|
||||
若实现 DisposableBean 接口,调用 destroy() 方法。
|
||||
执行自定义销毁方法(如 @Bean(destroyMethod = "destroy") 中指定的 destroy 方法)。
|
||||

|
||||
|
||||
# 三、BeanDefinition
|
||||
## 3.1 定义
|
||||
  BeanDefinition是Spring框架中描述Bean的元数据对象,它包含了创建Bean实例所需的所有信息。
|
||||
|
||||
## 3.2 作用
|
||||
- 定义Bean的类名
|
||||
- 设置Bean的作用域
|
||||
- 配置Bean的属性值
|
||||
- 定义依赖关系
|
||||
- 配置初始化/销毁方法
|
||||
|
||||
## 3.3 结构
|
||||
```xml
|
||||
BeanDefinition (接口)
|
||||
↑
|
||||
AbstractBeanDefinition (抽象类)
|
||||
↑
|
||||
RootBeanDefinition, GenericBeanDefinition, ChildBeanDefinition
|
||||
```
|
||||
|
||||
## 3.4 关键属性
|
||||
```java
|
||||
public interface BeanDefinition {
|
||||
// Bean的类名
|
||||
void setBeanClassName(String beanClassName);
|
||||
String getBeanClassName();
|
||||
|
||||
// 作用域(singleton、prototype等)
|
||||
void setScope(String scope);
|
||||
String getScope();
|
||||
|
||||
// 是否延迟初始化
|
||||
void setLazyInit(boolean lazyInit);
|
||||
boolean isLazyInit();
|
||||
|
||||
// 依赖的Bean名称
|
||||
void setDependsOn(String... dependsOn);
|
||||
String[] getDependsOn();
|
||||
|
||||
// 初始化方法
|
||||
void setInitMethodName(String initMethodName);
|
||||
String getInitMethodName();
|
||||
|
||||
// 销毁方法
|
||||
void setDestroyMethodName(String destroyMethodName);
|
||||
String getDestroyMethodName();
|
||||
|
||||
// 工厂方法名
|
||||
void setFactoryMethodName(String factoryMethodName);
|
||||
String getFactoryMethodName();
|
||||
}
|
||||
```
|
||||
|
||||
## 3.5 创建过程
|
||||
```java
|
||||
// 编程方式创建BeanDefinition
|
||||
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
|
||||
beanDefinition.setBeanClassName("com.example.UserService");
|
||||
beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
|
||||
beanDefinition.setLazyInit(false);
|
||||
|
||||
// 注册到BeanFactory
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerBeanDefinition("userService", beanDefinition);
|
||||
```
|
||||
|
||||
# 四、BeanFactory
|
||||
## 4.1 定义
|
||||
  BeanFactory是Spring IOC容器的根接口,提供了最基本的IOC功能。它是访问Spring容器的基本客户端视图。
|
||||
|
||||
## 4.2 结构
|
||||
```xml
|
||||
BeanFactory (根接口)
|
||||
↑
|
||||
ListableBeanFactory, HierarchicalBeanFactory, AutowireCapableBeanFactory
|
||||
↑
|
||||
ConfigurableBeanFactory, ConfigurableListableBeanFactory
|
||||
↑
|
||||
DefaultListableBeanFactory (主要实现类)
|
||||
```
|
||||
|
||||
## 4.3 核心方法
|
||||
```java
|
||||
public interface BeanFactory {
|
||||
// 根据名称获取Bean实例
|
||||
Object getBean(String name) throws BeansException;
|
||||
|
||||
// 根据名称和类型获取Bean实例
|
||||
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
|
||||
|
||||
// 根据类型获取Bean实例
|
||||
<T> T getBean(Class<T> requiredType) throws BeansException;
|
||||
|
||||
// 判断是否包含指定名称的Bean
|
||||
boolean containsBean(String name);
|
||||
|
||||
// 判断Bean是否是单例
|
||||
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
|
||||
|
||||
// 判断Bean是否是原型
|
||||
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
|
||||
|
||||
// 获取Bean的类型
|
||||
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
|
||||
}
|
||||
```
|
||||
|
||||
## 4.4 使用示例
|
||||
```java
|
||||
// 创建BeanFactory实例
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
|
||||
// 创建BeanDefinition
|
||||
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
|
||||
beanDefinition.setBeanClassName("com.example.UserService");
|
||||
|
||||
// 注册BeanDefinition
|
||||
beanFactory.registerBeanDefinition("userService", beanDefinition);
|
||||
|
||||
// 获取Bean实例
|
||||
UserService userService = beanFactory.getBean("userService", UserService.class);
|
||||
```
|
||||
|
||||
## 4.5 特点
|
||||
- 轻量级:功能相对简单,资源消耗少
|
||||
- 延迟加载:默认在第一次请求时才创建Bean实例
|
||||
- 基础功能:提供最基本的IOC功能
|
||||
|
||||
# 五、ApplicationContext ★★★
|
||||
## 5.1 定义
|
||||
  ApplicationContext是BeanFactory的子接口,在BeanFactory的基础上提供了更多企业级功能,是Spring容器的完整实现。
|
||||
|
||||
## 5.2 结构
|
||||
```xml
|
||||
BeanFactory
|
||||
↑
|
||||
ApplicationContext (扩展接口)
|
||||
↑
|
||||
ConfigurableApplicationContext
|
||||
↑
|
||||
AbstractApplicationContext
|
||||
↑
|
||||
ClassPathXmlApplicationContext, FileSystemXmlApplicationContext,
|
||||
AnnotationConfigApplicationContext, WebApplicationContext
|
||||
```
|
||||
|
||||
## 5.3 特点
|
||||
- 完整的IOC功能:包含BeanFactory的所有功能
|
||||
- 预实例化单例Bean:默认在启动时创建单例Bean
|
||||
- 企业级功能:支持AOP、事务管理、国际化等
|
||||
- 资源访问:统一的资源访问接口
|
||||
- 事件机制:支持应用内事件发布/订阅
|
||||
|
||||
## 5.4 refresh方法
|
||||
```java
|
||||
// AbstractApplicationContext.java
|
||||
/**
|
||||
* Spring ApplicationContext 的核心启动方法
|
||||
* 定义了容器初始化的完整生命周期
|
||||
*/
|
||||
public void refresh() throws BeansException, IllegalStateException {
|
||||
// 使用同步锁确保容器启动的线程安全性,防止并发刷新
|
||||
synchronized (this.startupShutdownMonitor) {
|
||||
|
||||
// [1] 准备刷新容器 - 设置启动状态,验证环境配置
|
||||
prepareRefresh();
|
||||
|
||||
// [2] 获取或创建BeanFactory - 创建DefaultListableBeanFactory实例
|
||||
// 如果是XML配置,会在这里加载和解析XML文件
|
||||
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
|
||||
|
||||
// [3] 配置BeanFactory - 设置类加载器、注册核心BeanPostProcessor等
|
||||
prepareBeanFactory(beanFactory);
|
||||
|
||||
try {
|
||||
// [4] 后处理BeanFactory - 模板方法,允许子类进行特殊配置
|
||||
// Web应用会在这里注册Servlet相关的Scope
|
||||
postProcessBeanFactory(beanFactory);
|
||||
|
||||
// [5] 🚀 关键步骤:调用BeanFactory后置处理器
|
||||
// 执行BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor
|
||||
// ConfigurationClassPostProcessor在这里扫描@Component、@Configuration等注解
|
||||
invokeBeanFactoryPostProcessors(beanFactory);
|
||||
|
||||
// [6] 注册Bean后置处理器 - 注册所有BeanPostProcessor
|
||||
// 包括AOP、事务、自定义的BeanPostProcessor,此时只是注册,还未执行
|
||||
registerBeanPostProcessors(beanFactory);
|
||||
|
||||
// [7] 初始化消息源 - 设置国际化消息资源
|
||||
// 用于支持多语言消息解析
|
||||
initMessageSource();
|
||||
|
||||
// [8] 初始化应用事件广播器 - 创建事件发布机制的核心组件
|
||||
// 用于发布ApplicationEvent事件
|
||||
initApplicationEventMulticaster();
|
||||
|
||||
// [9] 模板方法:子类可以在这里执行特殊的刷新逻辑
|
||||
// SpringBoot在这里启动内嵌的Tomcat服务器
|
||||
onRefresh();
|
||||
|
||||
// [10] 注册监听器 - 将所有ApplicationListener注册到事件广播器
|
||||
// 包括静态指定的监听器和从BeanFactory中获取的监听器
|
||||
registerListeners();
|
||||
|
||||
// [11] 🏭 核心步骤:完成BeanFactory的初始化
|
||||
// 实例化所有非懒加载的单例Bean,执行依赖注入、AOP代理等
|
||||
finishBeanFactoryInitialization(beanFactory);
|
||||
|
||||
// [12] 完成刷新过程 - 发布ContextRefreshedEvent事件
|
||||
// 标记容器启动完成,可以正常工作了
|
||||
finishRefresh();
|
||||
|
||||
} catch (BeansException ex) {
|
||||
// 如果刷新过程中出现异常,需要销毁已经创建的单例Bean,防止资源泄漏
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Exception encountered during context initialization - " +
|
||||
"cancelling refresh attempt: " + ex);
|
||||
}
|
||||
|
||||
// 销毁已经创建的单例Bean
|
||||
destroyBeans();
|
||||
|
||||
// 重置容器状态为未激活
|
||||
cancelRefresh(ex);
|
||||
|
||||
// 重新抛出异常
|
||||
throw ex;
|
||||
} finally {
|
||||
// 最后重置一些公共的缓存,比如反射缓存
|
||||
resetCommonCaches();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4.1 prepareRefresh()
|
||||
```java
|
||||
protected void prepareRefresh() {
|
||||
this.startupDate = System.currentTimeMillis();
|
||||
this.closed.set(false);
|
||||
this.active.set(true);
|
||||
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Refreshing " + this);
|
||||
}
|
||||
|
||||
initPropertySources();
|
||||
getEnvironment().validateRequiredProperties();
|
||||
this.earlyApplicationEvents = new LinkedHashSet<>();
|
||||
}
|
||||
```
|
||||
|
||||
- 这是容器启动的第一步,主要做初始化准备工作
|
||||
- 设置容器的启动时间戳和活跃状态
|
||||
- 验证必要的环境属性是否配置正确(比如通过@Value注解标记为必须的属性)
|
||||
- 初始化早期事件集合,用于存储在事件多播器创建前发布的事件
|
||||
- 为整个刷新过程奠定基础状态
|
||||
|
||||
### 5.4.2 obtainFreshBeanFactory()
|
||||
```java
|
||||
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
|
||||
refreshBeanFactory();
|
||||
return getBeanFactory();
|
||||
}
|
||||
|
||||
// 子类实现示例
|
||||
protected final void refreshBeanFactory() throws IllegalStateException {
|
||||
if (hasBeanFactory()) {
|
||||
destroyBeans();
|
||||
closeBeanFactory();
|
||||
}
|
||||
try {
|
||||
DefaultListableBeanFactory beanFactory = createBeanFactory();
|
||||
customizeBeanFactory(beanFactory);
|
||||
loadBeanDefinitions(beanFactory);
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ApplicationContextException("I/O error", ex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 创建或刷新底层的BeanFactory(Spring容器的核心工厂)
|
||||
- 如果是刷新操作,会先销毁旧的BeanFactory和所有Bean实例
|
||||
- 创建新的DefaultListableBeanFactory实例
|
||||
- 加载Bean定义(从XML文件或注解配置中解析Bean的元数据)
|
||||
- 这个步骤完成后,容器就知道了有哪些Bean需要管理,但还没有创建Bean实例
|
||||
|
||||
### 5.4.3 prepareBeanFactory()
|
||||
```java
|
||||
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
|
||||
beanFactory.setBeanClassLoader(getClassLoader());
|
||||
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver());
|
||||
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
|
||||
|
||||
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
|
||||
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
|
||||
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
|
||||
|
||||
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
|
||||
beanFactory.registerResolvableDependency(ApplicationContext.class, this);
|
||||
}
|
||||
```
|
||||
|
||||
- 配置BeanFactory的基本设置和功能组件
|
||||
- 设置类加载器、SPEL表达式解析器、属性编辑器
|
||||
- 注册ApplicationContextAwareProcessor,这是实现各种Aware接口的关键
|
||||
- 忽略某些接口的依赖注入(这些接口由容器特殊处理)
|
||||
- 注册可解析的依赖关系,当Bean需要这些类型时直接返回容器实例
|
||||
- 为BeanFactory添加基本的功能支持
|
||||
|
||||
### 5.4.4 postProcessBeanFactory()
|
||||
```java
|
||||
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
|
||||
// 空方法 - 模板方法模式
|
||||
// 子类可以覆盖此方法添加特定逻辑
|
||||
}
|
||||
```
|
||||
|
||||
- 模板方法,允许子类在标准初始化后对BeanFactory进行自定义配置
|
||||
- 在Web环境中,会在这里注册Servlet相关的Scope(request、session等)
|
||||
- 可以添加特定环境的BeanPostProcessor或进行其他特殊配置
|
||||
- 这是框架扩展的重要切入点
|
||||
|
||||
### 5.4.5 invokeBeanFactoryPostProcessors()
|
||||
```java
|
||||
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
|
||||
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
|
||||
}
|
||||
```
|
||||
|
||||
- 🚀 这是最关键的一步,负责扫描和注册所有的Bean定义
|
||||
- 执行所有BeanFactoryPostProcessor,它们可以修改Bean的定义信息
|
||||
- 特别是ConfigurationClassPostProcessor,它负责:
|
||||
扫描@Component、@Service、@Repository等注解
|
||||
处理@Configuration配置类
|
||||
解析@Bean方法定义
|
||||
处理@Import、@ComponentScan等注解
|
||||
- 这一步完成后,所有的Bean定义都已经被解析和注册
|
||||
|
||||
### 5.4.6 registerBeanPostProcessors()
|
||||
```java
|
||||
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
|
||||
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
|
||||
}
|
||||
```
|
||||
|
||||
- 注册所有的BeanPostProcessor到BeanFactory中
|
||||
- 包括AOP、事务管理、自定义的Bean后置处理器等
|
||||
- 注意:此时只是注册,真正的调用要等到Bean实例化时
|
||||
- 这些处理器按照优先级顺序注册,确保执行顺序正确
|
||||
- 这是Spring AOP和事务管理等高级功能的基础
|
||||
|
||||
### 5.4.7 initMessageSource()
|
||||
```java
|
||||
protected void initMessageSource() {
|
||||
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
|
||||
if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
|
||||
this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
|
||||
}
|
||||
else {
|
||||
DelegatingMessageSource dms = new DelegatingMessageSource();
|
||||
this.messageSource = dms;
|
||||
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 初始化国际化消息支持
|
||||
- 如果用户配置了自定义的MessageSource,就使用用户的配置
|
||||
- 否则创建默认的DelegatingMessageSource
|
||||
- 支持从属性文件读取多语言消息,实现国际化功能
|
||||
- 用于解析${}占位符和国际化消息
|
||||
|
||||
### 5.4.8 initApplicationEventMulticaster()
|
||||
```java
|
||||
protected void initApplicationEventMulticaster() {
|
||||
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
|
||||
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
|
||||
this.applicationEventMulticaster = beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
|
||||
}
|
||||
else {
|
||||
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
|
||||
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 初始化应用事件发布机制的核心组件
|
||||
- 如果用户配置了自定义的事件广播器,就使用用户的配置
|
||||
- 否则创建默认的SimpleApplicationEventMulticaster
|
||||
- 这个组件负责将应用事件广播给所有监听器
|
||||
- 支持观察者模式,实现应用内组件间的解耦通信
|
||||
|
||||
### 5.4.9 onRefresh() - 刷新时扩展点
|
||||
```java
|
||||
protected void onRefresh() throws BeansException {
|
||||
// 空方法 - 模板方法
|
||||
}
|
||||
```
|
||||
|
||||
- 模板方法,允许子类在特定时机执行自定义逻辑
|
||||
- 在Spring Boot中,这里会启动内嵌的Web服务器(Tomcat、Jetty等)
|
||||
- 在Spring MVC中,这里会初始化HandlerMapping、HandlerAdapter等组件
|
||||
- 这是框架扩展的重要切入点,不同的ApplicationContext实现有不同的行为
|
||||
|
||||
### 5.4.10 registerListeners()
|
||||
```java
|
||||
protected void registerListeners() {
|
||||
for (ApplicationListener<?> listener : getApplicationListeners()) {
|
||||
getApplicationEventMulticaster().addApplicationListener(listener);
|
||||
}
|
||||
|
||||
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
|
||||
for (String listenerBeanName : listenerBeanNames) {
|
||||
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
|
||||
}
|
||||
|
||||
// 发布早期事件
|
||||
if (this.earlyApplicationEvents != null) {
|
||||
for (ApplicationEvent earlyEvent : this.earlyApplicationEvents) {
|
||||
getApplicationEventMulticaster().multicastEvent(earlyEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 注册所有的事件监听器到事件广播器
|
||||
- 包括静态注册的监听器和从BeanFactory中获取的监听器Bean
|
||||
- 发布在监听器注册前收集的早期事件
|
||||
- 确保事件发布机制完整可用,应用事件能够正确分发
|
||||
|
||||
### 5.4.11 finishBeanFactoryInitialization()
|
||||
```java
|
||||
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
|
||||
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME)) {
|
||||
beanFactory.setConversionService(beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
|
||||
}
|
||||
|
||||
if (!beanFactory.hasEmbeddedValueResolver()) {
|
||||
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
|
||||
}
|
||||
|
||||
beanFactory.freezeConfiguration();
|
||||
beanFactory.preInstantiateSingletons();
|
||||
}
|
||||
```
|
||||
|
||||
- 🏭 这是最核心的步骤,真正创建Bean实例
|
||||
- 初始化类型转换服务(ConversionService)
|
||||
- 注册嵌入式值解析器(用于解析${}占位符)
|
||||
- 冻结Bean定义,防止后续修改
|
||||
- 实例化所有非懒加载的单例Bean,这个过程中会:
|
||||
创建Bean实例
|
||||
执行依赖注入(@Autowired、@Resource等)
|
||||
执行BeanPostProcessor(AOP代理在这里发生)
|
||||
调用初始化方法(@PostConstruct、InitializingBean)
|
||||
- 这是容器启动过程中最耗时的步骤
|
||||
|
||||
### 5.4.12 finishRefresh()
|
||||
```java
|
||||
protected void finishRefresh() {
|
||||
clearResourceCaches();
|
||||
initLifecycleProcessor();
|
||||
getLifecycleProcessor().onRefresh();
|
||||
publishEvent(new ContextRefreshedEvent(this));
|
||||
LiveBeansView.registerApplicationContext(this);
|
||||
}
|
||||
```
|
||||
|
||||
- 完成容器的启动过程,进行最后的清理和通知工作
|
||||
- 清除资源缓存,释放临时资源
|
||||
- 初始化生命周期处理器,管理Bean的生命周期
|
||||
- 发布ContextRefreshedEvent事件,通知所有监听器容器已启动完成
|
||||
- 注册LiveBeansView(用于监控和调试)
|
||||
- 标记整个容器启动流程结束,容器进入就绪状态
|
||||
|
||||
### 5.4.13 总结
|
||||
  refresh()方法按固定顺序执行12个步骤,核心可归纳为:**准备容器环境 → 解析并注册Bean定义→ 初始化事件、消息等基础设施 → 实例化所有非懒加载的单例Bean→ 完成启动并发布事件**。整个过程通过模板方法模式定义,确保了扩展性和一致性。
|
||||
  核心在于两个关键扩展机制:**BeanFactoryPostProcessor在Bean定义阶段介入,负责修改和注册Bean的定义信息;BeanPostProcessor在Bean实例化阶段介入,负责对创建好的Bean实例进行增强和包装。**
|
||||
94
docs/Web/SpringBoot/SpringMVC.md
Normal file
94
docs/Web/SpringBoot/SpringMVC.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Spring MVC简介
|
||||
date: 2025-11-27
|
||||
---
|
||||
|
||||
# 一、基础概念
|
||||
  Spring MVC(Spring Model-View-Controller)是 Spring Framework 中的一部分,它是一个基于 **请求驱动** 的 Web 框架,主要用于构建 Web 应用程序,并且遵循 MVC(模型-视图-控制器) 设计模式。它将 Web 应用的业务逻辑、用户界面和请求处理分离,使得代码更加模块化和可维护。
|
||||
|
||||
# 二、具体流程
|
||||

|
||||
|
||||
1. 客户端发起HTTP请求
|
||||
|
||||
  客户端(浏览器、API 客户端等)向服务器发送 HTTP 请求。请求包含了目标 URL、HTTP 方法(GET、POST 等)以及可能的请求参数。
|
||||
前端通过发送 HTTP 请求(通常是 AJAX 请求)到后端的 API 端点,并期望获得响应(通常是 JSON 数据)。
|
||||
|
||||
2. 请求到达 DispatcherServlet
|
||||
|
||||
  所有 HTTP 请求都会先到达 Spring MVC 的核心组件:**DispatcherServlet**。它是前端控制器,负责将请求分发给正确的处理器。DispatcherServlet 是所有请求的入口,Spring Boot 会自动配置它。
|
||||
  DispatcherServlet 接受所有的 HTTP 请求,根据请求 URL 和 HTTP 方法,定位匹配的控制器方法,将请求交给相应的 Handler Mapping 来匹配控制器。在请求处理前后,执行 拦截器(可选)。最终通过 View Resolver 返回一个合适的视图(对于 RESTful API,通常返回 JSON 格式的数据)。
|
||||
|
||||
3. 查找处理器(Handler Mapping)
|
||||
|
||||
  DispatcherServlet 根据请求的 URL 和 HTTP 方法,查找与之匹配的处理器方法(通常是一个控制器中的方法)。这是由 HandlerMapping 完成的。Spring MVC 提供了多种方式来映射 URL 到控制器方法,例如 @RequestMapping、@GetMapping 等注解。
|
||||
  @RestController:该注解标记的类会返回 JSON 数据。
|
||||
  @RequestMapping:用于映射 URL 路径和 HTTP 方法。
|
||||
  @GetMapping、@PostMapping 等:简化版本的 @RequestMapping。
|
||||
|
||||
4. 执行拦截器(Interceptor)
|
||||
|
||||
  在请求到达控制器之前,DispatcherServlet 会依次执行配置的拦截器。拦截器主要用于日志记录、安全检查、请求参数的预处理等。
|
||||
  preHandle:请求进入控制器方法之前。
|
||||
  postHandle:请求完成后,视图渲染之前。
|
||||
  afterCompletion:视图渲染完成后。
|
||||
  **如果拦截器的 preHandle 方法返回 false,请求会被拦截,后续的处理不会继续进行。**
|
||||
|
||||
5. 控制器方法处理请求(Handler Execution)
|
||||
|
||||
  DispatcherServlet 调用合适的控制器方法来处理请求。控制器方法负责处理请求的业务逻辑,并返回一个 Java 对象,这个对象会被自动转换成 JSON 格式返回给客户端。
|
||||
  Spring 通过 @ResponseBody 注解(或 @RestController)实现将 Java 对象自动转换为 JSON 格式。默认情况下,Spring 使用 Jackson 来将 Java 对象序列化成 JSON 数据。
|
||||
  @RestController:将控制器标记为返回 JSON 数据的 RESTful 控制器。
|
||||
  @ResponseBody:标记方法返回的数据需要直接作为响应体返回,而不是解析为视图。
|
||||
  @RequestParam:用于获取单个请求参数。
|
||||
  @PathVariable:用于从 URL 中提取变量(如 /users/{id} 中的 {id})。
|
||||
|
||||
6. 响应返回客户端
|
||||
|
||||
  DispatcherServlet 将处理完的结果(JSON 数据)返回给客户端。响应数据通常会包含:
|
||||
  **状态码**:如 200 OK(成功)、201 Created(资源创建成功)、404 Not Found(资源未找到)、500 Internal Server Error(服务器错误)等。
|
||||
  **响应头**:通常包括 Content-Type: application/json,表示响应体是 JSON 格式。
|
||||
  **响应体**:包含返回的数据,通常是一个 JSON 格式的对象或数组。
|
||||
|
||||
注:以上只适用于基于RESTful API 前后端分离的架构。
|
||||
|
||||
# 三、完整流程
|
||||

|
||||
  在SpringMVC前面还有Tomcat容器层。客户端发起请求后,先经过Tomcat容器,根据请求的 URL 确定该请求是否是静态资源请求(如 .html、.css、.js)。如果是静态资源请求,Tomcat 会直接返回文件,而不经过 Spring MVC 的请求处理链。对于动态请求(如请求 /user 等),Tomcat 会将这些请求交给 DispatcherServlet 进行处理。
|
||||
  在进入DispatcherServlet前可以增加过滤器进行处理。过滤器不属于Spring部分。
|
||||
  Tomcat 是一个轻量级的 Web 服务器和 Servlet 容器,支持 Java Servlet 和 JSP 规范,能够处理 HTTP 请求并生成动态 Web 内容。
|
||||
  Servlet 是 Java 编写的服务器端程序,能够接收 HTTP 请求并生成响应,它是动态 Web 内容生成的核心技术。
|
||||
  Tomcat 管理 Servlet 的生命周期,并负责处理客户端的请求和返回响应。
|
||||
|
||||
# 四、重要知识点
|
||||
## 4.1 拦截器和过滤器区别
|
||||
1. 拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
|
||||
2. 拦截器只能对action请求(DispatcherServlet 映射的请求)起作用,而过滤器则可以对几乎所有的请求起作用。
|
||||
3. 拦截器可以访问容器中的Bean(DI),而过滤器不能访问(基于spring注册的过滤器也可以访问容器中的bean)。
|
||||
4. 执行顺序:过滤器->拦截器
|
||||
|
||||
## 4.2 拦截器执行顺序
|
||||
  HandlerInterceptor 拦截器执行顺序:
|
||||
1. preHandle():请求处理前 按注册顺序依次执行。
|
||||
2. Controller:请求处理 请求到达Controller并被处理。
|
||||
3. postHandle():请求处理后,视图渲染前 按注册顺序逆序执行。
|
||||
4. afterCompletion():视图渲染后 按注册顺序逆序执行。
|
||||
|
||||
## 4.3 请求和响应体拦截器
|
||||
  RequestBodyAdvice, ResponseBodyAdvice主要发生在Controller执行前后:
|
||||
1. preHandle():请求处理前
|
||||
2. beforeBodyRead:请求体反序列化前
|
||||
3. @RequestBody:Controller方法参数绑定
|
||||
4. Controller:请求处理
|
||||
5. beforeBodyWrite():响应体序列化之前
|
||||
6. postHandle():请求处理后
|
||||
7. 视图渲染
|
||||
8. afterCompletion():请求结束
|
||||
|
||||
## 4.4 @Controller 和 @RestController区别
|
||||
  @Controller:用于定义一个 Spring MVC 控制器类。它通常用于返回视图(如 JSP 或 Thymeleaf)。
|
||||
  @RestController:是 @Controller 和 @ResponseBody 的组合,用于开发 RESTful API。它将方法的返回值直接写入 HTTP 响应体中,通常返回 JSON 或 XML 格式的数据。
|
||||
|
||||
## 4.5 异常处理
|
||||
  @ExceptionHandler:用于控制器类或方法中处理特定异常。它会捕获指定的异常并返回相应的响应(如错误页面、错误消息等)。
|
||||
  @ControllerAdvice:是一个全局异常处理器,用于捕获应用中所有控制器抛出的异常。
|
||||
367
docs/Web/Vue/Vue3-Architecture.md
Normal file
367
docs/Web/Vue/Vue3-Architecture.md
Normal file
@@ -0,0 +1,367 @@
|
||||
---
|
||||
title: Vue3架构原理
|
||||
date: 2025-12-15 23:25
|
||||
---
|
||||
|
||||
# 一、Vue3 整体架构图
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Vue 3] --> B[Compiler]
|
||||
A --> C[Runtime]
|
||||
A --> D[Reactivity]
|
||||
|
||||
B --> B1[Parser]
|
||||
B --> B2[Transformer]
|
||||
B --> B3[Codegen]
|
||||
|
||||
C --> C1[Virtual DOM]
|
||||
C --> C2[Renderer]
|
||||
C --> C3[Component]
|
||||
|
||||
D --> D1[Proxy]
|
||||
D --> D2[Effect]
|
||||
D --> D3[Dependency Graph]
|
||||
|
||||
B3 -->|生成| C1
|
||||
C1 -->|Diff/Patch| C2
|
||||
D3 -->|驱动更新| C1
|
||||
```
|
||||
|
||||
## 1.1 编译器模块
|
||||
  编译器负责将模板转换为可执行的渲染函数,包含**解析器(Parser)、转换器(Transformer)和代码生成器(Codegen)** 三个子模块。
|
||||
|
||||
- compiler-core: 核心编译器,负责基础的编译功能,如生成抽象语法树(AST)、基础的模板编译等,是平台无关的编译器核心。
|
||||
- compiler-dom: 基于compiler-core,针对浏览器环境的编译模块,重写了编译和解析方法,处理与原生 DOM API 相关的编译工作。
|
||||
- compiler-sfc: 用于处理 Vue 单文件组件(.vue)的编译,将单文件组件中的模板、脚本和样式进行解析和编译。
|
||||
- compiler-ssr:服务端渲染的编译模块,负责在服务端将 Vue 组件编译为适合服务器端渲染的代码。
|
||||
|
||||
## 1.2 运行时模块
|
||||
  运行时系统处理**虚拟DOM的创建、更新和渲染**,包含组件实例管理、虚拟DOM diff算法和平台渲染器。
|
||||
|
||||
- runtime-core:运行时核心模块,包含了 Vue 组件的生命周期、虚拟节点(vnode)、观察者(watch)等核心 API,是平台无关的运行时核心。
|
||||
- runtime-dom:运行时与 DOM 相关的核心模块,提供了与浏览器 DOM 交互的功能,如创建应用实例、操作 DOM 元素等。
|
||||
|
||||
## 1.3 响应式系统
|
||||
  响应式系统基于Proxy实现数据劫持,通过依赖收集和触发更新机制驱动视图变化。
|
||||
  三大系统协同工作:编译器生成的渲染函数由运行时执行,响应式系统追踪数据变化触发组件更新,形成完整的渲染闭环。
|
||||
|
||||
# 二、基础模块
|
||||
## 2.1 响应式系统
|
||||
```mermaid
|
||||
graph LR
|
||||
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
|
||||
|
||||
A(原始对象):::process -->|Proxy 包装| B(响应式对象):::process
|
||||
B -->|属性读取| C(依赖收集):::process
|
||||
B -->|属性修改| D(触发更新):::process
|
||||
C --> E(副作用函数):::process
|
||||
D --> E
|
||||
E -->|更新视图| F(真实 DOM):::process
|
||||
```
|
||||
|
||||
  Vue3的响应式系统通过Proxy代理对象属性,在get操作时通过track函数收集当前正在执行的副作用(如组件渲染函数)作为依赖,存储到依赖关系图中。当set操作发生时,trigger函数根据依赖关系图找到所有关联的副作用函数,通过调度器批量执行。
|
||||
  这种设计使得数据变化能精准触发组件更新,同时通过异步批处理避免重复渲染。
|
||||
  响应式系统独立于其他模块,可单独使用,为Vue3提供了高效的状态管理能力。
|
||||
|
||||
## 2.2 虚拟 DOM
|
||||
```mermaid
|
||||
graph LR
|
||||
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
|
||||
|
||||
A(旧虚拟 DOM 树):::process -->|Diff 算法| B(差异比较):::process
|
||||
C(新虚拟 DOM 树):::process --> B
|
||||
B -->|更新操作| D(真实 DOM):::process
|
||||
```
|
||||
|
||||
  当组件更新时,Vue会比较新旧虚拟DOM树的差异。通过Patch Flags快速识别动态节点类型(如文本、样式等),仅对标记为动态的部分进行比较;对于带key的子节点列表,使用最长递增子序列算法最小化DOM操作;完全静态的子树则直接跳过diff过程。
|
||||
  这种靶向更新策略将传统虚拟DOM算法O(n³)的时间复杂度优化到接近O(n),同时配合异步批量更新机制,确保复杂应用也能保持流畅渲染
|
||||
|
||||
## 2.3 编译器
|
||||
```mermaid
|
||||
graph LR
|
||||
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
|
||||
|
||||
A(模板字符串):::process -->|解析| B(抽象语法树 - AST):::process
|
||||
B -->|转换| C(优化后的 AST):::process
|
||||
C -->|生成| D(渲染函数代码):::process
|
||||
```
|
||||
|
||||
  Vue编译器将模板字符串转换为渲染函数,经历解析、转换和代码生成三个阶段。
|
||||
  解析阶段通过词法分析生成原始AST抽象语法树;转换阶段对AST进行优化,包括静态节点提升(将不变节点提取到渲染函数外部)、补丁标记(给动态节点添加优化标识)、树结构打平(减少动态子节点的嵌套层级);最后代码生成阶段输出优化的渲染函数。
|
||||
  这些优化使得运行时能跳过静态内容比较,直接定位动态节点,大幅提升渲染性能。
|
||||
|
||||
## 2.4 渲染器
|
||||
```mermaid
|
||||
graph LR
|
||||
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
|
||||
|
||||
A(虚拟 DOM 树):::process -->|创建节点| B(真实 DOM 节点):::process
|
||||
B -->|插入节点| C(页面):::process
|
||||
A -->|更新节点| B
|
||||
```
|
||||
|
||||
  Vue渲染器是连接虚拟DOM与真实平台的桥梁,分为虚拟DOM处理层、Patch算法层和平台操作层。
|
||||
  它通过抽象平台API(如createElement、insert等)实现跨平台能力,开发者可配置不同平台的底层操作来支持Web、Canvas甚至原生应用。
|
||||
  渲染器内部采用增量更新策略,结合编译器提供的优化提示(如静态标记、动态子节点索引),确保DOM操作最精简。
|
||||
  这种设计使Vue在保证Web端高性能的同时,也能灵活适配各种渲染环境。
|
||||
|
||||
# 三、Vue3应用生命周期
|
||||
## 3.1 初始化阶段
|
||||
  流程:
|
||||
1. 调用`createApp()`创建应用实例
|
||||
2. 创建平台专属渲染器(DOM/SSR/Test)
|
||||
3. 初始化应用上下文和插件系统
|
||||
|
||||
  关键代码:
|
||||
```typescript
|
||||
// runtime-dom/index.ts
|
||||
const renderer = createRenderer({
|
||||
patchProp, // DOM属性操作
|
||||
...nodeOps // DOM节点操作集合
|
||||
})
|
||||
|
||||
function createApp(rootComponent) {
|
||||
const context = createAppContext()
|
||||
const app = {
|
||||
_component: rootComponent,
|
||||
mount(container) {
|
||||
const vnode = createVNode(rootComponent)
|
||||
renderer.render(vnode, container)
|
||||
return vnode.component.proxy
|
||||
}
|
||||
}
|
||||
return app
|
||||
}
|
||||
```
|
||||
|
||||
## 3.2 组件挂载阶段
|
||||
  流程:
|
||||
1. 标准化容器(字符串选择器 → DOM元素)
|
||||
2. 创建根组件虚拟节点(vnode)
|
||||
3. 初始化组件:解析props和slots、执行setup函数、建立响应式连接等
|
||||
4. 建立渲染effect,为组件创建响应式的副作用函数
|
||||
|
||||
  关键代码:
|
||||
```typescript
|
||||
// runtime-core/renderer.ts
|
||||
function mountComponent(vnode, container) {
|
||||
// 1. 创建组件实例
|
||||
const instance = createComponentInstance(vnode)
|
||||
|
||||
// 2. 初始化组件
|
||||
setupComponent(instance)
|
||||
|
||||
// 3. 建立渲染effect
|
||||
setupRenderEffect(instance, container)
|
||||
}
|
||||
|
||||
function setupRenderEffect(instance, container) {
|
||||
instance.update = effect(() => {
|
||||
if (!instance.isMounted) {
|
||||
// 首次渲染
|
||||
const subTree = (instance.subTree = renderComponentRoot(instance))
|
||||
patch(null, subTree, container)
|
||||
instance.isMounted = true
|
||||
} else {
|
||||
// 更新渲染
|
||||
const nextTree = renderComponentRoot(instance)
|
||||
patch(instance.subTree, nextTree, container)
|
||||
instance.subTree = nextTree
|
||||
}
|
||||
}, { scheduler: queueJob })
|
||||
}
|
||||
```
|
||||
|
||||
## 3.3 响应式更新阶段
|
||||
  流程:
|
||||
1. 数据变更触发Proxy的set拦截
|
||||
2. 通过trigger触发依赖的effect
|
||||
3. 调度器将更新任务加入队列
|
||||
4. 下一个tick执行队列中的任务
|
||||
5. 执行组件render函数生成新vnode
|
||||
6. 调用patch进行差异更新
|
||||
|
||||
  关键代码:
|
||||
```typescript
|
||||
// reactivity/reactive.ts
|
||||
function createReactiveObject(target) {
|
||||
return new Proxy(target, {
|
||||
set(target, key, value, receiver) {
|
||||
const oldValue = target[key]
|
||||
const result = Reflect.set(target, key, value, receiver)
|
||||
if (hasChanged(value, oldValue)) {
|
||||
trigger(target, key) // 触发更新
|
||||
}
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// runtime-core/scheduler.ts
|
||||
const queue = []
|
||||
function queueJob(job) {
|
||||
if (!queue.includes(job)) {
|
||||
queue.push(job)
|
||||
queueFlush()
|
||||
}
|
||||
}
|
||||
|
||||
function queueFlush() {
|
||||
if (!isFlushing) {
|
||||
nextTick(flushJobs)
|
||||
}
|
||||
}
|
||||
|
||||
function flushJobs() {
|
||||
isFlushing = true
|
||||
queue.sort((a, b) => a.id - b.id) // 保证父组件先更新
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
queue[i]()
|
||||
}
|
||||
queue.length = 0
|
||||
isFlushing = false
|
||||
}
|
||||
```
|
||||
|
||||
## 3.4 虚拟DOM Patch阶段
|
||||
  流程:
|
||||
1. 比较新旧vnode的类型
|
||||
2. 不同类型:卸载旧节点,挂载新节点
|
||||
3. 相同类型:更新节点
|
||||
4. 子节点对比采用高效diff算法
|
||||
|
||||
  关键代码:
|
||||
```typescript
|
||||
// runtime-core/renderer.ts
|
||||
function patch(n1, n2, container) {
|
||||
// 1. 类型不同直接卸载
|
||||
if (n1 && !isSameVNodeType(n1, n2)) {
|
||||
unmount(n1)
|
||||
n1 = null
|
||||
}
|
||||
|
||||
const { type, patchFlag } = n2
|
||||
|
||||
// 2. 根据类型处理
|
||||
switch (type) {
|
||||
case Text:
|
||||
processText(n1, n2, container)
|
||||
break
|
||||
case Fragment:
|
||||
processFragment(n1, n2, container)
|
||||
break
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
processElement(n1, n2, container)
|
||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||
processComponent(n1, n2, container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processElement(n1, n2, container) {
|
||||
if (n1 == null) {
|
||||
mountElement(n2, container)
|
||||
} else {
|
||||
patchElement(n1, n2)
|
||||
}
|
||||
}
|
||||
|
||||
function patchElement(n1, n2) {
|
||||
// 根据patchFlag进行优化更新
|
||||
if (n2.patchFlag & PatchFlags.FULL_PROPS) {
|
||||
// 全量props更新
|
||||
} else {
|
||||
// 只更新动态props
|
||||
}
|
||||
|
||||
// 更新子节点
|
||||
patchChildren(n1, n2)
|
||||
}
|
||||
```
|
||||
|
||||
## 3.5 组件卸载阶段
|
||||
  流程:
|
||||
1. 触发beforeUnmount生命周期钩子
|
||||
2. 停止组件渲染effect
|
||||
3. 递归卸载子组件
|
||||
4. 移除DOM节点
|
||||
5. 触发unmounted生命周期钩子
|
||||
6. 清理响应式依赖
|
||||
|
||||
  关键代码:
|
||||
```typescript
|
||||
// runtime-core/renderer.ts
|
||||
function unmount(vnode) {
|
||||
if (vnode.type === Fragment) {
|
||||
unmountChildren(vnode.children)
|
||||
return
|
||||
}
|
||||
|
||||
const { component } = vnode
|
||||
if (component) {
|
||||
unmountComponent(component)
|
||||
} else {
|
||||
remove(vnode.el!)
|
||||
}
|
||||
}
|
||||
|
||||
function unmountComponent(instance) {
|
||||
// 1. 触发beforeUnmount
|
||||
if (instance.bum) {
|
||||
invokeArrayFns(instance.bum)
|
||||
}
|
||||
|
||||
// 2. 停止响应式effect
|
||||
stop(instance.update)
|
||||
|
||||
// 3. 递归卸载子树
|
||||
unmount(instance.subTree)
|
||||
|
||||
// 4. 触发unmounted
|
||||
if (instance.um) {
|
||||
queuePostRenderEffect(instance.um)
|
||||
}
|
||||
|
||||
// 5. 清理引用
|
||||
instance.isUnmounted = true
|
||||
}
|
||||
```
|
||||
|
||||
## 3.6 编译阶段
|
||||
  流程:
|
||||
1. 将模板解析为AST
|
||||
2. 转换AST(静态提升/标记PatchFlag)
|
||||
3. 生成可执行的渲染函数代码
|
||||
4. 运行时直接使用优化后的渲染函数
|
||||
|
||||
  关键代码:
|
||||
```typescript
|
||||
// compiler-core/src/compile.ts
|
||||
function baseCompile(template, options) {
|
||||
// 1. 解析阶段
|
||||
const ast = parse(template)
|
||||
|
||||
// 2. 转换阶段
|
||||
transform(ast, {
|
||||
hoistStatic: true, // 静态提升
|
||||
nodeTransforms: [
|
||||
transformIf,
|
||||
transformFor,
|
||||
transformExpression
|
||||
]
|
||||
})
|
||||
|
||||
// 3. 代码生成
|
||||
const code = generate(ast, {
|
||||
mode: 'function',
|
||||
runtimeGlobalName: 'Vue',
|
||||
prefixIdentifiers: true
|
||||
})
|
||||
|
||||
return {
|
||||
ast,
|
||||
code: `with(this){return ${code}}` // 生成渲染函数
|
||||
}
|
||||
}
|
||||
```
|
||||
419
docs/Web/Vue/Vue3-Common.md
Normal file
419
docs/Web/Vue/Vue3-Common.md
Normal file
@@ -0,0 +1,419 @@
|
||||
---
|
||||
title: Vue3-Common
|
||||
date: 2025-12-15 23:25
|
||||
---
|
||||
|
||||
# 一、依赖管理
|
||||
  本项目会传递安装的依赖有:
|
||||
|
||||
| 包名称 | 版本 | 含义和用途说明 |
|
||||
|-------|------|---------------|
|
||||
| `@fortawesome/fontawesome-free` | ^6.7.2 | FontAwesome 图标库的免费版本 |
|
||||
| `@popperjs/core` | ^2.11.8 | 工具提示和弹出框定位引擎 |
|
||||
| `axios` | ^1.4.0 | 基于 Promise 的 HTTP 客户端 |
|
||||
| `compressorjs` | ^1.2.1 | 纯 JavaScript 图片压缩库 |
|
||||
| `crypto-js` | ^4.2.0 | JavaScript 加密算法库 |
|
||||
| `dayjs` | ^1.11.13 | 轻量级的日期处理库 |
|
||||
| `echarts` | ^5.5.1 | 百度开源的数据可视化图表库 |
|
||||
| `element-plus` | ^2.6.0 | 基于 Vue 3 的桌面端 UI 组件库 |
|
||||
| `js-cookie` | ^3.0.5 | JavaScript Cookie 操作库 |
|
||||
| `lunar-calendar` | ^0.1.4 | 农历日历转换库 |
|
||||
| `lunar-javascript` | ^1.6.13 | 农历日期处理的 JavaScript 库 |
|
||||
| `path-browserify` | ^1.0.1 | Node.js path 模块的浏览器版本兼容实现 |
|
||||
| `qs` | ^6.13.0 | URL 查询字符串解析和序列化库 |
|
||||
| `v-calendar` | ^3.1.2 | Vue.js 的日历和日期选择器组件 |
|
||||
|
||||
::: tip
|
||||
如果使用pnpm安装,会传递peerDependencies部分。
|
||||
:::
|
||||
|
||||
::: warning
|
||||
建议将Vite、Typescript、@types等构建工具依赖放在devDependencies中。
|
||||
:::
|
||||
|
||||
# 二、vite.config.ts配置
|
||||
## 2.1 生成Typescript类型文件
|
||||
  安装vite-plugin-dts插件
|
||||
|
||||
```cmd
|
||||
pnpm add vite-plugin-dts -D
|
||||
```
|
||||
|
||||
```typescript
|
||||
import dts from 'vite-plugin-dts'
|
||||
|
||||
// 打包输出文件夹
|
||||
const outDirPath = 'dist'
|
||||
// 需要打包的类型文件
|
||||
const TARGET_TYPE_FOLDERS = ['src/components', 'src/utils', 'src/types', 'src/vue3-common.ts']
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
// 生成类型文件
|
||||
dts({
|
||||
// 需要处理的文件
|
||||
include: TARGET_TYPE_FOLDERS,
|
||||
// 使用特定的 tsconfig 配置
|
||||
tsconfigPath: path.resolve(__dirname, 'tsconfig.app.json'),
|
||||
// 类型文件输出目录
|
||||
outDir: path.resolve(__dirname, outDirPath)
|
||||
})
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
  打包后会在dist目录生成.d.ts文件,提供完整的Typescript类型支持。
|
||||
|
||||
## 2.2 build打包配置
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import dts from 'vite-plugin-dts'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { ICommonObj } from './src/types'
|
||||
|
||||
// 打包输出文件夹
|
||||
const outDirPath = 'dist'
|
||||
// 需要单个打包的文件夹(组件和工具类)
|
||||
const TARGET_LIB_FOLDERS = ['src/components', 'src/utils']
|
||||
// 需要排除的第三方依赖
|
||||
const EXTERNAL = ['vue', 'element-plus', 'echarts', 'axios', 'moment', 'crypto-js', 'spark-md5', 'path-browserify']
|
||||
|
||||
/**
|
||||
* 递归读取文件夹下的所有文件
|
||||
* @param folderPath 文件夹
|
||||
*/
|
||||
const getFilesFromFolder = (folderPath: string) => {
|
||||
const files: string[] = []
|
||||
|
||||
// 读取文件夹中的内容
|
||||
fs.readdirSync(folderPath).forEach((item: string) => {
|
||||
const fullPath = path.join(folderPath, item)
|
||||
// 读取文件状态 如果是文件夹,递归读取
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
files.push(...getFilesFromFolder(fullPath))
|
||||
} else {
|
||||
// 否则添加至文件列表
|
||||
files.push(fullPath)
|
||||
}
|
||||
})
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// 动态生成入口文件
|
||||
const inputEntries = TARGET_LIB_FOLDERS.reduce((entries: ICommonObj, folder) => {
|
||||
// 获取文件夹内的所有文件
|
||||
const files = getFilesFromFolder(path.resolve(__dirname, folder))
|
||||
|
||||
files.forEach((filePath) => {
|
||||
// 判断文件是否是 .vue 或 .ts 文件
|
||||
if (filePath.endsWith('.vue') || filePath.endsWith('.ts')) {
|
||||
// 获取相对路径并保持目录结构
|
||||
const relativePath = path.relative('src', filePath)
|
||||
const entryName = path.join('lib', relativePath.replace(/\.(vue|ts)$/, ''))
|
||||
|
||||
entries[entryName] = filePath // 为每个文件创建一个入口
|
||||
}
|
||||
})
|
||||
|
||||
return entries
|
||||
}, {})
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
// lib文件配置
|
||||
lib: {
|
||||
// 入口文件
|
||||
entry: path.resolve(__dirname, 'src/vue3-common.ts'),
|
||||
formats: ['es'],
|
||||
name: 'vue3-common',
|
||||
// 文件名
|
||||
fileName: (format) => `vue3-common.${format}.js`
|
||||
},
|
||||
// 输出文件路径
|
||||
outDir: outDirPath,
|
||||
// 是否将css文件分割
|
||||
cssCodeSplit: true,
|
||||
// Rollup打包配置
|
||||
rollupOptions: {
|
||||
// 需要排除的依赖 通常为第三方库
|
||||
external: EXTERNAL,
|
||||
// 输入配置
|
||||
input: {
|
||||
// 入口文件
|
||||
'vue3-common': path.resolve(__dirname, 'src/vue3-common.ts'),
|
||||
// 其余需要单独打包的文件
|
||||
...inputEntries
|
||||
},
|
||||
// 输出配置
|
||||
output: {
|
||||
dir: path.resolve(__dirname, outDirPath),
|
||||
// 入口文件
|
||||
entryFileNames: '[name].js',
|
||||
// chunk文件
|
||||
chunkFileNames: 'lib/[name].js',
|
||||
// 资源文件
|
||||
assetFileNames: 'styles/[name].[ext]'
|
||||
}
|
||||
},
|
||||
// 是否压缩代码
|
||||
minify: false
|
||||
}
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
  打包后会在dist目录生成源码文件。
|
||||
|
||||
# 三、package.json配置
|
||||
## 3.1 导出路径
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/vue3-common.js",
|
||||
"require": "./dist/vue3-common.js",
|
||||
"types": "./dist/vue3-common.d.ts"
|
||||
},
|
||||
"./components/SvgIcon.vue": {
|
||||
"import": "./dist/lib/components/SvgIcon.js",
|
||||
"types": "./dist/components/SvgIcon.vue.d.ts"
|
||||
},
|
||||
"./styles/SvgIcon.css": "./dist/styles/SvgIcon.css",
|
||||
"./components/LoginForm.vue": {
|
||||
"import": "./dist/lib/components/LoginForm.js",
|
||||
"types": "./dist/components/LoginForm.vue.d.ts"
|
||||
},
|
||||
"./styles/LoginForm.css": "./dist/styles/LoginForm.css",
|
||||
"./components/MenuItem.vue": {
|
||||
"import": "./dist/lib/components/MenuItem.js",
|
||||
"types": "./dist/components/MenuItem.vue.d.ts"
|
||||
},
|
||||
"./components/MenuList.vue": {
|
||||
"import": "./dist/lib/components/MenuList.js",
|
||||
"types": "./dist/components/MenuList.vue.d.ts"
|
||||
},
|
||||
"./components/Hamburger.vue": {
|
||||
"import": "./dist/lib/components/Hamburger.js",
|
||||
"types": "./dist/components/Hamburger.vue.d.ts"
|
||||
},
|
||||
"./styles/Hamburger.css": "./dist/styles/Hamburger.css",
|
||||
"./components/MultiInput.vue": {
|
||||
"import": "./dist/lib/components/MultiInput.js",
|
||||
"types": "./dist/components/MultiInput.vue.d.ts"
|
||||
},
|
||||
"./styles/MultiInput.css": "./dist/styles/MultiInput.css",
|
||||
"./types": {
|
||||
"types": "./dist/types/index.d.ts"
|
||||
},
|
||||
"./utils/axiosUtil": {
|
||||
"import": "./dist/lib/utils/axiosUtil.js",
|
||||
"types": "./dist/types/utils/axiosUtil.d.ts"
|
||||
},
|
||||
"./utils/cryptoUtil": {
|
||||
"import": "./dist/lib/utils/cryptoUtil.js",
|
||||
"types": "./dist/types/utils/cryptoUtil.d.ts"
|
||||
},
|
||||
"./utils/dataUtil": {
|
||||
"import": "./dist/lib/utils/dataUtil.js",
|
||||
"types": "./dist/types/utils/dataUtil.d.ts"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  在项目中引入common包后,通过配置tsconfig.json即可实现按需引入功能`import { fun1 } from 'vue3-common/utils/index'`。
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"vue3-common/*": ["node_modules/vue3-common/dist/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 四、发布框架
|
||||
1. 发布到本地
|
||||
执行`npm pack`命令,会在项目文件夹下生成`.tgz`文件,其他项目通过文件路径形式引入:
|
||||
```json
|
||||
"vue3-common": "file:../vue3-common/vue3-common-1.0.0.tgz"
|
||||
```
|
||||
|
||||
2. 发布到git
|
||||
将dist文件夹发布到git仓库,其他项目通过git形式引入:
|
||||
```json
|
||||
"vue3-common": "git+https://gitee.com/Cxx0822/vue3-common#master"
|
||||
```
|
||||
|
||||
# 五、组件使用说明
|
||||
## 5.1 菜单组件
|
||||
1. 安装Vue Router
|
||||
```cmd
|
||||
pnpm install vue-router
|
||||
```
|
||||
|
||||
2. 在src/views文件夹中新建vue文件,例如:
|
||||
```cmd
|
||||
views
|
||||
viewA
|
||||
index.vue
|
||||
viewB
|
||||
index.vue
|
||||
```
|
||||
|
||||
3. 在src/views文件夹下新建meta.ts配置文件
|
||||
```typescript
|
||||
import type { IRouteMetaConfig } from 'vue3-common/types'
|
||||
|
||||
export const metaList: IRouteMetaConfig = {
|
||||
'/viewA': {
|
||||
title: 'viewA',
|
||||
icon: 'viewA',
|
||||
order: 1,
|
||||
redirect: '/viewA/index'
|
||||
},
|
||||
'/viewA/index': {
|
||||
title: 'viewA-Index',
|
||||
icon: ''
|
||||
},
|
||||
'/viewB': {
|
||||
title: 'viewB',
|
||||
icon: 'viewB',
|
||||
order: 2,
|
||||
redirect: '/viewB/index'
|
||||
},
|
||||
'/viewB/index': {
|
||||
title: 'viewB-index',
|
||||
icon: ''
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
  order对应的显示顺序关系,icon对应的为src/icons/svg中的svg图标文件(参考下文图标组件)。
|
||||
|
||||
4. 在src目录新建router文件夹,新建menu.ts文件
|
||||
```typescript
|
||||
import { getRoutersByModules, sortRoutesByOrder } from 'vue3-common/utils/routerUtil'
|
||||
import Layout from '@/layout/index.vue'
|
||||
import { metaList } from '@/views/meta'
|
||||
|
||||
const modules = {
|
||||
...import.meta.glob('@/views/viewA/**/*.vue'),
|
||||
...import.meta.glob('@/views/viewB/**/*.vue'),
|
||||
}
|
||||
|
||||
const menuRoutes = getRoutersByModules(modules, Layout, metaList)
|
||||
|
||||
export default sortRoutesByOrder(menuRoutes)
|
||||
```
|
||||
|
||||
  导入刚才的views文件夹并生成路由菜单。
|
||||
|
||||
5. 如果还有其他的常量路由,可以在src/router中新建constant.ts文件:
|
||||
```typescript
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/login'
|
||||
},
|
||||
// 主页
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { hidden: true }
|
||||
}
|
||||
]
|
||||
|
||||
export default constantRoutes
|
||||
```
|
||||
|
||||
  该部分即Vue Router中的路由定义。
|
||||
|
||||
6. 在src/router中新建index.ts:
|
||||
```typescript
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import { setupRouteGuard } from 'vue3-common/utils/permissionUtil'
|
||||
|
||||
// 使用 import.meta.glob 自动导入所有 src/router 目录下的 .ts 文件
|
||||
const routeModules = import.meta.glob('./*.ts', { eager: true })
|
||||
|
||||
// 将所有模块的默认导出(即路由配置)合并成一个路由数组
|
||||
const routes: RouteRecordRaw[] = Object.values(routeModules)
|
||||
.map((module: any) => module.default) // 获取每个模块的默认导出
|
||||
.flat() // 扁平化数组,确保所有路由项都在一个数组中
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
scrollBehavior: () => ({ top: 0 }),
|
||||
routes
|
||||
})
|
||||
|
||||
// 设置路由守卫
|
||||
setupRouteGuard(router)
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
  遍历src/router文件夹下所有的路由文件,并添加路由守卫。
|
||||
|
||||
7. 在main.ts中配置路由:
|
||||
```ts
|
||||
// 引入路由
|
||||
import router from './router'
|
||||
|
||||
// 创建Vue3实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 使用路由
|
||||
app.use(router)
|
||||
```
|
||||
|
||||
## 5.2 图标组件
|
||||
1. 安装vite-plugin-svg-icons依赖
|
||||
```cmd
|
||||
pnpm install vite-plugin-svg-icons -D
|
||||
```
|
||||
|
||||
2. 配置vite.config.ts
|
||||
```typescript
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
createSvgIconsPlugin({
|
||||
// 指定 SVG图标 保存的文件夹路径
|
||||
iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
|
||||
// 指定 使用svg图标的格式
|
||||
symbolId: 'icon-[dir]-[name]'
|
||||
})
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
3. 在main.ts中注册
|
||||
```typescript
|
||||
// 注册svg-icon
|
||||
import 'virtual:svg-icons-register'
|
||||
```
|
||||
|
||||
4. 在src/icons/svg目录中添加svg图标
|
||||
5. 在组件中使用
|
||||
```vue
|
||||
<template>
|
||||
<svg-icon name="user"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
</script>
|
||||
```
|
||||
BIN
docs/Web/images/promise基础.png
Normal file
BIN
docs/Web/images/promise基础.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
docs/Web/images/普通函数作用域链.png
Normal file
BIN
docs/Web/images/普通函数作用域链.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/Web/images/理解原型.png
Normal file
BIN
docs/Web/images/理解原型.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/Web/images/闭包函数作用域链.png
Normal file
BIN
docs/Web/images/闭包函数作用域链.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
11
docs/Web/index.md
Normal file
11
docs/Web/index.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
layout: doc
|
||||
title: Web平台
|
||||
description: Web平台开发技术文档
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { routers } from '../.vitepress/theme/router'
|
||||
</script>
|
||||
|
||||
<MenuList :routers=routers[0] :isNested=true />
|
||||
Reference in New Issue
Block a user