读后笔记 -- FastAPI 构建Python微服务 Chapter4:构建微服务应用程序
4.3 挂载子模块
# content of main.py from fastapi import FastAPI from faculty_mgt import faculty_main from library_mgt import library_main from student_mgt import student_main app = FastAPI() app.mount("/ch04/student", student_main.student_app) app.mount("/ch04/faculty", faculty_main.faculty_app) app.mount("/ch04/library", library_main.library_app)
项目结构如下:
4.4 ~ 4.7 主端点+通用网关+利用异常跳转自服务
1. step1+step4: 在顶级应用程序中处理网关端点
# content of main.py from fastapi import FastAPI # step1: 顶级应用程序处理处理网关端点 app = FastAPI() # 通过方法 /ch04/university/{portal_id},指向通用的网关 app.include_router(university.router, dependencies=[Depends(call_api_gateway)], prefix="/ch04") # step3: 自定义异常处理,重定向到各微服务 @app.exception_handler(RedirectStudentPortalException) def exception_handler_student(request: Request, exc: RedirectStudentPortalException) -> Response: return RedirectResponse(url=f"http://localhost:{port}/ch04/student/index") @app.exception_handler(RedirectFacultyPortalException) def exception_handler_faculty(request: Request, exc: RedirectFacultyPortalException) -> Response: return RedirectResponse(url=f"http://localhost:{port}/ch04/faculty/docs") @app.exception_handler(RedirectLibraryPortalException) def exception_handler_library(request: Request, exc: RedirectLibraryPortalException) -> Response: return RedirectResponse(url=f"http://localhost:{port}/ch04/library/index")
2. step2: 实现主端点
# content of \controller\university.py from fastapi import APIRouter router = APIRouter() @router.get("/university/{portal_id}") def access_portal(portal_id: int): return {"message": "University ERP Systems"}
3. step3: 定义通用网关
# content of \gateway\api_router.py from fastapi import Request def call_api_gateway(request: Request): portal_id = request.path_params['portal_id'] logger.info(request.path_params) if portal_id == str(1): raise RedirectStudentPortalException() elif portal_id == str(2): raise RedirectFacultyPortalException() elif portal_id == str(3): raise RedirectLibraryPortalException() class RedirectStudentPortalException(Exception): pass class RedirectFacultyPortalException(Exception): pass class RedirectLibraryPortalException(Exception): pass
最终结果:
API 访问 http://localhost:8005/ch04/university/3
1)通过 app.include_router 路由 指向 university.py,
2)根据依赖, gateway.api_router.call_api_gateway,根据后面的 portal_id 指向不同的 Exception
3)main.py 中自定义异常处理,将各异常重定向到 各子服务上 (通过 url 上重定向 跳转到 http://localhost:8005/ch04/library/index)
4.8-4.9 使用 loguru,构建日志中间件
# content of main.py from uuid import uuid4 from loguru import logger # 使用 loguru 适合来处理 FastAPI 这种异步和同步都存在的架构,而 Python 自带的 logging 处理会出现异常 # 码配置了一个日志处理器,它会将级别为INFO及以上的日志记录,按照指定的格式,异步地写入到名为info.log的文件中,同时包含额外的上下文信息(如log_id),以增强日志的可追踪性和信息丰富度 logger.add("info.log", format="Log: [{extra[log_id]}: {time} - {level} - {message} ", level="INFO", enqueue=True) # 构建日志中间件 @app.middleware("http") async def log_middleware(request: Request, call_next): log_id = str(uuid4()) with logger.contextualize(log_id=log_id): logger.info("Request to access " + request.url.path) try: response = await call_next(request) except Exception as ex: logger.error(f"Request to {request.url.path } failed: {ex}") response = JSONResponse(content={"success": False}, status_code=500) finally: logger.info("Successfully accessed " + request.url.path) return response
4.10 实现 httpx 模块 实现微服务之间的跳转
1. 子服务的一端(可视为客户端)
# content of \student_mgmg\controllers\assignment.py import httpx from fastapi import APIRouter router = APIRouter() """ url 访问 http://127.0.0.1:8005/ch04/student/docs swagger, 1)运行接口 GET "/assignment/list" 2)通过 httpx.Client() GET 请求 http://localhost:8005/ch04/faculty/assignments/list 3)重定向到 faculty_mgmt.controllers.assignments.py 的 /assignments/list 即 /ch04/faculty/assignments/list 4) 剩下的就一路跳转,最终找到 faculty_mgmt.models.data.facultydb 的 faculty_assignments_tbl """ @router.get("/assignment/list") async def list_assignments(): async with httpx.AsyncClient() as client: response = await client.get(f"http://localhost:{port}/ch04/faculty/assignments/list") return response.json()
2. 子服务的另一端(视为服务端)
# content of faculty_mgmt.controllers.assignments.py from fastapi import APIRouter from faculty_mgt.services.assignments import AssignmentService router = APIRouter() @router.get("/assignments/list") async def provide_assignments(): assignment_service: AssignmentService = AssignmentService() return assignment_service.list_assignment()
4.11 实现 requests 模块实现子服务间的通信
1. 子服务的一端(可视为客户端)
# content of faculty_mgmt.controllers.book.py import requests from fastapi import APIRouter from constant import port router = APIRouter() """ url 访问 http://127.0.0.1:8005/ch04/faculty/docs swagger, 1)运行接口 GET "/books/access/list" 2)将在 session 中 GET 请求 http://localhost:8005/ch04/library/book/list 3)重定向到 library_mgmt.controllers.admin.py 的 /book/list 即 /ch04/library/book/list 4) 剩下的就一路跳转,最终找到 library_mgmt.models.data.librarydb 的 book_tbl """ @router.get("/books/access/list") def list_all_books(): with requests.Session() as sess: response = sess.get(f"http://localhost:{port}/ch04/library/book/list") return response.json()
2. 子服务的另一端(视为服务端)
# content of library_mgmt.controllers.admin.py from fastapi import APIRouter from library_mgt.services.books import BookService router = APIRouter() # view books @router.get("/book/list") def view_books(): book_service = BookService() return book_service.list_book()
4.12 应用领域建模方法
1. 领域建模:当设计在应用程序中建立和组织结构时,了解应用程序的行为并使用其背后的业务逻辑来派生领域模型。
下面是来自于 Chatgpt 的更详细的介绍:
领域建模在设计应用程序时扮演着至关重要的角色,它不仅帮助我们理解应用程序的行为,还指导我们如何根据业务逻辑有效地组织和结构化代码。以下是通过业务逻辑派生领域模型时的一些关键步骤和实践: 1. 需求调研与领域知识获取 访谈与工作坊:与业务专家、最终用户进行深入交流,了解业务流程、规则、术语以及痛点。 文档审查:查阅现有业务文档、流程图、政策手册,提取关键信息。 2. 领域分解与识别 识别子域:将复杂业务领域拆分成若干个小的、可管理的子域,每个子域聚焦特定的业务功能或领域知识。 核心域识别:确定哪些子域对业务竞争力至关重要,这些核心域将得到最多的关注和投资。 3. 领域概念提炼 实体(Entity):识别并定义领域中的关键对象,它们通常是持久化的,并具有唯一标识。 值对象(Value Object):描述实体的属性或描述,它们没有独立的身份,仅通过其属性来区分。 聚合(Aggregate):定义一组相关实体和值对象,作为数据修改的一致性单元。 领域事件(Domain Event):捕捉领域内发生的重大事件,触发其他领域的反应或改变领域状态。 4. 行为建模 领域服务(Domain Service):封装不属于任何特定实体或值对象的业务逻辑。 领域规则(Domain Rules):明确领域内的业务约束和规则,确保业务逻辑的正确性。 用例分析:通过用例描述系统如何响应外部事件,映射到领域模型中的行为。 5. 模型验证与迭代 原型与反馈:创建模型的初步版本,通过原型或简单的实现与业务专家验证。 持续迭代:根据反馈调整模型,直到它准确反映业务需求和逻辑。 6. 编码实现 领域驱动设计(DDD):依据领域模型,采用DDD的战术模式(如Repository、Factory、Service Layer等)来指导代码结构。 分层架构:设计清晰的分层架构,如表现层、应用服务层、领域层、基础设施层,确保领域模型的纯净性。 7. 测试与验证 单元测试:为领域模型的关键行为编写单元测试,确保业务规则的正确实施。 集成测试:验证不同子域之间的交互,确保系统作为一个整体的协调工作。
通过以上过程,我们可以确保应用程序的设计和结构紧密围绕业务逻辑展开,提高软件的可维护性、扩展性和对业务变化的适应能力。
# 其中一个微服务的结构 ch04-faculty ├── configuration/ # 服务配置 │ └── config │ └── erp_settings.properties ├── controllers/ # 控制层 │ └── assignments │ └── ... ├── models/ # 模型层,提供应用程序的上下文框架 │ └── data # 数据模型,在其临时数据存储中捕获和存储数据的模型 │ └── faculty │ └── facultydb │ └── request # 请求模型,在 API 服务中使用的 BaseModel 对象 │ └── faculty ├── repository/ # 存储库层,1)创建管理数据访问的策略;2)为应用程序提供了一个高级抽象 │ └── assignments │ └── ... ├── services/ # 服务层,定义了应用程序的算法、操作和流程。通常而言,与存储库交互,为应用程序的其他组件(如API服务或控制器)构建必要的业务逻辑、管理和控制 │ └── assignments │ └── ... └── main.py └── README.md
2. 微服务的配置
# content of config.py import os from datetime import date from pydantic_settings import BaseSettings # 方式一:将设置存储为类属性 class FacultySettings(BaseSettings): application: str = 'Faculty Management System' webmaster: str = 'sjctrags@university.com' created: date = '2021-11-10' # 方式二: 配置文件 + 继承 BaseSettings 的类 + Config 内部类及 env_file 属性读取配置文件 class ServerSettings(BaseSettings): production_server: str prod_port: int development_server: str dev_port: int # 定义一个内部类 Config 和 env_file 来读取配置文件 class Config: env_file = os.path.join(os.getcwd(), "configuration", "erp_settings.properties")