[{"content":"RAG项目案例 RAG回顾 RAG即检索、增强和生成，其主要分为2条线：\n离线处理：向私有知识库（向量存储）源源不断添加私有知识文档。 向知识库添加来自未来的知识文档（基于模型训练完成时间） 向模型添加私有知识文档 给出模型参考资料，规避模型幻觉（一本正经的胡说八道） 在线处理：用户提问会先基于私有知识库做检索，获取参考资料，同步组装新提示词询问大模型获取结果。 项目需求和思路 本次项目以\u0026quot;某东商品衣服\u0026quot;为例，以衣服属性构建本地知识。使用者可以自由更新本地知识，用户问题的答案也是基于本地知识生成的。\n项目主要会实现如下代码：\n离线流程 项目基础数据 - /data/xxx.txt\napp_file_uploader.py\n可以先按照以下步骤运行网页：\n1.编写测试代码\n1 2 3 4 import streamlit as st # 添加网页标题 st.title(\u0026#34;知识库更新服务\u0026#34;) 2.复制文件地址到命令提示符（cmd）中，进入到当前项目目录（c盘切到d盘：d:）\nD:\\RAG\\AI大模型RAG与智能体开发\\P4_RAG项目案例\u0026gt;\n3.在cmd中输入streamlit run app_file_uploader.py则会自动跳转到网页中\n4.使用 st.file_uploader 创建文件上传组件\n1 2 3 4 5 6 # file_uploader uploader_file = st.file_uploader( \u0026#34;请上传TXT文件\u0026#34;, type=[\u0026#39;txt\u0026#39;], accept_multiple_files=False, # False表示仅接受一个文件的上传[不接受多文件] ) 5.提取文件信息\n1 2 3 4 5 6 7 8 9 10 11 12 if uploader_file is not None: # 提取文件的信息 file_name = uploader_file.name file_type = uploader_file.type file_size = uploader_file.size / 1024 # KB st.subheader(f\u0026#34;文件名：{file_name}\u0026#34;) st.write(f\u0026#34;格式：{file_type} | 大小：{file_size:.2f} KB\u0026#34;) # get_value -\u0026gt; bytes -\u0026gt; decode(\u0026#39;utf-8\u0026#39;) text = uploader_file.getvalue().decode(\u0026#34;utf-8\u0026#34;) st.write(text) 完整代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 \u0026#34;\u0026#34;\u0026#34; 基于Streamlit完成WEB网页上传服务 pip install streamlit Streamlit：当WEB页面元素发生变化，则代码重新执行一遍 \u0026#34;\u0026#34;\u0026#34; import streamlit as st # 添加网页标题 st.title(\u0026#34;知识库更新服务\u0026#34;) # file_uploader uploader_file = st.file_uploader( \u0026#34;请上传TXT文件\u0026#34;, type=[\u0026#39;txt\u0026#39;], accept_multiple_files=False, # False表示仅接受一个文件的上传[不接受多文件] ) if uploader_file is not None: # 提取文件的信息 file_name = uploader_file.name file_type = uploader_file.type file_size = uploader_file.size / 1024 # KB st.subheader(f\u0026#34;文件名：{file_name}\u0026#34;) st.write(f\u0026#34;格式：{file_type} | 大小：{file_size:.2f} KB\u0026#34;) # get_value -\u0026gt; bytes -\u0026gt; decode(\u0026#39;utf-8\u0026#39;) text = uploader_file.getvalue().decode(\u0026#34;utf-8\u0026#34;) st.write(text) 知识库服务 knowledge_base.py\n工具类的实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 \u0026#34;\u0026#34;\u0026#34; 知识库 \u0026#34;\u0026#34;\u0026#34; import os import config_data as config import hashlib def check_md5(md5_str: str): \u0026#34;\u0026#34;\u0026#34; 检查传入的md5字符串是否已经被处理过了 return False(md5未处理过) True(已经处理过，已有记录) \u0026#34;\u0026#34;\u0026#34; if not os.path.exists(config.md5_path): # if进入表示文件不存在，那肯定没有处理过这个md5 open(config.md5_path, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;).close() return False else: for line in open(config.md5_path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;).readlines(): line = line.strip() # 处理字符串前后的空格和回车 if line == md5_str: # 已处理过 return True return False def save_md5(md5_str: str): \u0026#34;\u0026#34;\u0026#34;将传入的md5字符串，记录到文件内保存\u0026#34;\u0026#34;\u0026#34; with open(config.md5_path, \u0026#34;a\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: f.write(md5_str + \u0026#34;\\n\u0026#34;) def get_string_md5(input_str: str,encoding=\u0026#34;utf-8\u0026#34;): \u0026#34;\u0026#34;\u0026#34;将传入的字符串转换为md5字符串\u0026#34;\u0026#34;\u0026#34; # 将字符串转化为bytes字节数组 str_bytes = input_str.encode(encoding=encoding) # 创建md5对象 md5_obj = hashlib.md5() # 得到md5对象 md5_obj.update(str_bytes) # 更新内容（传入即将要转换的字节数组） md5_hex = md5_obj.hexdigest() # 得到md5的十六进制字符串 return md5_hex class KnowledgeBaseService(object): \u0026#34;\u0026#34;\u0026#34;知识库服务\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.chroma = None # 向量存储的实例 Chroma向量库对象 self.spliter = None # 文本分割器的对象 def upload_by_str(self,data,filename): \u0026#34;\u0026#34;\u0026#34;将传入的字符串，进行向量化，存入向量数据\u0026#34;\u0026#34;\u0026#34; 同样的内容计算的md5值是一样的（16进制字符串，长度32位），不管体积多大计算出来的结果都是32位，假设文本非常非常长，只要内容相同，计算的结果都是一样的（32位），方便做记录【节省空间，效率高】\n以下为测试代码（粘贴至上述代码下方）\n1 2 3 4 5 6 7 8 9 if __name__ == \u0026#39;__main__\u0026#39;: # 无论r1和r2里面内容多大，只要内容相同，输出结果就是一样的32位的16进制字符串 r1 = get_string_md5(\u0026#34;周杰伦\u0026#34;)\tr2 = get_string_md5(\u0026#34;周杰伦\u0026#34;) r3 = get_string_md5(\u0026#34;周杰伦2\u0026#34;) print(r1) print(r2) print(r3) 测试check_md5:\n1 2 if __name__ == \u0026#39;__main__\u0026#39;: print(check_md5(\u0026#34;7a8941058aaf4df5147042ce104568da\u0026#34;)) 无论运行几次都是False，怎样才能得到True呢\n只需要调用save方法将内容保存至md5.text中即可\n1 2 3 if __name__ == \u0026#39;__main__\u0026#39;: save_md5(\u0026#34;7a8941058aaf4df5147042ce104568da\u0026#34;) print(check_md5(\u0026#34;7a8941058aaf4df5147042ce104568da\u0026#34;)) 配置文件：config_data.py\n1 md5_path = \u0026#34;./md5.txt\u0026#34; 接下来继续完成知识库服务部分**(KnowledgeBaseService)**的代码编写\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 from langchain_chroma import Chroma from langchain_community.embeddings import DashScopeEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from datetime import datetime ​``` 此处省略mt5部分的代码 ​``` class KnowledgeBaseService(object): \u0026#34;\u0026#34;\u0026#34;知识库服务\u0026#34;\u0026#34;\u0026#34; def __init__(self): # 如果文件夹不存在则创建，如果存在则跳过 os.makedirs(config.persist_directory,exist_ok=True) self.chroma = Chroma( collection_name=config.collection_name, # 数据库的表名 embedding_function=DashScopeEmbeddings(model=\u0026#34;text-embedding-v4\u0026#34;), persist_directory=config.persist_directory, # 数据库本地存储文件夹 ) # 向量存储的实例 Chroma向量库对象 self.spliter = RecursiveCharacterTextSplitter( chunk_size=config.chunk_size, # 分割后的文本段最大长度 chunk_overlap=config.chunk_overlap, # 连续文本段之间的字符重叠数量 separators=config.separators, # 自然段落划分的符号 length_function=len, # 使用python自带的len函数做长度统计的依据 ) # 文本分割器的对象 def upload_by_str(self,data,filename): \u0026#34;\u0026#34;\u0026#34;将传入的字符串，进行向量化，存入向量数据\u0026#34;\u0026#34;\u0026#34; # 先得到传入字符串的md5值 md5_hex = get_string_md5(data) if check_md5(md5_hex): return \u0026#34;[跳过]内容已经存在知识库中\u0026#34; if len(data) \u0026gt; config.max_split_char_number: # 如果传入的字符串长度大于阈值，则进行文本分割 knowledge_chunk: list[str] = self.spliter.split_text(data) else: knowledge_chunk = [data] metadata = { \u0026#34;source\u0026#34;: filename, # 2026-01-01 10:00:00 \u0026#34;create_time\u0026#34;: datetime.now().strftime(\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;), \u0026#34;operator\u0026#34;: \u0026#34;小曹\u0026#34;, } self.chroma.add_texts( # 内容就加载到向量库中了 # iterable -\u0026gt; list \\ tuple knowledge_chunk, # 列表推导式 ：通过这个列表推导式，为每个文本块生成一份相同的元数据字典 metadatas=[metadata for _ in knowledge_chunk], ) # 保存md5 save_md5(md5_hex) return \u0026#34;[成功]内容已经成功载入到向量库中\u0026#34; 补充config_data.py\n1 2 3 4 5 6 7 8 9 10 # Chroma collection_name = \u0026#34;rag\u0026#34; persist_directory = \u0026#34;./chroma_db\u0026#34; # spliter chunk_size = 1000 chunk_overlap = 100 separators = [\u0026#34;\\n\\n\u0026#34;, \u0026#34;\\n\u0026#34;, \u0026#34;.\u0026#34;, \u0026#34;!\u0026#34;, \u0026#34;?\u0026#34;, \u0026#34;。\u0026#34;, \u0026#34;！\u0026#34;, \u0026#34;？\u0026#34;, \u0026#34; \u0026#34;, \u0026#34;\u0026#34;] max_split_char_number = 1000 # 文本分割的阈值 测试代码（删除先前创建的text文件）\n1 2 3 4 if __name__ == \u0026#39;__main__\u0026#39;: service = KnowledgeBaseService() r = service.upload_by_str(\u0026#34;周杰伦\u0026#34;,\u0026#34;testfile\u0026#34;) print(r) 再次运行\n接下来将前面两个文件合并（app_file+knowledge）\n我们可以在app_file文件中最下方写一行print语句\n然后打开网页进行测试，可以发现不管是刷新还是添加文件，控制台都会输出一遍print里的内容，说明Streamlit：当WEB页面元素发生变化，则代码重新执行一遍\n那么这个有什么问题呢？\n其实问题还是挺大的，因为有了这个问题，就相当于我们整个代码都重跑了一遍，这会造成状态的丢失\n我们可以来进行测试一下：\n将前面的app文件代码修改为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import streamlit as st # 添加网页标题 st.title(\u0026#34;知识库更新服务\u0026#34;) # file_uploader uploader_file = st.file_uploader( \u0026#34;请上传TXT文件\u0026#34;, type=[\u0026#39;txt\u0026#39;], accept_multiple_files=False, # False表示仅接受一个文件的上传[不接受多文件] ) count = 0 if uploader_file is not None: # 提取文件的信息 file_name = uploader_file.name file_type = uploader_file.type file_size = uploader_file.size / 1024 # KB st.subheader(f\u0026#34;文件名：{file_name}\u0026#34;) st.write(f\u0026#34;格式：{file_type} | 大小：{file_size:.2f} KB\u0026#34;) # get_value -\u0026gt; bytes -\u0026gt; decode(\u0026#39;utf-8\u0026#39;) text = uploader_file.getvalue().decode(\u0026#34;utf-8\u0026#34;) st.write(text) count += 1 print(f\u0026#34;上传了{count}个文件\u0026#34;) 第一次上传文件是正常的，但是后续再上传都是相当于每次将代码从头跑了一遍，count都是从0开始，可以发现，只要每次页面发生变化，代码都是从头开始执行一遍\n将content相关的代码替换为：\n1 2 3 # session_state就是一个字典 if \u0026#34;count\u0026#34; not in st.session_state: st.session_state[\u0026#34;count\u0026#34;] = 0 1 st.session_state[\u0026#34;count\u0026#34;] += 1 1 print(f\u0026#39;上传了{st.session_state[\u0026#34;count\u0026#34;]}个文件\u0026#39;) 打开网页进行测试\n这样就解决了状态维护的问题\n但是这样还是有缺陷，因为只要页面一刷新，KnowledgeBaseService对象还是留不下来（接下来完整代码中出现），需要重新创建，性能消耗大\n接下来就需要达成一个这样的效果：程序不停，只需要创建一个对象即可\n完整代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u0026#34;\u0026#34;\u0026#34; 基于Streamlit完成WEB网页上传服务 pip install streamlit Streamlit：当WEB页面元素发生变化，则代码重新执行一遍 \u0026#34;\u0026#34;\u0026#34; import time import streamlit as st from knowledge_base import KnowledgeBaseService # 添加网页标题 st.title(\u0026#34;知识库更新服务\u0026#34;) # file_uploader uploader_file = st.file_uploader( \u0026#34;请上传TXT文件\u0026#34;, type=[\u0026#39;txt\u0026#39;], accept_multiple_files=False, # False表示仅接受一个文件的上传[不接受多文件] ) # # session_state就是一个字典 if \u0026#34;service\u0026#34; not in st.session_state: st.session_state[\u0026#34;service\u0026#34;] = KnowledgeBaseService() if uploader_file is not None: # 提取文件的信息 file_name = uploader_file.name file_type = uploader_file.type file_size = uploader_file.size / 1024 # KB st.subheader(f\u0026#34;文件名：{file_name}\u0026#34;) st.write(f\u0026#34;格式：{file_type} | 大小：{file_size:.2f} KB\u0026#34;) # get_value -\u0026gt; bytes -\u0026gt; decode(\u0026#39;utf-8\u0026#39;) text = uploader_file.getvalue().decode(\u0026#34;utf-8\u0026#34;) # 增加一点用户体验的等待动画 with st.spinner(\u0026#34;载入知识库中。。。\u0026#34;): # 在spinner内的代码执行过程中，会有一个转圈动画 time.sleep(1) result = st.session_state[\u0026#34;service\u0026#34;].upload_by_str(text, file_name) st.write(result) # 可以直接在页面中显示结果 在线流程 首先先创创建一个向量存储文件\nvector_stores.py\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from langchain_chroma import Chroma import config_data as config class VectorStoreService(object): def __init__(self, embedding): \u0026#34;\u0026#34;\u0026#34; :param embedding: 嵌入模型的传入 \u0026#34;\u0026#34;\u0026#34; self.embedding = embedding self.vector_store = Chroma( collection_name=config.collection_name, embedding_function=self.embedding, persist_directory=config.persist_directory, ) def get_retriever(self): \u0026#34;\u0026#34;\u0026#34;返回向量检索器，方便加入chain\u0026#34;\u0026#34;\u0026#34; return self.vector_store.as_retriever(search_kwargs={\u0026#34;k\u0026#34;: config.similarity_threshold}) if __name__ == \u0026#39;__main__\u0026#39;: from langchain_community.embeddings import DashScopeEmbeddings retriever = VectorStoreService(DashScopeEmbeddings(model=\u0026#34;text-embedding-v4\u0026#34;)).get_retriever() res = retriever.invoke(\u0026#34;我的体重120斤，尺码推荐\u0026#34;) print(res) config_data中添加下面这一行代码：\n1 similarity_threshold = 1 # 检索返回匹配的文档数量 接下来实现rag.py\n基础实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough, RunnableWithMessageHistory, RunnableLambda from vector_stores import VectorStoreService from langchain_community.embeddings import DashScopeEmbeddings import config_data as config from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_community.chat_models.tongyi import ChatTongyi def print_prompt(prompt): print(\u0026#34;=\u0026#34;*20) print(prompt.to_string()) print(\u0026#34;=\u0026#34;*20) return prompt class RagService(object): def __init__(self): # 向量数据库服务 - 用于检索 self.vector_service = VectorStoreService( embedding=DashScopeEmbeddings(model=config.embedding_model_name) ) self.prompt_template = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;以我提供的已知参考资料为主，\u0026#34; \u0026#34;简洁和专业的回答用户问题。参考资料:{context}。\u0026#34;), (\u0026#34;user\u0026#34;, \u0026#34;请回答用户提问：{input}\u0026#34;) ] ) self.chat_model = ChatTongyi(model=config.chat_model_name) self.chain = self.__get_chain() def __get_chain(self): \u0026#34;\u0026#34;\u0026#34;获取最终的执行链\u0026#34;\u0026#34;\u0026#34; # 获取检索器 retriever = self.vector_service.get_retriever() def format_document(docs: list[Document]): if not docs: return \u0026#34;无相关参考资料\u0026#34; formatted_str = \u0026#34;\u0026#34; for doc in docs: formatted_str += f\u0026#34;文档片段：{doc.page_content}\\n文档元数据：{doc.metadata}\\n\\n\u0026#34; return formatted_str chain = ( { \u0026#34;input\u0026#34;: RunnablePassthrough(), \u0026#34;context\u0026#34;: retriever | format_document } | self.prompt_template | print_prompt | self.chat_model | StrOutputParser() ) return chain if __name__ == \u0026#39;__main__\u0026#39;: res = RagService().chain.invoke({\u0026#34;我身高170厘米，尺码推荐\u0026#34;}, ) print(res) 历史会话记录功能 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough, RunnableWithMessageHistory, RunnableLambda from file_history_store import get_history from vector_stores import VectorStoreService from langchain_community.embeddings import DashScopeEmbeddings import config_data as config from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_community.chat_models.tongyi import ChatTongyi def print_prompt(prompt): print(\u0026#34;=\u0026#34;*20) print(prompt.to_string()) print(\u0026#34;=\u0026#34;*20) return prompt class RagService(object): def __init__(self): # 向量数据库服务 - 用于检索 self.vector_service = VectorStoreService( embedding=DashScopeEmbeddings(model=config.embedding_model_name) ) self.prompt_template = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;以我提供的已知参考资料为主，\u0026#34; \u0026#34;简洁和专业的回答用户问题。参考资料:{context}。\u0026#34;), (\u0026#34;system\u0026#34;, \u0026#34;并且我提供用户的对话历史记录，如下：\u0026#34;), MessagesPlaceholder(\u0026#34;history\u0026#34;), (\u0026#34;user\u0026#34;, \u0026#34;请回答用户提问：{input}\u0026#34;) ] ) self.chat_model = ChatTongyi(model=config.chat_model_name) self.chain = self.__get_chain() def __get_chain(self): \u0026#34;\u0026#34;\u0026#34;获取最终的执行链\u0026#34;\u0026#34;\u0026#34; # 获取检索器 retriever = self.vector_service.get_retriever() # 将检索到的文档列表格式化为字符串 def format_document(docs: list[Document]): if not docs: # 如果没有检索到文档 return \u0026#34;无相关参考资料\u0026#34; formatted_str = \u0026#34;\u0026#34; for doc in docs: # 遍历每个文档片段 formatted_str += f\u0026#34;文档片段：{doc.page_content}\\n文档元数据：{doc.metadata}\\n\\n\u0026#34; return formatted_str # 返回拼接后的字符串 # 从检索器的输入中提取用户问题（为检索器做格式化） def format_for_retriever(value: dict) -\u0026gt; str: return value[\u0026#34;input\u0026#34;] # 格式化Prompt模板所需的输入变量 def format_for_prompt_template(value): # 将嵌套字典展平为 {input, context, history} 格式 new_value = {} new_value[\u0026#34;input\u0026#34;] = value[\u0026#34;input\u0026#34;][\u0026#34;input\u0026#34;] # 提取用户输入的问题 new_value[\u0026#34;context\u0026#34;] = value[\u0026#34;context\u0026#34;] # 提取检索到的上下文 new_value[\u0026#34;history\u0026#34;] = value[\u0026#34;input\u0026#34;][\u0026#34;history\u0026#34;] # 提取对话历史 return new_value chain = ( { \u0026#34;input\u0026#34;: RunnablePassthrough(), \u0026#34;context\u0026#34;: RunnableLambda(format_for_retriever) | retriever | format_document } | RunnableLambda(format_for_prompt_template) | self.prompt_template | print_prompt | self.chat_model | StrOutputParser() ) conversation_chain = RunnableWithMessageHistory( chain, get_history, input_messages_key=\u0026#34;input\u0026#34;, history_messages_key=\u0026#34;history\u0026#34;, ) return conversation_chain if __name__ == \u0026#39;__main__\u0026#39;: # session id 配置 session_config = { \u0026#34;configurable\u0026#34;: { \u0026#34;session_id\u0026#34;: \u0026#34;user_001\u0026#34;, } } # 此时（增强链）传入的是一个字典 res = RagService().chain.invoke({\u0026#34;input\u0026#34;: \u0026#34;春天穿什么颜色的衣服？\u0026#34;}, session_config) print(res) 聊天页面开发 进入到“智能客服”网页：\n1.编写测试代码：\n1 2 3 4 5 import streamlit as st # 标题 st.title(\u0026#34;智能客服\u0026#34;) st.divider() 2.复制文件地址到命令提示符（cmd）中，进入到当前项目目录：\nD:\\RAG\\AI大模型RAG与智能体开发\\P4_RAG项目案例\\app_qa.py\u0026gt;\n3.启动项目\n1 streamlit run app_qa.py 完整代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import time from rag import RagService # 导入RAG服务类 import streamlit as st # 导入Streamlit用于构建Web界面 import config_data as config # 导入配置文件 # ========== 页面标题 ========== st.title(\u0026#34;智能客服\u0026#34;) # 设置页面标题 st.divider() # 添加分隔符 # ========== 初始化会话状态 ========== # 存储聊天记录，初始时添加一条欢迎消息 if \u0026#34;message\u0026#34; not in st.session_state: st.session_state[\u0026#34;message\u0026#34;] = [{\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你好，有什么可以帮助你？\u0026#34;}] # 初始化RAG服务实例（只创建一次，避免重复初始化） if \u0026#34;rag\u0026#34; not in st.session_state: st.session_state[\u0026#34;rag\u0026#34;] = RagService() # ========== 显示历史聊天记录 ========== # 遍历所有聊天记录并渲染到页面 for message in st.session_state[\u0026#34;message\u0026#34;]: st.chat_message(message[\u0026#34;role\u0026#34;]).write(message[\u0026#34;content\u0026#34;]) # ========== 用户输入 ========== # 在页面最下方提供用户输入栏 prompt = st.chat_input() if prompt: # 显示用户提问 st.chat_message(\u0026#34;user\u0026#34;).write(prompt) # 将用户消息添加到聊天记录 st.session_state[\u0026#34;message\u0026#34;].append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt}) # ========== AI响应 ========== ai_res_list = [] # 用于缓存AI的流式响应 with st.spinner(\u0026#34;AI思考中...\u0026#34;): # 调用RAG链进行流式响应（res_stream：迭代器） res_stream = st.session_state[\u0026#34;rag\u0026#34;].chain.stream({\u0026#34;input\u0026#34;: prompt}, config.session_config) #yield # 定义捕获生成器：将流式数据同时返回并缓存 def capture(generator, cache_list): for chunk in generator: cache_list.append(chunk) # 缓存每个chunk yield chunk # 返回给Streamlit显示 # 流式输出AI响应 st.chat_message(\u0026#34;assistant\u0026#34;).write_stream(capture(res_stream, ai_res_list)) # 将AI完整响应添加到聊天记录 st.session_state[\u0026#34;message\u0026#34;].append({\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;\u0026#34;.join(ai_res_list)}) # [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;] \u0026#34;\u0026#34;.join(list) -\u0026gt; abc # [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;] \u0026#34;,\u0026#34;.join(list) -\u0026gt; a,b,c 配置文件config_data.py：\n在原本的文件上添加以下代码：\n1 2 3 4 5 session_config = { \u0026#34;configurable\u0026#34;: { \u0026#34;session_id\u0026#34;: \u0026#34;user_001\u0026#34;, } } ","date":"2026-07-29T14:40:49+08:00","permalink":"https://aurora156.github.io/p/rag%E9%A1%B9%E7%9B%AE%E6%A1%88%E4%BE%8B/","title":"RAG项目案例"},{"content":" 本篇为学习Langchain过程中用到的一篇资料，刚好可以用于学习python的基础知识【来自黑马课程】\n一、Python 环境与程序运行 Python 解释器：常用 CPython（官方默认），可通过命令行输入 python（Windows）/ python3（Linux/Mac）进入交互环境 程序运行方式： 交互环境：直接输入代码回车执行，适合简单测试 脚本文件：将代码写入 .py 后缀文件，命令行执行 python 文件名.py（Windows）/ python3 文件名.py（Linux/Mac） 注释：代码中不被执行的说明性文字，用于提高可读性 单行注释：以 # 开头，示例：# 这是一行单行注释 多行注释：用三个单引号 \u0026rsquo;\u0026rsquo;\u0026rsquo; 或三个双引号 \u0026quot;\u0026quot;\u0026quot; 包裹，示例： \u0026rsquo;\u0026rsquo;' 这是多行注释 第一行 第二行 \u0026rsquo;\u0026rsquo;' \u0026quot;\u0026quot;\u0026quot; 这也是多行注释 支持换行书写 \u0026quot;\u0026quot;\u0026quot; 二、变量与数据类型 变量定义：无需声明类型，直接赋值即可，变量名由字母、数字、下划线组成，不能以数字开头，区分大小写，不能使用Python关键字 赋值语法：变量名 = 赋值内容，示例：name = \u0026ldquo;Python\u0026rdquo;，age = 25，score = 98.5 多变量赋值：a, b, c = 1, 2, 3（一一对应）；a = b = c = 10（所有变量赋同一值） 基本数据类型 数字类型（Number） 整数（int）：正整数、负整数、0，示例：10，-5，0 浮点数（float）：带小数点的数字，示例：3.14，-0.99，10.0 布尔值（bool）：只有两个值 True（真，等价于1）和 False（假，等价于0），示例：is_ok = True，is_pass = False 复数（complex）：形如 a+bj，示例：num = 3+4j 字符串类型（str）：用单引号、双引号或三引号包裹的字符序列 示例：s1 = \u0026lsquo;hello\u0026rsquo;，s2 = \u0026ldquo;world\u0026rdquo;，s3 = \u0026lsquo;\u0026lsquo;\u0026lsquo;hello python\u0026rsquo;\u0026rsquo;\u0026rsquo; 字符串不可变：一旦创建，无法修改单个字符 容器类型（基础） 列表（list）：有序、可变、可重复存储不同类型数据，用 [] 包裹，示例：lst = [1, \u0026ldquo;python\u0026rdquo;, 3.14, True] 元组（tuple）：有序、不可变、可重复存储不同类型数据，用 () 包裹，示例：tup = (2, \u0026ldquo;java\u0026rdquo;, 9.9) 字典（dict）：无序（Python3.7+默认有序）、可变，以键值对（key: value）存储，key唯一且不可变，用 {} 包裹，示例：dic = {\u0026ldquo;name\u0026rdquo;: \u0026ldquo;张三\u0026rdquo;, \u0026ldquo;age\u0026rdquo;: 20} 集合（set）：无序、可变、不可重复存储数据，用 {} 包裹（空集合需用 set()，不能用 {}），示例：s = {1, 2, 3, 3}（最终为 {1,2,3}） 数据类型转换 转为整数：int(x)，示例：int(\u0026ldquo;123\u0026rdquo;) → 123，int(3.99) → 3 转为浮点数：float(x)，示例：float(\u0026ldquo;3.14\u0026rdquo;) → 3.14，float(10) → 10.0 转为字符串：str(x)，示例：str(123) → \u0026ldquo;123\u0026rdquo;，str(True) → \u0026ldquo;True\u0026rdquo; 转为列表：list(x)，示例：list((1,2,3)) → [1,2,3] 转为元组：tuple(x)，示例：tuple([1,2,3]) → (1,2,3) 转为集合：set(x)，示例：set([1,1,2]) → {1,2} 三、运算符 算术运算符：用于数值计算 +：加法，示例：10 + 20 → 30；字符串/列表拼接，示例：\u0026ldquo;hello\u0026rdquo; + \u0026ldquo;world\u0026rdquo; → \u0026ldquo;helloworld\u0026rdquo; -：减法，示例：20 - 10 → 10 *：乘法，示例：10 * 3 → 30；字符串/列表重复，示例：\u0026ldquo;ab\u0026rdquo; * 3 → \u0026ldquo;ababab\u0026rdquo; /：除法，结果为浮点数，示例：10 / 3 → 3.3333333333333335 //：整除（向下取整），示例：10 // 3 → 3；-10 // 3 → -4 %：取模（求余数），示例：10 % 3 → 1 **：幂运算，示例：2 ** 3 → 8；10 ** 2 → 100 赋值运算符：用于给变量赋值 =：基本赋值，示例：a = 10 +=：加后赋值，示例：a += 5 等价于 a = a + 5 -=：减后赋值，示例：a -= 5 等价于 a = a - 5 *=：乘后赋值，示例：a *= 5 等价于 a = a * 5 /=：除后赋值，示例：a /= 5 等价于 a = a / 5 //=：整除后赋值，示例：a //= 5 等价于 a = a // 5 %=：取模后赋值，示例：a %= 5 等价于 a = a % 5 **=：幂后赋值，示例：a **= 5 等价于 a = a ** 5 比较运算符：用于比较两个值，结果为布尔值 True/False ==：等于，示例：10 == 10 → True；\u0026ldquo;abc\u0026rdquo; == \u0026ldquo;abd\u0026rdquo; → False !=：不等于，示例：10 != 20 → True ：大于，示例：20 \u0026gt; 10 → True\n\u0026lt;：小于，示例：10 \u0026lt; 20 → True =：大于等于，示例：10 \u0026gt;= 10 → True；20 \u0026gt;= 15 → True\n\u0026lt;=：小于等于，示例：10 \u0026lt;= 20 → True；15 \u0026lt;= 15 → True 逻辑运算符：用于逻辑判断（与、或、非） and：逻辑与，所有条件为真才为真，有一个为假则为假，示例：(10 \u0026gt; 5) and (20 \u0026gt; 15) → True or：逻辑或，有一个条件为真就为真，所有条件为假才为假，示例：(10 \u0026gt; 5) or (20 \u0026lt; 15) → True not：逻辑非，取反，示例：not (10 \u0026gt; 5) → False 成员运算符：用于判断元素是否在容器中（列表、元组、字符串、集合、字典） in：判断元素是否存在，示例：\u0026ldquo;a\u0026rdquo; in \u0026ldquo;abc\u0026rdquo; → True；1 in [1,2,3] → True not in：判断元素是否不存在，示例：\u0026ldquo;d\u0026rdquo; not in \u0026ldquo;abc\u0026rdquo; → True；4 not in [1,2,3] → True 身份运算符：用于判断两个变量是否指向同一个内存对象 is：判断是否为同一对象，示例：a = 10; b = 10; a is b → True is not：判断是否不是同一对象，示例：a = [1,2]; b = [1,2]; a is not b → True 四、流程控制语句 条件判断语句（if-elif-else）：根据条件执行不同代码块 基本语法1（单条件）： if 条件表达式: 缩进的执行代码块（条件为True时执行） （注意：Python用缩进区分代码块，通常4个空格，不要混用空格和Tab） 示例： if score \u0026gt;= 60: print(\u0026ldquo;及格\u0026rdquo;) 基本语法2（双条件）： if 条件表达式1: 缩进的执行代码块1 else: 缩进的执行代码块2（条件表达式1为False时执行） 示例： if score \u0026gt;= 60: print(\u0026ldquo;及格\u0026rdquo;) else: print(\u0026ldquo;不及格\u0026rdquo;) 基本语法3（多条件）： if 条件表达式1: 缩进的执行代码块1 elif 条件表达式2: 缩进的执行代码块2 elif 条件表达式3: 缩进的执行代码块3 \u0026hellip; else: 缩进的执行代码块n（所有前面条件都为False时执行） 示例： if score \u0026gt;= 90: print(\u0026ldquo;优秀\u0026rdquo;) elif score \u0026gt;= 80: print(\u0026ldquo;良好\u0026rdquo;) elif score \u0026gt;= 60: print(\u0026ldquo;及格\u0026rdquo;) else: print(\u0026ldquo;不及格\u0026rdquo;) 嵌套条件：if语句内部可以嵌套if-elif-else，注意缩进层级 if a \u0026gt; 10: if a \u0026lt; 20: print(\u0026ldquo;a在10和20之间\u0026rdquo;) else: print(\u0026ldquo;a大于等于20\u0026rdquo;) else: print(\u0026ldquo;a小于等于10\u0026rdquo;) 循环语句：重复执行某段代码块 for 循环：用于遍历可迭代对象（列表、元组、字符串、字典等） 基本语法： for 变量名 in 可迭代对象: 缩进的循环执行代码块 示例1（遍历列表）： lst = [1,2,3,4] for num in lst: print(num) 示例2（遍历字符串）： s = \u0026ldquo;python\u0026rdquo; for char in s: print(char) 示例3（range() 函数辅助：生成整数序列，range(start, end, step)，start默认0，step默认1，不包含end） for i in range(5): # 0,1,2,3,4 print(i) for i in range(2, 8): # 2,3,4,5,6,7 print(i) for i in range(0, 10, 2): # 0,2,4,6,8 print(i) 示例4（遍历字典）： dic = {\u0026ldquo;name\u0026rdquo;: \u0026ldquo;张三\u0026rdquo;, \u0026ldquo;age\u0026rdquo;: 20} for key in dic: # 遍历键 print(key) for value in dic.values(): # 遍历值 print(value) for key, value in dic.items(): # 遍历键值对 print(key, value) while 循环：根据条件判断是否继续循环，先判断条件，再执行代码块 基本语法： while 条件表达式: 缩进的循环执行代码块 （注意：需设置循环终止条件，避免无限循环） 示例1（基本循环）： count = 0 while count \u0026lt; 5: print(count) count += 1 # 终止条件：count递增到5，条件不成立 示例2（无限循环，需用break终止）： while True: s = input(\u0026ldquo;输入内容（输入quit退出）：\u0026rdquo;) if s == \u0026ldquo;quit\u0026rdquo;: break print(\u0026ldquo;你输入了：\u0026rdquo;, s) 循环控制语句： break：立即终止当前循环，跳出循环体，不再执行后续循环代码 示例： for i in range(10): if i == 5: break print(i) # 输出0,1,2,3,4 continue：跳过当前循环的剩余代码，直接进入下一次循环 示例： for i in range(10): if i == 5: continue print(i) # 输出0,1,2,3,4,6,7,8,9 else：循环正常结束（未被break终止）时，执行else代码块 示例： for i in range(5): print(i) else: print(\u0026ldquo;循环正常结束\u0026rdquo;) 五、字符串操作 字符串索引与切片：索引用于获取单个字符，切片用于获取子字符串 索引：分为正向索引（从0开始，从左到右）和反向索引（从-1开始，从右到左） 示例：s = \u0026ldquo;python\u0026rdquo; s[0] → \u0026ldquo;p\u0026rdquo;（正向索引第1个字符） s[-1] → \u0026ldquo;n\u0026rdquo;（反向索引第1个字符） s[3] → \u0026ldquo;h\u0026rdquo;；s[-3] → \u0026ldquo;h\u0026rdquo; 切片语法：字符串[start:end:step]，start默认0，end默认字符串长度，step默认1，不包含end对应的字符 示例：s = \u0026ldquo;python\u0026rdquo; s[0:3] → \u0026ldquo;pyt\u0026rdquo;（从索引0到2） s[2:] → \u0026ldquo;thon\u0026rdquo;（从索引2到末尾） s[:4] → \u0026ldquo;pyth\u0026rdquo;（从开头到索引3） s[1:5:2] → \u0026ldquo;yh\u0026rdquo;（从索引1到4，步长2） s[::-1] → \u0026ldquo;nohtyp\u0026rdquo;（反转字符串） 常用字符串方法 len(s)：获取字符串长度，示例：len(\u0026ldquo;python\u0026rdquo;) → 6 s.lower()：转为全小写，示例：\u0026ldquo;Python\u0026rdquo;.lower() → \u0026ldquo;python\u0026rdquo; s.upper()：转为全大写，示例：\u0026ldquo;Python\u0026rdquo;.upper() → \u0026ldquo;PYTHON\u0026rdquo; s.strip()：去除字符串首尾空格（可指定去除的字符），示例：\u0026quot; hello \u0026ldquo;.strip() → \u0026ldquo;hello\u0026rdquo;；\u0026rdquo;###python###\u0026quot;.strip(\u0026quot;#\u0026quot;) → \u0026ldquo;python\u0026rdquo; s.split(sep)：按指定分隔符分割字符串，返回列表，示例：\u0026ldquo;a,b,c\u0026rdquo;.split(\u0026quot;,\u0026quot;) → [\u0026ldquo;a\u0026rdquo;,\u0026ldquo;b\u0026rdquo;,\u0026ldquo;c\u0026rdquo;]；\u0026ldquo;hello world\u0026rdquo;.split() → [\u0026ldquo;hello\u0026rdquo;,\u0026ldquo;world\u0026rdquo;] s.join(iter)：用字符串连接可迭代对象中的元素，示例：\u0026quot;,\u0026quot;.join([\u0026ldquo;a\u0026rdquo;,\u0026ldquo;b\u0026rdquo;,\u0026ldquo;c\u0026rdquo;]) → \u0026ldquo;a,b,c\u0026rdquo; s.replace(old, new)：替换字符串中的旧字符/子串为新字符/子串，示例：\u0026ldquo;hello world\u0026rdquo;.replace(\u0026ldquo;world\u0026rdquo;, \u0026ldquo;python\u0026rdquo;) → \u0026ldquo;hello python\u0026rdquo; s.find(sub)：查找子串首次出现的索引，未找到返回-1，示例：\u0026ldquo;python\u0026rdquo;.find(\u0026ldquo;th\u0026rdquo;) → 2 s.count(sub)：统计子串出现的次数，示例：\u0026ldquo;ababab\u0026rdquo;.count(\u0026ldquo;ab\u0026rdquo;) → 3 s.startswith(prefix)：判断字符串是否以指定前缀开头，返回布尔值，示例：\u0026ldquo;python\u0026rdquo;.startswith(\u0026ldquo;py\u0026rdquo;) → True s.endswith(suffix)：判断字符串是否以指定后缀结尾，返回布尔值，示例：\u0026ldquo;python\u0026rdquo;.endswith(\u0026ldquo;on\u0026rdquo;) → True 六、列表操作 列表索引与切片：与字符串用法一致，列表切片返回新列表，支持修改 索引：lst = [1,2,3,4,5] lst[0] → 1；lst[-1] → 5 切片：lst[1:4] → [2,3,4]；lst[::-1] → [5,4,3,2,1] 切片修改：lst[1:3] = [6,7] → lst变为 [1,6,7,4,5] 常用列表方法 len(lst)：获取列表长度，示例：len([1,2,3]) → 3 lst.append(x)：在列表末尾添加一个元素，示例：lst = [1,2]；lst.append(3) → [1,2,3] lst.extend(iter)：在列表末尾扩展可迭代对象的元素，示例：lst = [1,2]；lst.extend([3,4]) → [1,2,3,4] lst.insert(index, x)：在指定索引位置插入元素，示例：lst = [1,3]；lst.insert(1, 2) → [1,2,3] lst.remove(x)：删除列表中首次出现的指定元素，示例：lst = [1,2,3,2]；lst.remove(2) → [1,3,2] lst.pop(index)：删除指定索引位置的元素，默认删除最后一个元素，返回被删除的元素，示例：lst = [1,2,3]；lst.pop() → 3，lst变为 [1,2] lst.clear()：清空列表所有元素，示例：lst = [1,2,3]；lst.clear() → [] lst.index(x)：查找元素首次出现的索引，未找到报错，示例：[1,2,3].index(2) → 1 lst.count(x)：统计元素出现的次数，示例：[1,2,2,3].count(2) → 2 lst.sort()：对列表元素进行升序排序（原地修改），示例：lst = [3,1,2]；lst.sort() → [1,2,3] lst.reverse()：反转列表元素（原地修改），示例：lst = [1,2,3]；lst.reverse() → [3,2,1] 七、字典操作 字典的键值对访问与修改 访问值：通过键访问，语法：dic[key]，示例：dic = {\u0026ldquo;name\u0026rdquo;: \u0026ldquo;张三\u0026rdquo;}；dic[\u0026ldquo;name\u0026rdquo;] → \u0026ldquo;张三\u0026rdquo; 修改值：通过键修改，语法：dic[key] = new_value，示例：dic[\u0026ldquo;name\u0026rdquo;] = \u0026ldquo;李四\u0026rdquo; → dic变为 {\u0026ldquo;name\u0026rdquo;: \u0026ldquo;李四\u0026rdquo;} 添加键值对：语法：dic[new_key] = value，示例：dic[\u0026ldquo;age\u0026rdquo;] = 20 → dic变为 {\u0026ldquo;name\u0026rdquo;: \u0026ldquo;李四\u0026rdquo;, \u0026ldquo;age\u0026rdquo;: 20} 常用字典方法 len(dic)：获取字典键值对的个数，示例：len({\u0026ldquo;name\u0026rdquo;: \u0026ldquo;张三\u0026rdquo;, \u0026ldquo;age\u0026rdquo;: 20}) → 2 dic.keys()：返回所有键的可迭代对象，示例：dic.keys() → dict_keys([\u0026rsquo;name\u0026rsquo;, \u0026lsquo;age\u0026rsquo;]) dic.values()：返回所有值的可迭代对象，示例：dic.values() → dict_values([\u0026lsquo;张三\u0026rsquo;, 20]) dic.items()：返回所有键值对的可迭代对象，示例：dic.items() → dict_items([(\u0026rsquo;name\u0026rsquo;, \u0026lsquo;张三\u0026rsquo;), (\u0026lsquo;age\u0026rsquo;, 20)]) dic.get(key, default)：获取键对应的值，键不存在返回默认值（默认None），示例：dic.get(\u0026ldquo;name\u0026rdquo;) → \u0026ldquo;张三\u0026rdquo;；dic.get(\u0026ldquo;gender\u0026rdquo;, \u0026ldquo;未知\u0026rdquo;) → \u0026ldquo;未知\u0026rdquo; dic.pop(key, default)：删除指定键的键值对，返回对应的值，键不存在返回默认值（不指定则报错），示例：dic.pop(\u0026ldquo;age\u0026rdquo;) → 20 dic.clear()：清空字典所有键值对，示例：dic.clear() → {} dic.update(new_dic)：用新字典更新原字典，存在的键覆盖值，不存在的键添加，示例：dic.update({\u0026ldquo;age\u0026rdquo;: 21, \u0026ldquo;gender\u0026rdquo;: \u0026ldquo;男\u0026rdquo;}) 八、函数 函数定义与调用 定义语法： def 函数名(参数列表): 缩进的函数体代码块 [return 返回值] （说明：def是关键字，函数名遵循变量命名规则，参数列表可选，return可选，无return默认返回None） 示例1（无参数无返回值）： def say_hello(): print(\u0026ldquo;Hello Python!\u0026rdquo;) 示例2（有参数有返回值）： def add(a, b): result = a + b return result 函数调用：语法：函数名(参数列表)，示例： say_hello() # 调用无参函数 sum_result = add(10, 20) # 调用有参函数，接收返回值 print(sum_result) 函数参数 位置参数：按参数定义顺序传递参数，必须一一对应，个数一致，示例：add(10, 20)（10对应a，20对应b） 关键字参数：按参数名传递参数，顺序可打乱，示例：add(b=20, a=10) 默认参数：定义函数时给参数指定默认值，调用时可省略该参数（使用默认值），示例： def power(x, n=2): return x ** n power(3) → 9（使用默认n=2） power(3, 3) → 27（覆盖默认值） 可变参数： *args：接收任意个数的位置参数，打包为元组，示例： def sum_all(*args): total = 0 for num in args: total += num return total sum_all(1,2,3) → 6；sum_all(1,2,3,4,5) → 15 **kwargs：接收任意个数的关键字参数，打包为字典，示例： def print_info(**kwargs): for key, value in kwargs.items(): print(key, \u0026ldquo;:\u0026rdquo;, value) print_info(name=\u0026ldquo;张三\u0026rdquo;, age=20) return 语句：用于返回函数执行结果，执行到return后函数立即结束，可返回单个值或多个值（多个值打包为元组） 示例： def func(): return 1, 2, 3 # 等价于 return (1,2,3) a, b, c = func() # 解包，a=1, b=2, c=3 九、模块与包 模块：一个 .py 文件就是一个模块，用于封装相关的变量、函数、类等，提高代码复用性 导入模块： import 模块名，示例：import math（导入数学模块） 导入模块并指定别名，示例：import math as m（别名m，简化使用） 从模块中导入指定内容，示例：from math import pi, sqrt（导入pi和sqrt函数） 从模块中导入所有内容，示例：from math import *（不推荐，容易出现命名冲突） 使用模块中的内容： 导入整个模块：模块名.变量/函数，示例：math.pi → 3.141592653589793；math.sqrt(16) → 4.0 导入指定内容：直接使用变量/函数，示例：pi → 3.141592653589793；sqrt(16) → 4.0 包：包含 init.py 文件的文件夹，用于组织多个相关模块，避免模块名冲突 导入包中的模块： import 包名.模块名 from 包名 import 模块名 from 包名.模块名 import 函数/变量 十、异常处理 异常：程序运行过程中出现的错误（如语法错误、索引越界、键不存在等），未处理会导致程序终止 异常处理语法（try-except-else-finally）： try: 缩进的可能出现异常的代码块 except 异常类型1: 缩进的异常1处理代码块 except 异常类型2: 缩进的异常2处理代码块 except: 缩进的所有未捕获异常的处理代码块（万能异常捕获） else: 缩进的try代码块无异常时执行的代码块 finally: 缩进的无论是否出现异常都必须执行的代码块（如资源释放） 示例： try: a = 10 / int(input(\u0026ldquo;请输入一个数字：\u0026rdquo;)) except ZeroDivisionError: print(\u0026ldquo;错误：不能除以0\u0026rdquo;) except ValueError: print(\u0026ldquo;错误：请输入有效的整数\u0026rdquo;) else: print(\u0026ldquo;计算结果：\u0026rdquo;, a) finally: print(\u0026ldquo;程序执行完毕\u0026rdquo;) 十一、文件操作 文件打开与关闭 打开文件：open() 函数，语法：file_obj = open(file_path, mode, encoding=None) file_path：文件路径（绝对路径或相对路径） mode：打开模式，常用： r：只读模式（默认），文件不存在报错 w：写入模式，文件不存在创建，文件存在清空原有内容 a：追加模式，文件不存在创建，文件存在在末尾追加内容 r+：读写模式，文件不存在报错 w+：读写模式，文件不存在创建，文件存在清空原有内容 a+：读写模式，文件不存在创建，文件存在在末尾追加内容 带 b 的模式（如 rb、wb）：二进制模式，用于读写图片、视频等二进制文件，无需指定 encoding encoding：文件编码，常用 utf-8，示例：encoding=\u0026ldquo;utf-8\u0026rdquo; 关闭文件：file_obj.close()，必须关闭文件以释放系统资源 文件读写操作 读取文件： file_obj.read(size)：读取指定字节数（size可选，默认读取全部内容） file_obj.readline()：读取一行内容 file_obj.readlines()：读取所有行，返回列表，每行作为一个元素 写入文件： file_obj.write(content)：写入指定内容（字符串或二进制数据） file_obj.writelines(list)：写入列表中的所有元素（元素需为字符串） 上下文管理器（with 语句）：自动关闭文件，无需手动调用 close()，推荐使用 示例1（读取文件）： with open(\u0026ldquo;test.txt\u0026rdquo;, \u0026ldquo;r\u0026rdquo;, encoding=\u0026ldquo;utf-8\u0026rdquo;) as f: content = f.read() print(content) 示例2（写入文件）： with open(\u0026ldquo;test.txt\u0026rdquo;, \u0026ldquo;w\u0026rdquo;, encoding=\u0026ldquo;utf-8\u0026rdquo;) as f: f.write(\u0026ldquo;Hello Python!\\n\u0026rdquo;) f.writelines([\u0026ldquo;第一行\\n\u0026rdquo;, \u0026ldquo;第二行\\n\u0026rdquo;]) 示例3（追加文件）： with open(\u0026ldquo;test.txt\u0026rdquo;, \u0026ldquo;a\u0026rdquo;, encoding=\u0026ldquo;utf-8\u0026rdquo;) as f: f.write(\u0026ldquo;这是追加的内容\u0026rdquo;) Python 高级语法（纯文本格式，可直接复制保存为TXT） 十二、面向对象编程（OOP） 类与对象的基本概念\n类（class）：是对象的抽象模板，定义了对象的属性和方法 对象：是类的实例，通过类创建具体的对象 语法： class 类名: 类属性：属于类本身，所有实例共享 类属性名 = 属性值\n初始化方法：创建对象时自动调用，用于初始化对象属性 def init(self, 参数列表): # 实例属性：属于单个对象，每个实例独立拥有 self.实例属性名 = 参数值\n实例方法：第一个参数必须是self，代表当前对象 def 方法名(self, 参数列表): 方法体代码\n类方法：第一个参数必须是cls，代表类本身，用@classmethod装饰 @classmethod def 类方法名(cls, 参数列表): 方法体代码\n静态方法：无默认参数，和类、对象无关，用@staticmethod装饰 @staticmethod def 静态方法名(参数列表): 方法体代码\n示例： class Person: 类属性 species = \u0026ldquo;人类\u0026rdquo;\n初始化方法 def init(self, name, age): self.name = name # 实例属性 self.age = age\n实例方法 def introduce(self): print(f\u0026quot;我叫{self.name}，今年{self.age}岁\u0026quot;)\n类方法 @classmethod def get_species(cls): return cls.species\n静态方法 @staticmethod def is_adult(age): return age \u0026gt;= 18\n创建对象（实例化） p1 = Person(\u0026ldquo;张三\u0026rdquo;, 20) p2 = Person(\u0026ldquo;李四\u0026rdquo;, 17)\n访问实例属性和方法 print(p1.name) # 输出 张三 p1.introduce() # 输出 我叫张三，今年20岁\n访问类属性和类方法 print(Person.species) # 输出 人类 print(Person.get_species()) # 输出 人类\n调用静态方法 print(Person.is_adult(20)) # 输出 True print(Person.is_adult(17)) # 输出 False\n三大特性：封装、继承、多态\n封装：隐藏对象的属性和方法，仅通过公开接口访问，提高安全性 私有属性/方法：在名称前加两个下划线__，只能在类内部访问 示例： class Student: def init(self, name, score): self.name = name self.__score = score # 私有属性 def get_score(self): # 公开接口获取私有属性 return self.__score\ndef set_score(self, new_score): # 公开接口修改私有属性，可添加校验 if 0 \u0026lt;= new_score \u0026lt;= 100: self.__score = new_score else: print(\u0026ldquo;分数必须在0-100之间\u0026rdquo;)\ns = Student(\u0026ldquo;王五\u0026rdquo;, 90) print(s.get_score()) # 输出 90 s.set_score(95) print(s.get_score()) # 输出 95 s.set_score(101) # 输出 分数必须在0-100之间\n- 继承：子类继承父类的属性和方法，实现代码复用，子类可扩展或重写父类方法 - 语法：class 子类名(父类名): - 重写：子类定义和父类同名的方法，覆盖父类方法 - super()：调用父类的方法，常用于子类初始化 - 示例： 父类 class Animal: def init(self, name): self.name = name\ndef run(self): print(f\u0026quot;{self.name}在奔跑\u0026quot;) 子类继承父类 class Dog(Animal): # 重写父类方法 def run(self): print(f\u0026quot;{self.name}在飞快地奔跑\u0026quot;)\n# 子类扩展方法 def bark(self): print(f\u0026quot;{self.name}在叫\u0026quot;) class Cat(Animal): def init(self, name, color): super().init(name) # 调用父类初始化方法 self.color = color # 子类新增属性\ndef run(self): print(f\u0026quot;{self.color}的{self.name}在轻盈地奔跑\u0026quot;) dog = Dog(\u0026ldquo;旺财\u0026rdquo;) dog.run() # 输出 旺财在飞快地奔跑 dog.bark() # 输出 旺财在叫\ncat = Cat(\u0026ldquo;咪咪\u0026rdquo;, \u0026ldquo;白色\u0026rdquo;) cat.run() # 输出 白色的咪咪在轻盈地奔跑\n- 多态：不同子类对象调用同名方法，表现出不同行为，依赖于继承和方法重写 - 示例： def animal_run(animal): animal.run()\nanimal_run(Dog(\u0026ldquo;旺财\u0026rdquo;)) # 输出 旺财在飞快地奔跑 animal_run(Cat(\u0026ldquo;咪咪\u0026rdquo;, \u0026ldquo;白色\u0026rdquo;)) # 输出 白色的咪咪在轻盈地奔跑\n特殊方法（魔术方法）\n以双下划线__开头和结尾，在特定场景自动调用 常用特殊方法： init：初始化对象 str：打印对象时调用，返回字符串，示例： class Book: def init(self, title): self.title = title def str(self): return f\u0026quot;书名：{self.title}\u0026quot;\nb = Book(\u0026ldquo;Python编程\u0026rdquo;) print(b) # 输出 书名：Python编程 - repr：控制台输出对象时调用，用于调试 - len：调用len()函数时触发，示例： class MyList: def init(self, data): self.data = data\ndef __len__(self): return len(self.data) ml = MyList([1,2,3]) print(len(ml)) # 输出 3 - add：调用+运算符时触发，实现对象相加，示例： class Point: def init(self, x, y): self.x = x self.y = y\ndef __add__(self, other): return Point(self.x + other.x, self.y + other.y) p1 = Point(1,2) p2 = Point(3,4) p3 = p1 + p2 print(p3.x, p3.y) # 输出 4 6\n十三、装饰器 概念：装饰器是一个函数，用于增强另一个函数的功能，不修改原函数代码，遵循开放封闭原则 基础语法： 定义装饰器函数：接收被装饰函数作为参数，返回一个包装函数 使用@装饰器名 语法，放在被装饰函数定义上方 示例1（无参数装饰器）： 定义装饰器：计算函数执行时间 import time def timer_decorator(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) # 执行原函数 end_time = time.time() print(f\u0026quot;{func.name}执行时间：{end_time - start_time}秒\u0026quot;) return result return wrapper\n使用装饰器 @timer_decorator def my_func(): time.sleep(1) print(\u0026ldquo;函数执行完成\u0026rdquo;)\nmy_func() # 输出 函数执行完成 + 执行时间\n- 示例2（带参数装饰器）：需要嵌套三层函数，最外层函数接收装饰器参数 def repeat_decorator(num): def decorator(func): def wrapper(*args, **kwargs): for _ in range(num): result = func(*args, **kwargs) return result return wrapper return decorator\n使用带参数装饰器 @repeat_decorator(3) def say_hi(): print(\u0026ldquo;Hi!\u0026rdquo;)\nsay_hi() # 连续输出3次 Hi!\n常用内置装饰器：\n@classmethod：定义类方法 @staticmethod：定义静态方法 @property：将方法转换为属性，调用时无需加括号，示例： class Person: def init(self, name, age): self.name = name self.__age = age @property def age(self): return self.__age\n@age.setter def age(self, new_age): if 0 \u0026lt;= new_age \u0026lt;= 120: self.__age = new_age else: print(\u0026ldquo;年龄不合法\u0026rdquo;)\np = Person(\u0026ldquo;张三\u0026rdquo;, 20) print(p.age) # 调用@property装饰的方法，无需括号，输出20 p.age = 25 # 调用@age.setter装饰的方法，修改属性 print(p.age) # 输出25\n十四、迭代器与生成器 迭代器 可迭代对象（Iterable）：能被for循环遍历的对象，如列表、字符串、字典等，实现了__iter__方法 迭代器（Iterator）：实现了__iter__和__next__方法的对象，调用next()函数获取下一个元素，无元素时抛出StopIteration异常 转换方法：使用iter()函数将可迭代对象转为迭代器 示例： lst = [1,2,3] it = iter(lst) # 转为迭代器 print(next(it)) # 输出1 print(next(it)) # 输出2 print(next(it)) # 输出3 print(next(it)) # 抛出StopIteration异常 - 自定义迭代器： class MyIterator: def init(self, max_num): self.max_num = max_num self.current = 0\ndef __iter__(self): return self def __next__(self): if self.current \u0026lt; self.max_num: self.current += 1 return self.current else: raise StopIteration mi = MyIterator(3) for num in mi: print(num) # 输出1 2 3\n生成器 概念：是一种特殊的迭代器，通过yield关键字定义，无需手动实现__iter__和__next__方法，更简洁 工作原理：调用生成器函数时，函数不会立即执行，返回生成器对象；每次调用next()或for循环时，执行到yield暂停，返回yield后的值，下次从暂停处继续执行 示例1（生成器函数）： def my_generator(max_num): current = 1 while current \u0026lt;= max_num: yield current # 暂停并返回值 current += 1 gen = my_generator(3) print(next(gen)) # 输出1 print(next(gen)) # 输出2 print(next(gen)) # 输出3\n- 示例2（生成器表达式）：语法类似列表推导式，用()代替[]，节省内存（无需一次性生成所有元素） gen_expr = (x*2 for x in range(3)) for num in gen_expr: print(num) # 输出0 2 4\n十五、上下文管理器 概念：用于管理资源（如文件、网络连接、数据库连接），确保资源使用后自动释放，避免资源泄漏\n实现方式\n方式1：类实现__enter__和__exit__方法 enter：进入上下文时调用，返回资源对象 exit：退出上下文时调用，处理异常和资源释放 示例： class MyFile: def init(self, file_path, mode): self.file_path = file_path self.mode = mode self.file = None def enter(self): self.file = open(self.file_path, self.mode, encoding=\u0026ldquo;utf-8\u0026rdquo;) return self.file\ndef exit(self, exc_type, exc_val, exc_tb): if self.file: self.file.close() # 返回True可抑制异常，返回False则抛出异常 return False\n使用自定义上下文管理器 with MyFile(\u0026ldquo;test.txt\u0026rdquo;, \u0026ldquo;w\u0026rdquo;) as f: f.write(\u0026ldquo;Hello 上下文管理器\u0026rdquo;)\n- 方式2：使用contextlib模块的@contextmanager装饰器，通过生成器函数实现，更简洁 from contextlib import contextmanager\n@contextmanager def my_open(file_path, mode): f = open(file_path, mode, encoding=\u0026ldquo;utf-8\u0026rdquo;) try: yield f # yield之前是__enter__逻辑，之后是__exit__逻辑 finally: f.close()\nwith my_open(\u0026ldquo;test.txt\u0026rdquo;, \u0026ldquo;r\u0026rdquo;) as f: print(f.read())\n十六、多线程与多进程 多线程：适用于IO密集型任务（如文件读写、网络请求），Python中受GIL（全局解释器锁）限制，同一时间只有一个线程执行Python字节码 使用threading模块 示例： import threading import time def task(name): print(f\u0026quot;任务{name}开始\u0026quot;) time.sleep(2) # 模拟IO操作 print(f\u0026quot;任务{name}结束\u0026quot;)\n创建线程 t1 = threading.Thread(target=task, args=(\u0026ldquo;A\u0026rdquo;,)) t2 = threading.Thread(target=task, args=(\u0026ldquo;B\u0026rdquo;,))\n启动线程 t1.start() t2.start()\n等待线程结束 t1.join() t2.join() print(\u0026ldquo;所有任务完成\u0026rdquo;)\n多进程：适用于CPU密集型任务（如数值计算），不受GIL限制，通过multiprocessing模块实现 示例： from multiprocessing import Process import time def task(name): print(f\u0026quot;任务{name}开始\u0026quot;) time.sleep(2) print(f\u0026quot;任务{name}结束\u0026quot;)\nif name == \u0026ldquo;main\u0026rdquo;: # Windows系统必须加此判断 p1 = Process(target=task, args=(\u0026ldquo;A\u0026rdquo;,)) p2 = Process(target=task, args=(\u0026ldquo;B\u0026rdquo;,))\np1.start() p2.start() p1.join() p2.join() print(\u0026quot;所有任务完成\u0026quot;) 十七、正则表达式 概念：用于匹配、查找、替换字符串的模式，通过re模块实现 常用函数 re.match(pattern, string)：从字符串开头匹配，匹配成功返回匹配对象，否则返回None re.search(pattern, string)：扫描整个字符串，找到第一个匹配项 re.findall(pattern, string)：找到所有匹配项，返回列表 re.sub(pattern, repl, string)：替换所有匹配的子串 常用元字符 .：匹配任意单个字符（除换行符\\n） ^：匹配字符串开头 $：匹配字符串结尾 *：匹配前一个字符0次或多次 +：匹配前一个字符1次或多次 ?：匹配前一个字符0次或1次 []：匹配括号内任意一个字符 \\d：匹配数字，等价于[0-9] \\w：匹配字母、数字、下划线，等价于[a-zA-Z0-9_] \\s：匹配空白字符（空格、制表符、换行符等） 示例： import re 匹配手机号（11位数字，以1开头） pattern = r\u0026quot;^1\\d{10}$\u0026quot; print(re.match(pattern, \u0026ldquo;13812345678\u0026rdquo;)) # 匹配成功 print(re.match(pattern, \u0026ldquo;1234567890\u0026rdquo;)) # 匹配失败\n提取所有数字 string = \u0026ldquo;年龄：20，身高：180cm\u0026rdquo; print(re.findall(r\u0026quot;\\d+\u0026quot;, string)) # 输出 [\u0026lsquo;20\u0026rsquo;, \u0026lsquo;180\u0026rsquo;]\n替换敏感词 string = \u0026ldquo;这个产品真垃圾\u0026rdquo; print(re.sub(r\u0026quot;垃圾\u0026quot;, \u0026ldquo;\u0026rdquo;, string)) # 输出 这个产品真\n十八、高级数据结构 集合（Set）的高级操作\n交集：s1 \u0026amp; s2 或 s1.intersection(s2) 并集：s1 | s2 或 s1.union(s2) 差集：s1 - s2 或 s1.difference(s2) 对称差集：s1 ^ s2 或 s1.symmetric_difference(s2) 示例： s1 = {1,2,3} s2 = {3,4,5} print(s1 \u0026amp; s2) # 交集 {3} print(s1 | s2) # 并集 {1,2,3,4,5} print(s1 - s2) # 差集 {1,2} 有序字典（OrderedDict）：Python3.7前字典无序，需用collections.OrderedDict保证插入顺序\n示例： from collections import OrderedDict od = OrderedDict() od[\u0026ldquo;a\u0026rdquo;] = 1 od[\u0026ldquo;b\u0026rdquo;] = 2 od[\u0026ldquo;c\u0026rdquo;] = 3 for k, v in od.items(): print(k, v) # 按插入顺序输出 a 1, b 2, c 3 计数器（Counter）：用于统计可迭代对象中元素的出现次数，返回字典格式\n示例： from collections import Counter lst = [1,2,2,3,3,3] cnt = Counter(lst) print(cnt) # 输出 Counter({3: 3, 2: 2, 1: 1}) print(cnt.most_common(2)) # 输出出现次数最多的2个元素 [(3, 3), (2, 2)] 命名元组（namedtuple）：创建有名称字段的元组，可读性更高\n示例： from collections import namedtuple Point = namedtuple(\u0026ldquo;Point\u0026rdquo;, [\u0026ldquo;x\u0026rdquo;, \u0026ldquo;y\u0026rdquo;]) p = Point(1, 2) print(p.x) # 输出1 print(p.y) # 输出2 双端队列（deque）：高效实现两端添加和删除元素，比列表更高效，适用于队列和栈场景\n示例： from collections import deque dq = deque([1,2,3]) dq.append(4) # 右端添加 dq.appendleft(0) # 左端添加 print(dq) # 输出 deque([0, 1, 2, 3, 4]) dq.pop() # 右端删除 dq.popleft() # 左端删除 print(dq) # 输出 deque([1, 2, 3]) ","date":"2026-07-25T16:12:15+08:00","permalink":"https://aurora156.github.io/p/python%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95/","title":"Python基础语法"},{"content":" 本文基于黑马2026的课程AI大模型RAG与智能体开发编写，课程内容地址：黑马程序员大模型RAG与Agent智能体项目实战教程，基于主流的LangChain技术从大模型提示词到实战项目_哔哩哔哩_bilibili\nLangchain组件 【扩展】运算符的重载 前文代码中： chain = chat_prompt_template | model\n在语法上使用了 | 运算符的重载\n在 Python 中，运算符（如 +、|）的行为由类的魔法方法决定。例如：\na + b 本质调用的是 a.__add__(b)\t双下划线 a | b 本质调用的是 a.__or__(b) 只需要自行实现类的方法，即可对|符号的功能进行重写。\n示例：\n让 a|b|c 的代码得到一个自定义的类对象(类似列表即[a, b, c]) 调用run方法依次输出a、b、c 我们需要重写 | 即__or__方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 class Test(object): \u0026#34;\u0026#34;\u0026#34;测试类，代表一个带名字的对象\u0026#34;\u0026#34;\u0026#34; def __init__(self, name): \u0026#34;\u0026#34;\u0026#34;初始化方法，设置对象的名称属性\u0026#34;\u0026#34;\u0026#34; self.name = name def __or__(self, other): \u0026#34;\u0026#34;\u0026#34; 重载或运算符(|) 当执行 a | b 时，Python会调用这个方法 参数： self: 左边的操作数 (a) other: 右边的操作数 (b) 返回： 一个新的 MySequence 对象，包含 self 和 other \u0026#34;\u0026#34;\u0026#34; return MySequence(self, other) def __str__(self): \u0026#34;\u0026#34;\u0026#34; 重载字符串表示方法 当使用 print() 或 str() 时会调用此方法 \u0026#34;\u0026#34;\u0026#34; return self.name class MySequence(object): \u0026#34;\u0026#34;\u0026#34;序列类，用于存储多个对象的链式结构\u0026#34;\u0026#34;\u0026#34; def __init__(self, *args): \u0026#34;\u0026#34;\u0026#34; 初始化方法 参数： *args: 可变参数，可以接收任意数量的参数 例如 MySequence(a, b, c) 中 args = (a, b, c) \u0026#34;\u0026#34;\u0026#34; self.sequence = [] # 创建一个空列表用于存储序列元素 for arg in args: # 遍历所有传入的参数 self.sequence.append(arg) # 将每个参数添加到序列中 def __or__(self, other): \u0026#34;\u0026#34;\u0026#34; 重载或运算符(|) 当 MySequence 对象与其他对象进行 | 运算时调用 参数： self: 已有的 MySequence 对象 other: 要添加到序列的新对象 返回： self 自身，支持链式调用 \u0026#34;\u0026#34;\u0026#34; self.sequence.append(other) # 将新元素添加到序列末尾 return self # 返回自身，实现链式调用 def run(self): \u0026#34;\u0026#34;\u0026#34;运行方法，打印序列中的所有元素\u0026#34;\u0026#34;\u0026#34; for i in self.sequence: # 遍历序列中的每个元素 print(i) # 打印元素（会调用元素的 __str__ 方法） # 主程序入口 if __name__ == \u0026#39;__main__\u0026#39;: # 创建多个 Test 对象 a = Test(\u0026#39;a\u0026#39;) b = Test(\u0026#39;b\u0026#39;) c = Test(\u0026#39;c\u0026#39;) e = Test(\u0026#39;e\u0026#39;) f = Test(\u0026#39;f\u0026#39;) g = Test(\u0026#39;g\u0026#39;) # 使用 | 运算符进行链式调用 # 执行过程： # 1. a | b → a.__or__(b) → MySequence(a, b) # 2. MySequence(a, b) | c → MySequence.__or__(c) → MySequence(a, b, c) # 3. 继续添加 e, f, g，最终得到 MySequence(a, b, c, e, f, g) d = a | b | c | e | f | g # 运行序列，打印所有元素 d.run() # 打印 d 的类型，可以看到它是 MySequence 类 print(type(d)) Runnable接口 1 2 3 4 5 6 7 8 9 10 11 12 # 通过多次按住ctrl+鼠标左键点击PromptTemplate可以进入到源码中进行查看 from langchain_core.prompts import PromptTemplate from langchain_community.llms.tongyi import Tongyi prompt = PromptTemplate.from_template(\u0026#34;你是一个AI助手\u0026#34;) model = Tongyi(model=\u0026#34;qwen3-max\u0026#34;) chain = prompt | model | prompt | model # chain.invoke() # chain.stream() print(type(chain)) 通过结果我们可以看到不论 | 后面怎么追加，得到的类型都是RunnableSequence，而RunnableSequence又是Runnable接口的一个子类，所以chain不管怎么追加，invoke方法和stream方法都是有的，通过这个形式，在Langchain里面只要想添加链，在 | 后面添加就可以了\nStrOutputParser解析器 字符串输出解析器\n有如下代码，想要以第一次模型的输出结果，第二次去询问模型：\n链的构建完全符合要求（参与的组件） 但是运行报错（ValueError: Invalid input type \u0026lt;class \u0026rsquo;langchain_core.messages.ai.AIMessage\u0026rsquo;\u0026gt;. Must be a PromptValue, str, or list of BaseMessages.） 1 chain = prompt | model | model 错误的主要原因是：\nprompt的结果是PromptValue类型，输入给了model\nmodel的输出结果是：\n然后将AIMessage输入给下一个model，但是这个输入类型是不符合要求的，所以导致报错\n模型（ChatTongyi）源码中关于invoke方法明确指定了input的类型：\nStrOutputParser是LangChain内置的简单字符串解析器\n可以将AIMessage解析为简单的字符串，符合了模型invoke方法要求（可传入字符串，不接收AIMessage类型） 是Runnable接口的子类（可以加入链） 1 2 parser = StrOutputParser() chain = prompt | model | parser | model 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from langchain_core.messages import AIMessage from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import PromptTemplate from langchain_community.chat_models.tongyi import ChatTongyi parser = StrOutputParser() model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) prompt = PromptTemplate.from_template( \u0026#34;我邻居姓：{lastname}，刚生了{gender}，请起名，仅告知我名字无需其它内容。\u0026#34; ) chain = prompt | model | parser | model | parser # res: AIMessage = chain.invoke({\u0026#34;lastname\u0026#34;: \u0026#34;张\u0026#34;, \u0026#34;gender\u0026#34;: \u0026#34;女儿\u0026#34;}) # print(res:content) # 因为输出结果是AIMessage，所以拿到模型的回复内容需要用到:content # 如果不想用上述形式，解决方法：在第二个model后面再加一个parse![OP](assets/SOP2.png) res: str = chain.invoke({\u0026#34;lastname\u0026#34;: \u0026#34;张\u0026#34;, \u0026#34;gender\u0026#34;: \u0026#34;女儿\u0026#34;}) print(res) print(type(res)) 此处返回的类型本应显示为\u0026lt;class \u0026lsquo;str\u0026rsquo;\u0026gt;，但是返回了TextAccesser类型，显示TextAccesser是 LangChain v1.0+ 的正常行为：\n它本质上就是字符串 - 继承自 str，所有字符串操作都支持\n向后兼容设计 - 为了让旧代码（使用 .text() 方法）和新代码（使用 .text 属性）都能工作\n可以把它当作普通字符串使用\nJsonOutputParser\u0026amp;多模型执行链 1 chain = prompt | model | parser | model | parser 在前面我们完成了这样的需求去构建多模型链，不过这种做法并不标准，因为：\n上一个模型的输出，没有被处理就输入下一个模型。\n正常情况下我们应该有如下处理逻辑：\ninvoke｜stream 初始输入 → 提示词模板 → 模型 → 数据处理 → 提示词模板 → 模型 → 解析器 → 结果\n即：\n上一个模型的输出结果，应该作为提示词模版的输入，构建下一个提示词，用来二次调用模型。 根据输出和输入的要求：\ninvoke｜stream 初始输入 → 提示词模板 → 模型 → 数据处理 → 提示词模板 → 模型 → 解析器 → 结果\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from langchain_core.output_parsers import StrOutputParser, JsonOutputParser from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.prompts import PromptTemplate # 创建所需的解析器 str_parser = StrOutputParser() json_parser = JsonOutputParser() # 模型创建 model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # 第一个提示词模板 first_prompt = PromptTemplate.from_template( \u0026#34;我邻居姓：{lastname}，刚生了{gender}，请帮忙起名字，\u0026#34; # 如果没有下面这行模板，输出结果会报错 \u0026#34;并封装为JSON格式返回给我。要求key是name，value就是你起的名字，请严格遵守格式要求。\u0026#34; ) # 第二个提示词模板 second_prompt = PromptTemplate.from_template( \u0026#34;姓名：{name}，请帮我解析含义。\u0026#34; ) # 构建链 （AIMessage(\u0026#34;{name: 张若曦}\u0026#34;) chain = first_prompt | model | json_parser | second_prompt | model | str_parser for chunk in chain.stream({\u0026#34;lastname\u0026#34;: \u0026#34;张\u0026#34;, \u0026#34;gender\u0026#34;: \u0026#34;女儿\u0026#34;}): print(chunk, end=\u0026#34;\u0026#34;, flush=True) RunnableLambda\u0026amp;函数加入链 chain = first_prompt | model | json_parser | second_prompt | model | str_parser\n前文我们根据JsonOutputParser完成了多模型执行链条的构建。\n除了JsonOutputParser这类固定功能的解析器之外 我们也可以自己编写Lambda匿名函数来完成自定义逻辑的数据转换，想怎么转换就怎么转换，更自由。 想要完成这个功能，可以基于RunnableLambda类实现。\nRunnableLambda类是LangChain内置的，将普通函数等转换为Runnable接口实例，方便自定义函数加入chain。\n语法：\nRunnableLambda(函数对象或lambda匿名函数**)**\n函数直接如链\nchain = first_prompt | model | (lambda ai_msg: {\u0026ldquo;name\u0026rdquo;: ai_msg.content}) | second_prompt | model | str_parser\n跳过RunnableLambda类，直接让函数加入链也是可以的。\n因为Runnable接口类在实现__or__的时候，支持Callable接口的实例。\n函数就是Callable接口的实例 如上代码示例，|符号（底层是调用__or__）组链，是支持函数加入的。其本质是将函数自动转换为RunnableLambda\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import PromptTemplate from langchain_community.chat_models.tongyi import ChatTongyi model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) str_parser = StrOutputParser() first_prompt = PromptTemplate.from_template( \u0026#34;我邻居姓：{lastname}，刚生了{gender}，请帮忙起名字，仅生成一个名字，并告知我名字，不要额外信息。\u0026#34; ) second_prompt = PromptTemplate.from_template( \u0026#34;姓名{name}，请帮我解析含义。\u0026#34; ) # 函数的入参：AIMessage -\u0026gt; dict ({\u0026#34;name\u0026#34;: \u0026#34;xxx\u0026#34;}) # my_func = RunnableLambda(lambda ai_msg: {\u0026#34;name\u0026#34;: ai_msg.content}) chain = first_prompt | model | (lambda ai_msg: {\u0026#34;name\u0026#34;: ai_msg.content}) | second_prompt | model | str_parser for chunk in chain.stream({\u0026#34;lastname\u0026#34;: \u0026#34;曹\u0026#34;, \u0026#34;gender\u0026#34;: \u0026#34;女孩\u0026#34;}): print(chunk, end=\u0026#34;\u0026#34;, flush=True) Memory临时会话记忆 如果想要封装历史记录，除了自行维护历史消息外，也可以借助LangChain内置的历史记录附加功能。\nLangChain提供了History功能，帮助模型在有历史记忆的情况下回答。\n基于RunnableWithMessageHistory在原有链的基础上创建带有历史记录功能的新链（新Runnable实例） 基于InMemoryChatMessageHistory为历史记录提供内存存储（临时用） 完整代码\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.prompts import PromptTemplate, ChatPromptTemplate, MessagesPlaceholder from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.chat_history import InMemoryChatMessageHistory model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # prompt = PromptTemplate.from_template( # \u0026#34;你需要根据会话历史回应用户问题。对话历史：{chat_history}，用户提问：{input}，请回答\u0026#34; # ) prompt = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;你需要根据会话历史回应用户问题。对话历史：\u0026#34;), MessagesPlaceholder(\u0026#34;chat_history\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;请回答如下问题：{input}\u0026#34;) ] ) str_parser = StrOutputParser() # 创建一个打印prompt的函数 def print_prompt(full_prompt): print(\u0026#34;【\u0026#34;, full_prompt.to_string(), \u0026#34;】\u0026#34;) return full_prompt base_chain = prompt | print_prompt | model | str_parser store = {} # 创建一个空字典：key就是session，value就是InMemoryChatMessageHistory类对象 # 实现通过会话id获取InMemoryChatMessageHistory类对象 def get_history(session_id): if session_id not in store: store[session_id] = InMemoryChatMessageHistory() return store[session_id] # 创建一个新的链，对原有链增强功能：自动附加历史消息 conversation_chain = RunnableWithMessageHistory( base_chain, # 被增强的原有chain get_history, # 通过会话id获取InMemoryChatMessageHistory类对象 input_messages_key=\u0026#34;input\u0026#34;, # 表示用户输入在模板中的占位符 history_messages_key=\u0026#34;chat_history\u0026#34; # 表示历史模板中的占位符 ) # 业务 if __name__ == \u0026#39;__main__\u0026#39;: # 固定格式，添加LangChain的配置，为当前程序配置所属的session_id session_config = { \u0026#34;configurable\u0026#34;: { \u0026#34;session_id\u0026#34;: \u0026#34;user_001\u0026#34; } } res = conversation_chain.invoke({\u0026#34;input\u0026#34;: \u0026#34;小明有2个猫\u0026#34;}, session_config) print(\u0026#34;第1次执行：\u0026#34;, res) res = conversation_chain.invoke({\u0026#34;input\u0026#34;: \u0026#34;小刚有1只狗\u0026#34;}, session_config) print(\u0026#34;第2次执行：\u0026#34;, res) res = conversation_chain.invoke({\u0026#34;input\u0026#34;: \u0026#34;总共有几个宠物\u0026#34;}, session_config) print(\u0026#34;第3次执行：\u0026#34;, res) 当前会话是临时的，如果将第1次和第2次执行注释掉，直接执行第3次则会提示如下信息：\nMemory长期会话记忆 使用InMemoryChatMessageHistory仅可以在内存中临时存储会话记忆，一旦程序退出，则记忆丢失。\nInMemoryChatMessageHistory 类继承自 BaseChatMessageHistory\n在官方注释中给出了相关实现的指南，并给出了基于文件的历史消息存储示例代码。\n我们可以自行实现一个基于Json格式和本地文件的会话数据保存。\nFileChatMessageHistory类实现，核心思路：\n基于文件存储会话记录，以session_id为文件名，不同session_id有不同文件存储消息 继承BaseChatMessageHistory实现如下3个方法：\nadd_messages:同步模式，添加消息 messages:同步模式，获取消息 clear：同步模式，清除消息 如下方代码，官方在BaseChatMessageHistory类的注释中提供了一个基于文件存储的示例代码。\n其余核心代码\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 import os, json from typing import Sequence from langchain_community.chat_models import ChatTongyi from langchain_core.messages import message_to_dict, messages_from_dict, BaseMessage from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.runnables import RunnableWithMessageHistory # message_to_dict：单个消息对象（BaseMessage类实例） -\u0026gt; 字典 # messages_from_dict：[字典、字典...] -\u0026gt; [消息、消息...] # AIMessage、HumanMessage、SystemMessage 都是BaseMessage的子类 class FileChatMessageHistory(BaseChatMessageHistory): def __init__(self, session_id, storage_path): self.session_id = session_id # 会话id self.storage_path = storage_path # 不同会话id的存储文件，所在的文件夹路径 # 完整的文件路径 self.file_path = os.path.join(self.storage_path, self.session_id) # 确保文件夹是存在的 os.makedirs(os.path.dirname(self.file_path), exist_ok=True) def add_messages(self, messages: Sequence[BaseMessage]) -\u0026gt; None: # Sequence序列 类似list、tuple all_messages = list(self.messages) # 已有的消息列表 all_messages.extend(messages) # 新的和已有的融合成一个list # 将数据同步写入到本地文件中 # 类对象写入文件 -\u0026gt; 一堆二进制 # 为了方便，可以将BaseMessage消息转为字典（借助json模块以json字符串写入文件） # 官方message_to_dict：单个消息对象（BaseMessage类实例） -\u0026gt; 字典 # new_messages = [] # for message in all_messages: # d = message_to_dict(message) # new_messages.append(d) new_messages = [message_to_dict(message) for message in all_messages] # 将数据写入文件 with open(self.file_path, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: json.dump(new_messages, f) @property # @property装饰器将messages方法变成成员属性用 def messages(self) -\u0026gt; list[BaseMessage]: # 当前文件内： list[字典] try: with open(self.file_path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: messages_data = json.load(f) # 返回值就是：list[字典] return messages_from_dict(messages_data) except FileNotFoundError: return [] def clear(self) -\u0026gt; None: with open(self.file_path, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: json.dump([], f) model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # prompt = PromptTemplate.from_template( # \u0026#34;你需要根据会话历史回应用户问题。对话历史：{chat_history}，用户提问：{input}，请回答\u0026#34; # ) prompt = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;你需要根据会话历史回应用户问题。对话历史：\u0026#34;), MessagesPlaceholder(\u0026#34;chat_history\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;请回答如下问题：{input}\u0026#34;) ] ) str_parser = StrOutputParser() def print_prompt(full_prompt): print(\u0026#34;=\u0026#34;*20, full_prompt.to_string(), \u0026#34;=\u0026#34;*20) return full_prompt base_chain = prompt | print_prompt | model | str_parser def get_history(session_id): return FileChatMessageHistory(session_id, \u0026#34;./chat_history\u0026#34;) # 创建一个新的链，对原有链增强功能：自动附加历史消息 conversation_chain = RunnableWithMessageHistory( base_chain, # 被增强的原有chain get_history, # 通过会话id获取InMemoryChatMessageHistory类对象 input_messages_key=\u0026#34;input\u0026#34;, # 表示用户输入在模板中的占位符 history_messages_key=\u0026#34;chat_history\u0026#34; # 表示用户输入在模板中的占位符 ) if __name__ == \u0026#39;__main__\u0026#39;: # 固定格式，添加LangChain的配置，为当前程序配置所属的session_id session_config = { \u0026#34;configurable\u0026#34;: { \u0026#34;session_id\u0026#34;: \u0026#34;user_001\u0026#34; } } res = conversation_chain.invoke({\u0026#34;input\u0026#34;: \u0026#34;小明有2个猫\u0026#34;}, session_config) print(\u0026#34;第1次执行：\u0026#34;, res) res = conversation_chain.invoke({\u0026#34;input\u0026#34;: \u0026#34;小刚有1只狗\u0026#34;}, session_config) print(\u0026#34;第2次执行：\u0026#34;, res) # res = conversation_chain.invoke({\u0026#34;input\u0026#34;: \u0026#34;总共有几个宠物\u0026#34;}, session_config) # print(\u0026#34;第3次执行：\u0026#34;, res) 先执行第一次和第二次，然后将第一次和第二次注销掉，再执行第三次，可以发现还是能够正常执行\nDocument loaders：文档加载器 文档加载器提供了一套标准接口，用于将不同来源（如 CSV、PDF 或 JSON等）的数据读取为 LangChain 的文档格式。这确保了无论数据来源如何，都能对其进行一致性处理。\n文档加载器（内置或自行实现）需实现BaseLoader接口。\nClass Document，是LangChain内文档的统一载体，所有文档加载器最终返回此类的实例。一个基础的Document类实例，基于如下代码创建：\n可以看到，Document类其核心记录了：\npage_content：文档内容 metadata：文档元数据（字典） 不同的文档加载器可能定义了不同的参数，但是其都实现了统一的接口（方法）。\nload()：一次性加载全部文档 lazy_load()：延迟流式传输文档，对大型数据集很有用，避免内存溢出。 一个简单的CSVLoader的使用示例如下：\nLangChain内置了许多文档加载器，详细参见官方文档：https://docs.langchain.com/oss/python/integrations/document_loaders\n我们简单的学习如下几个常用的文档加载器：\nCSVLoader JSONLoader PDFLoader CSVLoader 自定义CSV文件的解析和加载\n代码示例\ndata.csv\n1 2 3 4 5 6 7 8 9 10 11 name,age,gender,hobby 王梓涵,25,男,\u0026#34;吃饭,rap\u0026#34; 刘若曦,22,女,\u0026#34;睡觉,rap\u0026#34; 陈俊宇,20,男,\u0026#34;吃饭,rap\u0026#34; 赵思瑶,28,女,\u0026#34;睡觉,rap\u0026#34; 黄浩然,15,男,\u0026#34;吃饭,rap\u0026#34; 林雨桐,20,女,\u0026#34;唱跳,rap\u0026#34; 周博文,20,男,\u0026#34;吃饭,rap\u0026#34; 吴诗琪,24,女,\u0026#34;吃饭,rap\u0026#34; 马子轩,22,男,\u0026#34;睡觉,rap\u0026#34; 孙悦然,27,女,\u0026#34;吃饭,rap\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from langchain_community.document_loaders import CSVLoader loader = CSVLoader( file_path=\u0026#34;./data/stu.csv\u0026#34;, csv_args={ \u0026#34;delimiter\u0026#34;: \u0026#34;,\u0026#34;, # 指定分隔符（需要与csv文件中的分隔符一致，否则会导致整个文件解析失败，或者所有数据都被错误地读成一行（或一列）） \u0026#34;quotechar\u0026#34;: \u0026#39;\u0026#34;\u0026#39;, # 指定带有分隔符文本的引号包围是单引号还是双引号 # 如果数据原本有表头，就不要下面的代码，如果没有可以使用 # \u0026#34;fieldnames\u0026#34;: [\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;, \u0026#39;gender\u0026#39;, \u0026#39;爱好\u0026#39;] }, encoding=\u0026#34;utf-8\u0026#34; # 指定编码为UTF-8 ) # 批量加载 .load() -\u0026gt; [Document, Document, ...] # documents = loader.load() # # for document in documents: # print(type(document), document) # 懒加载 .lazy_load() 迭代器[Document] for document in loader.lazy_load(): print(document) 以下为懒加载的结果，如果内存较大可以选择批量加载，否则选择懒加载即可\nJSONLoader JSONLoader用于将JSON数据加载为Document类型对象。\n使用JSONLoader需要额外安装： pip install jq\njq是一个跨平台的json解析工具，LangChain底层对JSON的解析就是基于jq工具实现的。\n将JSON数据的信息抽取出来，封装为Document对象，抽取的时候依赖jq_schema语法。\n了解jq的基本抽取规则后，即可使用JSONLoader加载JSON文件了。\n代码示例\nstu.json\n1 2 3 4 5 6 7 8 9 { \u0026#34;name\u0026#34;: \u0026#34;周杰轮\u0026#34;, \u0026#34;age\u0026#34;: 11, \u0026#34;hobby\u0026#34;: [\u0026#34;唱\u0026#34;, \u0026#34;跳\u0026#34;, \u0026#34;RAP\u0026#34;], \u0026#34;other\u0026#34;: { \u0026#34;addr\u0026#34;: \u0026#34;深圳\u0026#34;, \u0026#34;tel\u0026#34;: \u0026#34;12332112321\u0026#34; } } stu_json_lines.json\n1 2 3 {\u0026#34;name\u0026#34;: \u0026#34;周杰轮\u0026#34;, \u0026#34;age\u0026#34;: 11, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34;} {\u0026#34;name\u0026#34;: \u0026#34;蔡依临\u0026#34;, \u0026#34;age\u0026#34;: 12, \u0026#34;gender\u0026#34;: \u0026#34;女\u0026#34;} {\u0026#34;name\u0026#34;: \u0026#34;王力鸿\u0026#34;, \u0026#34;age\u0026#34;: 11, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34;} stus.json\n1 2 3 4 5 6 7 8 9 10 11 name,age,gender,hobby 王梓涵,25,男,\u0026#34;吃饭,rap\u0026#34; 刘若曦,22,女,\u0026#34;睡觉,rap\u0026#34; 陈俊宇,20,男,\u0026#34;吃饭,rap\u0026#34; 赵思瑶,28,女,\u0026#34;睡觉,rap\u0026#34; 黄浩然,15,男,\u0026#34;吃饭,rap\u0026#34; 林雨桐,20,女,\u0026#34;唱跳,rap\u0026#34; 周博文,20,男,\u0026#34;吃饭,rap\u0026#34; 吴诗琪,24,女,\u0026#34;吃饭,rap\u0026#34; 马子轩,22,男,\u0026#34;睡觉,rap\u0026#34; 孙悦然,27,女,\u0026#34;吃饭,rap\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from langchain_community.document_loaders import JSONLoader loader = JSONLoader( file_path=\u0026#34;./data/stu.json\u0026#34;, # 单独抽取 # jq_schema=\u0026#34;.name\u0026#34;, # jq_schema=\u0026#34;.other.addr\u0026#34;,\t#抽取深圳 # 整体抽取 jq_schema=\u0026#34;.\u0026#34;, # text_content=False, # 告知JSONLoader 我抽取的内容不是字符串 ) # 由于文件比较小，此处使用全加载 document = loader.load() print(document) 1 2 3 4 5 6 7 8 9 10 11 12 from langchain_community.document_loaders import JSONLoader loader = JSONLoader( file_path=\u0026#34;./data/stus.json\u0026#34;, # 告诉JSONLoader，我需要抽取的JSON对象是全部的name字段 jq_schema=\u0026#34;.[].name\u0026#34;, text_content=False, # 告知JSONLoader 我抽取的内容不是字符串 ) # 由于文件比较小，此处使用全加载 document = loader.load() print(document) 1 2 3 4 5 6 7 8 9 10 11 from langchain_community.document_loaders import JSONLoader loader = JSONLoader( file_path=\u0026#34;./data/stu_json_lines.json\u0026#34;, jq_schema=\u0026#34;.name\u0026#34;, text_content=False, # 告知JSONLoader 我抽取的内容不是字符串 json_lines=True # 告知JSONLoader 这是一个JSONLines文件（每一行都是一个独立的标准JSON） ) document = loader.load() print(document) 运行结果同上\n总结\nTextLoader 除了前文学习的三个Loader以外，还有一个基本的加载器：TextLoader\n作用：读取文本文件（如.txt），将全部内容放入一个Document对象中。\nRecursiveCharacterTextSplitter，递归字符文本分割器，主要用于按自然段落分割大文档。是LangChain官方推荐的默认字符分割器。\n它在保持上下文完整性和控制片段大小之间实现了良好平衡，开箱即用效果佳。\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # pip install langchain_text_splitters loader = TextLoader(\u0026#34;./data/Python基础语法.txt\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) docs = loader.load() # [Document] splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 分段的最大字符数 chunk_overlap=50, # 分段之间允许重叠字符数 # 文本自然段落分隔的依据符号 separators=[\u0026#34;\\n\\n\u0026#34;, \u0026#34;\\n\u0026#34;, \u0026#34;。\u0026#34;, \u0026#34;！\u0026#34;, \u0026#34;？\u0026#34;, \u0026#34;.\u0026#34;, \u0026#34;!\u0026#34;, \u0026#34;?\u0026#34;, \u0026#34; \u0026#34;, \u0026#34;\u0026#34;], length_function=len, # 统计字符的依据函数 ) split_docs = splitter.split_documents(docs) print(len(split_docs)) for doc in split_docs: print(\u0026#34;=\u0026#34;*20) print(doc) print(\u0026#34;=\u0026#34;*20) PyPDFLoader LangChain内支持许多PDF的加载器，我们选择其中的PyPDFLoader使用。\nPyPDFLoader加载器，依赖PyPDF库，所以，需要安装它：\npip install pypdf\nPyPDFLoader使用还是比较简单的，如下代码即可快速加载PDF中的文字内容了：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader( file_path=\u0026#34;./data/pdf2.pdf\u0026#34;, mode=\u0026#34;single\u0026#34;, # 默认是page模式，每个页面形成一个Document文档对象， # single模式，不管有多少页，只返回1个Document对象 password=\u0026#34;itheima\u0026#34;\t# 用于读取含有密码的pdf文件 ) i = 0 for doc in loader.lazy_load(): i += 1 print(doc) print(\u0026#34;=\u0026#34;*20, i) Vector stories向量存储 基于LangChain的向量存储，存储嵌入数据，并执行相似性搜索。\n代码示例\nInfo.csv\n1 2 3 4 5 6 7 8 9 10 11 source,info 黑马程序员,Python是世界上最好的编程语言 传智教育,股票代码003032 黑马程序员,LangChain极大地方便了大模型开发 黑马程序员,AI和Python是下一个十年的风口 传智教育,Python学起来很简单的 黑马程序员,学习Python键盘敲烂月薪过万 黑马程序员,努力带来成就，Python助力辉煌 黑马程序员,学习Python的时候也要记得好好休息打打篮球 黑马程序员,明天晚上吃啥子呀 黑马程序员,如何快速减肥呢 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from langchain_core.vectorstores import InMemoryVectorStore from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.document_loaders import CSVLoader vector_store = InMemoryVectorStore( embedding=DashScopeEmbeddings() ) loader = CSVLoader( file_path=\u0026#34;./data/info.csv\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;, source_column=\u0026#34;source\u0026#34;, # 指定本条数据的来源是哪里 ) documents = loader.load() # id1 id2 id3 id4 ... # 向量存储的 新增、删除、检索 vector_store.add_documents( documents=documents, # 被添加的文档，类型：list[Document] ids=[\u0026#34;id\u0026#34;+str(i) for i in range(1, len(documents)+1)] # 给添加的文档提供id（字符串） list[str] ) # 删除 传入[id, id...] vector_store.delete([\u0026#34;id1\u0026#34;, \u0026#34;id2\u0026#34;]) # 检索 返回类型list[Document] result = vector_store.similarity_search( \u0026#34;瑞达法\u0026#34;, 3 # 检索的结果要几个 ) print(result) 由于这是内存存储，只要程序停止数据就丢失了，所以下面我们基于外部数据库的存储：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from langchain_chroma import Chroma from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.document_loaders import CSVLoader # Chroma 向量数据库（轻量级的） # 确保 langchain-chroma chromadb 这两个库安装了的，没有的话请pip install vector_store = Chroma( collection_name=\u0026#34;test\u0026#34;, # 当前向量存储起个名字，类似数据库的表名称 embedding_function=DashScopeEmbeddings(), # 嵌入模型 persist_directory=\u0026#34;./chroma_db\u0026#34; # 指定数据存放的文件夹 ) loader = CSVLoader( file_path=\u0026#34;./data/info.csv\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;, source_column=\u0026#34;source\u0026#34;, # 指定本条数据的来源是哪里 ) documents = loader.load() # id1 id2 id3 id4 ... # 向量存储的 新增、删除、检索 vector_store.add_documents( documents=documents, # 被添加的文档，类型：list[Document] ids=[\u0026#34;id\u0026#34;+str(i) for i in range(1, len(documents)+1)] # 给添加的文档提供id（字符串） list[str] ) # 删除 传入[id, id...] vector_store.delete([\u0026#34;id1\u0026#34;, \u0026#34;id2\u0026#34;]) # 检索 返回类型list[Document] result = vector_store.similarity_search( \u0026#34;Python是不是简单易学呀\u0026#34;, 3, # 检索的结果要几个 filter={\u0026#34;source\u0026#34;: \u0026#34;黑马程序员\u0026#34;} # 过滤的值和values ) print(result) 检索向量并构建提示词 向量存储的实例，通过**add_texts(list[str])**方法可以快速添加到向量存储中。\n流程：\n1.先通过向量存储检索匹配信息\n2.将用户提问和匹配信息一同封装到提示词模板中提问模型\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 \u0026#34;\u0026#34;\u0026#34; 提示词：用户的提问 + 向量库中检索到的参考资料 \u0026#34;\u0026#34;\u0026#34; from langchain_community.chat_models import ChatTongyi from langchain_core.vectorstores import InMemoryVectorStore from langchain_community.embeddings import DashScopeEmbeddings from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser def print_prompt(prompt): print(prompt.to_string()) print(\u0026#34;=\u0026#34; * 20) return prompt model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) prompt = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;以我提供的已知参考资料为主，简洁和专业的回答用户问题。参考资料:{context}。\u0026#34;), (\u0026#34;user\u0026#34;, \u0026#34;用户提问：{input}\u0026#34;) ] ) vector_store = InMemoryVectorStore(embedding=DashScopeEmbeddings(model=\u0026#34;text-embedding-v4\u0026#34;)) # 准备一下资料（向量库的数据） # add_texts 传入一个 list[str] vector_store.add_texts( [\u0026#34;减肥就是要少吃多练\u0026#34;, \u0026#34;在减脂期间吃东西很重要,清淡少油控制卡路里摄入并运动起来\u0026#34;, \u0026#34;跑步是很好的运动哦\u0026#34;]) input_text = \u0026#34;怎么减肥？\u0026#34; # 检索向量库 result = vector_store.similarity_search(input_text, 2) reference_text = \u0026#34;[\u0026#34; for doc in result: reference_text += doc.page_content reference_text += \u0026#34;]\u0026#34; chain = prompt | print_prompt | model | StrOutputParser() res = chain.invoke({\u0026#34;input\u0026#34;: input_text, \u0026#34;context\u0026#34;: reference_text}) print(res) RunnablePassthrough的使用 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 \u0026#34;\u0026#34;\u0026#34; 核心思路：构建一个 RAG（检索增强生成）链 流程：用户提问 → 向量库检索相关资料 → 将提问+资料拼成提示词 → 发送给大模型 → 输出回答 RunnablePassthrough 的作用： 当链的输入是一个简单的字符串（而非 dict）时， RunnablePassthrough 负责将原始输入原封不动地传递到下游。 这样用户调用 chain.invoke(\u0026#34;怎么减肥？\u0026#34;) 时， \u0026#34;怎么减肥？\u0026#34; 既会送给 retriever 做检索，也会作为 \u0026#34;input\u0026#34; 填入提示词模板。 \u0026#34;\u0026#34;\u0026#34; from langchain_community.chat_models import ChatTongyi from langchain_core.documents import Document from langchain_core.runnables import RunnablePassthrough from langchain_core.vectorstores import InMemoryVectorStore from langchain_community.embeddings import DashScopeEmbeddings from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser def print_prompt(prompt): \u0026#34;\u0026#34;\u0026#34;调试用：打印最终拼装好的提示词，方便观察模板变量是否被正确替换\u0026#34;\u0026#34;\u0026#34; print(prompt.to_string()) print(\u0026#34;=\u0026#34; * 20) return prompt # 注意：必须把 prompt 返回回去，否则链就断了 # ======================== 1. 初始化大模型 ======================== model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # ======================== 2. 定义提示词模板 ======================== # 模板中有两个变量： # {context} —— 来自向量检索的结果（参考资料） # {input} —— 来自用户的原始提问 prompt = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;以我提供的已知参考资料为主，简洁和专业的回答用户问题。参考资料:{context}。\u0026#34;), (\u0026#34;user\u0026#34;, \u0026#34;用户提问：{input}\u0026#34;) ] ) # ======================== 3. 创建内存向量库 ======================== # InMemoryVectorStore：数据只存在内存中，程序结束就没了（适合学习演示） # DashScopeEmbeddings：调用通义千问的 embedding 模型，把文本转成向量 vector_store = InMemoryVectorStore(embedding=DashScopeEmbeddings(model=\u0026#34;text-embedding-v4\u0026#34;)) # 准备一下资料（向量库的数据） # add_texts 传入一个 list[str]，内部会自动调用 embedding 模型将文本转为向量并存储 vector_store.add_texts( [\u0026#34;减肥就是要少吃多练\u0026#34;, \u0026#34;在减脂期间吃东西很重要,清淡少油控制卡路里摄入并运动起来\u0026#34;, \u0026#34;跑步是很好的运动哦\u0026#34;]) input_text = \u0026#34;怎么减肥？\u0026#34; # ======================== 4. 创建检索器（Retriever） ======================== # as_retriever() 将向量库包装成一个 Retriever 对象 # search_kwargs={\u0026#34;k\u0026#34;: 2} 表示每次检索返回最相似的 2 条文档 # 重要：retriever 实现了 Runnable 接口，所以可以用 | 管道符与其他组件串联 # - 输入：str（用户的提问） # - 输出：list[Document]（检索到的文档列表） retriever = vector_store.as_retriever(search_kwargs={\u0026#34;k\u0026#34;: 2}) def format_func(docs: list[Document]): \u0026#34;\u0026#34;\u0026#34; 将检索到的 Document 列表转成一个拼接好的字符串， 方便嵌入到提示词的 {context} 中。 例如输出：\u0026#34;[减肥就是要少吃多练跑步是很好的运动哦]\u0026#34; \u0026#34;\u0026#34;\u0026#34; if not docs: return \u0026#34;无相关参考资料\u0026#34; formatted_str = \u0026#34;[\u0026#34; for doc in docs: formatted_str += doc.page_content formatted_str += \u0026#34;]\u0026#34; return formatted_str # ======================== 5. 构建链（Chain） ======================== # 这是最核心的部分，拆解来看： # # 第一层（dict 构造）：{\u0026#34;input\u0026#34;: ..., \u0026#34;context\u0026#34;: ...} # 这一层负责生成一个 dict，作为 prompt 模板的输入参数： # # \u0026#34;input\u0026#34;: RunnablePassthrough() # → RunnablePassthrough() 会把链的原始输入（即 input_text）原样透传 # → 等价于：lambda x: x （把 \u0026#34;怎么减肥？\u0026#34; 直接传过去） # # \u0026#34;context\u0026#34;: retriever | format_func # → 先用 retriever 对输入做向量检索，得到 list[Document] # → 再用 format_func 将 list[Document] 格式化为字符串 # → 等价于：lambda x: format_func(retriever.invoke(x)) # # 第二层：| prompt # → 将 {\u0026#34;input\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;context\u0026#34;: \u0026#34;...\u0026#34;} 填入提示词模板，得到完整 prompt # # 第三层：| print_prompt # → 调试用，打印出拼装好的提示词（并透传给下一步） # # 第四层：| model # → 将提示词发送给大模型，得到 AI 的回复 # # 第五层：| StrOutputParser() # → 将大模型返回的 Message 对象提取为纯字符串 # chain = ( {\u0026#34;input\u0026#34;: RunnablePassthrough(), \u0026#34;context\u0026#34;: retriever | format_func} | prompt | print_prompt | model | StrOutputParser() ) # ======================== 6. 执行链 ======================== # invoke() 触发整条链的执行： # \u0026#34;怎么减肥？\u0026#34; → 向量检索 → 格式化 → 填充模板 → 大模型 → 纯文本回答 res = chain.invoke(input_text) print(res) \u0026#34;\u0026#34;\u0026#34; 完整数据流总结： chain.invoke(\u0026#34;怎么减肥？\u0026#34;) │ ▼ {\u0026#34;input\u0026#34;: RunnablePassthrough(), \u0026#34;context\u0026#34;: retriever | format_func} │ │ │ ▼ │ retriever(\u0026#34;怎么减肥？\u0026#34;) │ │ │ ▼ │ list[Document] （检索到的 2 条文档） │ │ │ ▼ │ format_func(docs) → \u0026#34;[减肥就是要少吃多练跑步是很好的运动哦]\u0026#34; │ ▼ {\u0026#34;input\u0026#34;: \u0026#34;怎么减肥？\u0026#34;, \u0026#34;context\u0026#34;: \u0026#34;[减肥就是要少吃多练跑步是很好的运动哦]\u0026#34;} │ ▼ prompt（填充模板） → PromptValue（完整的提示词） │ ▼ model（调用大模型） → AI 回复 │ ▼ StrOutputParser() → 纯字符串结果 \u0026#34;\u0026#34;\u0026#34; ","date":"2026-07-22T16:04:41+08:00","permalink":"https://aurora156.github.io/p/langchain%E7%BB%84%E4%BB%B6%E7%BB%AD/","title":"Langchain组件（续）"},{"content":"RAG开发 LangChain简介 主要功能 提供：\n提示词优化的相关功能API 调用各类模型的功能API 会话记忆的相关功能API 各类文档管理分析的功能API 构建Agent智能体的相关功能API 各类功能链式执行的能力 LangChain是后续学习RAG开发的主力框架\nLangChain环境部署 LangChain安装 如果清华镜像源不行，可以使用以下命令：\n（社区版）\n官方PyPI源：python -m pip install langchain-community langchain-ollama dashscope chromadb -i https://pypi.org/simple/\n阿里云镜像：python -m pip install langchain-community langchain-ollama dashscope chromadb -i https://mirrors.aliyun.com/pypi/simple/\n基础langchain包：\n官方PyPI源：python -m pip install langchain -i https://pypi.org/simple/\n阿里云镜像：python -m pip install langchain -i https://mirrors.aliyun.com/pypi/simple/\n安装完可以输入python进入python环境进行验证\n进入到python环境中输入\nimport langchain\nprint(langchain.version)\n进行验证\nRAG 介绍 通⽤的基础⼤模型存在一些问题：\nLLM的知识不是实时的，模型训练好后不具备自动更新知识的能力，会导致部分信息滞后 LLM领域知识是缺乏的，大模型的知识来源于训练数据，这些数据主要来自公开的互联网和开源数据集，无法覆盖特定领域或高度专业化的内部知识 幻觉问题，LLM有时会在回答中⽣成看似合理但实际上是错误的信息 数据安全性 RAG（Retrieval-Augmented Generation）即检索增强生成，为大模型提供了从特定数据源检索到的信息，以此来修正和补充生成的答案。可以总结为一个公式：RAG = 检索技术 + LLM 提示\n工作原理 工作流程图解：\nRAG标准流程：\n简单来说，RAG工作分为两条线：离线准备线 / 在线服务线\n标准流程 RAG 标准流程由索引（Indexing）、检索（Retriever）和生成（Generation）三个核心阶段组成。\n索引阶段：通过处理多种来源多种格式的文档提取其中文本，将其切分为标准长度的文本块（chunk），并进行嵌入向量化（embedding），向量存储在向量数据库（vector database）中。 加载文件 内容提取 文本分割 ，形成chunk 文本向量化 存向量数据库 检索阶段，用户输入的查询（query）被转化为向量表示，通过相似度匹配从向量数据库中检索出最相关的文本块。 query向量化 在文本向量中匹配出与问句向量相似的top_k个 生成阶段，检索到的相关文本与原始查询共同构成提示词（Prompt），输入大语言模型（LLM），生成精确且具备上下文关联的回答。 匹配出的文本作为上下文和问题一起添加到prompt中 提交给LLM生成答案 主流向量数据库：\nRAG的核心价值：\n解决知识实效性问题：大模型的训练数据有截止时间，RAG 可以接入最新文档（如公司财报、政策文件），让模型输出 “与时俱进”。 降低模型幻觉：模型的回答基于检索到的事实性资料，而非纯靠自身记忆，大幅减少编造信息的概率。 无需重新训练模型：相比微调（Fine-tuning），RAG 只需更新知识库，成本更低、效率更高。 向量 基础概念 RAG流程中，向量库是一个重要的节点。\n离线流程：知识和信息 → 向量嵌入（向量化） → 存入向量库 在线流程：用户的提问 → 向量嵌入（向量化） → 在向量库中匹配 向量（Vector）就是文本的 “数学身份证”：它把一段文字的语义信息，转换成一串固定长度的数字列表，让计算机能 “看懂” 文字的含义并做相似度计算。\n简单来说，就是让计算机更方便的理解不同的文本内容，是否表述的是一个意思。\n文本嵌入模型（如text-embedding-v1）通过深度学习等技术，从文本提取语义特征并映射为固定长度的数字序列。\n向量嵌入的过程，我们一般选用合适的文本嵌入模型来完成。\n在向量匹配的过程中，如何识别2段文本是否表述相似的含义，主要可以通过如余弦相似度等算法来完成。比如（下列案例中向量为示例，仅描述概念，非真实向量）：\nA： “如何快速学打篮球” → [0.2, 0.5, 0.8] B： “打篮球怎么学得快” → [0.18, 0.52, 0.79] C： “运动后吃什么好呢” → [0.9, 0.1, 0.2] 通过余弦相似度算法可以计算得到：A和B相似度0.999789，A和C相似度0.361446\n由此可通过精确的数学计算，去匹配2段文本是否描述同一个意思，提高语义匹配的效率和精度。\n如何更为精准的完成语义匹配，生成向量的维度是一个很重要的指标。\n如text-embedding-v1模型，可以生成1536维的向量（一段文本固定得到1536个数字序列），比较实用。\n1536个数字表示，这段文本在1536个主题（抽象的语义特征）方向上的得分（强度） 生成向量的维度越多，就更好的记录文本的语义特征，做语义匹配会更加精准。 更多的向量会在计算、存储和匹配过程中，带来更大的压力。 选择合适的向量维度需要在精确和性能之间做平衡。一般1536维算是比较好的选择。\n【扩展】余弦相似度 向量的数字序列，共同决定了向量在高维空间中的方向和长度.而余弦相似度主要就是撇除长度的影响，得到方向的夹角。夹角越小越相似，即方向相同。\n如何体现向量的方向和长度呢？以一维向量为例：\n向量：[-0.5]、[0.5]、[1]的方向和长度。\n向量：[0.5, 0.5]\n[0.7, 0.7]\n[0.7, 0.5]\n[0.5, -0.5]\n[-0.5, -0.5]\n[-0.6, 0.5]\n的方向和长度\nPS：3维乃至更高纬度难以描述，但概念一致\n余弦相似度主要匹配的就是：同向（无所谓长度）\n我们能直接发现\n[0.5, 0.5]和[0.7, 0.7]是同向不同长那计算机如何判定就依赖余弦相似度算法了。 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import numpy as np \u0026#34;\u0026#34;\u0026#34; 计算两个向量的余弦相似度（衡量方向相似性，剔除长度影响） 参数： vec_a (np.array): 向量A vec_b (np.array): 向量B 返回： float: 余弦相似度结果（范围[-1,1]，越接近1方向越一致） 公式： cos_sim = (vec_a · vec_b) / (||vec_a|| × ||vec_b||) 拆解： 1. 点积：vec_a · vec_b = vec_a[0]×vec_b[0] + vec_a[1]×vec_b[1] + ... + vec_a[n]×vec_b[n] 2. 模长：||vec_a|| = √(vec_a[0]² + vec_a[1]² + ... + vec_a[n]²) 3. 模长：||vec_b|| = √(vec_b[0]² + vec_b[1]² + ... + vec_b[n]²) A: [0.5, 0.5] B: [0.7, 0.7] C: [0.7, 0.5] D: [-0.6, -0.5] \u0026#34;\u0026#34;\u0026#34; def get_dot(vec_a, vec_b): \u0026#34;\u0026#34;\u0026#34;计算2个向量的点积，2个向量同维度数字乘积之和\u0026#34;\u0026#34;\u0026#34; if len(vec_a) != len(vec_b): raise ValueError(\u0026#34;2个向量必须维度数量相同\u0026#34;) dot_sum = 0 for a, b in zip(vec_a, vec_b): dot_sum += a * b return dot_sum def get_norm(vec): \u0026#34;\u0026#34;\u0026#34;计算单个向量的模长：对向量的每个数字求平方再求和再开根号\u0026#34;\u0026#34;\u0026#34; sum_square = 0 for v in vec: sum_square += v * v # numpy sqrt函数完成开根号 return np.sqrt(sum_square) def cosine_similarity(vec_a, vec_b): \u0026#34;\u0026#34;\u0026#34;余弦相似度：2个向量的点积 除以 2个向量模长的乘积\u0026#34;\u0026#34;\u0026#34; result = get_dot(vec_a, vec_b) / (get_norm(vec_a) * get_norm(vec_b)) return result if __name__ == \u0026#39;__main__\u0026#39;: vec_a = [0.5, 0.5] vec_b = [0.7, 0.7] vec_c = [0.7, 0.5] vec_d = [-0.6, -0.5] print(\u0026#34;ab:\u0026#34;, cosine_similarity(vec_a, vec_b)) print(\u0026#34;ac:\u0026#34;, cosine_similarity(vec_a, vec_c)) print(\u0026#34;ad:\u0026#34;, cosine_similarity(vec_a, vec_d)) Langchain组件 Models：大语言模型的使用 现在市面上的模型多如牛毛，各种各样的模型不断出现，LangChain模型组件提供了与各种模型的集成，并为所有模型提供一个精简的统一接口。\nLangChain目前支持三种类型的模型：LLMs（大语言模型）、Chat Models(聊天模型)、Embeddings Models(嵌入模型）.\nLLMs:是技术范畴的统称，指基于大参数量、海量文本训练的 Transformer 架构模型，核心能力是理解和生成自然语言，主要服务于文本生成场景 聊天模型:是应用范畴的细分，是专为对话场景优化的 LLMs，核心能力是模拟人类对话的轮次交互，主要服务于聊天场景 文本嵌入模型: 文本嵌入模型接收文本作为输入, 得到文本的向量.LangChain支持的三类模型，它们的使用场景不同，输入和输出不同，开发者需要根据项目需要选择相应。 我们所用的阿里云通义千问系列主要来自于：langchain_community包\nLLMs（阿里云大语言模型的访问） LLMs使用场景最多，常用大模型的下载库：\nhttps://huggingface.co/models https://modelscope.cn/models 同时LangChain支持对许多模型的调用，以通义千问为例：\n1 2 3 4 5 6 from langchain_community.llms.tongyi import Tongyi ### 实例化模型 llm = Tongyi(model=\u0026#39;qwen-max\u0026#39;) ### 模型推理 res = llm.invoke(\u0026#34;帮我讲个笑话吧\u0026#34;) print(res) 如果用的python太新了(比如我的是python3.14) - langchain-tongyi 尚未发布 3.14 的 wheel 包，所以无法安装。\n会出现以下错误:\nDeprecationWarning: langchain-community is being sunset and is no longer actively maintained. See https://github.com/langchain-ai/langchain-community/issues/674 for details and migration guidance toward standalone integration packages.\n有两个解决方案：\n方案一（推荐）：用 Python 3.10~3.12 运行项目\n通义千问相关的 LangChain 包对 3.14 支持不完善，建议用 conda 或 pyenv 创建一个 3.11 环境：\n1 2 3 4 5 conda create -n rag python=3.11 -y conda activate rag pip install langchain langchain-tongyi 方案二：忽略警告继续用旧包\n当前代码功能上是正常的，DeprecationWarning 只是提醒 langchain-community 未来不再维护，现在还能用。如果只是学习用途，可以先忽略：\n1 2 3 4 5 import warnings warnings.filterwarnings(\u0026#34;ignore\u0026#34;, category=DeprecationWarning) from langchain_community.llms.tongyi import Tongyi 如果要访问本地Ollama的模型，简单更改一下代码。\n通过langchain_ollama包导入OllamaLLM类即可（请确保Ollama已经启动并提前下载好要使用的模型）。\n1 2 3 4 5 6 7 from langchain_ollama import OllamaLLM model = OllamaLLM(model=\u0026#34;qwen3\u0026#34;) # 通过invoke方法去调用模型 res = model.invoke(input=\u0026#34;你是谁呀能做什么？\u0026#34;) print(res) Models的流失输出 如果需要流式输出结果，需要将模型的invoke方法改为stream方法即可。\ninvoke方法：一次型返回完整结果 stream方法：逐段返回结果流式输出 阿里云：\n1 2 3 4 5 6 7 8 9 10 11 # langchain_community from langchain_community.llms.tongyi import Tongyi # 不用qwen3-max，因为qwen3-max是聊天模型，qwen-max是大语言模型 model = Tongyi(model=\u0026#34;qwen-max\u0026#34;) # 调用invoke向模型提问 res = model.stream(input=\u0026#34;你是谁呀能做什么？\u0026#34;) for chunk in res: print(chunk, end=\u0026#34;\u0026#34;, flush=True) ollama：\n1 2 3 4 5 6 7 8 from langchain_ollama import OllamaLLM model = OllamaLLM(model=\u0026#34;qwen3\u0026#34;) res = model.stream(input=\u0026#34;你是谁呀能做什么？\u0026#34;) for chunk in res: print(chunk, end=\u0026#34;\u0026#34;, flush=True) Models：聊天模型的使用 Chat Models\n聊天消息包含下面几种类型，使用时需要按照约定传入合适的值：\nAIMessage: 就是 AI 输出的消息，可以是针对问题的回答. (OpenAI库中的assistant角色） HumanMessage: 人类消息就是用户信息，由人给出的信息发送给LLMs的提示信息，比如“实现一个快速排序方法”. (OpenAI库中的user角色） SystemMessage: 可以用于指定模型具体所处的环境和背景，如角色扮演等。你可以在这里给出具体的指示，比如“作为一个代码专家”，或者“返回json格式”. (OpenAI库中的system角色） 单独使用HumanMessage\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.messages import HumanMessage, AIMessage, SystemMessage # 得到模型对象, qwen3-max就是聊天模型 model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # 准备消息列表 messages = [ HumanMessage(content=\u0026#34;写一首唐诗\u0026#34;), ] # 调用stream流式执行 res = model.stream(input=messages) # for循环迭代打印输出，通过.content来获取到内容 for chunk in res: print(chunk.content, end=\u0026#34;\u0026#34;, flush=True) SystemMessage + HumanMessage\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.messages import HumanMessage, AIMessage, SystemMessage # 得到模型对象, qwen3-max就是聊天模型 model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # 准备消息列表 messages = [ SystemMessage(content=\u0026#34;你是一个边塞诗人。\u0026#34;), HumanMessage(content=\u0026#34;写一首唐诗\u0026#34;), ] # 调用stream流式执行 res = model.stream(input=messages) # for循环迭代打印输出，通过.content来获取到内容 for chunk in res: print(chunk.content, end=\u0026#34;\u0026#34;, flush=True) SystemMessage + HumanMessage + AIMessage\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.messages import HumanMessage, AIMessage, SystemMessage # 得到模型对象, qwen3-max就是聊天模型 model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # 准备消息列表 messages = [ SystemMessage(content=\u0026#34;你是一个边塞诗人。\u0026#34;), HumanMessage(content=\u0026#34;写一首唐诗\u0026#34;), AIMessage(content=\u0026#34;锄禾日当午，汗滴禾下土，谁知盘中餐，粒粒皆辛苦。\u0026#34;), HumanMessage(content=\u0026#34;按照你上一个回复的格式，在写一首唐诗。\u0026#34;) ] # 调用stream流式执行 res = model.stream(input=messages) # for循环迭代打印输出，通过.content来获取到内容 for chunk in res: print(chunk.content, end=\u0026#34;\u0026#34;, flush=True) Models：消息的简写形式 SystemMessage、HumanMessage、AIMessage可以有如下的简写形式\n原：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from langchain_community.chat_models.tongyi import ChatTongyi from langchain_core.messages import HumanMessage, AIMessage, SystemMessage # 得到模型对象, qwen3-max就是聊天模型 model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # 准备消息列表 messages = [ SystemMessage(content=\u0026#34;你是一个边塞诗人。\u0026#34;), HumanMessage(content=\u0026#34;写一首唐诗\u0026#34;), AIMessage(content=\u0026#34;锄禾日当午，汗滴禾下土，谁知盘中餐，粒粒皆辛苦。\u0026#34;), HumanMessage(content=\u0026#34;按照你上一个回复的格式，在写一首唐诗。\u0026#34;) ] # 调用stream流式执行 res = model.stream(input=messages) # for循环迭代打印输出，通过.content来获取到内容 for chunk in res: print(chunk.content, end=\u0026#34;\u0026#34;, flush=True) 简：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 得到模型对象, qwen3-max就是聊天模型 model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # 准备消息列表 messages = [ #(角色，内容) 角色：system/human/ai (\u0026#34;system\u0026#34;, \u0026#34;你是一个边塞诗人。\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;写一首唐诗\u0026#34;), (\u0026#34;ai\u0026#34;, \u0026#34;锄禾日当午，汗滴禾下土，谁知盘中餐，粒粒皆辛苦。\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;按照你上一个回复的格式，再写一首唐诗。\u0026#34;) ] # for循环迭代打印输出，通过.content来获取到内容 for chunk in model.stream(input=messages): print(chunk.content, end=\u0026#34;\u0026#34;, flush=True) 通过2元元组封装信息；第一个元素为角色字符串：system/human/ai第二个元素为内容\n区别和优势在于，使用类对象的方式，如下：\n是静态的，一步到位\n直接就得到了Message类的类对象\n简写形式如下：\n是动态的，需要在运行时\n由LangChain内部机制转换为Message类对象\n好处就在于，简写形式避免导包、写起来更简单，更重要的是支持：\n由于是动态，需要转换步骤\n所以简写形式支持内部填充{变量}占位\n可在运行时填充具体值(后续学习提示词模板时用到）\nModels：嵌入模型的使用 Embeddings Models(文本嵌入模型）\nEmbeddings Models嵌入模型的特点：将字符串作为输入，返回一个浮点数的列表（向量）。\n在NLP中，Embedding的作用就是将数据进行文本向量化。\n阿里云千问模型访问方式：\n1 2 3 4 5 6 7 8 9 10 from langchain_community.embeddings import DashScopeEmbeddings # 初始化嵌入模型对象，其默认使用模型是：text-embedding-v1 embed = DashScopeEmbeddings() # 测试 # 单次转换 print(embed.embed_query(\u0026#34;我喜欢你\u0026#34;)) # 批量转换 print(embed.embed_documents([\u0026#39;我喜欢你\u0026#39;, \u0026#39;我稀饭你\u0026#39;, \u0026#39;晚上吃啥\u0026#39;])) 本地Ollama模型访问方式：\n通过langchain_ollama导入OllamaEmbeddings使用，其余不变。\n1 2 3 4 5 6 7 8 9 from langchain_ollama import OllamaEmbeddings model = OllamaEmbeddings(model=\u0026#34;qwen3-embedding\u0026#34;) # 不用invoke stream # embed_query、embed_documents print(model.embed_query(\u0026#34;我喜欢你\u0026#34;)) print(model.embed_documents([\u0026#34;我喜欢你\u0026#34;, \u0026#34;我稀饭你\u0026#34;, \u0026#34;晚上吃啥\u0026#34;])) 在本地下载模型：\n打开命令行，输入ollama pull 模型\n目前所掌握的LangChain API如下：\n方式 LLMs大语言模型 聊天模型 文本嵌入模型 阿里云千问 from langchain_community.llms.tongyi import Tongyi from langchain_community.chat_models.tongyi import ChatTongyi from langchain_community.embeddings import DashScopeEmbeddings ollama本地模型 from langchain_ollama import OllamaLLM from langchain_ollama import ChatOllama from langchain_ollama import OllamaEmbeddings 反复噶 invoke批量 / stream流式 invoke批量 / stream流式 embed_query单次转换 / embed_documents批量转换 通用prompts【zero-shot】 提示词优化在模型应用中非常重要，LangChain提供了PromptTemplate类，用来协助优化提示词。\nPromptTemplate表示提示词模板，可以构建一个自定义的基础提示词模板，支持变量的注入，最终生成所需的提示词。\n标准写法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 from langchain_core.prompts import PromptTemplate from langchain_community.llms.tongyi import Tongyi prompt_template = PromptTemplate.from_template( “我的邻居姓{lastname}, 刚生了{gender}, 帮忙起名字，请简略回答。” ) # 变量注入，生成提示词文本 prompt_text = prompt_template.format(lastname=\u0026#34;张\u0026#34;, gender=\u0026#34;女儿\u0026#34;)\u000bmodel = Tongyi(model=\u0026#34;qwen-max\u0026#34;) # 创建模型对象 res = model.invoke(input=prompt_text) # 调用模型获取结果 print(res) 基于chain链的写法\n1 2 3 4 5 6 7 8 9 10 11 12 13 from langchain_core.prompts import PromptTemplate from langchain_community.llms.tongyi import Tongyi #zero-shot prompt_template = PromptTemplate.from_template( “我的邻居姓{lastname}, 刚生了{gender}, 帮忙起名字，请简略回答。” ) model = Tongyi(model=“qwen-max”) # 创建模型对象 # 生成链 chain = prompt_template | model # 基于链，调用模型获取结果 res = chain.invoke(input={\u0026#34;lastname\u0026#34;: \u0026#34;张\u0026#34;, \u0026#34;gender\u0026#34;: \u0026#34;女儿\u0026#34;}) print(res) zero-sho思想下，可以基于PromptTemplate直接完成 few-shot思想下，需要更换为FewShotPromptTemplate PS：使用PromptTemplate还不如自己手动拼接字符串？\n使用Template模板构建提示词，在大型工程中更容易做标准化模板 Template模板类，支持LangChian框架的链式调用（Runnable接口） PromptTemplate FewShotPromptTemplate ChatPromptTemplate FewShotPromptTemplate的使用 1 2 3 4 5 6 7 8 9 from langchain_core.prompts import FewShotPromptTemplate FewShotPromptTemplate( examples=None, example_prompt=None, prefix=None, suffix=None, input_variables=None ) 参数：\nexamples：示例数据，list，内套字典\nexample_prompt：示例数据的提示词模板\nprefix：组装提示词，示例数据前内容\nsuffix：组装提示词，示例数据后内容\ninput_variables：列表，注入的变量列表\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate from langchain_community.llms.tongyi import Tongyi # 示例的模板 example_template = PromptTemplate.from_template(\u0026#34;单词：{word}, 反义词：{antonym}\u0026#34;) # 示例的动态数据注入 要求是list内部套字典 examples_data = [ {\u0026#34;word\u0026#34;: \u0026#34;大\u0026#34;, \u0026#34;antonym\u0026#34;: \u0026#34;小\u0026#34;}, {\u0026#34;word\u0026#34;: \u0026#34;上\u0026#34;, \u0026#34;antonym\u0026#34;: \u0026#34;下\u0026#34;}, ] few_shot_template = FewShotPromptTemplate( example_prompt=example_template, # 示例数据的模板 examples=examples_data, # 示例的数据（用来注入动态数据的），list内套字典 prefix=\u0026#34;告知我单词的反义词，我提供如下的示例：\u0026#34;, # 示例之前的提示词 suffix=\u0026#34;基于前面的示例告知我，{input_word}的反义词是？\u0026#34;, # 示例之后的提示词 input_variables=[\u0026#39;input_word\u0026#39;] # 声明在前缀或后缀中所需要注入的变量名（可以多个） ) prompt_text = few_shot_template.invoke(input={\u0026#34;input_word\u0026#34;: \u0026#34;左\u0026#34;}).to_string() # to_string()转为字符串输出 print(prompt_text) # 创建模型对象 model = Tongyi(model=\u0026#34;qwen-max\u0026#34;) print(model.invoke(input=prompt_text)) 模板类的format和invoke方法 在PromptTemplate（通用提示词模板）和FewShotPromptTemplate（FewShot提示词模板）的使用中，我们使用了如下：\nPromptTemplate、FewShotPromptTemplate、ChatPromptTemplate都拥有format和invoke这2类方法。\nformat和invoke的区别在于：\n区别 format invoke 功能 纯字符串替换 Runnable接口标准方法，解析占位符生辰提示词 返回值 字符串 PromptValue类对象 传参 .format(k=v，k=v，\u0026hellip;) .invoke({\u0026ldquo;k\u0026rdquo;:v，\u0026ldquo;k\u0026rdquo;:v，\u0026hellip;}) 解析 支持解析{}占位符 支持解析{}占位符和MessagePlaceholder结构化占位符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from langchain_core.prompts import PromptTemplate from langchain_core.prompts import FewShotPromptTemplate from langchain_core.prompts import ChatPromptTemplate from langchain_community.llms.tongyi import Tongyi from langchain_community.chat_models.tongyi import ChatTongyi # 模板继承关系 \u0026#34;\u0026#34;\u0026#34; PromptTemplate -\u0026gt; StringPromptTemplate -\u0026gt; BasePromptTemplate -\u0026gt; RunnableSerializable -\u0026gt; Runnable FewShotPromptTemplate -\u0026gt; StringPromptTemplate -\u0026gt; BasePromptTemplate -\u0026gt; RunnableSerializable -\u0026gt; Runnable ChatPromptTemplate -\u0026gt; BaseChatPromptTemplate -\u0026gt; BasePromptTemplate -\u0026gt; RunnableSerializable -\u0026gt; Runnable Tongyi -\u0026gt; BaseLLM -\u0026gt; BaseLanguageModel -\u0026gt; RunnableSerializable -\u0026gt; Runnable ChatTongyi -\u0026gt; BaseChatModel -\u0026gt; BaseLanguageModel -\u0026gt; RunnableSerializable -\u0026gt; Runnable \u0026#34;\u0026#34;\u0026#34; template = PromptTemplate.from_template(\u0026#34;我的邻居是：{lastname}，最喜欢：{hobby}\u0026#34;) res = template.format(lastname=\u0026#34;张大明\u0026#34;, hobby=\u0026#34;钓鱼\u0026#34;) print(res, type(res)) res2 = template.invoke({\u0026#34;lastname\u0026#34;: \u0026#34;周杰伦\u0026#34;, \u0026#34;hobby\u0026#34;: \u0026#34;唱歌\u0026#34;}) print(res2, type(res2)) ChatPromptTemplate的使用 **PromptTemplate：**通用提示词模板，支持动态注入信息。\n**FewShotPromptTemplate：**支持基于模板注入任意数量的示例信息。\nChatPromptTemplate：支持注入任意数量的历史会话信息。\n通过from_messages方法，从列表中获取多轮次会话作为聊天的基础模板 PS: 前面PromptTemplate类用的from_template仅能接入一条消息，而from_messages可以接入一个list的消息 历史会话信息并不是静态的（固定的），而是随着对话的进行不停地积攒，即动态的。所以，历史会话信息需要支持动态注入。\nMessagePlaceholder作为占位 提供history作为占位的key 基于invoke动态注入历史会话记录 必须是invoke，format无法注入\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_community.chat_models.tongyi import ChatTongyi chat_prompt_template = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;你是一个边塞诗人，可以作诗。\u0026#34;), MessagesPlaceholder(\u0026#34;history\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;请再来一首唐诗\u0026#34;), ] ) history_data = [ (\u0026#34;human\u0026#34;, \u0026#34;你来写一首唐诗\u0026#34;), (\u0026#34;ai\u0026#34;, \u0026#34;床前明月光，疑是地上霜，举头望明月，低头思故乡\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;好诗再来一首\u0026#34;), (\u0026#34;ai\u0026#34;, \u0026#34;锄禾日当午，汗滴禾下锄，谁知盘中餐，粒粒皆辛苦\u0026#34;), ] # invoke的返回类型是StringPromptValue → to_string() 变为字符串 prompt_text = chat_prompt_template.invoke({\u0026#34;history\u0026#34;: history_data}).to_string() #聊天模型 model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) res = model.invoke(prompt_text) print(res.content, type(res))# .content的作用是只取模型返回的内容 也可以基于聊天模型，并组装聊天历史的模式，做提示词工程。\nfew-shot提示方式：求反义词 examples = [ {\u0026ldquo;word\u0026rdquo;: \u0026ldquo;开心\u0026rdquo;, \u0026ldquo;antonym\u0026rdquo;: \u0026ldquo;难过\u0026rdquo;}, {\u0026ldquo;word\u0026rdquo;: \u0026ldquo;高\u0026rdquo;, \u0026ldquo;antonym\u0026rdquo;: \u0026ldquo;矮\u0026rdquo;}, ]\nchain链的基础使用 「将组件串联，上一个组件的输出作为下一个组件的输入」是 LangChain 链（尤其是 | 管道链）的核心工作原理，这也是链式调用的核心价值：实现数据的自动化流转与组件的协同工作，如下。\nchain = prompt_template | model\n核心前提：即Runnable子类对象才能入链（以及Callable、Mapping接口子类对象也可加入（后续了解用的不多））。\n我们目前所学习到的组件，均是Runnable接口的子类，如下类的继承关系：\n通过|链接提示词模板对象和模型对象 返回值chain对象是RunnableSerializable对象 是Runnable接口的直接子类 也是绝大多数组件的父类 通过invoke或stream进行阻塞执行或流式执行 组成的链在执行上有：上一个组件的输出作为下一个组件的输入的特性。\n所以有如下执行流程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_community.chat_models.tongyi import ChatTongyi chat_prompt_template = ChatPromptTemplate.from_messages( [ (\u0026#34;system\u0026#34;, \u0026#34;你是一个边塞诗人，可以作诗。\u0026#34;), MessagesPlaceholder(\u0026#34;history\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;请再来一首唐诗\u0026#34;), ] ) history_data = [ (\u0026#34;human\u0026#34;, \u0026#34;你来写一个唐诗\u0026#34;), (\u0026#34;ai\u0026#34;, \u0026#34;床前明月光，疑是地上霜，举头望明月，低头思故乡\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;好诗再来一个\u0026#34;), (\u0026#34;ai\u0026#34;, \u0026#34;锄禾日当午，汗滴禾下锄，谁知盘中餐，粒粒皆辛苦\u0026#34;), ] model = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) # 组成链，要求每一个组件都是Runnable接口的子类 chain = chat_prompt_template | model #| xxx | xxx | model2 | model3 | ... # 通过链去调用invoke或stream # 通过invoke去触发链条，原始输入是\u0026#34;history\u0026#34;: history_data（提供给chat_prompt_template） # 然后再给到model # res = chain.invoke({\u0026#34;history\u0026#34;: history_data}) # print(res.content) # 通过stream流式输出 for chunk in chain.stream({\u0026#34;history\u0026#34;: history_data}): print(chunk.content, end=\u0026#34;\u0026#34;, flush=True) ","date":"2026-07-13T16:48:40+08:00","permalink":"https://aurora156.github.io/p/rag%E5%BC%80%E5%8F%91-langchain/","title":"RAG开发-LangChain"},{"content":" 本篇博客为学习RAG的前置准备\nRAG前置准备 大模型接入 大模型接入有两种方法：\n此处学习所用，就使用第二种方法了\n1.输入百炼网址\n大模型服务平台百炼控制台\n2.注册/登录\n3.创建自己的API Key\n代码调用云端大模型 创建API Key\n通过pip为Python程序提供OpenAI库：pip install openai（如果pip用不了，就用python -m pip install openai）\n编写代码测试（可从百炼平台官网复制代码，直接执行）\n注意：要将代码中的APIKEY替换为自己的\n点击运行：\n如果想要更换模型，只需要将代码中的model替换为自己需要的模型即可\n那么如何进行配置呢？（win）\n打开电脑的高级系统设置，在环境变量中的用户变量中新建两个变量：OPENAI_API_KEY和DASHSCOPE_API_KEY，将自己的api_key输入进去并保存。 保存完以后就可以将项目中的APIKEY那行代码删除了，然后重启电脑进行测试 以后写代码就不用明文的将APIKEY写出来了，这样既安全又方便\nOllama简介 官网：Ollama\n如何查看ollama支持的模型？\n通过ollama官方模型库：https://ollama.com/library\nOllama的部署 Ollama的部署还是很简单的，只需要进入官方网站点击Download按钮下载安装。\n安装完成后，通过命令：\nollama run 模型名称 即可运行对应的模型，并在命令行内做交互\n也可以在安装完成后直接打开ollama，然后在控制台直接进行交互。不过需要注意的是最好将下载地址更改一下，一般原始的下载地址在C盘，对于C盘空间不太友好的人不太方便。（因为里面的模型需要下载才能使用）\n使用代码调用ollama 注意：需要将本地的ollama启动起来才可以在编译器中调用\n使用ollama仅为备用方案，是为了如果阿里云免费额度用完了，又或者阿里云不再提供免费的活动了时进行使用\nOpenAI库基础使用 OpenAI的基础使用 获取客户端对象 调用模型 处理结果 代码示例\n注意：一般把AI的性格（system）设定为话不多，否则消耗的Token会很多\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from urllib import response from httpx import Client from openai import OpenAI # 1.获取Client对象 Client = OpenAI( base_url=\u0026#34;https://ws-bt194c2c9a0aiu59.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\u0026#34; ) # 2.调用模型 response = Client.chat.completions.create( model=\u0026#34;qwen3-max\u0026#34;, messages=[ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一个Python编程专家，并且不说废话简单回答\u0026#34;},# 设置AI性格 {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;好的，我是编程专家，并且话不多，你要问什么？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;输出1-10的数字，使用pyhton代码\u0026#34;} ] ) # 3.处理结果 print(response.choices[0].message.content) OpenAI库的流式输出 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from urllib import response from httpx import Client from openai import OpenAI # 1.获取Client对象 Client = OpenAI( base_url=\u0026#34;https://ws-bt194c2c9a0aiu59.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\u0026#34; ) # 2.调用模型 response = Client.chat.completions.create( model=\u0026#34;qwen3-max\u0026#34;, messages=[ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一个Python编程专家，并且话非常多\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;好的，我是编程专家，并且话不多，你要问什么？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;输出1-10的数字，使用pyhton代码\u0026#34;} ], stream=True ) # 3.处理结果 # print(response.choices[0].message.content) for chunk in response: print(chunk.choices[0].delta.content, end=\u0026#34; \u0026#34;, # 每一段之间以空格分隔 flush=True # 立刻刷新缓冲区 ) OpenAI库附带历史消息调用模型 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from urllib import response from httpx import Client from openai import OpenAI # 1.获取Client对象 Client = OpenAI( base_url=\u0026#34;https://ws-bt194c2c9a0aiu59.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\u0026#34; ) # 2.调用模型 response = Client.chat.completions.create( model=\u0026#34;qwen3-max\u0026#34;, messages=[ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一个AI助理，回答很简洁，不要使用Markdown格式\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;小明有两条宠物狗\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;好的\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;小红有三只宠物猫\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;好的\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;总共有多少只宠物\u0026#34;}, ], stream=True ) # 3.处理结果 for chunk in response: if not chunk.choices: continue delta = chunk.choices[0].delta # 打印思考过程（如果有） if hasattr(delta, \u0026#34;reasoning_content\u0026#34;) and delta.reasoning_content: print(delta.reasoning_content, end=\u0026#34;\u0026#34;, flush=True) # 打印正式回答（过滤掉 None） if delta.content: print(delta.content, end=\u0026#34;\u0026#34;, flush=True) 提示词工程 大模型prompt工程指南 提示词工程概念 提示词技巧 任何Prompt技巧，都不如清晰的表达你的需求。这就类似人与人沟通，如果话都说不明白，不可能让别人理解你的思想。因此，写出清晰的指令，是核心。\n技巧1：详细的描述 技巧2：让模型充当某个角色 技巧3：使用分隔符标明输入的不同部分\n中括号、XML标签、三引号等分隔符可以帮助划分要区别对待的文本，也可以帮助模型更好的理解文本内容。常用\u0026rsquo;\u0026lsquo;\u0026lsquo;\u0026lsquo;\u0026lsquo;\u0026lsquo;把内容框起来\n技巧4：对任务指定步骤\n对于可以拆分的任务可以尽量拆开，最好能为其指定一系列步骤，明确步骤可以让模型更容易实现它们。\n技巧5：提供例子\n本质类似于few-shot learning。先扔给大模型举例，然后让模型按照例子来输出\n基于文本文档，辅助大模型回答，降低模型“幻觉”（一本正经的胡说八道）问题。\n即使用参考文本作答 经典的知识库用法，让大模型使用我们提供的信息来组成答案。\n实战案例 背景 Zero-shot思想 Few-shot思想 LLM文本分类 任务介绍 Prompt设计 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 from openai import OpenAI # 1. 获取client对象，OpenAI类对象 client = OpenAI( base_url=\u0026#34;https://dashscope.aliyuncs.com/compatible-mode/v1\u0026#34; # 基于本地ollama运行 # base_url=\u0026#34;http://localhost:11434/v1\u0026#34; ) examples_data = { # 示例数据 \u0026#39;新闻报道\u0026#39;: \u0026#39;今日，股市经历了一轮震荡，受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整，以适应市场的不确定性。\u0026#39;, \u0026#39;财务报告\u0026#39;: \u0026#39;本公司年度财务报告显示，去年公司实现了稳步增长的盈利，同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。\u0026#39;, \u0026#39;公司公告\u0026#39;: \u0026#39;本公司高兴地宣布成功完成最新一轮并购交易，收购了一家在人工智能领域领先的公司。这一战略举措将有助于扩大我们的业务领域，提高市场竞争力\u0026#39;, \u0026#39;分析师报告\u0026#39;: \u0026#39;最新的行业分析报告指出，科技公司的创新将成为未来增长的主要推动力。云计算、人工智能和数字化转型被认为是引领行业发展的关键因素，投资者应关注这些趋势\u0026#39; } # 分类列表 examples_types = [\u0026#39;新闻报道\u0026#39;, \u0026#39;财务报道\u0026#39;, \u0026#39;公司公告\u0026#39;, \u0026#39;分析师报告\u0026#39;] # 提问数据 questions = [ \u0026#34;今日，央行发布公告宣布降低利率，以刺激经济增长。这一降息举措将影响贷款利率，并在未来几个季度内对金融市场产生影响。\u0026#34;, \u0026#34;ABC公司今日发布公告称，已成功完成对XYZ公司股权的收购交易。本次交易是ABC公司在扩大业务范围、加强市场竞争力方面的重要举措。据悉，此次收购将进一步巩固ABC公司在行业中的地位，并为未来业务发展提供更广阔的发展空间。详情请见公司官方网站公告栏\u0026#34;, \u0026#34;公司资产负债表显示，公司偿债能力强劲，现金流充足，为未来投资和扩张提供了坚实的财务基础。\u0026#34;, \u0026#34;最新的分析报告指出，可再生能源行业预计将在未来几年经历持续增长，投资者应该关注这一领域的投资机会\u0026#34;, \u0026#34;小明喜欢小新哟\u0026#34; ] \u0026#34;\u0026#34;\u0026#34; [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是金融专家，将文本分类为[\u0026#39;新闻报道\u0026#39;, \u0026#39;财务报道\u0026#39;, \u0026#39;公司公告\u0026#39;, \u0026#39;分析师报告\u0026#39;]，不清楚的分类为\u0026#39;不清楚类别\u0026#39; 下面有示例：\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;今日，央行发布公告宣布降.............\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;新闻报道\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;ABC公司今日发布公告称，已成功完成对XYZ公司股.................\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;财务报告}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;公司资产负债表显示，公司偿债能力强劲，现金流充足...................\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;公司公告\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;最新的分析报告指出，可再生能源.............\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;分析师报告\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;要提问的问题\u0026#34;} ] \u0026#34;\u0026#34;\u0026#34; messages = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是金融专家，将文本分类为[\u0026#39;新闻报道\u0026#39;, \u0026#39;财务报道\u0026#39;, \u0026#39;公司公告\u0026#39;, \u0026#39;分析师报告\u0026#39;]，不清楚的分类为\u0026#39;不清楚类别\u0026#39; 下面有示例：\u0026#34;}, ] for key, value in examples_data.items(): messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: value}) messages.append({\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: key}) # 向模型提问 for q in questions: response = client.chat.completions.create( model=\u0026#34;qwen3-max\u0026#34;, # model=\u0026#34;qwen3\u0026#34;, messages=messages + [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;按照示例，回答这段文本的分类类别：{q}\u0026#34;}] ) print(response.choices[0].message.content) Json数据格式 Python中使用Json\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import json d = { \u0026#34;name\u0026#34;: \u0026#34;周杰轮\u0026#34;, \u0026#34;age\u0026#34;: 11, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34; } # print(str(d)) 输出格式为单引号，json标准格式为双引号 s = json.dumps(d, ensure_ascii=False) #防止中文乱码 print(s) l = [ { \u0026#34;name\u0026#34;: \u0026#34;周杰轮\u0026#34;, \u0026#34;age\u0026#34;: 11, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;蔡依临\u0026#34;, \u0026#34;age\u0026#34;: 12, \u0026#34;gender\u0026#34;: \u0026#34;女\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;小明\u0026#34;, \u0026#34;age\u0026#34;: 16, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34; } ] print(json.dumps(l, ensure_ascii=False)) json_str = \u0026#39;{\u0026#34;name\u0026#34;: \u0026#34;周杰轮\u0026#34;, \u0026#34;age\u0026#34;: 11, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34;}\u0026#39; json_array_str = \u0026#39;[{\u0026#34;name\u0026#34;: \u0026#34;周杰轮\u0026#34;, \u0026#34;age\u0026#34;: 11, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;蔡依临\u0026#34;, \u0026#34;age\u0026#34;: 12, \u0026#34;gender\u0026#34;: \u0026#34;女\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;小明\u0026#34;, \u0026#34;age\u0026#34;: 16, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34;}]\u0026#39; res_dict = json.loads(json_str) print(res_dict, type(res_dict)) res_list = json.loads(json_array_str) print(res_list, type(res_list)) LLM信息抽取 任务介绍 Prompt设计 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 from openai import OpenAI import json client = OpenAI( base_url=\u0026#34;https://dashscope.aliyuncs.com/compatible-mode/v1\u0026#34; # base_url=\u0026#34;http://localhost:11434/v1\u0026#34; ) schema = [\u0026#39;日期\u0026#39;, \u0026#39;股票名称\u0026#39;, \u0026#39;开盘价\u0026#39;, \u0026#39;收盘价\u0026#39;, \u0026#39;成交量\u0026#39;] examples_data = [ # 示例数据 { \u0026#34;content\u0026#34;: \u0026#34;2023-01-10，股市震荡。股票强大科技A股今日开盘价100人民币，一度飙升至105人民币，随后回落至98人民币，最终以102人民币收盘，成交量达到520000。\u0026#34;, \u0026#34;answers\u0026#34;: { \u0026#34;日期\u0026#34;: \u0026#34;2023-01-10\u0026#34;, \u0026#34;股票名称\u0026#34;: \u0026#34;强大科技A股\u0026#34;, \u0026#34;开盘价\u0026#34;: \u0026#34;100人民币\u0026#34;, \u0026#34;收盘价\u0026#34;: \u0026#34;102人民币\u0026#34;, \u0026#34;成交量\u0026#34;: \u0026#34;520000\u0026#34; } }, { \u0026#34;content\u0026#34;: \u0026#34;2024-05-16，股市利好。股票英伟达美股今日开盘价105美元，一度飙升至109美元，随后回落至100美元，最终以116美元收盘，成交量达到3560000。\u0026#34;, \u0026#34;answers\u0026#34;: { \u0026#34;日期\u0026#34;: \u0026#34;2024-05-16\u0026#34;, \u0026#34;股票名称\u0026#34;: \u0026#34;英伟达美股\u0026#34;, \u0026#34;开盘价\u0026#34;: \u0026#34;105美元\u0026#34;, \u0026#34;收盘价\u0026#34;: \u0026#34;116美元\u0026#34;, \u0026#34;成交量\u0026#34;: \u0026#34;3560000\u0026#34; } } ] questions = [ # 提问问题 \u0026#34;2025-06-16，股市利好。股票传智教育A股今日开盘价66人民币，一度飙升至70人民币，随后回落至65人民币，最终以68人民币收盘，成交量达到123000。\u0026#34;, \u0026#34;2025-06-06，股市利好。股票黑马程序员A股今日开盘价200人民币，一度飙升至211人民币，随后回落至201人民币，最终以206人民币收盘。\u0026#34; ] \u0026#34;\u0026#34;\u0026#34; [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;你帮我完成信息抽取，我给你句子，你抽取{schema}信息，按JSON字符串输出，如果某些信息不存在，用\u0026#39;原文未提及\u0026#39;表示，请参考如下示例：\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;2023-01-10，股市震荡。股票强大科技A股今日开盘价100人民币，一度飙升至105人民币，随后回落至98人民币，最终以102人民币收盘，成交量达到520000。\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#39;{\u0026#34;日期\u0026#34;:\u0026#34;2023-01-10\u0026#34;,\u0026#34;股票名称\u0026#34;:\u0026#34;强大科技A股\u0026#34;,\u0026#34;开盘价\u0026#34;:\u0026#34;100人民币\u0026#34;,\u0026#34;收盘价\u0026#34;:\u0026#34;102人民币\u0026#34;,\u0026#34;成交量\u0026#34;:\u0026#34;520000\u0026#34;}\u0026#39;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;2024-05-16，股市利好。股票英伟达美股今日开盘价105美元，一度飙升至109美元，随后回落至100美元，最终以116美元收盘，成交量达到3560000。\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#39;{\u0026#34;日期\u0026#34;:\u0026#34;2024-05-16\u0026#34;,\u0026#34;股票名称\u0026#34;:\u0026#34;英伟达美股\u0026#34;,\u0026#34;开盘价\u0026#34;:\u0026#34;105美元\u0026#34;,\u0026#34;收盘价\u0026#34;:\u0026#34;116美元\u0026#34;,\u0026#34;成交量\u0026#34;:\u0026#34;3560000\u0026#34;}\u0026#39;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;按照上述示例，现在抽取这个句子的信息：{要抽取的句子文本}\u0026#34;}]} ] \u0026#34;\u0026#34;\u0026#34; messages = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;你帮我完成信息抽取，我给你句子，你抽取{schema}信息，按JSON字符串输出，如果某些信息不存在，用\u0026#39;原文未提及\u0026#39;表示，请参考如下示例：\u0026#34;} ] for example in examples_data: messages.append( {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: example[\u0026#34;content\u0026#34;]} ) messages.append( {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: json.dumps(example[\u0026#34;answers\u0026#34;], ensure_ascii=False)} ) for q in questions: response = client.chat.completions.create( model=\u0026#34;qwen3-max\u0026#34;, messages=messages + [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;按照上述的示例，现在抽取这个句子的信息：{q}\u0026#34;}] ) print(response.choices[0].message.content) LLM信息文本匹配 任务介绍 Prompt设计 代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 from openai import OpenAI client = OpenAI( base_url=\u0026#34;https://dashscope.aliyuncs.com/compatible-mode/v1\u0026#34; # base_url=\u0026#34;http://localhost:11434/v1\u0026#34; ) examples_data = { \u0026#34;是\u0026#34;: [ (\u0026#34;公司ABC发布了季度财报，显示盈利增长。\u0026#34;, \u0026#34;财报披露，公司ABC利润上升。\u0026#34;), (\u0026#34;公司ITCAST发布了年度财报，显示盈利大幅度增长。\u0026#34;, \u0026#34;财报披露，公司ITCAST更赚钱了。\u0026#34;) ], \u0026#34;不是\u0026#34;: [ (\u0026#34;黄金价格下跌，投资者抛售。\u0026#34;, \u0026#34;外汇市场交易额创下新高。\u0026#34;), (\u0026#34;央行降息，刺激经济增长。\u0026#34;, \u0026#34;新能源技术的创新。\u0026#34;) ] } questions = [ (\u0026#34;利率上升，影响房地产市场。\u0026#34;, \u0026#34;高利率对房地产有一定的冲击。\u0026#34;), (\u0026#34;油价大幅度下跌，能源公司面临挑战。\u0026#34;, \u0026#34;未来智能城市的建设趋势越加明显。\u0026#34;), (\u0026#34;股票市场今日大涨，投资者乐观。\u0026#34;, \u0026#34;持续上涨的市场让投资者感到满意。\u0026#34;) ] \u0026#34;\u0026#34;\u0026#34; {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;你帮我完成文本匹配，我给你2个句子，被[]包围，你判断它们是否匹配，回答是或不是，请参考如下示例：\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;句子1：[公司ABC发布了季度财报，显示盈利增长。]句子2：[财报披露，公司ABC利润上升。]\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;是\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;句子1：[公司ITCAST发布了年度财报，显示盈利大幅度增长。]句子2：[财报披露，公司ITCAST更赚钱了。]\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;是\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;句子1：[黄金价格下跌，投资者抛售。]句子2：[外汇市场交易额创下新高。]\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;不是\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;句子1：[央行降息，刺激经济增长。]句子2：[新能源技术的创新。]\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;不是\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;按照上述示例，回答这2个句子的情况。句子1: [...]，句子2: [...]\u0026#34;} \u0026#34;\u0026#34;\u0026#34; messages = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;你帮我完成文本匹配，我给你2个句子，被[]包围，你判断它们是否匹配，回答是或不是，请参考如下示例：\u0026#34;}, ] for key, value in examples_data.items(): for t in value: messages.append( {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;句子1：[{t[0]}]，句子2：[{t[1]}]\u0026#34;} ) messages.append( {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: key} ) for q in questions: response = client.chat.completions.create( model=\u0026#34;qwen3-max\u0026#34;, messages=messages + [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;句子1：[{q[0]}]，句子2：[{q[1]}]\u0026#34;}] ) print(response.choices[0].message.content) ","date":"2026-07-01T13:41:02+08:00","permalink":"https://aurora156.github.io/p/rag%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87/","title":"RAG（前置准备）"},{"content":"Docker 在学习完Linux的常见命令，安装软件，以及如何部署一个单体项目后，对于Linux的最大感受就是麻烦，核心体现在三点：\n命令太多了，记不住 软件安装包名字复杂，不知道去哪里找 安装和部署步骤复杂，容易出错 上述问题在运维时进行安装、部署的时候一样会觉得麻烦、容易出错\n今天要学习的Docker技术能够避免部署对服务器环境的依赖，减少复杂的部署流程\n通过今天的学习，要能够达成下面的学习目标：\n能利用Docker部署常见软件 能利用Docker打包并部署Java应用 理解Docker数据卷的基本作用 能看懂DockerCompose文件 快速入门 要想让Docker帮我们安装和部署软件，肯定要保证你的机器上有Docker。由于大家的操作系统各不相同，安装方式也不同。在Linux的学习中，根据资料提供的CentOS的虚拟机中已经安装了Docker，统一学习环境\n如果大家需要自己在别的机器上安装Docker环境，可以参照最后的 附录中的：Docker安装文档\n在学习Docker的时候，先将前面Linux中的进程关闭，可以通过ps -ef|grep nginx和ps -ef|grep java来查看对应的进程并杀死，还有关闭mysql\n部署MySQL 首先，我们利用Docker来安装一个MySQL软件，大家可以对比一下之前传统的安装方式，看看哪个效率更高一些。\n如果是利用传统方式部署MySQL，大概的步骤有：\n搜索并下载MySQL安装包 上传至Linux环境 解压和配置环境 安装 初始化和配置 而使用Docker安装，仅仅需要一步即可，在命令行输入下面的命令（建议采用CV大法）：\n注意：使用时应该先启动docker：systemctl start docker\n1 2 3 4 5 6 docker run -d \\ --name mysql \\ -p 3307:3306 \\ -e TZ=Asia/Shanghai \\ -e MYSQL_ROOT_PASSWORD=123 \\ mysql:8 运行效果如图：\nMySQL安装完毕！通过任意客户端工具即可连接到MySQL。\n以下为通过CMD连接到MySQL\n说明：\n其实，当我们执行命令后，Docker做的第一件事情，是去自动搜索并下载了MySQL，然后会自动运行MySQL，我们完全不用插手，是非常方便的 。\n而且，这种安装方式你完全不用考虑运行的操作系统环境，它不仅仅在CentOS系统是这样，在Ubuntu系统、macOS系统、甚至是装了WSL的Windows下，都可以使用这条命令来安装MySQL 。\n要知道，不同操作系统下其安装包、运行环境是都不相同的！如果是手动安装，必须手动解决安装包不同、环境不同的、配置不同的问题！\n而使用Docker，这些完全不用考虑。就是因为Docker会自动搜索并下载MySQL。\n注意：\n这里下载的不是安装包，而是镜像。镜像中不仅包含了MySQL本身，还包含了其运行所需要的环境、配置、系统级函数库。因此它在运行时就有自己独立的环境，就可以跨系统运行，也不需要手动再次配置环境了。这套独立运行的隔离环境我们称为容器。\n说明：\n镜像：英文是image 容器：英文是container 因此，Docker安装软件的过程，就是自动搜索下载镜像，然后创建并运行容器的过程。\nDocker会根据命令中的镜像名称自动搜索并下载镜像，那么问题来了，它是去哪里搜索和下载镜像的呢？这些镜像又是谁制作的呢？\nDocker官方提供了一个专门管理、存储镜像的网站，并对外开放了镜像上传、下载的权利。Docker官方提供了一些基础镜像，然后各大软件公司又在基础镜像基础上，制作了自家软件的镜像，全部都存放在这个网站。这个网站就成了Docker镜像交流的社区：https://hub.docker.com\n基本上我们常用的各种软件都能在这个网站上找到，我们甚至可以自己制作镜像上传上去。【但是该网站，目前国内上不去了】（但是有魔法的话，还是可以上去的）\n像这种提供存储、管理Docker镜像的服务器，被称为DockerRegistry，可以翻译为镜像仓库。DockerHub网站是官方仓库，阿里云、华为云会提供一些第三方仓库，我们也可以自己搭建私有的镜像仓库。\n官方仓库在国外，下载速度较慢，一般我们都会使用第三方仓库提供的镜像加速功能，提高下载速度。而企业内部的机密项目，往往会采用私有镜像仓库。\n总之，镜像的来源有两种：\n基于官方基础镜像自己制作 直接去DockerRegistry下载 总结一下：\nDocker本身包含一个后台服务，我们可以利用Docker命令告诉Docker服务，帮助我们快速部署指定的应用。Docker服务部署应用时，首先要去搜索并下载应用对应的镜像，然后根据镜像创建并允许容器，应用就部署完成了。\n用一幅图表示如下：\n命令解读 利用Docker快速的安装了MySQL，非常的方便，不过我们执行的命令到底是什么意思呢？\n1 2 3 4 5 6 docker run -d \\ --name mysql \\ -p 3307:3306 \\ -e TZ=Asia/Shanghai \\ -e MYSQL_ROOT_PASSWORD=123 \\ mysql:8 解读：\ndocker run -d ：创建并运行一个容器，-d则是让容器以后台进程运行\n--name mysql : 给容器起个名字叫mysql，你可以叫别的\n-p 3307:3306 : 设置端口映射。\n容器是隔离环境，外界不可访问。但是可以将宿主机端口映射容器内到端口，当访问宿主机指定端口时，就是在访问容器内的端口了。 容器内端口往往是由容器内的进程决定，例如MySQL进程默认端口是3306，因此容器内端口一定是3306；而宿主机端口则可以任意指定，一般与容器内保持一致。 格式： -p 宿主机端口:容器内端口，示例中就是将宿主机的3307映射到容器内的3306端口 -e TZ=Asia/Shanghai : 配置容器内进程运行时的一些参数\n格式：-e KEY=VALUE，KEY和VALUE都由容器内进程决定 案例中，TZ=Asia/Shanghai是设置时区；MYSQL_ROOT_PASSWORD=123是设置MySQL默认密码 可以到官网去查看有哪些参数配置 mysql:8 : 设置镜像名称，Docker会根据这个名字搜索并下载镜像\n格式：REPOSITORY:TAG，例如mysql:8.0，其中REPOSITORY可以理解为镜像名，TAG是版本号 在未指定TAG的情况下，默认是最新版本，也就是mysql:latest 镜像的名称不是随意的，而是要到DockerRegistry中寻找，镜像运行时的配置也不是随意的，要参考镜像的帮助文档，这些在DockerHub网站或者软件的官方网站中都能找到。\n如果我们要安装其它软件，也可以到DockerRegistry中寻找对应的镜像名称和版本，阅读相关配置即可。\nDocker基础 接下来，我们一起来学习Docker使用的一些基础知识，为将来部署项目打下基础。\n常见命令 命令介绍 其中，比较常见的命令有：\n命令 说明 文档地址 docker pull 拉取镜像 docker pull docker push 推送镜像到DockerRegistry docker push docker images 查看本地镜像 docker images docker rmi 删除本地镜像 docker rmi docker run 创建并运行容器（不能重复创建） docker run docker stop 停止指定容器 docker stop docker start 启动指定容器 docker start docker restart 重新启动容器 docker restart docker rm 删除指定容器 docs.docker.com docker ps 查看容器 docker ps docker logs 查看容器运行日志 docker logs docker exec 进入容器 docker exec docker save 保存镜像到本地压缩文件 docker save docker load 加载本地压缩文件到镜像 docker load docker inspect 查看容器详细信息 docker inspect 用一副图来表示这些命令的关系：\n补充：\n默认情况下，每次重启虚拟机我们都需要手动启动Docker和Docker中的容器。通过命令可以实现开机自启：\n1 2 3 4 5 # Docker开机自启 systemctl enable docker # Docker容器开机自启 docker update --restart=always [容器名/容器id] 演示 我们以Nginx为例给大家演示上述命令。\n需求：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 # 第1步，去DockerHub查看nginx镜像仓库及相关信息 # 第2步，拉取Nginx镜像 (比较耗时) docker pull nginx:1.20.2 # 第3步，查看镜像 docker images # 第4步，创建并允许Nginx容器 docker run -d --name nginx -p 80:80 nginx # 第5步，查看运行中容器 docker ps # 也可以加格式化方式访问，格式会更加清爽 docker ps --format \u0026#34;table {{.ID}}\\t{{.Image}}\\t{{.Ports}}\\t{{.Status}}\\t{{.Names}}\u0026#34; # 第6步，访问网页，地址：http://虚拟机地址（192.168.240.128） # 第7步，停止容器 docker stop nginx # 第8步，查看所有容器 docker ps -a # 第9步，再次启动nginx容器 docker start nginx # 第10步，再次查看容器 docker ps # 第11步，查看容器详细信息 docker inspect nginx # 第12步，进入容器,查看容器内目录 docker exec -it nginx bash # 或者，可以进入MySQL docker exec -it mysql mysql -uroot -p # 第13步，删除容器 docker rm nginx # 发现无法删除，因为容器运行中，强制删除容器（容器名/容器ID） docker rm -f nginx 拉取完Nginx可以通过docker save -o nginx-版本号.tar nginx:版本号来进行保存，然后打开文件所在就可以将其下载到本地，然后就可以将这个.tar文件发给其他人，其他人通过上传到自己的Linux上，使用docker load -i nginx-版本号.tar恢复成镜像\n每个容器可以被当成是一个小的系统，它是具备自己的文件系统以及网络环境的\n数据卷 容器是隔离环境，容器内程序的文件、配置、运行时产生的容器都在容器内部，我们要读写容器内的文件非常不方便。大家思考几个问题：\n如果要升级MySQL版本，需要销毁旧容器，那么数据岂不是跟着被销毁了？ MySQL、Nginx容器运行后，如果我要修改其中的某些配置该怎么办？ 我想要让Nginx代理我的静态资源怎么办？ 因此，容器提供程序的运行环境，但是 程序运行产生的数据、程序运行依赖的配置都应该与容器解耦。\n介绍 数据卷（volume）是一个虚拟目录，是容器内目录与宿主机目录之间映射的桥梁。\n以Nginx为例，我们知道Nginx中有两个关键的目录：\nhtml：放置一些静态资源 conf：放置配置文件 如果我们要让Nginx代理我们的静态资源，最好是放到html目录；如果我们要修改Nginx的配置，最好是找到conf下的nginx.conf文件。\n但遗憾的是，容器运行的Nginx所有的文件都在容器内部。所以我们必须利用数据卷将两个目录与宿主机目录关联，方便我们操作。\n如图：\n在上图中：\n我们创建了两个数据卷：conf、html Nginx容器内部的conf目录和html目录分别与两个数据卷关联。 而数据卷conf和html分别指向了宿主机的/var/lib/docker/volumes/conf/_data目录和/var/lib/docker/volumes/html/_data目录 这样一来，容器内的conf和html目录就 与宿主机的conf和html目录关联起来，我们称为挂载。\n此时，我们操作宿主机的/var/lib/docker/volumes/html/_data就是在操作容器内的/usr/share/nginx/html/_data目录。只要我们将静态资源放入宿主机对应目录，就可以被Nginx代理了。\n小提示：\n/var/lib/docker/volumes这个目录就是默认的存放所有容器数据卷的目录，其下再根据数据卷名称创建新目录，格式为/数据卷名/_data。\n为什么不让容器目录直接指向宿主机目录呢？\n因为直接指向宿主机目录就与宿主机强耦合了，如果切换了环境，宿主机目录就可能发生改变了。由于容器一旦创建，目录挂载就无法修改，这样容器就无法正常工作了。 但是容器指向数据卷，一个逻辑名称，而数据卷再指向宿主机目录，就不存在强耦合。如果宿主机目录发生改变，只要改变数据卷与宿主机目录之间的映射关系即可。 不过，我们通过由于数据卷目录比较深，不好寻找，通常我们也允许让容器直接与宿主机目录挂载而不使用数据卷，具体参考2.2.3小节。\n命令 数据卷的相关命令有：【可以直接使用命令 docker volume \u0026ndash;help进行查看】\n命令 说明 文档地址 docker volume create 创建数据卷 docker volume create docker volume ls 查看所有数据卷 docs.docker.com docker volume rm 删除指定数据卷 docs.docker.com docker volume inspect 查看某个数据卷的详情 docs.docker.com docker volume prune 清除数据卷 docker volume prune 注意：容器与数据卷的挂载要在创建容器时配置，对于创建好的容器，是不能设置数据卷的。而且创建容器的过程中，数据卷会自动创建。\n在执行docker run命令时，使用-v 数据卷:容器内目录 形式可以完成数据卷挂载\n演示环节：演示一下nginx的html目录挂载\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 1.首先创建容器并指定数据卷，注意通过 -v 参数来指定数据卷（静态资源存放目录：/usr/share/nginx/html） docker run -d --name nginx -p 80:80 -v html:/usr/share/nginx/html nginx:1.20.2 # 2.然后查看容器 docker ps # 3.然后查看数据卷（可以新开一个标签进行查看数据卷中的内容） docker volume ls # 4.查看数据卷详情 docker volume inspect html # 4.查看/var/lib/docker/volumes/html/_data目录 ll /var/lib/docker/volumes/html/_data #或cd /var/lib/docker/volumes/ #然后ll进行查看 # 5.进入该目录，并随意修改index.html内容 cd /var/lib/docker/volumes/html/_data vi index.html # 6.打开页面，查看效果 # 7.进入新开标签中容器内部，查看/usr/share/nginx/html目录内的文件是否变化 docker exec -it nginx bash cd /usr/share/nginx/html/ ls -l cat index.html 教学演示环节：演示一下MySQL的匿名数据卷\n1 2 3 4 # 1.查看MySQL容器详细信息 docker inspect mysql # 关注其中.Config.Volumes部分和.Mounts部分 我们关注两部分内容，第一是.Config.Volumes部分：\n1 2 3 4 5 6 7 8 9 { \u0026#34;Config\u0026#34;: { // ... 略 \u0026#34;Volumes\u0026#34;: { \u0026#34;/var/lib/mysql\u0026#34;: {} } // ... 略 } } 可以发现这个容器声明了一个本地目录，需要挂载数据卷，但是数据卷未定义。这就是匿名卷。\n然后，我们再看结果中的.Mounts部分：\n1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;Mounts\u0026#34;: [ { \u0026#34;Type\u0026#34;: \u0026#34;volume\u0026#34;, \u0026#34;Name\u0026#34;: \u0026#34;29524ff09715d3688eae3f99803a2796558dbd00ca584a25a4bbc193ca82459f\u0026#34;, \u0026#34;Source\u0026#34;: \u0026#34;/var/lib/docker/volumes/29524ff09715d3688eae3f99803a2796558dbd00ca584a25a4bbc193ca82459f/_data\u0026#34;, \u0026#34;Destination\u0026#34;: \u0026#34;/var/lib/mysql\u0026#34;, \u0026#34;Driver\u0026#34;: \u0026#34;local\u0026#34;, } ] } 可以发现，其中有几个关键属性：\nName：数据卷名称。由于定义容器未设置容器名，这里的就是匿名卷自动生成的名字，一串hash值。 Source：宿主机目录 Destination : 容器内的目录 上述配置是将容器内的/var/lib/mysql这个目录，与数据卷29524ff09715d3688eae3f99803a2796558dbd00ca584a25a4bbc193ca82459f挂载。于是在宿主机中就有了/var/lib/docker/volumes/29524ff09715d3688eae3f99803a2796558dbd00ca584a25a4bbc193ca82459f/_data这个目录。这就是匿名数据卷对应的目录，其使用方式与普通数据卷没有差别。\n接下来，可以查看该目录下的MySQL的data文件：\n1 ls -l /var/lib/docker/volumes/29524ff09715d3688eae3f99803a2796558dbd00ca584a25a4bbc193ca82459f/_data 匿名数据卷作用：数据备份\n注意：每一个不同的镜像，将来创建容器后内部有哪些目录可以挂载，可以参考DockerHub对应的页面 。\n挂载本地目录或文件 可以发现，数据卷的目录结构较深，如果我们去操作数据卷目录会不太方便。在很多情况下，我们会直接将容器目录与宿主机指定目录挂载。挂载语法与数据卷类似：\n1 2 3 4 5 # 挂载本地目录 docker run -d --name 容器名 -p 宿主机端口:容器端口 -v 本地目录:容器内目录 # 挂载本地文件 -v 本地文件:容器内文件 注意：本地目录或文件必须以 / 或 ./开头，如果直接以名字开头，会被识别为数据卷名而非本地目录名。\n例如：\n1 2 3 -v mysql:/var/lib/mysql # 会被识别为一个数据卷叫mysql，运行时会自动创建这个数据卷 -v ./mysql:/var/lib/mysql # 会被识别为当前目录下的mysql目录，运行时如果不存在会创建目录 演示，删除并重新创建mysql容器，并完成本地目录挂载：\n挂载/root/mysql/data到容器内的/var/lib/mysql目录 挂载/root/mysql/init到容器内的/docker-entrypoint-initdb.d目录（初始化的SQL脚本目录） 挂载/root/mysql/conf到容器内的/etc/mysql/conf.d目录（这个是MySQL配置文件目录） 在课前资料中已经准备好了mysql 的init目录、conf目录、data目录，可以直接将其上传到Linux服务器中的 /root/mysql 目录下。\n最终执行的指令如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #停止容器：查看正在运行的容器，找到 MySQL 的容器 ID 或名称： docker ps -a #删除容器： docker rm \u0026lt;容器名或ID\u0026gt; #查看残留的数据卷： docker volume ls #删除数据卷 docker volume rm \u0026lt;数据卷ID\u0026gt; docker run -d \\ --name mysql \\ -p 3307:3306 \\ -e MYSQL_ROOT_PASSWORD=123 \\ -e TZ=Asia/Shanghai \\ -v /root/mysql/data:/var/lib/mysql \\ -v /root/mysql/init:/docker-entrypoint-initdb.d \\ -v /root/mysql/conf:/etc/mysql/conf.d \\ mysql:8 #检验 docker ps cd data/ ll #cmd查看 mysql -h 192.168.240.128 -P3307 -uroot -p123 自定义镜像 前面我们一直在使用别人准备好的镜像，那如果我要部署一个Java项目，把它打包为一个镜像该怎么做呢？ 那接下来，我们就来介绍一下如何自定义镜像。\n镜像结构 要想自己构建镜像，必须先了解镜像的结构。\n之前我们说过，镜像之所以能让我们快速跨操作系统部署应用而忽略其运行环境、配置，就是因为镜像中包含了程序运行需要的系统函数库、环境、配置、依赖。\n因此，自定义镜像本质就是依次准备好程序运行的基础环境、依赖、应用本身、运行配置等文件，并且打包而成。或者说镜像就是包含了应用程序、程序运行的系统函数库、运行配置等文件的文件包，构建镜像的过程其实就是把上述文件打包的过程。\n举个例子，我们要从0部署一个Java应用，大概流程是这样：\n准备一个linux服务（CentOS或者Ubuntu均可） 安装并配置JDK 上传Jar包 运行jar包 那因此，我们打包镜像也是分成这么几步：\n准备Linux运行环境（java项目并不需要完整的操作系统，仅仅是基础运行环境即可） 安装并配置JDK 拷贝jar包 配置启动脚本 上述步骤中的每一次操作其实都是在生产一些文件（系统运行环境、函数库、配置最终都是磁盘文件），所以镜像就是一堆文件的集合。\n但需要注意的是，镜像文件不是随意堆放的，而是按照操作的步骤分层叠加而成，每一层形成的文件都会单独打包并标记一个唯一id，称为Layer（层）。这样，如果我们构建时用到的某些层其他人已经制作过，就可以直接拷贝使用这些层，而不用重复制作。\n例如，第一步中需要的Linux运行环境，通用性就很强，所以Docker官方就制作了这样的只包含Linux运行环境的镜像。我们在制作java镜像时，就无需重复制作，直接使用Docker官方提供的CentOS或Ubuntu镜像作为基础镜像。然后再搭建其它层即可，这样逐层搭建，最终整个Java项目的镜像结构如图所示：\nDockerfile 由于制作镜像的过程中，需要逐层处理和打包，比较复杂，所以Docker就提供了自动打包镜像的功能。我们只需要将打包的过程，每一层要做的事情用固定的语法写下来，交给Docker去执行即可。而这种记录镜像结构的文件就称为Dockerfile。\n其中的语法比较多，比较常用的有：\n指令 说明 示例 FROM 指定基础镜像 FROM centos:7 ENV 设置环境变量，可在后面指令使用 ENV key value COPY 拷贝本地文件到镜像的指定目录 COPY ./xx.jar /tmp/app.jar RUN 执行Linux的shell命令，一般是安装过程的命令 RUN yum install gcc EXPOSE 执行Linux的shell命令，一般是安装过程的命令 EXPOSE 8080 ENTRYPOINT 镜像中应用的启动命令，容器运行时调用 ENTRYPOINT java -jar xx.jar 例如，要基于 centos:7 镜像来构建一个Java应用，其Dockerfile内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 使用 CentOS 7 作为基础镜像 FROM centos:7 # 添加 JDK 到镜像中 COPY jdk17.tar.gz /usr/local/ RUN tar -xzf /usr/local/jdk17.tar.gz -C /usr/local/ \u0026amp;\u0026amp; rm /usr/local/jdk17.tar.gz # 设置环境变量 ENV JAVA_HOME=/usr/local/jdk-17.0.10 # 此处名字为17.0.10是因为这是压缩包解压完后目录的名字 ENV PATH=$JAVA_HOME/bin:$PATH # 创建应用目录 RUN mkdir -p /app WORKDIR /app # 复制应用 JAR 文件到容器 COPY app.jar app.jar # 暴露端口 EXPOSE 8080 # 运行命令 ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-Djava.security.egd=file:/dev/./urandom\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;/app/app.jar\u0026#34;] Dockerfile文件编写好了之后，就可以使用如下命令来构建镜像了。\n1 2 3 docker build -t 镜像名 . #例如： docker build -t myImage:1.0 . -t ：是给镜像起名，格式依然是repository:tag的格式，不指定tag时，默认为latest . ：是指定Dockerfile所在目录，如果就在当前目录，则指定为\u0026quot;.\u0026quot; 演示:\n前端访问端口：192.168.240.128:8080/emps，如果出现json格式数据就说明镜像构建成功了\n网络 上一小节我们创建了一个Java项目的容器，而Java项目往往需要访问其它各种中间件，例如MySQL、Redis等。现在，我们的容器之间能否互相访问呢？我们来测试一下\n首先，我们查看下MySQL容器的详细信息，重点关注其中的网络IP地址：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1.用基本命令，寻找Networks.bridge.IPAddress属性 docker inspect mysql # 也可以使用format过滤结果 docker inspect --format=\u0026#39;{{range .NetworkSettings.Networks}}{{println .IPAddress}}{{end}}\u0026#39; mysql # 得到IP地址如下： 172.17.0.2 # 2.然后通过命令进入myapp容器 docker exec -it myapp bash # 3.在容器内，通过ping命令测试网络 ping 172.17.0.2 # 结果 PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data. 64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.053 ms 64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.059 ms 64 bytes from 172.17.0.2: icmp_seq=3 ttl=64 time=0.058 ms ... #Ctrl + C退出 发现可以互联，没有问题。\n但是，容器的网络IP其实是一个虚拟的IP，其值并不固定与某一个容器绑定，如果我们在开发时写死某个IP，而在部署时很可能MySQL容器的IP会发生变化，连接会失败。\n这时候我们可以通过docker当中的自定义指令来创建自己的网卡，然后在创建容器的时候，让容器连接到我们的自己创建的虚拟网卡当中，如果说我们自己的创建的多个容器都连接到我们自己创建的虚拟网卡当中，那么接下来我们容器之间在进行相互通信的时候就可以通过容器的名字进行通信了，就不用再去写对应的IP地址了，因为容器的名字是固定的、不会变化的\n常见命令有：\n命令 说明 docker network create 创建一个网络 docker network ls 查看所有网络 docker network rm 删除指定网络 docker network prune 清除未使用的网络 docker network connect 使指定容器连接加入某网络 docker network disconnect 使指定容器连接离开某网络 docker network inspect 查看网络详细信息 演示：自定义网络\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 # 1.首先通过命令创建一个网络 docker network create itheima # 2.然后查看网络 docker network ls # 结果： NETWORK ID NAME DRIVER SCOPE c5e8fe6f8384 bridge bridge local 0b8260f77da2 host host local 9ad62123bb7e itheima bridge local 7dc2000c90f6 none null local # 其中，除了itheima以外，其它都是默认的网络 # 查看网络接口ip ip addr # 查看容器信息 docker ps # 3.让 myapp 和 mysql 都加入该网络 # 3.1.mysql容器，加入 itheima 网络 docker network connect itheima mysql # 查看容器是否加入，查看NetworkSettings中是否有myapp和mysql docker inspect mysql #myapp同理 # 3.2.myapp容器，也就是我们的java项目, 加入 itheima 网络 docker network connect itheima myapp # 也可以在创建时加入itheima网络，可以先删除myapp，然后再重新创建，并在创建过程中加入itheima docker run -d --name myapp -p 8080:8080 --network itheima app:1.0 #此次创建的容器中就没有默认的网络，只有itheima这个网络 # 4.进入myapp容器，尝试利用别名访问db # 4.1.进入容器 docker exec -it myapp bash # 4.2.用容器名访问 ping mysql # 结果： PING mysql (172.18.0.2) 56(84) bytes of data. 64 bytes from mysql.itheima (172.18.0.2): icmp_seq=1 ttl=64 time=0.044 ms 64 bytes from mysql.itheima (172.18.0.2): icmp_seq=2 ttl=64 time=0.054 ms ... OK，现在无需记住IP地址也可以实现容器互联了。\n项目部署 部署服务端 需求：将我们开发的 tlias-web-management 项目打包为镜像，并部署。\n步骤：\n1.准备MySQL容器，并创建tlias数据库及表结构。（已完成）\n2.准备java应用（tlias项目）镜像，部署Docker容器，运行测试\n修改tlias项目的配置文件，修改数据库服务地址及Logback日志文件存放地址（打包package）。 编写Dockerfile文件。 构建Docker镜像。 部署Docker容器，运行测试。 1). 修改tlias项目的配置文件，修改数据库服务地址及Logback日志文件存放地址（打包package）。\n然后，执行maven中的package生命周期，进行打包(跳过测试)，并将打包后的jar包命名为 tlias.jar 。\n2). 编写Dockerfile文件。\n文件名 Dockerfile:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 使用 CentOS 7 作为基础镜像 FROM centos:7 # 添加 JDK 到镜像中 COPY jdk17.tar.gz /usr/local/ RUN tar -xzf /usr/local/jdk17.tar.gz -C /usr/local/ \u0026amp;\u0026amp; rm /usr/local/jdk17.tar.gz # 设置环境变量 ENV JAVA_HOME=/usr/local/jdk-17.0.10 ENV PATH=$JAVA_HOME/bin:$PATH # 阿里云OSS环境变量 ENV OSS_ACCESS_KEY_ID=... ENV OSS_ACCESS_KEY_SECRET=... #统一编码 ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV LC_ALL=en_US.UTF-8 # 创建应用目录 RUN mkdir -p /tlias WORKDIR /tlias # 复制应用 JAR 文件到容器 COPY tilas.jar tilas.jar # 暴露端口 EXPOSE 8080 # 运行命令 ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;/tlias/tlias.jar\u0026#34;] 由于项目要运行，需要依赖jdk的环境，所以这里我们需要将tlias.jar，jdk17.tar.gz，Dockerfile三个文件，上传到Linux服务器的 /root/tlias 目录下（如果没有这个目录，提前创建好）。\n3). 构建Docker镜像，部署Docker容器，运行测试。\n构建Docker镜像 1 docker build -t tlias:1.0 . 部署Docker容器 1 docker run -d --name tlias-server --network itheima -p 8080:8080 tlias:1.0 \u0026ndash;network itheima ：将创建的容器，加入到itheima网络，就可以和itheima网络中的容器通信了。\n通过 docker logs -f 容器名，就可以查看容器的运行日志。\n这样后端服务，就已经启动起来了。\n我们可以打开aipfox进行查看\n部署前端 需求：创建一个新的nginx容器，将资料中提供的前端项目的静态资源部署到nginx中。\n步骤：\n在宿主机上准备静态文件及配置文件存放目录(在 /usr/local 目录下创建 tlias-web 目录)。\n​ -v /usr/local/tlias-web/html:/usr/share/nginx/html\n​ -v /usr/local/tlias-web/conf/nginx.conf:/etc/nginx/nginx.conf\n部署nginx容器\n1). 部署nginx容器（设置目录映射）。\n将 资料中的 目录html和 配置文件存放目录 conf，上传至服务器端的 root/tlias-nginx目录下。 执行如下命令，部署nginx容器 1 2 3 4 5 6 7 docker run -d \\ --name nginx-tlias \\ -v /root/tlias-nginx/html:/usr/share/nginx/html \\ -v /root/tlias-nginx/conf/nginx.conf:/etc/nginx/nginx.conf \\ --network itheima \\ -p 80:80 \\ nginx:1.20.2 前后端都部署完毕后，就可以打开浏览器，来测试一下。访问前端的nginx服务器 。\n并且头像上传功能也可以正常使用\nDockerCompose 大家可以看到，我们部署一个简单的java项目，其中包含3个容器：\nMySQL Nginx Java项目 而稍微复杂的项目，其中还会有各种各样的其它中间件，需要部署的东西远不止3个。如果还像之前那样手动的逐一部署，就太麻烦了。\n而Docker Compose就可以帮助我们实现多个相互关联的Docker容器的快速部署。它允许用户通过一个单独的 docker-compose.yml 模板文件（YAML 格式）来定义一组相关联的应用容器。\n基本语法 docker-compose文件中可以定义多个相互关联的应用容器，每一个应用容器被称为一个服务（service）。由于service就是在定义某个应用的运行时参数，因此与docker run参数非常相似。\n举例来说，用docker run部署MySQL的命令如下：\n1 2 3 4 5 6 7 docker run -d \\ --name nginx-tlias \\ -p 80:80 \\ -v /usr/local/app/html:/usr/share/nginx/html \\ -v /usr/local/app/conf/nginx.conf:/etc/nginx/nginx.conf \\ --network itheima \\ nginx:1.20.2 如果用docker-compose.yml文件来定义，就是这样：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 services: mysql: image: \u0026#34;nginx:1.20.2\u0026#34; container_name: nginx-tlias ports: - \u0026#34;80:80\u0026#34; volumes: - \u0026#34;/usr/local/app/html:/usr/share/nginx/html\u0026#34; - \u0026#34;/usr/local/app/conf/nginx.conf:/etc/nginx/nginx.conf\u0026#34; networks: - itheima networks: itheima: name: itheima 对比如下：\ndocker run 参数 docker compose 指令 说明 \u0026ndash;name container_name 容器名称 -p ports 端口映射 -e environment 环境变量 -v volumes 数据卷配置 \u0026ndash;network networks 网络 明白了其中的对应关系，接下来编写docker-compose。\n注意：将之前配置的三个容器需要删除，将docker环境恢复到最初的状态\n只保留这几个镜像和网络即可\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 services: mysql: image: mysql:8 container_name: mysql ports: - \u0026#34;3307:3306\u0026#34; environment: TZ: Asia/Shanghai MYSQL_ROOT_PASSWORD: 123 volumes: - \u0026#34;/usr/local/app/mysql/conf:/etc/mysql/conf.d\u0026#34; - \u0026#34;/usr/local/app/mysql/data:/var/lib/mysql\u0026#34; - \u0026#34;/usr/local/app/mysql/init:/docker-entrypoint-initdb.d\u0026#34; networks: - tlias-net tlias: build: context: . dockerfile: Dockerfile container_name: tlias-server ports: - \u0026#34;8080:8080\u0026#34; networks: - tlias-net depends_on: - mysql nginx: image: nginx:1.20.2 container_name: nginx ports: - \u0026#34;80:80\u0026#34; volumes: - \u0026#34;/usr/local/app/nginx/conf/nginx.conf:/etc/nginx/nginx.conf\u0026#34; - \u0026#34;/usr/local/app/nginx/html:/usr/share/nginx/html\u0026#34; depends_on: - tlias networks: - tlias-net networks: tlias-net: name: itheima 基础命令 编写好docker-compose.yml文件，就可以部署项目了。语法如下：\n1 docker compose [OPTIONS] [COMMAND] 其中，OPTIONS和COMMAND都是可选参数，比较常见的有：\n操作演示 1). 在 Linux 服务器的 /usr/local 目录下创建目录 app，并切换到 /usr/local/app 目录。\n2). 上传资料中提供的 \u0026ldquo;Docker Compose\u0026rdquo; 中的文件及文件夹到 /usr/local/app 目录中，如下所示：\n注意，资料中提供的Dockerfile文件中的阿里云OSS的 AccessKeyId，AccessKeySecret需要替换成自己的。\n3). 执行如下指令，基于DockerCompose部署项目。\n1 docker compose up -d 项目启动完毕之后，就可以启动服务器测试了 。\n当运行完后就可以使用docker compose down删除，等下次需要的时候再使用 docker compose up -d一键构建启动即可\n附录：Docker安装 卸载旧版 首先如果系统中已经存在旧的Docker，则先卸载：\n1 2 3 4 5 6 7 8 9 yum remove docker \\ docker-client \\ docker-client-latest \\ docker-common \\ docker-latest \\ docker-latest-logrotate \\ docker-logrotate \\ docker-engine \\ docker-selinux 配置Docker的yum库 首先要安装一个yum工具\n1 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 安装成功后，执行命令，配置Docker的yum源（已更新为阿里云源）：\n1 2 3 sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sudo sed -i \u0026#39;s+download.docker.com+mirrors.aliyun.com/docker-ce+\u0026#39; /etc/yum.repos.d/docker-ce.repo 更新yum，建立缓存\n1 sudo yum makecache fast 安装Docker 最后，执行命令，安装Docker\n1 yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 启动和校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 启动Docker systemctl start docker # 停止Docker systemctl stop docker # 重启 systemctl restart docker # 设置开机自启 systemctl enable docker # 执行docker ps命令，如果不报错，说明安装启动成功 docker ps 配置镜像加速 镜像地址可能会变更，如果失效可以百度找最新的docker镜像。\n配置镜像步骤如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 创建目录 rm -f /etc/docker/daemon.json # 复制内容 tee /etc/docker/daemon.json \u0026lt;\u0026lt;-\u0026#39;EOF\u0026#39; { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;http://hub-mirror.c.163.com\u0026#34;, \u0026#34;https://mirrors.tuna.tsinghua.edu.cn\u0026#34;, \u0026#34;http://mirrors.sohu.com\u0026#34;, \u0026#34;https://ustc-edu-cn.mirror.aliyuncs.com\u0026#34;, \u0026#34;https://ccr.ccs.tencentyun.com\u0026#34;, \u0026#34;https://docker.m.daocloud.io\u0026#34;, \u0026#34;https://docker.awsl9527.cn\u0026#34; ] } EOF # 重新加载配置 systemctl daemon-reload # 重启Docker systemctl restart docker ","date":"2026-05-30T08:38:52+08:00","permalink":"https://aurora156.github.io/p/docker/","title":"Docker"},{"content":"Linux Linux概述 主流操作系统 不同领域的主流操作系统，主要分为以下这么几类： 桌面操作系统、服务器操作系统、移动设备操作系统、嵌入式操作系统。接下来，这几个领域中，代表性的操作系统是那些?\n1). 桌面操作系统\n操作系统 特点 Windows 用户数量多 MacOS 操作体验好，办公人士首选 Linux 用户数量少（桌面操作系统，Linux使用较少） 2). 服务器操作系统\n操作系统 特点 Unix 安全、稳定、付费 Linux 安全、稳定、免费、占有率高 Windows Server 付费、占有率低 3). 移动设备操作系统\n操作系统 特点 Android 基于 Linux 、开源，主要用于智能手机、平板电脑和智能电视 IOS 苹果公司开发、不开源，用于苹果公司的产品，例如：iPhone、 iPad HarmonyOS 华为公司开发、开源，目前用于华为公司的产品 Linux系统版本 Linux系统的版本分为两种，分别是： 内核版 和 发行版。\n1). 内核版\n由Linus Torvalds及其团队开发、维护 免费、开源 负责控制硬件 2). 发行版\n基于Linux内核版进行扩展 由各个Linux厂商开发、维护 有收费版本和免费版本 我们使用Linux操作系统，实际上选择的是Linux的发行版本。在linux系统中，有各种各样的发行版本，具体如下：\n除了上述罗列出来的发行版，还有很多Linux发行版，这里，我们就不再一一列举了。\n系统安装 安装方式 Linux系统的安装方式，主要包含以下两种：\n虚拟机（Virtual Machine）指通过软件模拟的具有完整硬件系统功能、运行在完全隔离环境中的完整计算机系统。常用虚拟机软件：\nVMWare VirtualBox VMLite WorkStation Qemu HopeddotVOS 那么我们就可以在课程中将Linux操作系统安装在虚拟机中，我们学习选择的虚拟机软件是VMware。\n安装VMware 可以自行去网上查找安装程序\n安装步骤:\n以上就是VMware在安装时的每一步操作，基本上就是点击 \u0026ldquo;下一步\u0026rdquo; 一直进行安装。\n挂载Linux系统 1). 打开Vmware虚拟机，打开 编辑 -\u0026gt; 虚拟网络编辑器(N)...\n选择 NAT模式，然后选择右下角的 更改设置。\n设置子网IP为 192.168.100.0，然后选择 应用 -\u0026gt; 确定。\n由于学习上课需要使用到虚拟机所以我的ip是上课使用的ip就不是教程中所展示的ip了\n2). 解压CentOS7-1.zip到一个比较大的磁盘中 (没有中文的目录)。\n3). 打开解压目录，双击.vmx文件，选择以Vmware Workstation打开这个文件。\n4). 挂载完毕之后，启动Linux服务器。\n启动过程中，如果出现如下界面，选择 我已移动该虚拟机。\n5). 启动完毕之后，登录服务器。 输入用户名：root，密码：1234 （注意：linux系统输入密码是不显示的，输入完毕，回车即可登录）\n安装SSH连接工具 SSH连接工具介绍 Linux已经安装并且配置好了，接下来我们要来学习Linux的基本操作指令。而在学习之前，我们还需要做一件事情，由于我们企业开发时，Linux服务器一般都是在远程的机房部署的，我们要操作服务器，不会每次都跑到远程的机房里面操作，而是会直接通过SSH连接工具进行连接操作。\nSSH（Secure Shell），建立在应用层基础上的安全协议。常用的SSH连接工具:\nFinalShell安装 双击.exe文件，然后进行正常的安装即可。\n连接Linux 打开FinalShell，选择SSH连接。 连接服务器，输入服务器的信息，IP固定的：192.168.100.128，用户名：root，密码：1234 我的ip为192.168.240.128 配置完毕后,双击即可连接服务器。 如果一直连接失败，可以参考我下面的截图进行操作\n目录结构 登录到Linux系统之后，我们需要先来熟悉一下Linux的目录结构。在Linux系统中，也是存在目录的概念的，但是Linux的目录结构和Windows的目录结构是存在比较多的差异的 在Windows目录下，是一个一个的盘符(C盘、D盘、E盘)，目录是归属于某一个盘符的。Linux系统中的目录有以下特点：\n/ 是所有目录的顶点 目录结构像一颗倒挂的树 Linux 和 Windows的目录结构对比: Linux的目录结构，如下：\n根目录 / 下各个目录的作用及含义说明:\n编号 目录 含义 1 /bin 存放二进制可执行文件 2 /boot 存放系统引导时使用的各种文件 3 /dev 存放设备文件 4 /etc 存放系统配置文件 5 /home 存放系统用户的文件 6 /lib 存放程序运行所需的共享库和内核模块 7 /opt 额外安装的可选应用程序包所放置的位置 8 /root 超级用户目录 9 /sbin 存放二进制可执行文件，只有root用户才能访问 10 /tmp 存放临时文件 11 /usr 存放系统应用程序 12 /var 存放运行时需要改变数据的文件，例如日志文件 Linux常用命令 Linux命令初体验 Linux 命令格式：command [-options] [parameter] 说明： command：命令名 [-options]：选项，可用来对命令进行控制，也可以省略 （可选） [parameter]：参数，可以是零个、一个或多个（可选） 例如： 使用技巧：\nTab键自动补全 连续两次Tab键，给出操作提示 使用上下箭头快速调出曾经使用过的命令 使用clear命令或Ctrl+l快捷键实现清屏 目录操作命令 ls 用: 显示指定目录下的内容 语法: ls [-al] [dir] 说明: ​ -a 显示所有文件及目录 (. 开头的隐藏文件也会列出)\n​ -l 除文件名称外，同时将文件型态(d表示目录，-表示文件)、权限、拥有者、文件大小等信息详细列出\n注意: ​ 由于我们使用ls命令时经常需要加入-l选项，所以Linux为 ls -l 命令提供了一种简写方式，即 ll\n常见用法: ​ ls -al 查看当前目录的所有文件及目录详细信息\n​ ls -al /etc 查看/etc目录下所有文件及目录详细信息\n​ ll 查看当前目录文件及目录的详细信息\n操作示例：\ncd 作用: 用于切换当前工作目录，即进入指定目录 语法: cd [dirName] ​\n特殊说明: ​ ~ 表示用户的home目录\n​ . 表示目前所在的目录\n​ .. 表示目前目录位置的上级目录\n​\n举例: ​ cd .. 切换到当前目录的上级目录\n​ cd ~ 切换到用户的home目录\n​ cd /usr/local 切换到/usr/local目录\n备注: ​ 用户的home目录\n​ root用户 /root\n​ 其他用户 /home/xxx\n操作示例:\ncd .. 切换到当前目录位置的上级目录; 可以通过 cd ../.. 来切换到上级目录的上级目录\nmkdir 作用: 创建目录 语法: mkdir [-p] dirName 说明: ​ -p: 确保目录名称存在，不存在的就创建一个。通过此选项，可以实现多层目录同时创建\n举例: ​ mkdir itcast 在当前目录下，建立一个名为itcast的子目录\n​ mkdir -p itcast/test 在工作目录下的itcast目录中建立一个名为test的子目录，若itcast目录不存在，则建立一个\n操作演示:\nrm 作用: 删除文件或者目录 语法: rm [-rf] name 说明: ​ -r: 将目录及目录中所有文件（目录）逐一删除，即递归删除\n​ -f: 无需确认，直接删除\n举例: ​ rm -r itcast/ 删除名为itcast的目录和目录中所有文件，删除前需确认\n​ rm -rf itcast/ 无需确认，直接删除名为itcast的目录和目录中所有文件\nrm -f hello.txt 无需确认，直接删除hello.txt文件\n操作演示：\n注意: 对于 rm -rf xxx 这样的指令，在执行的时候，一定要慎重，确认无误后再进行删除，避免误删。\n文件操作命令 cat 作用: 用于显示文件内容 语法: cat [-n] fileName 说明: ​ -n: 由1开始对所有输出的行数编号\n举例: ​ cat /etc/profile 查看/etc目录下的profile文件内容\n操作演示:\ncat 指令会一次性查看文件的所有内容，如果文件内容比较多，这个时候查看起来就不是很方便了，这个时候我们可以通过一个新的指令more。\nmore 作用: 以分页的形式显示文件内容 语法: more fileName 操作说明: 回车键 向下滚动一行 空格键 向下滚动一屏 b 返回上一屏 q或者Ctrl+C 退出more 举例： more /etc/profile 以分页方式显示/etc目录下的profile文件内容 操作示例：\n当我们在查看一些比较大的文件时，我们可能需要经常查询文件尾部的数据信息，那这个时候如果文件很大，我们要一直向下翻页，直到最后一页，去看最新添加的数据，这种方式就比较繁琐了，此时，我们可以借助于tail指令。\nhead 作用：查看文件开头的内容 语法：head [-n] fileName 说明: -n ：输出文件开头的n行内容 举例： head 1.log 默认显示1.log文件开头的10行内容 head -20 1.log 显示1.log文件开头的20行内容 tail 作用: 查看文件末尾的内容 语法: tail [-f] fileName 说明: ​ -f : 动态读取文件末尾内容并显示，通常用于日志文件的内容输出\n举例: tail /etc/profile 显示/etc目录下的profile文件末尾10行的内容 tail -20 /etc/profile 显示/etc目录下的profile文件末尾20行的内容 tail -f /itcast/my.log 动态读取/itcast目录下的my.log文件末尾内容并显示 操作示例：\nA. 默认查询文件尾部10行记录\nB. 可以通过指定参数设置查询尾部指定行数的数据\nC. 动态读取文件尾部的数据\n在窗口1中执行指令 tail -f 1.txt 动态查看文件尾部的数据。然后在顶部的标签中右键选择 \u0026ldquo;复制标签\u0026rdquo;，打开新的窗口2 , 此时再新打开的窗口2中执行指令 echo 1 \u0026gt;\u0026gt; 1.txt , 往1.txt文件尾部追加内容，然后我们就可以在窗口1中看到最新的文件尾部的数据。\necho：将指定的文本、字符串或变量的值输出到终端屏幕（标准输出）。\n如果我们不想查看文件尾部的数据了，可以直接使用快捷键 Ctrl+C ， 结束当前进程。\n拷贝移动命令 cp 作用: 用于复制文件或目录 语法: cp [-r] source dest 说明: ​ -r: 如果复制的是目录需要使用此选项，此时将复制该目录下所有的子目录和文件\n举例: ​ cp hello.txt itcast/ 将hello.txt复制到itcast目录中\n​ cp hello.txt ./hi.txt 将hello.txt复制到当前目录，并改名为hi.txt\n​ cp -r itcast/ ./itheima/ 将itcast目录和目录下所有文件复制到itheima目录下\n​ cp -r itcast/* ./itheima/ 将itcast目录下所有文件复制到itheima目录下\n操作示例:\n如果拷贝的内容是目录，需要加上参数 -r\nmv 作用: 为文件或目录改名、或将文件或目录移动到其它位置 语法: mv source dest 举例: mv hello.txt hi.txt 将hello.txt改名为hi.txt mv hi.txt itheima/ 将文件hi.txt移动到itheima目录中 mv hi.txt itheima/hello.txt 将hi.txt移动到itheima目录中，并改名为hello.txt mv itcast/ itheima/ 如果itheima目录不存在，将itcast目录改名为itheima mv itcast/ itheima/ 如果itheima目录存在，将itcast目录移动到itheima目录中 操作示例:\nmv 命令既能够改名，又可以移动，具体是改名还是移动,系统会根据我们输入的参数进行判定(如果第二个参数dest是一个已存在的目录,将执行移动操作,其他情况都是改名)\n打包压缩命令 作用: 对文件进行打包、解包、压缩、解压 语法: tar [-zcxvf] ``fileName ``[files] ​ 包文件后缀为.tar表示只是完成了打包，并没有压缩\n​ 包文件后缀为.tar.gz表示打包的同时还进行了压缩\n说明: -z: z代表的是gzip，通过gzip命令处理文件，gzip可以对文件压缩或者解压 -c: c代表的是create，即创建新的包文件 -x: x代表的是extract，实现从包文件中还原文件 -v: v代表的是verbose，显示命令的执行过程 -f: f代表的是file，用于指定包文件的名称 举例： 打包 tar -cvf hello.tar ./* 将当前目录下所有文件打包，打包后的文件名为hello.tar tar -zcvf hello.tar.gz ./* 将当前目录下所有文件打包并压缩，打包后的文件名为 hello.tar.gz 解包 tar -xvf hello.tar 将hello.tar文件进行解包，并将解包后的文件放在当前目录 tar -zxvf hello.tar.gz 将hello.tar.gz文件进行解压，并将解压后的文件放在当前目录 tar -zxvf hello.tar.gz -C /usr/local 将hello.tar.gz文件进行解压，并将解压后的文件放 在/usr/local目录 操作示例:\nA. 打包\nB. 打包并压缩\nC. 解包\nD. 解压\n解压到指定目录,需要加上参数 -C\n区别：\n文本编辑命令 文本编辑的命令，主要包含两个: vi 和 vim，两个命令的用法类似，我们课程中主要讲解vim的使用。\nvi \u0026amp; vim介绍 作用: vi命令是Linux系统提供的一个文本编辑工具，可以对文件内容进行编辑，类似于Windows中的记事本 语法: vi fileName 说明: 1). vim是从vi发展来的一个功能更加强大的文本编辑工具，编辑文件时可以对文本内容进行着色，方便我们对文件进行编辑处理，所以实际工作中vim更加常用。 2). 要使用vim命令，需要我们自己完成安装。可以使用下面的命令来完成安装：yum install vim vim安装 命令： yum install vim\n安装过程中，会有确认提示，此时输入 y，然后回车，继续安装：\n想要知道自己是否已经安装过vim可以使用命令：which vim\nvim使用 作用: 对文件内容进行编辑，vim其实就是一个文本编辑器 语法: vim fileName 说明: 1). 在使用vim命令编辑文件时，如果指定的文件存在则直接打开此文件。如果指定的文件不存在则新建文件。 2). vim在进行文本编辑时共分为三种模式，分别是 命令模式（Command mode），插入模式（Insert mode）和底行模式（Last line mode）。这三种模式之间可以相互切换。我们在使用vim时一定要注意我们当前所处的是哪种模式。 三种模式: 命令模式 A. 命令模式下可以查看文件内容、移动光标（上下左右箭头、gg、G) B. 通过vim命令打开文件后，默认进入命令模式 C. 另外两种模式需要首先进入命令模式，才能进入彼此 命令模式指令 含义 gg 定位到文本内容的第一行 G 定位到文本内容的最后一行 dd 删除光标所在行的数据 ndd 删除当前光标所在行及之后的n行数据 u 撤销操作 i或a或o 进入插入模式(进入后光标所处的位置不同而已) 插入模式\nA. 插入模式下可以对文件内容进行编辑 B. 在命令模式下按下[i,a,o]任意一个，可以进入插入模式。进入插入模式后，下方会出现【insert】字样 C. 在插入模式下按下ESC键，回到命令模式 底行模式\nA. 底行模式下可以通过命令对文件内容进行查找、显示行号、退出等操作 B. 在命令模式下按下[:,/]任意一个，可以进入底行模式 C. 通过/方式进入底行模式后，可以对文件内容进行查找 D. 通过:方式进入底行模式后，可以输入wq（保存并退出）、q!（不保存退出）、set nu（显示行号） 底行模式指令 含义 :wq 保存并退出 :q! 不保存退出 :set nu 显示行号 :set nonu 取消行号显示 :n 定位到第n行，如 :10 就是定位到第10行 查找命令 find 作用: 在指定目录下查找文件 语法: find dirName -option fileName 举例: find . –name \u0026quot;*.java\u0026quot; 在当前目录及其子目录下查找.java结尾文件 find /itcast -name \u0026quot;*.java\u0026quot; 在/itcast目录及其子目录下查找.java结尾的文件 操作示例:\ngrep 作用: 从指定文件中查找指定的文本内容 语法: grep word fileName 选项： -i: 检索的关键字忽略(ignore)大小写 -n: 显示关键字所在的这一行的行号 -A: 输出关键字所在行及之后(After)的几行记录 (如:-A5 表示输出关键字所在行之后的5行记录) -B: 输出关键字所在行及之前(Before)的几行记录 (如:-B5 表示输出关键字所在行之前的5行记录) 举例: grep Hello HelloWorld.java 查找HelloWorld.java文件中出现的Hello字符串的位置 grep hello *.java 查找当前目录中所有.java结尾的文件中包含hello字符串的位置 操作示例:\n软件安装 软件安装方式 在Linux系统中，安装软件的方式主要有四种，这四种安装方式的特点如下：\n安装JDK 上述我们介绍了Linux系统软件安装的四种形式，接下来我们就通过第一种(二进制发布包)形式来安装JDK。\nJDK具体安装步骤如下：\n1). 上传安装包\n使用FinalShell自带的上传工具将jdk的二进制发布包上传到Linux\n2). 解压安装包\n执行如下指令，将上传上来的压缩包进行解压，并通过-C参数指定解压文件存放目录为 /usr/local。\n1 tar -zxvf jdk-21_linux-x64_bin.tar.gz -C /usr/local/ 3). 配置环境变量\n使用vim命令修改/etc/profile文件，在文件末尾加入如下配置\n1 2 export JAVA_HOME=/usr/local/jdk-17.0.10 export PATH=$JAVA_HOME/bin:$PATH 具体操作指令如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1). 编辑/etc/profile文件，进入命令模式 vim /etc/profile 2). 在命令模式中，输入指令 G ， 切换到文件最后 G 3). 在命令模式中输入 i/a/o 进入插入模式，然后切换到文件最后一行 i 4). 将上述的配置拷贝到文件中 export JAVA_HOME=/usr/local/jdk-17.0.10 export PATH=$JAVA_HOME/bin:$PATH 5). 从插入模式，切换到指令模式 ESC 6). 按:进入底行模式，然后输入wq，回车保存 :wq 4). 重新加载profile文件\n为了使更改的配置立即生效，需要重新加载profile文件，执行命令:\n1 source /etc/profile 5). 检查安装是否成功\n1 java -version 安装MySQL MySQL安装 对于MySQL数据库的安装，我们将要使用前面讲解的第一种安装方式进行安装。\n1). 准备工作\n在安装MySQL数据库之前，我们需要先检查一下当前Linux系统中，是否安装的有MySQL的相关服务（很多linux安装完毕之后，自带了低版本的mysql的依赖包），如果有，先需要卸载掉，然后再进行安装。\nA. 通过rpm相关指令，来查询当前系统中是否存在已安装的mysql软件包，执行指令如下：\nrpm -qa 查询当前系统中安装的所有软件 rpm -qa | grep mysql 查询当前系统中安装的名称带mysql的软件 rpm -qa | grep mariadb 查询当前系统中安装的名称带mariadb的软件 通过rpm -qa 查询到系统通过rpm安装的所有软件，太多了，不方便查看，所以我们可以通过管道符 | 配合着grep进行过滤查询。\n通过查询，我们发现在当前系统不存在mysqlh和mariadb数据库。\n**RPM：**全称为 Red-Hat Package Manager，RPM软件包管理器，是红帽Linux用于管理和安装软件的工具。\n如果发现在当前系统中有这两个任意一个或都有，可以换通过以下方法卸载\nB. 通过 rpm 相关指令，来卸载对应的组件，执行指令如下：\n在rpm中，卸载软件的语法为：rpm -e --nodeps 软件名称\n那么，我们就可以通过指令，卸载 mariadb，具体指令为： rpm -e --nodeps mariadb-libs-5.5.60-1.el7_5.x86_64\n2). 将资料中提供的MySQL安装包上传到Linux并解压\nA. 上传MySQL安装包\n我们需要将该MySQL安装包上传到Linux系统的根目录 /root 下面。\nB. 解压到 当前目录\n执行如下指令:\n1 tar -xvf mysql-8.0.30-linux-glibc2.12-x86_64.tar.xz C. 将解压后的文件夹移动到 /usr/local 目录下， 并改名为 mysql\n1 2 3 mv mysql-8.0.30-linux-glibc2.12-x86_64 /usr/local/mysql cd /usr/local/mysql 并执行如下指令, 注册MySQL为系统服务:\n1 2 cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql chkconfig --add mysql 5). 初始化数据库(因为我的用户组和用户都不为空，所以我就新建了一个叫mysql1)\n1 2 3 4 5 #创建一个用户组, 组名就叫mysql groupadd mysql1 #创建一个系统用户 mysql, 并归属于用户组 mysql useradd -r -g mysql1 -s /bin/false mysql 1 2 #初始化mysql mysqld --initialize --user=mysql1 --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data 如果mysql中数据目录不为空，可能就会报出下面的错误\n我去问ai，ai给出的解释是：\n你正在尝试初始化一个新的数据库实例，但是指定的 --datadir=/usr/local/mysql/data 目录里已经存在文件了。MySQL 的初始化机制要求数据目录必须是空的或者不存在，以防止意外覆盖已有的重要数据。 给出的解决方案如下：\n删除前最好将原文件备份一份\n创建备份目录\n1 mkdir -p /backup 使用 tar 命令打包（推荐）\n1 tar -czvf /backup/mysql_data_backup.tar.gz -C /usr/local/mysql data 如果你不想压缩，只想单纯复制一份副本，可以使用 cp 命令。但必须加上 -a 参数以保留权限。\n1 cp -a /usr/local/mysql/data /backup/data_backup_copy 验证备份是否成功\n1 ls -lh /backup 如果你看到了 mysql_data_backup.tar.gz（或 data_backup_copy 目录），且文件大小不是 0，说明备份成功。\n恢复方法（万一出错了怎么办？）\n如果你清空了原目录后发现出错了，可以用以下命令恢复：\n如果是用 tar 备份的：\n1 2 3 4 # 进入 mysql 目录 cd /usr/local/mysql # 解压覆盖 tar -xzvf /backup/mysql_data_backup.tar.gz 如果是用 cp 备份的：\n1 2 rm -rf /usr/local/mysql/data cp -a /backup/data_backup_copy /usr/local/mysql/data 启动MySQL A. 启动MySQL服务\n1 systemctl start mysql B. 通过命令, 登录MySQL\n1 2 #xxxxx 代表上述生成的root的临时密码 mysql -uroot -pxxxxx 配置MySQL A. 修改root用户的密码\n1 ALTER USER \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED WITH mysql_native_password BY \u0026#39;1234\u0026#39;; 注意: 这个root账号仅仅能够在本机localhost上访问，我们在windows上是无法访问的。如果需要在window上或其他服务器上也能远程访问，需要创建一个账号，用于远程访问的。\nB. 创建账号, 并授权远程访问\n1 2 3 4 5 CREATE USER \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;1234\u0026#39;; GRANT ALL PRIVILEGES ON *.* TO \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39;; FLUSH PRIVILEGES; 我们已经开启了MySQL的远程访问的权限，为什么还是连接不上MySQL服务器呢？？ 这是因为Linux系统的防火墙，将我们的访问拦截了。\n防火墙操作 Linux系统安装完毕后，系统启动时，防火墙自动启动，防火墙拦截了所有端口的访问。接下来我们就需要学习一下，如何操作防火墙，具体指令如下：\n操作 指令 备注 查看防火墙状态 systemctl status firewalld / firewall-cmd \u0026ndash;state 暂时关闭防火墙 systemctl stop firewalld 永久关闭防火墙(禁用开机自启) systemctl disable firewalld 下次启动,才生效 暂时开启防火墙 systemctl start firewalld 永久开启防火墙(启用开机自启) systemctl enable firewalld 下次启动,才生效 开放指定端口 firewall-cmd \u0026ndash;zone=public \u0026ndash;add-port=8080/tcp \u0026ndash;permanent 需要重新加载生效 关闭指定端口 firewall-cmd \u0026ndash;zone=public \u0026ndash;remove-port=8080/tcp \u0026ndash;permanent 需要重新加载生效 立即生效(重新加载) firewall-cmd \u0026ndash;reload 查看开放端口 firewall-cmd \u0026ndash;zone=public \u0026ndash;list-ports 注意： 要想在windows上能够访问MySQL，需要开放防火墙的3306端口 或者 直接关闭防火墙 ，执行如下指令:\n1 2 3 4 5 6 7 8 #开放防火墙的3306端口号 firewall-cmd --zone=public --add-port=3306/tcp --permanent #重新加载 firewall-cmd --reload #查看开放的端口号 firewall-cmd --zone=public --list-ports 或者直接关闭防火墙:\n1 systemctl stop firewalld 连接测试 关闭掉linux系统的防火墙之后，我们就可以打开windows上的命令行，通过mysql的客户端(命令行/图形化界面)来连接远程linux上安装的MySQL数据库了。\n1). 客户端连接\n2). 打开DataGrip图形化工具连接\n执行资料中提供的SQL脚本 tlias.sql 。\n安装Nginx 安装 Nginx的安装包，从官方下载下来的是c语言的源码包，我们需要自己编译安装。具体操作步骤如下：\n1). 安装Nginx运行时需要的依赖\n1 yum install -y pcre pcre-devel zlib zlib-devel openssl openssl-devel 安装C语言的编译环境.\n1 yum install gcc-c++ 安装完后可以检查是否安装成功，安装前也可以使用以下命令查看是否已经安装过了\n1 2 3 yum list installed | grep pcre yum list installed | grep zlib yum list installed | grep openssl 1 gcc -v 2). 上传Nginx的源码包\n3). 解压源码包到当前目录\n1 tar -zxvf nginx-1.20.2.tar.gz 4). 进入到解压目录后，执行指令\n1 2 3 4 5 #进入解压目录 cd nginx-1.20.2 #执行命令配置, 生成Makefile文件 ./configure --prefix=/usr/local/nginx 5). 执行命令进行编译和安装\n1 2 3 4 5 #编译 make #编译安装 make install 安装完成效果如下：\n启动Nginx 进入到nginx安装目录/usr/local/nginx，启动nginx服务\n1 2 cd /usr/local/nginx/ sbin/nginx 启动完毕之后，我们可以通过 ps 指令查询当前系统中的nginx进程，从而确认nginx是否启动 。\n然后，我们就可以打开浏览器，访问服务器上的nginx 。\n项目部署 前端项目部署 1). 将nginx的安装目录的html中的静态资源文件先删除掉。\n2). 将资料中提供的 \u0026ldquo;资料/06. 项目部署/前端/页面资源\u0026rdquo; 目录下的静态资源文件，全部上传到nginx安装目录下的 html 目录中.\n3). 修改资料中提供的nginx.conf配置文件，将其上传到nginx安装目录下的 conf 目录中.\n4). 重新加载nginx服务的配置文件\n1 2 #重新加载配置文件（nginx安装目录下操作） sbin/nginx -s reload 5). 再次访问nginx (可能会存在浏览器缓存, 可以按Ctrl+F5, 强制刷新清理缓存)\n如果前端无响应，可能有以下几个原因：\n关闭nginx的命令\n1 sbin/nginx -s quit 后端项目部署 之前我们讲解Linux操作系统时，就提到，我们服务端开发工程师学习Linux系统的目的就是将来我们开发的项目绝大部分情况下都需要部署在Linux系统中。\n操作步骤：\n环境准备 那现在，项目要上线了，要部署到linux服务器上了，我们也需要使用linux服务器上所安装的mysql数据库。\n那此时，我们就可以再准备一份文件 application.yml 将里面的配置的mysql的ip地址及相关配置信息修改一下（配置Linux上安装的MySQL的信息）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #配置数据库连接信息 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.100.128:3306/tlias username: root password: 1234 servlet: multipart: max-file-size: 10MB #单个文件最大大小限制10MB max-request-size: 100MB #单个请求最大大小限制100MB #配置mybatis的日志输出到控制台 mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #配置mybatis的驼峰命名的映射开关 map-underscore-to-camel-case: true #查看事务管理的日志 logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug #阿里云oss配置 aliyun: oss: endpoint: https://oss-cn-beijing.aliyuncs.com bucketName: java422-web-ai 由于我们的Linux并没有在D盘上，所以我们的日志文件地址也需要进行修改：\n改造完毕之后，可以在本地的idea中先启动当前项目，然后访问一下，看看工程是否正常访问。\n打包部署 1). 执行 package 指令，进行打包操作，将当前的springboot项目，打成一个jar包。 (跳过测试)\njar包地址：\n2). 在Linux服务器上创建一个目录，将jar包上传到服务器 。\n1 mkdir -p /usr/local/tlias-app 3). 通过java命令，启动项目\n1 2 3 4 5 #进入目录/usr/local/app cd /usr/local/tlias-app #运行jar包 java -jar tilas-web-management-0.0.1-SNAPSHOT.jar 项目启动起来之后，就可以打开浏览器测试啦。\n阿里云OSS秘钥配置 由于在开发的时候，我们将访问阿里云OSS的AccessKeyId，AccessKeySecret都配置在了系统的环境变量中了。那现在项目部署到了Linux服务器中，调用阿里云OSS进行文件上传时，程序就会获取Linux系统中的环境变量。所以此时，我们需要将AccessKeyId，AccessKeySecret配置为Linux系统的环境变量。\n1). 查看Windows系统之前配置的环境变量\n1 2 3 echo %OSS_ACCESS_KEY_ID% echo %OSS_ACCESS_KEY_SECRET% 我们将上述自己的 AccessKeyId 与 AccessKeySecret 复制出来，然后在linux系统中配置环境变量。\n2). 执行如下指令：\n1 2 3 4 5 echo \u0026#34;export OSS_ACCESS_KEY_ID=***\u0026#34; \u0026gt;\u0026gt; /etc/profile echo \u0026#34;export OSS_ACCESS_KEY_SECRET=***\u0026#34; \u0026gt;\u0026gt; /etc/profile source /etc/profile 当前程序中存在的问题:\n线上程序不会采用控制台霸屏的形式运行程序，而是将程序在后台运行 线上程序不会将日志输出到控制台，而是输出到日志文件，方便运维查阅信息 要解决上述这两个问题，我们就可以通过 nohup 指令让程序在后台运行。\n后台运行 1). 后台运行程序\n1 nohup java -jar tilas-web-management-0.0.1-SNAPSHOT.jar \u0026amp;\u0026gt; tlias.log \u0026amp; 通过上述指令就可以后台运行服务，服务运行之后， 所有的日志信息都会输出到 tlias.log 文件中。\n2). 停止服务\n1 2 3 4 5 #查看服务的进程信息 ps -ef|grep tlias #杀掉进程 kill -9 xxxxx nohup命令说明:\nnohup命令：英文全称 no hang up（不挂起），用于不挂断地运行指定命令，退出终端不会影响程序的运行 语法格式： nohup command [ args … ] [\u0026amp;] 参数说明： command：要执行的命令 args：一些参数，可以指定输出文件 \u0026amp;：让命令在后台运行 举例： nohup java -jar boot工程.jar \u0026amp;\u0026gt; hello.log \u0026amp; ​ 上述指令的含义为： 后台运行 java -jar 命令，并将日志输出到hello.log文件\n","date":"2026-05-26T21:31:09+08:00","permalink":"https://aurora156.github.io/p/linux/","title":"Linux"},{"content":"元注解 Java中的元注解（Meta-Annotation），简单来说就是**“用来修饰注解的注解”**。\n在Java中，我们可以自己定义注解，但光定义一个空壳注解是没用的。我们需要通过元注解来告诉编译器或JVM：这个自定义的注解能写在哪里？它的生命周期有多长？会不会被继承？\nJDK原生提供了几个核心的元注解，其中最常用、最核心的有以下4个：\n1. @Target（指定作用范围） 它用来限制你的注解可以写在哪些位置。比如，你希望某个注解只能写在方法上，或者只能写在类上，就需要用到它。它的取值是 ElementType 枚举，常见的有：\nElementType.TYPE：可以作用在类、接口、枚举上。 ElementType.FIELD：可以作用在成员变量（字段）上。 ElementType.METHOD：可以作用在成员方法上。 ElementType.PARAMETER：可以作用在方法参数上。 2. @Retention（指定生命周期） 它用来决定你的注解能保留到什么时候，这是元注解里最核心的一个。它的取值是 RetentionPolicy 枚举，分为三个阶段：\nRetentionPolicy.SOURCE：注解只保留在源码中，编译成 .class 文件时就会被丢弃（比如我们常用的 @Override）。 RetentionPolicy.CLASS：注解保留在 .class 字节码文件中，但在JVM运行时不会被加载到内存（这是默认值）。 RetentionPolicy.RUNTIME：注解不仅保留在 .class 中，还会被JVM加载到内存。只有设置为 RUNTIME，你的程序才能在运行时通过反射（Reflection）获取到注解信息。我们平时用的 Spring 框架中的 @Autowired、@Controller 等，底层都是 RUNTIME。 3. @Documented（生成文档） 这是一个标记注解。默认情况下，使用 javadoc 工具生成API文档时，是不会把注解信息包含进去的。如果你的自定义注解上加了 @Documented，那么在生成文档时，这个注解也会作为说明的一部分出现在生成的 HTML 文档里。\n4. @Inherited（允许继承） 它用来规定注解是否具有继承性。如果一个父类上加了被 @Inherited 修饰的注解，那么它的子类在没有加任何注解的情况下，会自动继承父类的这个注解。 注意：这个元注解只对类级别的注解（即 @Target 为 TYPE）有效，对方法和字段无效。\n结合黑马课程实战举例 为了更好理解，我们正在学的 Spring 来举个例子。在 Spring Boot 中，我们经常用到 @RestController 或 @Service，这些其实就是 Spring 框架帮咱们定义好的自定义注解。\n如果我们自己来模拟定义一个类似 @Service 的注解，代码大概长这样：\n1 2 3 4 5 6 7 8 9 10 11 12 import java.lang.annotation.*; // 1. @Target：规定这个 @MyService 注解只能写在类、接口或枚举上 @Target(ElementType.TYPE) // 2. @Retention：规定这个注解要保留到程序运行时，这样Spring框架才能在启动时通过反射扫描到它 @Retention(RetentionPolicy.RUNTIME) // 3. @Documented：生成文档时带上这个注解信息 @Documented public @interface MyService { // 可以定义属性，比如给Bean起个名字 String value() default \u0026#34;\u0026#34;; } 因为 @Retention 设置成了 RUNTIME，Spring 容器在启动的时候，才能通过反射机制去扫描你项目里的所有类，看看哪些类头上顶着 @MyService（或 @Service），然后把这些类自动创建成对象（Bean）并管理起来。\n简单总结一下：元注解就是给自定义注解立规矩的“幕后大佬”，尤其是 @Target 和 @Retention，在后续学习 Spring 等框架的底层原理时会非常关键！\n","date":"2026-05-22T21:39:37+08:00","permalink":"https://aurora156.github.io/p/%E5%85%83%E6%B3%A8%E8%A7%A3/","title":"元注解"},{"content":"后端web进阶(SpringBoot原理) 在前面的课程当中，我们学习的都是web开发的技术使用，都是面向应用层面的，我们学会了怎么样去用。而我们今天所要学习的是web后端开发的最后一个篇章springboot原理篇，主要偏向于底层原理。\n这一篇的内容包括这么三个部分：\n配置优先级 Bean的管理 剖析Springboot的底层原理 配置优先级 在我们前面的课程当中，我们已经讲解了SpringBoot项目当中支持的三类配置文件：\napplication.properties application.yml application.yaml 在SpringBoot项目当中，我们要想配置一个属性，可以通过这三种方式当中的任意一种来配置都可以，那么如果项目中同时存在这三种配置文件，且都配置了同一个属性，如：Tomcat端口号，到底哪一份配置文件生效呢？\napplication.properties 1 server.port=8081 application.yml 1 2 server: port: 8082 application.yaml 1 2 server: port: 8082 我们启动SpringBoot程序，测试下三个配置文件中哪个Tomcat端口号生效：\nproperties、yaml、yml三种配置文件同时存在。 配置好了，启动服务，测试一下： properties、yaml、yml三种配置文件，优先级最高的是properties\nyaml、yml两种配置文件同时存在 aml、yml 两种配置文件，优先级最高的是yml。\n配置文件优先级排名（从高到低）：\nproperties配置文件 yml配置文件 yaml配置文件 注意事项：虽然springboot支持多种格式配置文件，但是在项目开发时，推荐统一使用一种格式的配置。（yml是主流）\n在SpringBoot项目当中除了以上3种配置文件外，SpringBoot为了增强程序的扩展性，除了支持配置文件的配置方式以外，还支持另外两种常见的配置方式：\nJava系统属性配置 （格式： -Dkey=value） 1 -Dserver.port=9000 命令行参数 （格式：\u0026ndash;key=value） 1 --server.port=10010 那在idea当中运行程序时，如何来指定Java系统属性和命令行参数呢？\n编辑启动程序的配置信息 打开之后，选择 Modify options , 选择 Add VM options, Program arguments\n重启服务，同时配置Tomcat端口(系统属性、命令行参数)，测试哪个Tomcat端口号生效：\n说明，命令行参数的优先级是大于系统属性的，同时配置的情况下，命令行参数的配置项生效\n重启服务，同时配置Tomcat端口(application.properties、系统属性)，测试哪个Tomcat端口号生效：\n说明，系统属性的优先级是大于application.properties的，同时配置的情况下，系统属性的配置项生效\n五种配置方式的优先级： 命令行参数 \u0026gt; 系统属性参数 \u0026gt; properties参数 \u0026gt; yml参数 \u0026gt; yaml参数\n思考：如果项目已经打包上线了，这个时候我们又如何来设置Java系统属性和命令行参数呢？\n1 java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010 下面我们来演示下打包程序运行时指定Java系统属性和命令行参数：\n执行maven打包指令package，把项目打成jar文件 此处打好的jar包并没有我们刚刚配置的命令行参数和系统属性\n在当前目录下打开cmd命令行，使用命令：java -jar 方式运行jar文件程序。\n如果不知道该如何操作可以先执行一个指令：java，执行之后就会给我们输出一个对应的帮助文档\n系统属性和命令行参数两个都不设置\n此时端口号就是我们刚刚设置的properties的端口号\n仅设置Java系统属性\n但此时会发现下面报错了，说端口号9000已经被占用了，这是因为我们的idea此时还开着，且端口号就是9000\n同时设置Java系统属性和命令行参数\n注意事项：\nSpringboot项目进行打包时，需要引入插件 spring-boot-maven-plugin (基于官网骨架创建项目，会自动添加该插件) 在SpringBoot项目当中，常见的属性配置方式有5种， 3种配置文件，加上2种外部属性的配置(Java系统属性、命令行参数)。通过以上的测试，我们也得出了优先级(从低到高)：\napplication.yaml（忽略） application.yml application.properties java系统属性（-Dxxx=xxx） 命令行参数（\u0026ndash;xxx=xxx） Bean的管理 在前面的课程当中，我们已经讲过了我们可以通过Spring当中提供的注解@Component以及它的三个衍生注解（@Controller、@Service、@Repository）来声明IOC容器中的bean对象，同时我们也学习了如何为应用程序注入运行时所需要依赖的bean对象，也就是依赖注入DI。\n我们今天主要学习IOC容器中Bean的其他使用细节，主要学习以下三方面：\nbean的作用域配置 管理第三方的bean对象 接下来我们先来学习第一方面，Bean的作用域。\n作用域 说明 singleton 容器内同名称的bean只有一个实例（单例）（默认） prototype 每次使用bean时会创建新的实例（非单例） request 每个请求范围内会创建新的实例（web环境中，了解） session 每个会话范围内会创建新的实例（web环境中，了解） application 每个应用范围内会创建新的实例（web环境中，了解） 知道了bean的5种作用域了，我们要怎么去设置一个bean的作用域呢？\n可以借助Spring中的@Scope注解来进行配置作用域 1). 测试一:如果循环结果拿到的是同一个对象，那么就是单例的，否则是非单例的\n控制器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //默认bean的作用域为：singleton (单例)：默认单例的bean是在项目启动时创建的，创建完毕后，会将bean存入IOC容器中 @Lazy\t//延迟初始化 --\u0026gt; 延迟到第一次使用的时候，再来创建这个bean @RestController @RequestMapping(\u0026#34;/depts\u0026#34;) public class DeptController { @Autowired private DeptService deptService; public DeptController(){ System.out.println(\u0026#34;DeptController constructor ....\u0026#34;); } //省略其他代码... } 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.itheima; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; @SpringBootTest class SpringbootWebConfig2ApplicationTests { @Autowired private ApplicationContext applicationContext; //IOC容器对象 //bean的作用域 @Test public void testScope(){ for (int i = 0; i \u0026lt; 10; i++) { Object deptController = applicationContext.getBean(\u0026#34;deptController\u0026#34;); System.out.println(deptController); } } } 重启SpringBoot服务，运行测试方法，查看控制台打印的日志：\n默认单例是在项目启动时构建的（图中报红仅为一个JVM警告，不影响实际运行）\n注意事项：\nIOC容器中的bean默认使用的作用域：singleton (单例) 默认singleton的bean，在容器启动时被创建，可以使用@Lazy注解来延迟初始化(延迟到第一次使用时) 2). 测试二\n修改控制器DeptController代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Scope(\u0026#34;![rototype日](assets/prototype日志.png)\u0026#34;) //bean作用域为非单例 @RestController @RequestMapping(\u0026#34;/depts\u0026#34;) public class DeptController { @Autowired private DeptService deptService; public DeptController(){ System.out.println(\u0026#34;DeptController constructor ....\u0026#34;); } //省略其他代码... } 重启SpringBoot服务，再次执行测试方法，查看控制台打印的日志：\n注意事项：\nprototype的bean，每一次使用该bean的时候都会创建一个新的实例 实际开发当中，绝大部分的Bean是单例的，也就是说绝大部分Bean不需要配置scope属性 小结：\n第三方bean 学习完bean的获取、bean的作用域之后，接下来我们再来学习第三方bean的配置。\n之前我们所配置的bean，像controller、service，dao三层体系下编写的类，这些类都是我们在项目当中自己定义的类(自定义类)。当我们要声明这些bean，也非常简单，我们只需要在类上加上@Component以及它的这三个衍生注解（@Controller、@Service、@Repository），就可以来声明这个bean对象了。\n但是在我们项目开发当中，还有一种情况就是这个类它不是我们自己编写的，而是我们引入的第三方依赖当中提供的，那么此时我们是无法使用 @Component 及其衍生注解来声明bean的，此时就需要使用**@Bean**注解来声明bean 了。\n演示1：\n在启动类中直接声明这个Bean。比如：我们可以将我们之前使用的阿里云OSS操作的工具类，基于@Bean注解的方式来声明Bean。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.itheima; import com.itheima.utils.AliyunOSSOperator; import com.itheima.utils.AliyunOSSProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class SpringbootWebConfigApplication { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfigApplication.class, args); } @Bean public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) { return new AliyunOSSOperator(ossProperties); } } 测试： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.itheima; import cn.hutool.core.io.FileUtil; import com.itheima.utils.AliyunOSSOperator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; import java.io.File; @SpringBootTest class SpringbootWebConfig2ApplicationTests { @Autowired private ApplicationContext applicationContext; //IOC容器对象 @Autowired private AliyunOSSOperator aliyunOSSOperator; //bean的作用域 @Test public void testScope(){ for (int i = 0; i \u0026lt; 10; i++) { Object deptController = applicationContext.getBean(\u0026#34;deptController\u0026#34;); System.out.println(deptController); } } @Test public void testupload() throws Exception { File file = new File(\u0026#34;D:\\\\picture\\\\111.jpg\u0026#34;); String url = aliyunOSSOperator.upload(FileUtil.readBytes(file), \u0026#34;111.jpg\u0026#34;); System.out.println(url); } } 演示2：\n若要管理的第三方 bean 对象，建议对这些bean进行集中分类配置，可以通过 @Configuration 注解声明一个配置类。【推荐】\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.itheima.config; import com.itheima.utils.AliyunOSSOperator; import com.itheima.utils.AliyunOSSProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OSSConfig { @Bean public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) { return new AliyunOSSOperator(ossProperties); } } SpringBoot原理 SpringBoot使我们能够集中精力地去关注业务功能的开发，而不用过多地关注框架本身的配置使用。而我们前面所讲解的都是面向应用层面的技术，接下来我们开始学习SpringBoot的原理，这部分内容偏向于底层的原理分析。\n在剖析SpringBoot的原理之前，我们先来快速回顾一下我们前面所讲解的Spring家族的框架。\nSpring是目前世界上最流行的Java框架，它可以帮助我们更加快速、更加容易的来构建Java项目。而在Spring家族当中提供了很多优秀的框架，而所有的框架都是基于一个基础框架的SpringFramework(也就是Spring框架)。而前面我们也提到，如果我们直接基于Spring框架进行项目的开发，会比较繁琐。\n这个繁琐主要体现在两个地方：\n在pom.xml中依赖配置比较繁琐，在项目开发时，需要自己去找到对应的依赖，还需要找到依赖它所配套的依赖以及对应版本，否则就会出现版本冲突问题。\n在使用Spring框架进行项目开发时，需要在Spring的配置文件中做大量的配置，这就造成Spring框架入门难度较大，学习成本较高。\n基于Spring存在的问题，官方在Spring框架4.0版本之后，又推出了一个全新的框架：SpringBoot。 通过 SpringBoot来简化Spring框架的开发(是简化不是替代)。我们直接基于SpringBoot来构建Java项目，会让我们的项目开发更加简单，更加快捷。 SpringBoot框架之所以使用起来更简单更快捷，是因为SpringBoot框架底层提供了两个非常重要的功能：一个是起步依赖，一个是自动配置。\n通过SpringBoot所提供的起步依赖，就可以大大的简化pom文件当中依赖的配置，从而解决了Spring框架当中依赖配置繁琐的问题。 通过自动配置的功能就可以大大的简化框架在使用时bean的声明以及bean的配置。我们只需要引入程序开发时所需要的起步依赖，项目开发时所用到常见的配置都已经有了，我们直接使用就可以了。 简单回顾之后，接下来我们来学习下SpringBoot的原理。其实学习SpringBoot的原理就是来解析SpringBoot当中的起步依赖与自动配置的原理。我们首先来学习SpringBoot当中起步依赖的原理。\n起步依赖 假如我们没有使用SpringBoot，用的是Spring框架进行web程序的开发，此时我们就需要引入web程序开发所需要的一些依赖。\n当我们引入了 spring-boot-starter-web 之后，maven会通过依赖传递特性，将web开发所需的常见依赖都传递下来。\n所以，起步依赖的原理就是Maven的依赖传递。\n在SpringBoot给我们提供的这些起步依赖当中，已提供了当前程序开发所需要的所有的常见依赖(官网地址：https://docs.spring.io/spring-boot/docs/2.7.7/reference/htmlsingle/#using.build-systems.starters)。 比如：springboot-starter-web，这是web开发的起步依赖，在web开发的起步依赖当中，就集成了web开发中常见的依赖：json、web、webmvc、tomcat等。我们只需要引入这一个起步依赖，其他的依赖都会自动的通过Maven的依赖传递进来。 自动配置 SpringBoot的自动配置就是当spring容器启动后，一些配置类、bean对象就自动存入到了IOC容器中，不需要我们手动去声明，从而简化了开发，省去了繁琐的配置操作。\n比如，在我们前面讲解AOP记录日志的那个案例中，我们要将一个对象转为json，直接注入一个Gson，然后就可以直接使用了。而我们在我们整个项目中，也并未配置Gson这个类型的bean，为什么可以直接注入使用呢？ 原因就是因为这个bean，springboot中已经帮我们自动配置完毕了，我们是可以直接使用的。\n我们可以在SpringBootWebTests中进行一个测试：在文件中添加这几行代码\n1 2 3 4 5 6 7 @Autowired private Gson gson; @Test public void testJson() { System.out.println(gson.toJson(Result.success(\u0026#34;Hello Gson\u0026#34;))); } 可以发现当前项目中我们没有去声明gson这个bean，但是我们却可以直接将这个bean注入进来使用它，这是Springboot给我们提供的功能\n那接下来，我们就要来解析，SpringBoot中到底是如何完成自动配置的。\n实现方案 我们知道了什么是自动配置之后，接下来我们就要来剖析自动配置的原理。解析自动配置的原理就是分析在 SpringBoot项目当中，我们引入对应的依赖之后，是如何将依赖jar包当中所提供的bean以及配置类直接加载到当前项目的SpringIOC容器当中的。\n接下来，我们就直接通过代码来分析自动配置原理。\n准备工作：在Idea中导入\u0026quot;资料\\03. 自动配置原理\u0026quot; 下的 itheima-utils 工程 在SpringBoot项目 spring-boot-web-config 工程中，通过坐标引入itheima-utils依赖 1、引入的 itheima-utils 中配置如下:\n1 2 3 4 5 6 @Component public class TokenParser { public void parse(){ System.out.println(\u0026#34;TokenParser ... parse ...\u0026#34;); } } 2、在测试类中，添加测试方法\n1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest public class AutoConfigurationTests { @Autowired private ApplicationContext applicationContext; @Test public void testTokenParse(){ tokenParser.parse(); } //省略其他代码... } 3、执行测试方法\n（框架报的错一定要从下往上看）\n异常信息描述： 没有com.example.TokenParse类型的bean\n说明：在Spring容器中没有找到com.example.TokenParse类型的bean对象\n思考：引入进来的第三方依赖当中的bean以及配置类为什么没有生效？\n原因在我们之前讲解IOC的时候有提到过，在类上添加@Component注解来声明bean对象时，还需要保证@Component注解能被Spring的组件扫描到。 SpringBoot项目中的@SpringBootApplication注解，具有包扫描的作用，但是它只会扫描启动类所在的当前包以及子包。 当前包：com.itheima， 第三方依赖中提供的包：com.example（扫描不到） 那么如何解决以上问题的呢？\n方案1：@ComponentScan 组件扫描 方案2：@Import 导入（使用@Import导入的类会被Spring加载到IOC容器 方案一 @ComponentScan组件扫描【在启动类上添加@ComponentScan要扫描的包】\n1 2 3 4 5 6 7 @SpringBootApplication @ComponentScan({\u0026#34;com.itheima\u0026#34;,\u0026#34;com.example\u0026#34;}) //指定要扫描的包 public class SpringbootWebConfigApplication { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfigApplication.class, args); } } 重新执行测试方法，控制台日志输出：\n如果采用以上这种方式来完成自动配置，那我们进行项目开发时，就需要引入大量的第三方的依赖，就需要在启动类上配置N多要扫描的包，这种方式会很繁琐。而且这种大面积的扫描性能也比较低。\n缺点：\n使用繁琐 性能低 结论：SpringBoot中并没有采用以上这种方案。\n方案二 @Import导入\n导入形式主要有以下几种： 导入普通类 导入配置类 导入ImportSelector接口实现类 1). 使用@Import导入普通类：\n1 2 3 4 5 6 7 @Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中 @SpringBootApplication public class SpringbootWebConfigApplication { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfigApplication.class, args); } } 重新执行测试方法，控制台日志输出：\n2). 使用@Import导入配置类：\n配置类 1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class HeaderConfig { @Bean public HeaderParser headerParser(){ return new HeaderParser(); } @Bean public HeaderGenerator headerGenerator(){ return new HeaderGenerator(); } } 启动类 1 2 3 4 5 6 7 @Import(HeaderConfig.class) //导入配置类 @SpringBootApplication public class SpringbootWebConfig2Application { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfig2Application.class, args); } } 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest public class AutoConfigurationTests { @Autowired private ApplicationContext applicationContext; @Test public void testHeaderParser(){ System.out.println(applicationContext.getBean(HeaderParser.class)); } @Test public void testHeaderGenerator(){ System.out.println(applicationContext.getBean(HeaderGenerator.class)); } //省略其他代码... } 执行测试方法：\n这里就测试testHeaderParser，第二个方法结果一样就不展示了\n3). 使用@Import导入ImportSelector接口实现类：\nImportSelector接口实现类 1 2 3 4 5 6 public class MyImportSelector implements ImportSelector { public String[] selectImports(AnnotationMetadata importingClassMetadata) { //返回值字符串数组（数组中封装了全限定名称的类，可以一次性导入多个类） return new String[]{\u0026#34;com.example.HeaderConfig\u0026#34;,\u0026#34;com.example.TokenParser\u0026#34;}; } } 启动类 1 2 3 4 5 6 7 @Import(MyImportSelector.class) //导入ImportSelector接口实现类 @SpringBootApplication public class SpringbootWebConfig2Application { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfig2Application.class, args); } } 执行测试方法：\n我们就拿上面的配置类的测试方案来测试，虽然注释掉了配置类的@import会导致报错，但是由于我们导入了接口实现类，而接口实现类中又有配置类的方法，所以此时报错是误报，直接运行即可\n我们使用@Import注解通过这三种方式都可以导入第三方依赖中所提供的bean或者是配置类。\n思考：如果基于以上方式完成自动配置，当要引入一个第三方依赖时，是不是还要知道第三方依赖中有哪些配置类和哪些Bean对象？\n答案：是的。 （对程序员来讲，很不友好，而且比较繁琐） 思考：当我们要使用第三方依赖，依赖中到底有哪些bean和配置类，谁最清楚？\n答案：第三方依赖自身最清楚。 结论：我们不用自己指定要导入哪些bean对象和配置类了，让第三方依赖它自己来指定。\n怎么让第三方依赖自己指定bean对象和配置类？\n比较常见的方案就是第三方依赖给我们提供一个注解，这个注解一般都以@EnableXxxx开头的注解，注解中封装的就是@Import注解 4). 使用第三方依赖提供的 @EnableXxxxx注解\n第三方依赖中提供的注解 1 2 3 4 5 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类 public @interface EnableHeaderConfig { } 在使用时只需在启动类上加上@EnableXxxxx注解即可 1 2 3 4 5 6 7 ![HC注](assets/EHC注解.png) //使用第三方依赖提供的Enable开头的注解 @SpringBootApplication public class SpringbootWebConfig2Application { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfig2Application.class, args); } } 执行测试方法：\n以上四种方式都可以完成导入操作，但是第4种方式会更方便更优雅，而这种方式也是SpringBoot当中所采用的方式。\n原理分析 源码跟踪 前面我们讲解了在项目当中引入第三方依赖之后，如何加载第三方依赖中定义好的bean对象以及配置类，从而完成自动配置操作。那下面我们通过源码跟踪的形式来剖析下SpringBoot底层到底是如何完成自动配置的。\n源码跟踪技巧：\n在跟踪框架源码的时候，一定要抓住关键点，找到核心流程。一定不要从头到尾一行代码去看，一个方法的去研究，一定要找到关键流程，抓住关键点，先在宏观上对整个流程或者整个原理有一个认识，有精力再去研究其中的细节。\n要搞清楚SpringBoot的自动配置原理，要从SpringBoot启动类上使用的核心注解@SpringBootApplication开始分析：【按住Ctrl点击@SpringBootApplication】\n在@SpringBootApplication注解中包含了：\n元注解（详见博客\u0026lt;元注解\u0026gt;）\n@SpringBootConfiguration\n@EnableAutoConfiguration\n@ComponentScan\n我们先来看第一个注解：@SpringBootConfiguration\n@SpringBootConfiguration注解上使用了@Configuration，表明SpringBoot启动类就是一个配置类。\n@Indexed注解，是用来加速应用启动的（不用关心）。\n接下来再先看@ComponentScan注解： @ComponentScan注解是用来进行组件扫描的，扫描启动类所在的包及其子包下所有被@Component及其衍生注解声明的类。\nSpringBoot启动类，之所以具备扫描包功能，就是因为包含了@ComponentScan注解。\n最后我们来看看@EnableAutoConfiguration注解（自动配置核心注解）： 使用@Import注解，导入了实现ImportSelector接口的实现类。\nAutoConfigurationImportSelector类是ImportSelector接口的实现类。\nAutoConfigurationImportSelector类中重写了ImportSelector接口的selectImports()方法：\nselectImports()方法底层调用getAutoConfigurationEntry()方法，获取可自动配置的配置类信息集合\ngetAutoConfigurationEntry()方法通过调用getCandidateConfigurations(annotationMetadata, attributes)方法获取在配置文件中配置的所有自动配置类的集合\ngetCandidateConfigurations方法的功能：\n获取所有基于 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置类的集合\nMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件这两个文件在哪里呢？\n通常在引入的起步依赖中，都有包含以上文件 在前面在给大家演示自动配置的时候，我们直接在测试类当中注入了一个叫gson的bean对象，进行JSON格式转换。虽然我们没有配置bean对象，但是我们是可以直接注入使用的。原因就是因为在自动配置类当中做了自动配置。到底是在哪个自动配置类当中做的自动配置呢？我们通过搜索来查询一下。\n在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 配置文件中指定了第三方依赖Gson的配置类：GsonAutoConfiguration\n打开上面的第三方依赖中提供的 GsonAutoConfiguration 类：【按两下shift搜索完双击进入】\n在GsonAutoConfiguration类上，添加了注解@AutoConfiguration，通过查看源码，可以明确：GsonAutoConfiguration 类是一个配置。\n完成自动配置的原理就是在配置类中定义一个@Bean标识的方法，而Spring会自动调用配置类中使用@Bean标识的方法，并把方法的返回值注册到IOC容器中。\n注意：在低版本(2.7.0以前)的springboot中，自动配置类(XxxAutoConfiguration)是定义在spring.factories文件中\n自动配置源码小结\n自动配置原理源码入口就是 @SpringBootApplication 注解，在这个注解中封装了3个注解，分别是：\n@SpringBootConfiguration\n声明当前类是一个配置类 @ComponentScan\n进行组件扫描（SpringBoot中默认扫描的是启动类所在的当前包及其子包） @EnableAutoConfiguration\n封装了@Import注解（Import注解中指定了一个ImportSelector接口的实现类）\n在实现类重写的selectImports()方法，读取当前项目下所有依赖jar包中META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports两个文件里面定义的配置类（配置类中定义了@Bean注解标识的方法）。\n当SpringBoot程序启动时，就会加载配置文件当中所定义的配置类，并将这些配置类信息(类的全限定名)封装到String类型的数组中，最终通过@Import注解将这些配置类全部加载到Spring的IOC容器中，交给IOC容器管理。\n在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中定义的配置类非常多，而且每个配置类中又可以定义很多的bean，那这些bean都会注册到Spring的IOC容器中吗？\n答案：并不是。 在声明bean对象时，上面有加一个 @ConditionalOnMissingBean 注解，这种注解的作用就是按照条件进行装配，只有满足条件之后，才会将bean注册到Spring的IOC容器中（下面会详细来讲解）\n@Conditional 我们在跟踪SpringBoot自动配置的源码的时候，在自动配置类声明bean的时候，除了在方法上加了一个@Bean注解以外，还会经常用到一个注解，就是以Conditional开头的这一类的注解。以Conditional开头的这些注解都是条件装配的注解。下面我们就来介绍下条件装配注解。\n@Conditional注解：\n作用：按照一定的条件进行判断，在满足给定条件后才会注册对应的bean对象到Spring的IOC容器中。 位置：方法、类 @Conditional本身是一个父注解，派生出大量的子注解，最常见的有以下三种： @ConditionalOnClass：判断环境中有对应字节码文件，才注册bean到IOC容器。 @ConditionalOnMissingBean：判断环境中没有对应的bean(类型或名称)，才注册bean到IOC容器。 @ConditionalOnProperty：判断配置文件中有对应属性和值，才注册bean到IOC容器。 下面我们通过代码来演示下Conditional注解的使用：\n@ConditionalOnClass注解 1 2 3 4 5 6 7 8 9 10 11 @Configuration public class HeaderConfig { @Bean @ConditionalOnClass(name=\u0026#34;io.jsonwebtoken.Jwts\u0026#34;)//环境中存在指定的这个类，才会将该bean加入IOC容器 public HeaderParser headerParser(){ return new HeaderParser(); } //省略其他代码... } pom.xml（如果没有就会报错） 1 2 3 4 5 6 \u0026lt;!--JWT令牌--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.9.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 测试类 1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest public class SpringbootWebConfig2ApplicationTests { @Autowired private ApplicationContext applicationContext; @Test public void testHeaderParser(){ System.out.println(applicationContext.getBean(HeaderParser.class)); } //省略其他代码... } 执行testHeaderParser()测试方法：\n因为 io.jsonwebtoken.Jwts 字节码文件在启动SpringBoot程序时已存在，所以创建HeaderParser对象并注册到IOC容器中。\n@ConditionalOnMissingBean注解 1 2 3 4 5 6 7 8 9 10 11 @Configuration public class HeaderConfig { @Bean @ConditionalOnMissingBean //不存在该类型的bean，才会将该bean加入IOC容器 public HeaderParser headerParser(){ return new HeaderParser(); } //省略其他代码... } 执行testHeaderParser()测试方法：\nSpringBoot在调用@Bean标识的headerParser()前，IOC容器中是没有HeaderParser类型的bean，所以HeaderParser对象正常创建，并注册到IOC容器中。\n再次修改@ConditionalOnMissingBean注解：如果已经存在，条件不满足，Spring就会跳过当前被该注解标注的Bean的创建（此时已经存在指定类型的bean了） 1 2 3 4 5 6 7 8 9 10 11 @Configuration public class HeaderConfig { @Bean @ConditionalOnMissingBean//不存在指定类型的bean，才会将该bean加入IOC容器 public HeaderParser headerParser(){ return new HeaderParser(); } //省略其他代码... } 执行testHeaderParser()测试方法：\n@ConditionalOnProperty注解（这个注解和配置文件当中配置的属性有关系） 先在application.yml配置文件中添加如下的键值对：\n1 name: itheima 在声明bean的时候就可以指定一个条件@ConditionalOnProperty\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class HeaderConfig { @Bean @ConditionalOnProperty(name =\u0026#34;name\u0026#34;,havingValue = \u0026#34;itheima\u0026#34;)//配置文件中存在指定属性名与值，才会将bean加入IOC容器 public HeaderParser headerParser(){ return new HeaderParser(); } @Bean public HeaderGenerator headerGenerator(){ return new HeaderGenerator(); } } 执行testHeaderParser()测试方法：\n修改@ConditionalOnProperty注解： havingValue的值修改为\u0026quot;itheima2\u0026quot;\n再次执行testHeaderParser()测试方法：\n因为 application.yml 配置文件中，不存在： name: itheima2，所以HeaderParser对象在IOC容器中不存在\n我们再回头看看之前讲解SpringBoot源码时提到的一个配置类：GsonAutoConfiguration\n最后再来梳理一下自动配置原理：\n自动配置的核心就在@SpringBootApplication注解上，SpringBootApplication这个注解底层包含了3个注解，分别是：\n@SpringBootConfiguration @ComponentScan @EnableAutoConfiguration @EnableAutoConfiguration这个注解才是自动配置的核心。\n它封装了一个@Import注解，Import注解里面指定了一个ImportSelector接口的实现类。 在这个实现类中，重写了ImportSelector接口中的selectImports()方法。 而selectImports()方法中会去读取两份配置文件，并将配置文件中定义的配置类作为selectImports()方法的返回值返回，返回值代表的就是需要将哪些类交给Spring的IOC容器进行管理。 那么所有自动配置类的中声明的bean都会加载到Spring的IOC容器中吗? 其实并不会，因为这些配置类中在声明bean时，通常都会添加@Conditional开头的注解，这个注解就是进行条件装配。而Spring会根据Conditional注解有选择性的进行bean的创建。 @Enable 开头的注解底层，它就封装了一个注解： import 注解，它里面指定了一个类，是 ImportSelector 接口的实现类。在实现类当中，我们需要去实现 ImportSelector 接口当中的一个方法 selectImports 这个方法。这个方法的返回值代表的就是我需要将哪些类交给 spring 的 IOC容器进行管理。 此时它会去读取两份配置文件，一份是 spring.factories，另外一份是autoConfiguration.imports。而在 autoConfiguration.imports 这份文件当中，它就会去配置大量的自动配置的类。 而前面我们也提到过这些所有的自动配置类当中，所有的 bean都会加载到 spring 的 IOC 容器当中吗？其实并不会，因为这些配置类当中，在声明 bean 的时候，通常会加上这么一类@Conditional 开头的注解。这个注解就是进行条件装配。所以SpringBoot非常的智能，它会根据 @Conditional 注解来进行条件装配。只有条件成立，它才会声明这个bean，才会将这个 bean 交给 IOC 容器管理。 自定义starter 分析 前面我们解析了SpringBoot中自动配置的原理，下面我们就通过一个自定义starter案例来加深对于自动配置原理的理解。首先介绍一下自定义starter的业务场景，再来分析一下具体的操作步骤。\n所谓starter指的就是SpringBoot当中的起步依赖。在SpringBoot当中已经给我们提供了很多的起步依赖了，我们为什么还需要自定义 starter 起步依赖？\n这是因为在实际的项目开发当中，我们可能会用到很多第三方的技术，并不是所有的第三方的技术官方都给我们提供了与SpringBoot整合的starter起步依赖，但是这些技术又非常的通用，在很多项目组当中都在使用。\n业务场景：\n我们前面案例当中所使用的阿里云OSS对象存储服务，现在阿里云的官方是没有给我们提供对应的起步依赖的，这个时候使用起来就会比较繁琐，我们需要引入对应的依赖。我们还需要在配置文件当中进行配置，还需要基于官方SDK示例来改造对应的工具类，我们在项目当中才可以进行使用。 大家想在我们当前项目当中使用了阿里云OSS，我们需要进行这么多步的操作。在别的项目组当中要想使用阿里云OSS，是不是也需要进行这么多步的操作，所以这个时候我们就可以自定义一些公共组件，在这些公共组件当中，我就可以提前把需要配置的bean都提前配置好。将来在项目当中，我要想使用这个技术，我直接将组件对应的坐标直接引入进来，就已经自动配置好了，就可以直接使用了。我们也可以把公共组件提供给别的项目组进行使用，这样就可以大大的简化我们的开发。 在SpringBoot项目中，一般都会将这些公共组件封装为SpringBoot当中的starter，也就是我们所说的起步依赖。\n而在springboot中，官方提供的起步依赖 或 第三方提供的起步依赖，基本都会包含两个模块，如下所示：\n其中，spring-boot-starter 或 xxx-spring-boot-starter 这个模块主要是依赖管理的功能。 而 spring-boot-autoconfigure 或 xxxx-spring-boot-autoconfigure 主要是起到自动配置的作用，自动配置的核心代码就在这个模块中编写。\n例如我们当前项目中的起步依赖：\n：\nSpringBoot官方starter命名： spring-boot-starter-xxxx\n第三组织提供的starter命名： xxxx-spring-boot-starter\nSpringBoot官方的自动配置依赖 spring-boot-autoconfigure 中就提供了配置类，并且也提供了springboot会自动读取的配置文件。当SpringBoot项目启动时，会读取到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports配置文件中的配置类并加载配置类，生成相关bean对象注册到IOC容器中。\n结果：我们可以直接在SpringBoot程序中使用自动配置的bean对象。\n在自定义一个起步依赖starter的时候，按照规范需要定义两个模块：\nstarter模块（进行依赖管理[把程序开发所需要的依赖都定义在starter起步依赖中]） autoconfigure模块（自动配置） 将来在项目当中进行相关功能开发时，只需要引入一个起步依赖就可以了，因为它会将autoconfigure自动配置的依赖给传递下来。\n需求 上面我们简单介绍了自定义starter的场景，以及自定义starter时涉及到的模块之后，接下来我们就来完成一个自定义starter的案例。\n需求：自定义aliyun-oss-spring-boot-starter，完成阿里云OSS操作工具类 AliyunOSSOperator 的自动配置。 目标：引入起步依赖引入之后，要想使用阿里云OSS，注入AliyunOSSOperator 直接使用即可。 之前我们的用法：\n1). 在pom.xml中引入阿里云oss的所有依赖\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;!--阿里云OSS--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-sdk-oss\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.17.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.xml.bind\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.activation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activation\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- no more than 2.3.3--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.glassfish.jaxb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2). application.yml 中配置阿里云OSS的配置信息\n1 2 3 4 5 6 #阿里云oss配置 aliyun: oss: endpoint: https://oss-cn-beijing.aliyuncs.com bucketName: java-ai-26-5-07 region: cn-beijing 3). 定义实体类封装配置信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 package com.itheima.utils; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = \u0026#34;aliyun.oss\u0026#34;) public class AliyunOSSProperties { private String endpoint; private String bucketName; } 4). 定义工具类AliyunOSSOperator\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 package com.itheima.utils; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.common.auth.CredentialsProviderFactory; import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider; import com.aliyun.oss.model.OSSObjectSummary; import com.aliyun.oss.model.ObjectListing; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @Component public class AliyunOSSOperator { @Autowired private AliyunOSSProperties aliyunOSSProperties; /** * 文件上传 */ public String upload(byte[] content, String originalFilename) throws Exception { String endpoint = aliyunOSSProperties.getEndpoint(); String bucketName = aliyunOSSProperties.getBucketName(); // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 填写Object完整路径，例如202406/1.png。Object完整路径中不能包含Bucket名称。 //获取当前系统日期的字符串,格式为 yyyy/MM String dir = LocalDate.now().format(DateTimeFormatter.ofPattern(\u0026#34;yyyy/MM\u0026#34;)); //根据原始文件名originalFilename, 生成一个新的不重复的文件名 String newFileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf(\u0026#34;.\u0026#34;)); String objectName = dir + \u0026#34;/\u0026#34; + newFileName; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); //文件上传 try { ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content)); } finally { if (ossClient != null) { ossClient.shutdown(); } } return endpoint.split(\u0026#34;//\u0026#34;)[0] + \u0026#34;//\u0026#34; + bucketName + \u0026#34;.\u0026#34; + endpoint.split(\u0026#34;//\u0026#34;)[1] + \u0026#34;/\u0026#34; + objectName; } /** * 查询文件列表 */ public List\u0026lt;String\u0026gt; listFiles() throws Exception { String endpoint = aliyunOSSProperties.getEndpoint(); String bucketName = aliyunOSSProperties.getBucketName(); // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 指定前缀，例如exampledir/object。 String keyPrefix = null; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); try { // 列举文件。如果不设置keyPrefix，则列举存储空间下的所有文件。如果设置keyPrefix，则列举包含指定前缀的文件。 ObjectListing objectListing = ossClient.listObjects(bucketName, keyPrefix); List\u0026lt;OSSObjectSummary\u0026gt; sums = objectListing.getObjectSummaries(); if(sums != null \u0026amp;\u0026amp; !sums.isEmpty()){ return sums.stream().map(OSSObjectSummary::getKey).collect(Collectors.toList()); } } finally { if (ossClient != null) { ossClient.shutdown(); } } return null; } /** * 删除指定对象 */ public void deleteFile(String objectName) throws Exception { String endpoint = aliyunOSSProperties.getEndpoint(); String bucketName = aliyunOSSProperties.getBucketName(); // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); try { // 删除文件或目录。如果要删除目录，目录必须为空。 ossClient.deleteObject(bucketName, objectName); } finally { if (ossClient != null) { ossClient.shutdown(); } } } } 5). 其他地方要使用阿里云OSS，注入工具类，再使用\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Slf4j @RestController public class UploadController { @Autowired private AliyunOSSOperator aliyunOSSOperator; /** * 文件上传 */ @PostMapping(\u0026#34;/upload\u0026#34;) public Result upload(MultipartFile file) throws Exception { log.info(\u0026#34;上传文件：{}\u0026#34;,file.getOriginalFilename()); //调用aliyun OSS进行文件上传 String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename()); //返回结果 return Result.success(url); } } 我们可以看到，在项目中使用阿里云OSS，需要这么五步操作，而阿里云OSS这个云服务还是非常常见的，很多项目中都要使用。\n现在我们使用阿里云OSS，需要做这么几步，将来在开发其他的项目的时候，使用阿里云OSS，这几步要不要做？当团队中其他小伙伴也在使用阿里云OSS的时候，步骤不也是一样的。\n所以这个时候我们就可以制作一个公共组件(自定义starter)。starter定义好之后，将来要使用阿里云OSS进行文件上传，只需要将起步依赖引入进来之后，就可以直接注入 AliyunOSSOperator 使用了。\n实现 需求明确了，接下来我们再来分析一下具体的实现步骤：\n第1步：创建自定义starter模块 aliyun-oss-spring-boot-starter（进行依赖管理） 把阿里云OSS所有的依赖统一管理起来 第2步：创建autoconfigure模块 aliyun-oss-spring-boot-autoconfigure 在starter中引入autoconfigure （我们使用时只需要引入starter起步依赖即可） 第3步：在autoconfigure模块aliyun-oss-spring-boot-autoconfigure中完成自动配置 定义一个自动配置类，在自动配置类中将所要配置的bean都提前配置好 定义配置文件，把自动配置类的全类名定义在配置文件(META-INF/spring/xxxx.imports)中 下面我们就按照分析的步骤来实现自定义starter。\n首先我们先来创建两个Maven模块：\n1). 创建aliyun-oss-spring-boot-starter\n选择springboot的版本，不需要勾选任何的依赖。直接点击 create 创建项目。\n创建完starter模块后，删除多余的文件，只保留一个pom.xml文件。最终保留内容如下：\npom.xml 中的配置如下:（版本尽量保持一致）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-oss-springboot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;name\u0026gt;aliyun-oss-springboot-starter\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;aliyun-oss-springboot-starter\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;17\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 2). 创建aliyun-oss-spring-boot-autoconfigure模块\n选择Springboot的版本，不用勾选任何依赖。\n截图见第一步\n创建完starter模块后，删除多余的文件，只保留 src 和 pom.xml 。最终保留内容如下：\n该模块的pom.xml内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-oss-spring-boot-autoconfigure\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;name\u0026gt;aliyun-oss-spring-boot-autoconfigure\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;aliyun-oss-spring-boot-autoconfigure\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;17\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 按照我们之前的分析，是需要在starter模块中来引入autoconfigure这个模块的。打开starter模块中的pom文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-oss-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;17\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-oss-spring-boot-autoconfigure\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 前两步已经完成了，接下来是最关键的就是第三步：在aliyun-oss-spring-boot-autoconfigure模块当中来完成自动配置操作。\n我们将之前案例中所使用的阿里云OSS部分的代码直接拷贝到autoconfigure模块下，然后进行改造就行了。\n拷贝过来后，还缺失一些相关的依赖，需要把相关依赖也拷贝过来：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-oss-spring-boot-autoconfigure\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;17\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--阿里云OSS依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-sdk-oss\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.18.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.xml.bind\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.activation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activation\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- no more than 2.3.3--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.glassfish.jaxb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 此时需要将之前常见的AliyunOSSOperator文件和AliyunOSSProperties文件拷贝到com.aliyun.oss目录下\n此时会发现这两个文件报错了，原因是没有引入如下依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 但是此时没有别的地方需要到这个依赖，能少引入依赖是最好的，所以我们此时直接在AliyunOSSProperties文件中添加构造方法和toString()方法即可\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public String getEndpoint() { return endpoint; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } public String getBucketName() { return bucketName; } public void setBucketName(String bucketName) { this.bucketName = bucketName; } public String getRegion() { return region; } public void setRegion(String region) { this.region = region; } @Override public String toString() { return \u0026#34;AliyunOSSProperties{\u0026#34; + \u0026#34;endpoint=\u0026#39;\u0026#34; + endpoint + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, bucketName=\u0026#39;\u0026#34; + bucketName + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, region=\u0026#39;\u0026#34; + region + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } 那此时在类上添加的 @Component 注解还有用吗？\n答案：没用了。 在SpringBoot项目中，并不会去扫描com.aliyun.oss这个包，不扫描这个包那类上的注解也就失去了作用。\n@Component注解不需要使用了，可以从类上删除了。\n1). 删除 AliyunOSSOperator 工具类上的 @Component 注解 和 @Autowired 注解。\n2). 删除 AliyunOSSProperties 实体类上的 @Component 注解。\n删除后如果报红色错误，暂时不理会，后面再来处理。\n3). 既然不能用 @Component 注解声明bean，那就需要按照 starter 的定义规范，定义一个自动配置类，在自动配置类中声明bean。\n下面我们就要定义一个自动配置类 AliOSSAutoConfiguration 了，在自动配置类当中来声明 AliOSSOperator 的bean对象。\n具体代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.aliyun.oss; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @EnableConfigurationProperties(AliyunOSSProperties.class) public class AliyunOSSAutoConfiguration { @Bean public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties aliyunOSSProperties) { return new AliyunOSSOperator(aliyunOSSProperties); } } AliyunOSSOperator 的代码中需要增加一个有参构造，将 AliyunOSSProperties 对象传递给工具类。代码改造如下：\n4). 在 aliyun-oss-spring-boot-autoconfigure 模块中的resources下，新建自动配置文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports\n将自动配置类的全类名，配置在文件中，这样在springboot启动的时候，就会加载到这份文件，并加载到其中的配置类了。\n配置内容如下：\n1 com.aliyun.oss.AliyunOSSAutoConfiguration 到此呢，这个 aliyun-oss-spring-boot-stater 就定义好了，哪里要想使用，就可以直接导入依赖，直接注入使用了。\n测试 阿里云OSS的starter我们刚才已经定义好了，接下来我们就来做一个测试。\n导入课程资料中的测试工程springboot-autoconfiguration-test\n测试前准备：\n1). 在导入的test工程中引入阿里云starter依赖\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-oss-springboot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2). 在导入的test工程中的 application.yml 中配置阿里云OSS的配置信息\n1 2 3 4 5 6 #阿里云OSS aliyun: oss: endpoint: https://oss-cn-beijing.aliyuncs.com bucketName: java-ai-26-5-07 region: cn-beijing 3). 在test工程中的 UploadController 类编写代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.itheima.controller; import com.aliyun.oss.AliyunOSSOperator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController public class UploadController { @Autowired private AliyunOSSOperator aliyunOSSOperator; @PostMapping(\u0026#34;/upload\u0026#34;) public String upload(MultipartFile image) throws Exception { //上传文件到阿里云 OSS String url = aliyunOSSOperator.upload(image.getBytes(), image.getOriginalFilename()); return url; } } 编写完代码后，我们启动当前的SpringBoot测试工程，使用Apifox工具进行文件上传：\n这样，我们就完成了starter的定义。在其他项目中要想使用，引入依赖，配置一下阿里云OSS的信息，就可以直接注入是用了 。\n","date":"2026-05-21T20:52:49+08:00","permalink":"https://aurora156.github.io/p/%E5%90%8E%E7%AB%AFweb%E8%BF%9B%E9%98%B6springboot%E5%8E%9F%E7%90%86/","title":"后端web进阶（SpringBoot原理）"},{"content":"后端web进阶(Maven高级) Web开发讲解完毕之后，我们再来学习Maven高级。其实在前面的课程当中，我们已经学习了Maven。\n我们讲到 Maven 是一款构建和管理 Java 项目的工具。经过前面十几天 web 开发的学习，相信大家对于 Maven 这款工具的基本使用应该没什么问题了。我们掌握了 Maven 工具的基本使用之后，其实对于一些简单的项目的构建及管理基本上就没什么问题了。\n但是如果我们需要开发一些中大型的项目，此时仅凭我们前面所学习的 Maven 的基础知识就比较难以应对了。所以我们接下来还需要学习 Maven 提供的一些高级的功能，这些功能在构建和管理 Java 项目的时候用的也是非常多的。\nMaven高级内容包括:\n分模块设计与开发 继承与聚合 私服 分模块设计与开发 介绍 所谓分模块设计，顾名思义指的就是我们在设计一个 Java 项目的时候，将一个 Java 项目拆分成多个模块进行开发。\n1). 未分模块设计的问题\n如果项目不分模块，也就意味着所有的业务代码是不是都写在这一个 Java 项目当中。随着这个项目的业务扩张，项目当中的业务功能可能会越来越多。\n假如我们开发的是一个大型的电商项目，里面可能就包括了商品模块的功能、搜索模块的功能、购物车模块、订单模块、用户中心等等。这些所有的业务代码我们都在一个 Java 项目当中编写。\n此时大家可以试想一下，假如我们开发的是一个大型的电商网站，这个项目组至少几十号甚至几百号开发人员，这些开发人员全部操作这一个 Java 项目。此时大家就会发现我们项目管理和维护起来将会非常的困难。而且大家再来看，假如在我们的项目当中，我们自己定义了一些通用的工具类以及通用的组件，而公司还有其他的项目组，其他项目组也想使用我们所封装的这些组件和工具类，其实是非常不方便的。因为 Java 项目当中包含了当前项目的所有业务代码，所以就造成了这里面所封装的一些组件会难以复用。\n总结起来，主要两点问题：不方便项目的维护和管理、项目中的通用组件难以复用。\n2). 分模块设计\n分模块设计我们在进行项目设计阶段，就可以将一个大的项目拆分成若干个模块，每一个模块都是独立的。\n比如我们可以将商品的相关功能放在商品模块当中，搜索的相关业务功能我都封装在搜索模块当中，还有像购物车模块、订单模块。而为了组件的复用，我们也可以将项目当中的实体类、工具类以及我们定义的通用的组件都单独的抽取到一个模块当中。\n如果当前这个模块，比如订单模块需要用到这些实体类以及工具类或者这些通用组件，此时直接在订单模块当中引入工具类的坐标就可以了。这样我们就将一个项目拆分成了若干个模块儿，这就是分模块设计。\n分模块设计之后，大家再来看。我们在进行项目管理的时候，我就可以几个人一组，几个人来负责订单模块，另外几个人来负责购物车模块，这样更加便于项目的管理以及项目的后期维护。\n而且分模块设计之后，如果我们需要用到另外一个模块的功能，我们直接依赖模块就可以了。比如商品模块、搜索模块、购物车订单模块都需要依赖于通用组件当中封装的一些工具类，我只需要引入通用组件的坐标就可以了。\n分模块设计就是将项目按照功能/结构拆分成若干个子模块，方便项目的管理维护、拓展，也方便模块键的相互调用、资源共享。\n策略 策略一：按照功能模块拆分，比如：公共组件、商品模块、搜索模块、购物车模块、订单模块等。 策略二：按层拆分，比如：公共组件、实体类、控制层、业务层、数据访问层。 策略三：按照功能模块 + 层拆分。 实践 分析 好，我们明白了什么是分模块设计以及分模块设计的优势之后，接下来我们就来看一下我们之前所开发的案例工程。\n我们可以看到在这个项目当中，除了我们所开发的部门管理以及员工管理、登录认证等相关业务功能以外，我们是不是也定义了一些实体类，也就是pojo包下存放的一些类，像分页结果的封装类PageBean、 统一响应结果Result，我们还定义了一些通用的工具类，像Jwts、阿里云OSS操作的工具类等等。\n如果在当前公司的其他项目组当中，也想使用我们所封装的这些公共的组件，该怎么办？大家可以思考一下。\n方案一：直接依赖我们当前项目 tlias-web-management ，但是存在两大缺点：\n这个项目当中包含所有的业务功能代码，而想共享的资源，仅仅是pojo下的实体类，以及 utils 下的工具类。如果全部都依赖进来，项目在启动时将会把所有的类都加载进来，会影响性能。 如果直接把这个项目都依赖进来了，那也就意味着我们所有的业务代码都对外公开了，这个是非常不安全的。 方案二：分模块设计\n将pojo包下的实体类，抽取到一个maven模块中 tlias-pojo 将utils包下的工具类，抽取到一个maven模块中 tlias-utils 其他的业务代码，放在tlias-web-management这个模块中，在该模块中需要用到实体类pojo、工具类utils，直接引入对应的依赖即可。 注意：分模块开发需要先针对模块功能进行设计，再进行编码。不会先将工程开发完毕，然后进行拆分。\nPS：当前我们是为了演示分模块开发，所以是基于我们前面开发的案例项目进行拆分的，实际中都是分模块设计，然后再开发的。\n实现 思路我们分析完毕，接下来，我们就根据我们分析的思路，按照如下模块进行拆分：\n【此处建议将源文件复制一份到新的项目目录下进行操作，创建一个空项目，然后配置对应的jdk】\n1. 创建maven模块tlias-pojo，存放实体类\nA. 创建一个正常的Maven模块，模块名 tlias-pojo\nB. 然后在tlias-pojo中创建一个包 com.itheima.pojo (和原来案例项目中的pojo包名一致)\nC. 将原来案例项目 tlias-web-management 中的pojo包下的实体类，复制到 tlias-pojo 模块中\nD. 在 tlias-pojo 模块的pom.xml文件中引入依赖\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.34\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 因为在实体类中，还用到了Spring框架中的 @DateTimeFormat 这样的注解，所以这里再引入一个springboot的基础起步依赖。\n【注意：如果在引入了依赖后项目依然报红，那么可以将原代码剪切以后再复制进来即可，但是这种方法可能有些麻烦，需要一个一个点开查看是否报错，也可以将这个工程关闭重新打开一下就行了】\nE. 删除原有案例项目 tlias-web-management 的pojo包【直接删除不要犹豫，我们已经将该模块拆分出去了】，然后在pom.xml中引入 tlias-pojo的依赖\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.itheima\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;tlias-pojo\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2. 创建Maven模块 tlias-utils，存放相关工具类\nA. 创建一个正常的Maven模块，模块名 tlias-utils\nB. 然后在 tlias-utils 中创建一个包 com.itheima.utils (和原来案例项目中的utils包名一致)\nC. 将原来案例项目 tlias-web-management 中的util包下的实体类，复制到 tlias-utils 模块中\nD. 在 tlias-utils 模块的 pom.xml 文件中引入依赖。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 \u0026lt;dependencies\u0026gt; \u0026lt;!-- JWT依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.9.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-sdk-oss\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.17.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.xml.bind\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.activation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activation\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- no more than 2.3.3--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.glassfish.jaxb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.34\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; E. 删除原有案例项目 tlias-web-management 的util包【直接删除不要犹豫，我们已经将该模块拆分出去了】，然后在management的 pom.xml中引入 tlias-utils 的依赖。\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.itheima\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;tlias-utils\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 到此呢，就已经完成了模块的拆分，拆分出了tlias-pojo、tlias-utils、tlias-web-management，如果其他项目中需要用到 pojo，或者 utils工具类，就可以直接引入依赖。\n**什么是分模块设计：**将项目按照功能拆分成若干个子模块 **为什么要分模块设计：**方便项目的管理维护、扩展，也方便模块间的相互调用，资源共享 **注意事项：**分模块设计需要先针对模块功能进行设计，再进行编码。不会先将工程开发完毕，然后进行拆分 继承与聚合 在案例项目分模块开发之后啊，我们会看到 tlias-pojo、tlias-utils、tlias-web-management 中都引入了一个依赖 lombok 的依赖。我们在三个模块中分别配置了一次。\n如果是做一个大型的项目，这三个模块当中重复的依赖可能会很多很多。如果每一个 Maven 模块里面，我们都来单独的配置一次，功能虽然能实现，但是配置是比较 繁琐 的 。\n而接下来我们要讲解的 Maven 的继承用来解决这问题的。\n继承 我们可以再创建一个父工程 tlias-parent ，然后让上述的三个模块 tlias-pojo、tlias-utils、tlias-web-management 都来继承这个父工程 。 然后再将各个模块中都共有的依赖，都提取到父工程 tlias-parent中进行配置，只要子工程继承了父工程，依赖它也会继承下来，这样就无需在各个子工程中进行配置了。\n概念：继承描述的是两个工程间的关系，与java中的继承相似，子工程可以继承父工程中的配置信息，常见于依赖关系的继承。 作用：简化依赖配置、统一管理依赖 实现： 1 2 3 4 5 6 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;...\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;...\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;...\u0026lt;/version\u0026gt; \u0026lt;relativePath\u0026gt;....\u0026lt;/relativePath\u0026gt; \u0026lt;/parent\u0026gt; 这是我们在这里先介绍一下什么是继承以及继承的作用，以及在 maven 当中如何来实现这层继承关系。接下来我们就来创建这样一个 parent 父工程，我们就可以将各个子工程当中共有的这部分依赖统一的定义在父工程 parent 当中，从而来简化子工程的依赖配置。接下来我们来看一下具体的操作步骤。\n我们在这里先介绍一下什么是继承以及继承的作用，以及在 maven 当中如何来实现这层继承关系。接下来我们就来创建这样一个 parent 父工程，我们就可以将各个子工程当中共有的这部分依赖，统一的定义在父工程 parent 当中，从而来简化子工程的依赖配置。\n继承关系 思路分析 我们当前的项目 tlias-web-management，还稍微有一点特殊，因为是一个springboot项目，而所有的springboot项目都有一个统一的父工程，就是 spring-boot-starter-parent。 与java语言类似，Maven不支持多继承，一个maven项目只能继承一个父工程，如果继承了spring-boot-starter-parent，就没法继承我们自己定义的父工程 tlias-parent了。\n那我们怎么来解决这个问题呢？\n那此时，大家可以想一下，Java虽然不支持多继承，但是可以支持多重继承，比如：A 继承 B， B 继承C。 那在Maven中也是支持多重继承的，所以呢，我们就可以让 我们自己创建的三个模块，都继承 tlias-parent，而 tlias-parent 再继承 spring-boot-starter-parent，就可以了。 具体结构如下：\n实现 1). 创建maven模块tlias-parent，该工程为父工程，设置打包方式pom(默认jar)。\n工程结构如下（src目录不需要，直接删除）：\n父工程tlias-parent的 pom.xml 文件配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.itheima\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;tlias-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;17\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;17\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/project\u0026gt; Maven打包方式：\njar：普通模块打包，springboot项目基本都是jar包（内嵌tomcat运行） war：普通web程序打包，需要部署在外部的tomcat服务器中运行 pom：父工程或聚合工程，该模块不写代码，仅进行依赖管理 2). 在子工程（tlias-pojo、tlias-utils、tlias-web-management）的pom.xml文件中，配置继承关系。\n1 2 3 4 5 6 7 8 9 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;com.itheima\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;tlias-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;relativePath\u0026gt;../tlias-parent/pom.xml\u0026lt;/relativePath\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;artifactId\u0026gt;tlias-utils\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; 这里是以 tlias-utils 为例，指定了其父工程。其他的模块，都是相同的配置方式。\n注意：\n在子工程中，配置了继承关系之后，坐标中的groupId是可以省略的，因为会自动继承父工程的 。 relativePath指定父工程的pom文件的相对位置（如果不指定，将从本地仓库/远程仓库查找该工程）。 ../ 代表的上一级目录 3). 在父工程中配置各个工程共有的依赖（子工程会自动继承父工程的依赖）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.itheima\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;tlias-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;17\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;17\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.8\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.34\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 此时，我们已经将各个子工程中共有的依赖（lombok 和 spring-boot-starter），都定义在了父工程中，子工程中的这一项依赖，就可以直接删除了。删除之后，我们会看到父工程中配置的依赖 lombok，子工程直接继承下来了。\n工程结构说明:\n我们当前的项目结构为： tlias-parent tlias-pojo tlias-utils tlias-web-management 因为我们是项目开发完毕之后，给大家基于现有项目拆分的各个模块，tlias-web-management 已经存在了，然后再创建各个模块与父工程，所以父工程与模块之间是平级的。\n工程结构说明:\n而实际项目中，可能还会见到下面的工程结构： tlias-parent tlias-pojo tlias-utils tlias-web-management 而在真实的企业开发中，都是先设计好模块之后，再开始创建模块，开发项目。 那此时呢，一般都会先创建父工程 tlias-parent，然后将创建的各个子模块，都放在父工程parent下面。 这样层级结构会更加清晰一些。\n版本锁定 场景 如果项目中各个模块中都公共的这部分依赖，我们可以直接定义在父工程中，从而简化子工程的配置。 然而在项目开发中，还有一部分依赖，并不是各个模块都共有的，可能只是其中的一小部分模块中使用到了这个依赖。\n比如：在tlias-web-management、tlias-web-system、tlias-web-report这三个子工程中，都使用到了jwt的依赖。 但是 tlias-pojo、tlias-utils中并不需要这个依赖，那此时，这个依赖，我们不会直接配置在父工程 tlias-parent中，而是哪个模块需要，就在哪个模块中配置。\n而由于是一个项目中的多个模块，那多个模块中，我们要使用的同一个依赖的版本要一致，这样便于项目依赖的统一管理。比如：这个jwt依赖，我们都使用的是 0.9.1 这个版本。\n那假如说，我们项目要升级，要使用到jwt最新版本 0.9.2 中的一个新功能，那此时需要将依赖的版本升级到0.9.2，那此时该怎么做呢 ？\n第一步：去找当前项目中所有的模块的pom.xml配置文件，看哪些模块用到了jwt的依赖。\n第二步：找到这个依赖之后，将其版本version，更换为 0.9.2。\n问题：如果项目拆分的模块比较多，每一次更换版本，我们都得找到这个项目中的每一个模块，一个一个的更改。 很容易就会出现，遗漏掉一个模块，忘记更换版本的情况。\n那我们又该如何来解决这个问题，如何来统一管理各个依赖的版本呢？\n答案：Maven的版本锁定功能。\n介绍 在maven中，可以在父工程的pom文件中通过 \u0026lt;dependencyManagement\u0026gt; 来统一管理依赖版本。\n父工程： 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;!--统一管理依赖版本--\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!--JWT令牌--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.9.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 子工程： 1 2 3 4 5 6 7 \u0026lt;dependencies\u0026gt; \u0026lt;!--JWT令牌--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 在父工程中所配置的 \u0026lt;dependencyManagement\u0026gt; 只能统一管理依赖版本，并不会将这个依赖直接引入进来。 这点和 \u0026lt;dependencies\u0026gt; 是不同的。 子工程要使用这个依赖，还是需要引入的，只是此时就无需指定 \u0026lt;version\u0026gt; 版本号了，父工程统一管理。变更依赖版本，只需在父工程中统一变更。 实现 接下来，我们就可以将 tlias-utils 模块中单独配置的依赖，将其版本统一交给 tlias-parent 进行统一管理。\n具体步骤如下：\n1). tlias-parent中的配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- JWT依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.9.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-sdk-oss\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.17.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.xml.bind\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.activation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activation\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- no more than 2.3.3--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.glassfish.jaxb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 2). tlias-utils 中的pom.xml配置\n如果依赖的版本已经在父工程进行了统一管理，所以在子工程中就无需再配置依赖的版本了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;dependencies\u0026gt; \u0026lt;!-- JWT依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-sdk-oss\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.xml.bind\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-api\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.activation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activation\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- no more than 2.3.3--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.glassfish.jaxb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 我们之所以，在springboot项目中很多时候，引入依赖坐标，都不需要指定依赖的版本 \u0026lt;version\u0026gt; ，是因为在父工程 spring-boot-starter-parent中已经通过 \u0026lt;dependencyManagement\u0026gt;对依赖的版本进行了统一的管理维护。\n属性配置 我们也可以通过自定义属性及属性引用的形式，在父工程中将依赖的版本号进行集中管理维护。\n具体语法为：\n1). 自定义属性\n1 2 3 \u0026lt;properties\u0026gt; \u0026lt;lombok.version\u0026gt;1.18.34\u0026lt;/lombok.version\u0026gt; \u0026lt;/properties\u0026gt; 2). 引用属性\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${lombok.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 接下来，我们就可以在父工程中，将所有的版本号，都集中管理维护起来。\n接下来，我们就可以在父工程中，将所有的版本号，都集中管理维护起来。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.itheima\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;tlias-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;17\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;17\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;spring-boot.version\u0026gt;3.5.13\u0026lt;/spring-boot.version\u0026gt; \u0026lt;lombok.version\u0026gt;1.18.34\u0026lt;/lombok.version\u0026gt; \u0026lt;jwt.version\u0026gt;0.9.1\u0026lt;/jwt.version\u0026gt; \u0026lt;aliyun.oss.version\u0026gt;3.17.4\u0026lt;/aliyun.oss.version\u0026gt; \u0026lt;jaxb.version\u0026gt;2.3.1\u0026lt;/jaxb.version\u0026gt; \u0026lt;javax.activation.version\u0026gt;1.1.1\u0026lt;/javax.activation.version\u0026gt; \u0026lt;jaxb.runtime.version\u0026gt;2.3.3\u0026lt;/jaxb.runtime.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-boot.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${lombok.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- JWT依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${jwt.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-sdk-oss\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${aliyun.oss.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.xml.bind\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${jaxb.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.activation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activation\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${javax.activation.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- no more than 2.3.3--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.glassfish.jaxb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${jaxb.runtime.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; \u0026lt;/project\u0026gt; 版本集中管理之后，我们要想修改依赖的版本，就只需要在父工程中自定义属性的位置，修改对应的属性值即可。\n面试题： 与 的区别是什么?\n\u0026lt;dependencies\u0026gt; 是直接依赖，在父工程配置了依赖，子工程会直接继承下来。 \u0026lt;dependencyManagement\u0026gt; 是统一管理依赖版本，不会直接依赖，还需要在子工程中引入所需依赖(无需指定版本) 聚合 分模块设计与开发之后啊，我们的项目被拆分为多个模块，而模块之间的关系，可能错综复杂。 那就比如我们当前的案例项目，结构如下（相对还是比较简单的）：\n此时，tlias-web-management 模块的父工程是 tlias-parent，该模块又依赖了 tlias-pojo、tlias-utils 模块。 那此时，我们要想将 tlias-web-management 模块打包，是比较繁琐的。因为在进行项目打包时，maven会从本地仓库中来查找 tlias-parent 父工程，以及它所依赖的模块 tlias-pojo、tlias-utils，而本地仓库目前是没有这几个依赖的。\n所以，我们再打包tlias-web-management 模块前，需要将 tlias-parent、tlias-pojo、tlias-utils 分别执行install生命周期安装到maven的本地仓库，然后再针对于 tlias-web-management 模块执行package进行打包操作。\n那此时，大家试想一下，如果开发一个大型项目，拆分的模块很多，模块之间的依赖关系错综复杂，那此时要进行项目的打包、安装操作，是非常繁琐的。 而我们接下来，要讲解的maven的聚合就是来解决这个问题的，通过maven的聚合就可以轻松实现项目的一键构建（清理、编译、测试、打包、安装等）。\n介绍 **聚合：**将多个模块组织成一个整体，同时进行项目的构建。 **聚合工程：**一个不具有业务功能的“空”工程（有且仅有一个pom文件） 【PS：一般来说，继承关系中的父工程与聚合关系中的聚合工程是同一个】 **作用：**快速构建项目（无需根据依赖关系手动构建，直接在聚合工程上构建即可） 实现 在maven中，我们可以在聚合工程中通过 \u0026lt;moudules\u0026gt; 设置当前聚合工程所包含的子模块的名称。我们可以在 tlias-parent中，添加如下配置，来指定当前聚合工程，需要聚合的模块：\n1 2 3 4 5 6 \u0026lt;!-- 聚合其他模块 --\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;module\u0026gt;../tlias-pojo\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;../tlias-utils\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;../tlias-web-management\u0026lt;/module\u0026gt; \u0026lt;/modules\u0026gt; 那此时，我们要进行编译、打包、安装操作，就无需在每一个模块上操作了。只需要在聚合工程上，统一进行操作就可以了。添加了聚合的module后Maven面板就会变成这样\n**测试：**执行在聚合工程 tlias-parent 中执行 package 打包指令\n那 tlias-parent 中所聚合的其他模块全部都会执行 package 指令，这就是通过聚合实现项目的一键构建（一键清理clean、一键编译compile、一键测试test、一键打包package、一键安装install等）。\n继承与聚合对比 作用 聚合用于快速构建项目 继承用于简化依赖配置、统一管理依赖 相同点： 聚合与继承的pom.xml文件打包方式均为pom，通常将两种关系制作到同一个pom文件中 聚合与继承均属于设计型模块，并无实际的模块内容 不同点： 聚合是在聚合工程中配置关系，聚合可以感知到参与聚合的模块有哪些 继承是在子模块中配置关系，父模块无法感知哪些子模块继承了自己 私服 前面我们在讲解多模块开发的时候，我们讲到我们所拆分的模块是可以在同一个公司各个项目组之间进行资源共享的。这个模块的资源共享，就需要通过我们接下来所讲解的 Maven 的私服来实现。\n首先我们先介绍一下什么是私服，以及它的作用是什么。再来介绍一下我们如何将每位模块打包上传到私服，以及从私服当中来下载。\n场景 在介绍什么是私服之前，我们先来分析一下同一个公司，两个项目组之间如何基于私服进行资源的共享。\n假设现在有两个团队，A 和 B。 A 开发了一个模块 tlias-utils，模块开发完毕之后，将模块打成jar包，并安装到了A的本地仓库。\n那此时，该公司的B团队开发项目时，要想使用 tlias-utils 中提供的工具类，该怎么办呢？ 对于maven项目来说，是不是在pom.xml文件中引入 tlias-utils的坐标就可以了呢？\n大家可以思考一下，当B团队在maven项目的pom.xml配置文件中引入了依赖的坐标之后，maven是如何查找这个依赖的？ 查找顺序为：\n1). 本地仓库：本地仓库中是没有这个依赖jar包的。\n2). 远程中央仓库：由于该模块时自己公司开发的，远程仓库中也没有这个依赖。\n因为目前tlias-utils这个依赖，还在A的本地仓库中的。 B电脑上的maven项目，是不可能找得到A电脑上maven本地仓库的jar包的。 那此时，大家可能会有一个想法：因为A和B都会连接中央仓库，我们可以将A本地仓库的jar包，直接上传到中央仓库，然后B从中央仓库中下载tlias-utils这个依赖。\n这个想法很美好，但是现实很残酷。这个方案是行不通的，因为中央仓库全球只有一个，不是什么人都可以往中央仓库中来上传jar包的，我们是没有权限操作的。\n那此时，maven的私服就出场了，私服其实就是架设在公司局域网内部的一台服务器，就是一种特殊的远程仓库。\n有了私服之后，各个团队就可以直接来连接私服了。 A 连接上私服之后，他就可以把jar包直接上传到私服当中。我公司自己内部搭建的服务器，我是不是有权限操作呀，把jar包上传到私服之后，我让 B 团队的所有开发人员也连接同一台私服。连接上这一台私服之后，他就会根据坐标的信息，直接从私服当中将对应的jar包下载到自己的本地仓库，这样就可以使用到依赖当中所提供的一些工具类了。这样我们就可以通过私服来完成资源的共享。\n而如果我们在项目中需要使用其他第三方提供的依赖，如果本地仓库没有，也会自动连接私服下载，如果私服没有，私服此时会自动连接中央仓库，去中央仓库中下载依赖，然后将下载的依赖存储在私服仓库及本地仓库中。\n介绍 **私服：**是一种特殊的远程仓库，它是架设在局域网内的仓库服务，用来代理位于外部的中央仓库，用于解决团队内部的资源共享与资源同步问题。 依赖查找顺序： 本地仓库 私服仓库 中央仓库 **注意事项：**私服在企业项目开发中，一个项目/公司，只需要一台即可（无需我们自己搭建，会使用即可）。 资源上传与下载 步骤分析 资源上传与下载，我们需要做三步配置，执行一条指令。\n第一步配置：在maven的配置文件中配置访问私服的用户名、密码。 第二步配置：在maven的配置文件中配置连接私服的地址(url地址)。 第三步配置：在项目的pom.xml文件中配置上传资源的位置(url地址)。 配置好了上述三步之后，要上传资源到私服仓库，就执行执行maven生命周期：deploy。 私服仓库说明：\nRELEASE：存储自己开发的RELEASE发布版本的资源。 SNAPSHOT：存储自己开发的SNAPSHOT发布版本的资源。 Central：存储的是从中央仓库下载下来的依赖。 项目版本说明：\nRELEASE(发布版本)：功能趋于稳定、当前更新停止，可以用于发行的版本，存储在私服中的RELEASE仓库中。 SNAPSHOT(快照版本)：功能不稳定、尚处于开发中的版本，即快照版本，存储在私服的SNAPSHOT仓库中。 具体操作 私服已经搭建好了，已经启动起来了，我们可以访问私服测试：http://localhost:8081。\n我的启动地址：D:\\develop\\nexus\\nexus\\nexus-3.75.1-01\\bin下的start.bat文件\n私服准备好了之后，我们要做如下几步配置：\n1. 设置私服的访问用户名/密码（在自己maven安装目录下的conf/settings.xml中的servers中配置）\n我的Maven配置地址：D:\\develop\\.m2\\apache-maven-3.9.11\\conf\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;maven-releases\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;admin\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;admin\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;maven-snapshots\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;admin\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;admin\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; 2. 设置私服依赖下载的仓库组地址（在自己maven安装目录下的conf/settings.xml中的mirrors中配置）\n1 2 3 4 5 \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;maven-public\u0026lt;/id\u0026gt; \u0026lt;mirrorOf\u0026gt;*\u0026lt;/mirrorOf\u0026gt; \u0026lt;url\u0026gt;http://localhost:8081/repository/maven-public/\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; 3. 设置私服依赖下载的仓库组地址（在自己maven安装目录下的conf/settings.xml中的profiles中配置）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;allow-snapshots\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;repositories\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;maven-public\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;http://localhost:8081/repository/maven-public/\u0026lt;/url\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; \u0026lt;/profile\u0026gt; 4. IDEA的maven工程的pom文件中配置上传（发布）地址(直接在tlias-parent中配置发布地址)\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;distributionManagement\u0026gt; \u0026lt;!-- release版本的发布地址 --\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;maven-releases\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;http://localhost:8081/repository/maven-releases/\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;!-- snapshot版本的发布地址 --\u0026gt; \u0026lt;snapshotRepository\u0026gt; \u0026lt;id\u0026gt;maven-snapshots\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;http://localhost:8081/repository/maven-snapshots/\u0026lt;/url\u0026gt; \u0026lt;/snapshotRepository\u0026gt; \u0026lt;/distributionManagement\u0026gt; 配置完成之后，我们就可以在tlias-parent中执行deploy生命周期，将项目发布到私服仓库中。\n通过日志，我们可以看到，这几个模块打的jar包确实已经上传到了私服仓库中（由于当前我们的项目是SNAPSHOT版本，所以jar包是上传到了snapshot仓库中）。\n那接下来，我们再来打开私服来看一下：\n我们看到，我们项目中的这几个模块，在私服中都有了。 那接下来，当其他项目组的开发人员在项目中，就可以直接通过依赖的坐标，就可以完成引入对应的依赖，此时本地仓库没有，就会自动从私服仓库中下载。\n此时我们就可以在其他的项目中引入这个tlias-utils的依赖，但是这个依赖不一定就是从私服中引入的，有可能是本地仓库中引入进来的\n备注说明：在真实的企业开发中，私服都是在远程服务器中的，并不是在本地的。这里我们只是为了方便演示。\n注意：此次学习完记得将私服关闭，可以将过程中关闭的阿里云开起来\nweb后端总结 到此，全部的web后端开发内容就全部完成了，以下为web后端整体内容的框架与总结\n其中SpringMVC就是Spring framework的一部分，是Spring framework对web、apps的支持，它并不是一个独立的模块\n其中SpringMVC、Spring framework、Mybatis统称为SSM\n","date":"2026-05-21T20:50:02+08:00","permalink":"https://aurora156.github.io/p/%E5%90%8E%E7%AB%AFweb%E8%BF%9B%E9%98%B6maven%E9%AB%98%E7%BA%A7/","title":"后端web进阶（Maven高级）"},{"content":"后端Web进阶（AOP） 接下来我们进入到AOP的学习。 AOP也是spring框架的第二大核心，我们先来学习AOP的基础。\n什么是AOP？\nAOP：Aspect Oriented Programming（面向切面编程、面向方面编程），其实说白了，面向切面编程就是面向特定方法编程。\n那什么又是面向方法编程呢，为什么又需要面向方法编程呢？\n来，我们举个例子做一个说明：\n比如，我们这里有一个项目，项目中开发了很多的业务功能。然而有一些业务功能执行效率比较低，执行耗时较长，我们需要针对于这些业务方法进行优化。 那首先第一步就需要定位出执行耗时比较长的业务方法，再针对于业务方法再来进行优化。\n此时我们就需要统计当前这个项目当中每一个业务方法的执行耗时。那么统计每一个业务方法的执行耗时该怎么实现？\n可能多数人首先想到的就是在每一个业务方法运行之前，记录这个方法运行的开始时间。在这个方法运行完毕之后，再来记录这个方法运行的结束时间。拿结束时间减去开始时间，不就是这个方法的执行耗时吗。\n而这个功能如果通过AOP来实现，我们只需要单独定义下面这一小段代码即可，不需要修改原始的任何业务方法即可记录每一个业务方法的执行耗时。\n所以，AOP的优势主要体现在以下四个方面：\n减少重复代码：不需要在业务方法中定义大量的重复性的代码，只需要将重复性的代码抽取到AOP程序中即可。 代码无侵入：在基于AOP实现这些业务功能时，对原有的业务代码是没有任何侵入的，不需要修改任何的业务代码。 提高开发效率 维护方便 AOP是一种思想，而在Spring框架中，对这种思想进行了实现，那我们要学习的就是Spring AOP。\nAOP基础 AOP入门 在了解了什么是AOP后，我们下面通过一个快速入门程序，体验下AOP的开发，并掌握Spring中AOP的开发步骤。\n需求：统计部门管理各个业务层方法执行耗时。 原始方式： 在原始的实现方式中，我们需要在业务层的也一个方法执行执行，获取方法运行的开始时间； 然后运行原始的方法逻辑； 最后在每一个方法运行结束时，获取方法运行结束时间，计算执行耗时。\nSpringAOP实现步骤： 此处直接导入黑马学习资料中提供的springboot-aop-quickstart项目工程\n1). 导入依赖：在 pom.xml 文件中导入 AOP 的依赖\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-aop\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 2). 编写AOP程序：针对于特定方法根据业务需要进行编程\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Component @Aspect //当前类为切面类 @Slf4j public class RecordTimeAspect { //针对部门管理层所有的方法 @Around(\u0026#34;execution(* com.itheima.service.impl.DeptServiceImpl.*(..))\u0026#34;) public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //记录方法执行开始时间 long begin = System.currentTimeMillis(); //执行原始方法 Object result = pjp.proceed(); //记录方法执行结束时间 long end = System.currentTimeMillis(); //计算方法执行耗时 log.info(\u0026#34;方法执行耗时: {}毫秒\u0026#34;,end-begin); return result; } } 重新启动SpringBoot服务，打开浏览器访问部门管理的功能进行测试：\n我们可以看到，在控制台中输出了方法的执行耗时：\n但是当我们测试了其他功能时也仅显示方法执行时间，不知道是哪个方法，此时就可以在第三步中添加pjp.getSignature()方法获取当前运行的原始方法的签名\n1 2 3 4 //3. 记录方法运行的结束时间, 记录耗时 long end = System.currentTimeMillis(); log.info(\u0026#34;方法 {} 执行耗时: {}ms\u0026#34;, pjp.getSignature() ,end-begin); return result; 我们通过AOP入门程序完成了业务方法执行耗时的统计，那其实AOP的功能远不止于此，常见的应用场景如下：\n记录系统的操作日志 权限控制 事务管理：我们前面所讲解的Spring事务管理，底层其实也是通过AOP来实现的，只要添加@Transactional注解之后，AOP程序自动会在原始方法运行前先来开启事务，在原始方法运行完毕之后提交或回滚事务 这些都是AOP应用的典型场景。\n通过入门程序，我们也应该感受到了AOP面向切面编程的一些优势：\n代码无侵入：没有修改原始的业务方法，就已经对原始的业务方法进行了功能的增强或者是功能的改变 减少了重复代码 提高开发效率 维护方便 AOP核心概念 通过SpringAOP的快速入门，感受了一下AOP面向切面编程的开发方式。下面我们再来学习AOP当中涉及到的一些核心概念。\n连接点：JoinPoint，可以被AOP控制的方法（暗含方法执行时的相关信息） 连接点指的是可以被aop控制的方法。例如：入门程序当中所有的业务方法都是可以被aop控制的方法。 在SpringAOP提供的JoinPoint当中，封装了连接点方法在执行时的相关信息。（后面会有具体的讲解） 通知：Advice，指哪些重复的逻辑，也就是共性功能（最终体现为一个方法） 在入门程序中是需要统计各个业务方法的执行耗时的，此时我们就需要在这些业务方法运行开始之前，先记录这个方法运行的开始时间，在每一个业务方法运行结束的时候，再来记录这个方法运行的结束时间。 在AOP面向切面编程当中，我们只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑，也就是共性的功能。 切入点：PointCut，匹配连接点的条件，通知仅会在切入点方法执行时被应用。 在通知当中，我们所定义的共性功能到底要应用在哪些方法上？此时就涉及到了切入点pointcut概念。切入点指的是匹配连接点的条件。通知仅会在切入点方法运行时才会被应用。 在aop的开发当中，我们通常会通过一个切入点表达式来描述切入点(后面会有详解)。 假如：切入点表达式改为DeptServiceImpl.list()，此时就代表仅仅只有list这一个方法是切入点。只有list()方法在运行的时候才会应用通知。\n切面：Aspect，描述通知与切入点的对应关系（通知+切入点）\n当通知和切入点结合在一起，就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法，在什么时候执行什么样的操作。\n而切面所在的类，称之为切面类（被@Aspect注解标识的类）。\n目标对象：Target，通知所应用的对象 目标对象指的就是通知所应用的对象，我们就称之为目标对象。\nAOP的核心概念我们介绍完毕之后，接下来我们再来分析一下我们所定义的通知是如何与目标对象结合在一起，对目标对象当中的方法进行功能增强的。\nSpring的AOP底层是基于动态代理技术来实现的，也就是说在程序运行的时候，会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。\n为什么叫“动态”代理？\n静态代理： 如果我们要手动去写那个 DeptServiceProxy 类，那叫静态代理。如果有 100 个 Service 类都要加日志，你就得手写 100 个代理类，工作量巨大且代码冗余。\n动态代理：\n在这张图的技术（Spring AOP）中，右下角的 DeptServiceProxy 实际上是不存在的源码文件。它是 Spring 框架在程序运行时，利用 JDK 动态代理或 CGLIB 技术，在内存中动态生成出来的。\n程序员只需要写好“目标对象”和“切面”。 框架会自动把它们组装成一个代理对象。 AOP进阶 AOP的基础知识学习完之后，下面我们对AOP当中的各个细节进行详细的学习。主要分为4个部分：\n通知类型 通知顺序 切入点表达式 连接点 我们先来学习第一部分通知类型。\n在入门程序当中，我们已经使用了一种功能最为强大的通知类型：Around环绕通知。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Component @Aspect //当前类为切面类 @Slf4j public class TimeAspect { @Around(\u0026#34;execution(* com.itheima.service.impl.DeptServiceImpl.*(..))\u0026#34;) public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //记录方法执行开始时间 long begin = System.currentTimeMillis(); //执行原始方法 Object result = pjp.proceed(); //记录方法执行结束时间 long end = System.currentTimeMillis(); //计算方法执行耗时 log.info(\u0026#34;方法执行耗时: {}毫秒\u0026#34;,end-begin); return result; } } 只要我们在通知方法上加上了@Around注解，就代表当前通知是一个环绕通知。\nSpring AOP 通知类型 说明 @Around 环绕通知，此注解标注的通知方法在目标方法前、后都被执行 @Before 前置通知，此注解标注的通知方法在目标方法前被执行 @After 后置通知，此注解标注的通知方法在目标方法后被执行，无论是否有异常都会执行 @AfterReturning 返回后通知，此注解标注的通知方法在目标方法后被执行，有异常不会执行 @AfterThrowing 异常后通知，此注解标注的通知方法发生异常后执行 五种类型标志：前置、环绕、后置、返回后、异常后\n下面我们通过代码演示，来加深对于不同通知类型的理解：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Slf4j @Component @Aspect public class MyAspect1 { //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(JoinPoint joinPoint){ log.info(\u0026#34;before ...\u0026#34;); } //环绕通知 @Around(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info(\u0026#34;around before ...\u0026#34;); //调用目标对象的原始方法执行 Object result = proceedingJoinPoint.proceed(); //原始方法如果执行时有异常，环绕通知中的后置代码不会在执行了 log.info(\u0026#34;around after ...\u0026#34;); return result; } //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void after(JoinPoint joinPoint){ log.info(\u0026#34;after ...\u0026#34;); } //返回后通知（程序在正常执行的情况下，会执行的后置通知） @AfterReturning(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void afterReturning(JoinPoint joinPoint){ log.info(\u0026#34;afterReturning ...\u0026#34;); } //异常通知（程序在出现异常的情况下，执行的后置通知） @AfterThrowing(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void afterThrowing(JoinPoint joinPoint){ log.info(\u0026#34;afterThrowing ...\u0026#34;); } } 重新启动SpringBoot服务，进行测试：\n1). 没有异常情况下：\n使用 Apifox 测试查询所有部门数据\n查看idea中控制台日志输出：\n程序没有发生异常的情况下，@AfterThrowing标识的通知方法不会执行。\n2). 出现异常情况下：\n修改DeptServiceImpl业务实现类中的代码： 添加异常\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override public List\u0026lt;Dept\u0026gt; list() { List\u0026lt;Dept\u0026gt; deptList = deptMapper.list(); //模拟异常 int num = 10/0; return deptList; } //省略其他代码... } 重新启动SpringBoot服务，测试发生异常情况下通知的执行：\n查看idea中控制台日志输出\n程序发生异常的情况下：\n@AfterReturning标识的通知方法不会执行，@AfterThrowing标识的通知方法执行了 @Around环绕通知中原始方法调用时有异常，通知中的环绕后的代码逻辑也不会在执行了 （因为原始方法调用已经出异常了） 在使用通知时的注意事项：\n@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行，其他通知不需要考虑目标方法执行 @Around环绕通知方法的返回值，必须指定为Object，来接收原始方法的返回值，否则原始方法执行完毕，是获取不到返回值的。 五种常见的通知类型，我们已经测试完毕了，此时我们再来看一下刚才所编写的代码，有什么问题吗？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) //环绕通知 @Around(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) //返回后通知（程序在正常执行的情况下，会执行的后置通知） @AfterReturning(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) //异常通知（程序在出现异常的情况下，执行的后置通知） @AfterThrowing(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) 我们发现啊，每一个注解里面都指定了切入点表达式，而且这些切入点表达式都一模一样。此时我们的代码当中就存在了大量的重复性的切入点表达式，假如此时切入点表达式需要变动，就需要将所有的切入点表达式一个一个的来改动，就变得非常繁琐了。\n怎么来解决这个切入点表达式重复的问题？ 答案就是：抽取\nSpring提供了@PointCut注解，该注解的作用是将公共的切入点表达式抽取出来，需要用到时引用该切入点表达式即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Slf4j @Component @Aspect public class MyAspect1 { //切入点方法（公共的切入点表达式） @Pointcut(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) private void pt(){} //前置通知（引用切入点） @Before(\u0026#34;pt()\u0026#34;) public void before(JoinPoint joinPoint){ log.info(\u0026#34;before ...\u0026#34;); } //环绕通知 @Around(\u0026#34;pt()\u0026#34;) public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info(\u0026#34;around before ...\u0026#34;); //调用目标对象的原始方法执行 Object result = proceedingJoinPoint.proceed(); //原始方法在执行时：发生异常 //后续代码不在执行 log.info(\u0026#34;around after ...\u0026#34;); return result; } //后置通知 @After(\u0026#34;pt()\u0026#34;) public void after(JoinPoint joinPoint){ log.info(\u0026#34;after ...\u0026#34;); } //返回后通知（程序在正常执行的情况下，会执行的后置通知） @AfterReturning(\u0026#34;pt()\u0026#34;) public void afterReturning(JoinPoint joinPoint){ log.info(\u0026#34;afterReturning ...\u0026#34;); } //异常通知（程序在出现异常的情况下，执行的后置通知） @AfterThrowing(\u0026#34;pt()\u0026#34;) public void afterThrowing(JoinPoint joinPoint){ log.info(\u0026#34;afterThrowing ...\u0026#34;); } } 需要注意的是：当切入点方法使用private修饰时，仅能在当前切面类中引用该表达式， 当外部其他切面类中也要引用当前类中的切入点表达式，就需要把private改为public，而在引用的时候，具体的语法为：\n1 2 3 4 5 6 7 8 9 10 @Slf4j @Component @Aspect public class MyAspect2 { //引用MyAspect1切面类中的切入点表达式 @Before(\u0026#34;com.itheima.aspect.MyAspect1.pt()\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect2 -\u0026gt; before ...\u0026#34;); } } 通知顺序 讲解完了Spring中AOP所支持的5种通知类型之后，接下来我们再来研究通知的执行顺序。\n当在项目开发当中，我们定义了多个切面类，而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候，这多个切面类当中的这些通知方法都会运行。\n此时我们就有一个疑问，这多个通知方法到底哪个先运行，哪个后运行？ 下面我们通过程序来验证（这里呢，我们就定义两种类型的通知进行测试，一种是前置通知@Before，一种是后置通知@After）\n定义多个切面类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j @Component @Aspect public class MyAspect2 { //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect2 -\u0026gt; before ...\u0026#34;); } //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void after(){ log.info(\u0026#34;MyAspect2 -\u0026gt; after ...\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j @Component @Aspect public class MyAspect3 { //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect3 -\u0026gt; before ...\u0026#34;); } //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void after(){ log.info(\u0026#34;MyAspect3 -\u0026gt; after ...\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j @Component @Aspect public class MyAspect4 { //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect4 -\u0026gt; before ...\u0026#34;); } //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void after(){ log.info(\u0026#34;MyAspect4 -\u0026gt; after ...\u0026#34;); } } 重新启动SpringBoot服务，测试通知的执行顺序：\n备注：\n把DeptServiceImpl实现类中模拟异常的代码删除或注释掉。 注释掉其他切面类(把@Aspect注释即可)，仅保留MyAspect2、MyAspect3、MyAspect4 ，这样就可以清晰看到执行的结果，而不被其他切面类干扰。 使用 Apifox 测试查询所有部门数据。\n查看idea中控制台日志输出\n通过以上程序运行可以看出在不同切面类中，默认按照切面类的类名字母排序： 目标方法前的通知方法：字母排名靠前的先执行 目标方法后的通知方法：字母排名靠前的后执行 如果我们想控制通知的执行顺序有两种方式：\n修改切面类的类名（这种方式非常繁琐、而且不便管理） 使用Spring提供的@Order注解 使用@Order注解，控制通知的执行顺序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @Component @Aspect @Order(2) //切面类的执行顺序（前置通知：数字越小先执行; 后置通知：数字越小越后执行） public class MyAspect2 { //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect2 -\u0026gt; before ...\u0026#34;); } //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void after(){ log.info(\u0026#34;MyAspect2 -\u0026gt; after ...\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @Component @Aspect @Order(3) //切面类的执行顺序（前置通知：数字越小先执行; 后置通知：数字越小越后执行） public class MyAspect3 { //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect3 -\u0026gt; before ...\u0026#34;); } //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void after(){ log.info(\u0026#34;MyAspect3 -\u0026gt; after ...\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @Component @Aspect @Order(1) //切面类的执行顺序（前置通知：数字越小先执行; 后置通知：数字越小越后执行） public class MyAspect4 { //前置通知 @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect4 -\u0026gt; before ...\u0026#34;); } //后置通知 @After(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void after(){ log.info(\u0026#34;MyAspect4 -\u0026gt; after ...\u0026#34;); } } 重新启动SpringBoot服务，测试通知执行顺序：\n通知的执行顺序大家主要知道两点即可：\n不同的切面类当中，默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的 可以在切面类上面加上@Order注解，来控制不同的切面类通知的执行顺序 切入点表达式 从AOP的入门程序到现在，我们一直都在使用切入点表达式来描述切入点。下面我们就来详细的介绍一下切入点表达式的具体写法。\n切入点表达式：描述切入点方法的一种表达式\n作用：主要用来决定项目中的哪些方法需要加入通知\n常见形式：\nexecution(……)：根据方法的签名来匹配 @annotation(……) ：根据注解匹配\n首先我们先学习第一种最为常见的execution切入点表达式。\nexecution execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配，语法为：\n1 execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) 其中带?的表示可以省略的部分\n访问修饰符：可省略（比如: public、protected） 包名.类名： 可省略（强烈不建议省略） throws 异常：可省略（注意是方法上声明抛出的异常，不是实际抛出的异常） 示例：\n1 @Before(\u0026#34;execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))\u0026#34;) 可以使用通配符描述切入点\n* ：单个独立的任意符号，可以通配任意返回值、包名、类名、方法名、任意类型的一个参数，也可以通配包、类、方法名的一部分 .. ：多个连续的任意符号，可以通配任意层级的包，或任意类型、任意个数的参数 切入点表达式的语法规则：\n方法的访问修饰符可以省略 返回值可以使用*号代替（任意返回值类型） 包名可以使用*号代替，代表任意包（一层包使用一个*） 使用..配置包名，标识此包以及此包下的所有子包 类名可以使用*号代替，标识任意类 方法名可以使用*号代替，表示任意方法 可以使用 * 配置参数，一个任意类型的参数 可以使用.. 配置参数，任意个任意类型的参数 切入点表达式示例\n省略方法的修饰符号 1 execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)) 使用*代替返回值类型 1 execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)) 使用*代替包名（一层包使用一个*） 1 execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer)) 使用..省略包名 1 execution(* com..DeptServiceImpl.delete(java.lang.Integer)) 使用*代替类名 1 execution(* com..*.delete(java.lang.Integer)) 使用*代替方法名 1 execution(* com..*.*(java.lang.Integer)) 使用 * 代替参数 1 execution(* com.itheima.service.impl.DeptServiceImpl.delete(*)) 使用..省略参数 1 execution(* com..*.*(..)) 将上述切入点表达式挨个放入示例中的代码里进行测试，测试时将其他@Before注释掉，然后点击左侧的红色m按钮看是可以跳转到对应的逻辑代码中，可以即为成功\n注意事项：\n根据业务需要，可以使用 且（\u0026amp;\u0026amp;）、或（||）、非（!） 来组合比较复杂的切入点表达式。 1 execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..)) 切入点表达式的书写建议：\n所有业务方法名在命名时尽量规范，方便切入点表达式快速匹配。如：查询类方法都是 find 开头，更新类方法都是update开头 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 //业务类 @Service public class DeptServiceImpl implements DeptService { public List\u0026lt;Dept\u0026gt; findAllDept() { //省略代码... } public Dept findDeptById(Integer id) { //省略代码... } public void updateDeptById(Integer id) { //省略代码... } public void updateDeptByMoreCondition(Dept dept) { //省略代码... } //其他代码... } //匹配DeptServiceImpl类中以find开头的方法 1 execution(* com.itheima.service.impl.DeptServiceImpl.find*(..)) 描述切入点方法通常基于接口描述，而不是直接描述实现类，增强拓展性 1 execution(* com.itheima.service.DeptService.*(..)) 在满足业务需要的前提下，尽量缩小切入点的匹配范围。如：包名匹配尽量不使用 ..，使用 * 匹配单个包 1 execution(* com.itheima.*.*.DeptServiceImpl.find*(..)) 切入点表达式书写建议：\n所有业务方法名在命名时尽量规范，方便切入点表达式快速匹配。如：findXxx，updateXxx。 描述切入点方法通常基于接口描述，而不是直接描述实现类，增强拓展性。 在满足业务需要的前提下，尽量缩小切入点的匹配范围。如：包名尽量不使用..，使用 * 匹配单个包。 @annotation 已经学习了execution切入点表达式的语法。那么如果我们要匹配多个无规则的方法，比如：list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求，这个是比较繁琐的。\n我们可以借助于另一种切入点表达式 @annotation 来描述这一类的切入点，从而来简化切入点表达式的书写。\n实现步骤：\n编写自定义注解 在业务类要做为连接点的方法上添加自定义注解 新建一个类：com.itheima.anno.LogOperation\n自定义注解：LogOperation\n1 2 3 4 5 6 //定义注解 @Target(ElementType.METHOD) //保留注解 @Retention(RetentionPolicy.RUNTIME) public @interface LogOperation { } 业务类：DeptServiceImpl\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override @LogOperation //自定义注解（表示：当前方法属于目标方法） public List\u0026lt;Dept\u0026gt; list() { List\u0026lt;Dept\u0026gt; deptList = deptMapper.list(); //模拟异常 //int num = 10/0; return deptList; } @Override @LogOperation //自定义注解（表示：当前方法属于目标方法） public void delete(Integer id) { //1. 删除部门 deptMapper.delete(id); } @Override public void save(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept); } @Override public Dept getById(Integer id) { return deptMapper.getById(id); } @Override public void update(Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.update(dept); } } 切面类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Slf4j @Component @Aspect public class MyAspect5 { //针对list方法、delete方法进行前置通知和后置通知 //前置通知 @Before(\u0026#34;@annotation(com.itheima.anno.LogOperation)\u0026#34;) public void before(){ log.info(\u0026#34;MyAspect5 -\u0026gt; before ...\u0026#34;); } //后置通知 @After(\u0026#34;@annotation(com.itheima.anno.LogOperation)\u0026#34;) public void after(){ log.info(\u0026#34;MyAspect5 -\u0026gt; after ...\u0026#34;); } } 重启SpringBoot服务，测试查询所有部门数据，查看控制台日志：\n到此我们两种常见的切入点表达式我已经介绍完了。\nexecution切入点表达式 根据我们所指定的方法的描述信息来匹配切入点方法，这种方式也是最为常用的一种方式 如果我们要匹配的切入点方法的方法名不规则，或者有一些比较特殊的需求，通过execution切入点表达式描述比较繁琐 annotation 切入点表达式 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作，我们需要自定义一个注解，但是相对来比较灵活。我们需要匹配哪个方法，就在方法上加上对应的注解就可以了 根据业务需要，可以使用 \u0026amp;\u0026amp; ，||，！ 来组合比较复杂的切入点表达式。\n连接点 我们前面在讲解AOP核心概念的时候，我们提到过什么是连接点，连接点可以简单理解为可以被AOP控制的方法。\n我们目标对象当中所有的方法是不是都是可以被AOP控制的方法。而在SpringAOP当中，连接点又特指方法的执行。\n在Spring中用JoinPoint抽象了连接点，用它可以获得方法执行时的相关信息，如目标类名、方法名、方法参数等。\n对于@Around通知，获取连接点信息只能使用ProceedingJoinPoint类型 对于其他四种通知，获取连接点信息只能使用JoinPoint，它是ProceedingJoinPoint的父类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Slf4j @Aspect @Component public class MyAspect6 { @Before(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public void before(JoinPoint joinPoint){ log.info(\u0026#34;before ....\u0026#34;); //1. 获取目标对象 Object target = joinPoint.getTarget(); log.info(\u0026#34;获取目标对象: {}\u0026#34;, target); //2. 获取目标类 String className = joinPoint.getTarget().getClass().getName(); log.info(\u0026#34;获取目标类: {}\u0026#34;, className); //3. 获取目标方法 String methodName = joinPoint.getSignature().getName(); log.info(\u0026#34;获取目标方法: {}\u0026#34;, methodName); //4. 获取目标方法参数 Object[] args = joinPoint.getArgs(); log.info(\u0026#34;获取目标方法参数: {}\u0026#34;, Arrays.toString(args)); } @Around(\u0026#34;execution(* com.itheima.service.*.*(..))\u0026#34;) public Object around(ProceedingJoinPoint pjp) throws Throwable { log.info(\u0026#34;around ... before ....\u0026#34;); Object result = pjp.proceed(); log.info(\u0026#34;around ... after ....\u0026#34;); return result; } } AOP案例 SpringAOP的相关知识我们就已经全部学习完毕了。最后我们要通过一个案例来对AOP进行一个综合的应用。\n需求 需求：将案例（Tlias智能学习辅助系统）中增、删、改相关接口的操作日志记录到数据库表中\n就是当访问部门管理和员工管理当中的增、删、改相关功能接口时，需要详细的操作日志，并保存在数据表中，便于后期数据追踪。 操作日志信息包含：\n操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长 所记录的日志信息包括当前接口的操作人是谁操作的，什么时间点操作的，以及访问的是哪个类当中的哪个方法，在访问这个方法的时候传入进来的参数是什么，访问这个方法最终拿到的返回值是什么，以及整个接口方法的运行时长是多长时间。\n分析 问题1：项目当中增删改相关的方法是不是有很多？ 很多 问题2：我们需要针对每一个功能接口方法进行修改，在每一个功能接口当中都来记录这些操作日志吗？ 这种做法比较繁琐 以上两个问题的解决方案：可以使用AOP解决(每一个增删改功能接口中要实现的记录操作日志的逻辑代码是相同)。\n可以把这部分记录操作日志的通用的、重复性的逻辑代码抽取出来定义在一个通知方法当中，我们通过AOP面向切面编程的方式，在不改动原始功能的基础上来对原始的功能进行增强。目前我们所增强的功能就是来记录操作日志，所以也可以使用AOP的技术来实现。使用AOP的技术来实现也是最为简单，最为方便的。\n问题3：既然要基于AOP面向切面编程的方式来完成的功能，那么我们要使用 AOP五种通知类型当中的哪种通知类型？\n答案：环绕通知 @Around。因为所记录的操作日志当中包括：操作人、操作时间，访问的是哪个类、哪个方法、方法运行时参数、方法的返回值、方法的运行时长。方法返回值，是在原始方法执行后才能获取到的。方法的运行时长，需要原始方法运行之前记录开始时间，原始方法运行之后记录结束时间。通过计算获得方法的执行耗时。基于以上的分析我们确定要使用Around环绕通知。 问题4：最后一个问题，切入点表达式我们该怎么写？\n答案：使用 @annotation 来描述切入点表达式。要匹配业务接口当中所有的增删改的方法，而增删改方法在命名上没有共同的前缀或后缀。此时如果使用execution切入点表达式也可以，但是会比较繁琐。 当遇到增删改的方法名没有规律时，就可以使用 @annotation切入点表达式，其中切入点表达式匹配的是controller的方法（如下图） 步骤 简单分析了一下大概的实现思路后，接下来我们就要来完成案例了。案例的实现步骤其实就两步：\n准备工作 引入AOP的起步依赖 导入资料中准备好的数据库表结构，并引入对应的实体类 编码实现(基于AI实现) 自定义注解@LogOperation 定义切面类，完成记录操作日志的逻辑 代码实现 请帮我基于Spring AOP中的环绕通知 @Around 实现记录系统所有增、删、改功能接口的操作日志。具体信息如下：\n日志信息包含：操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长 功能接口所在包为 com.itheima.controller 日志表为 operate_log 表，对应的实体类为 OperateLog。 具体表结构如下： create table operate_log(\nid int unsigned primary key auto_increment comment \u0026lsquo;ID\u0026rsquo;,\noperate_emp_id int unsigned comment \u0026lsquo;操作人ID\u0026rsquo;,\noperate_time datetime comment \u0026lsquo;操作时间\u0026rsquo;,\nclass_name varchar(100) comment \u0026lsquo;操作的类名\u0026rsquo;,\nmethod_name varchar(100) comment \u0026lsquo;操作的方法名\u0026rsquo;,\nmethod_params varchar(1000) comment \u0026lsquo;方法参数\u0026rsquo;,\nreturn_value varchar(2000) comment \u0026lsquo;返回值\u0026rsquo;,\ncost_time int comment \u0026lsquo;方法执行耗时, 单位:ms\u0026rsquo;\n) comment \u0026lsquo;操作日志表\u0026rsquo;;\n并且已经提供了OperateLogMapper接口来操作 operate_log, 并在其中已经定义好了 insert 方法用来保存日志数据. 1). 准备工作\n在 pom.xml 中引入AOP的依赖 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-aop\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 创建数据库表结构 1 2 3 4 5 6 7 8 9 10 11 -- 操作日志表 create table operate_log( id int unsigned primary key auto_increment comment \u0026#39;ID\u0026#39;, operate_emp_id int unsigned comment \u0026#39;操作人ID\u0026#39;, operate_time datetime comment \u0026#39;操作时间\u0026#39;, class_name varchar(100) comment \u0026#39;操作的类名\u0026#39;, method_name varchar(100) comment \u0026#39;操作的方法名\u0026#39;, method_params varchar(1000) comment \u0026#39;方法参数\u0026#39;, return_value varchar(2000) comment \u0026#39;返回值, 存储json格式\u0026#39;, cost_time int comment \u0026#39;方法执行耗时, 单位:ms\u0026#39; ) comment \u0026#39;操作日志表\u0026#39;; 引入资料中准备的实体类引入资料中准备的实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.itheima.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; //ID private Integer operateEmpId; //操作人ID private LocalDateTime operateTime; //操作时间 private String className; //操作类名 private String methodName; //操作方法名 private String methodParams; //操作方法参数 private String returnValue; //操作方法返回值 private Long costTime; //操作耗时 } 引入资料中准备的日志操作Mapper接口 OperateLogMapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.mapper; import com.itheima.pojo.OperateLog; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OperateLogMapper { //插入日志数据 @Insert(\u0026#34;insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) \u0026#34; + \u0026#34;values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});\u0026#34;) public void insert(OperateLog log); } 1). 自定义注解 @Log\n1 2 3 4 5 6 7 /** * 自定义注解，用于标识哪些方法需要记录日志 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log { } 2). 定义AOP记录日志的切面类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package com.itheima.aop; import com.itheima.anno.Log; import com.itheima.mapper.OperateLogMapper; import com.itheima.pojo.OperateLog; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Arrays; @Slf4j @Aspect @Component public class OperationLogAspect { @Autowired private OperateLogMapper operateLogMapper; // 环绕通知 @Around(\u0026#34;@annotation(com.itheima.anno.Log)\u0026#34;) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 记录开始时间 long startTime = System.currentTimeMillis(); // 执行方法 Object result = joinPoint.proceed(); // 当前时间 long endTime = System.currentTimeMillis(); // 耗时 long costTime = endTime - startTime; // 构建日志对象 OperateLog olog = new OperateLog(); olog.setOperateEmpId(getCurrentUserId()); // 需要实现 getCurrentUserId 方法 olog.setOperateTime(LocalDateTime.now()); olog.setClassName(joinPoint.getTarget().getClass().getName()); olog.setMethodName(joinPoint.getSignature().getName()); olog.setMethodParams(Arrays.toString(joinPoint.getArgs())); olog.setReturnValue(result.toString()); olog.setCostTime(costTime); //保存日志 log.info(\u0026#34;记录操作日志:{}\u0026#34;,log); // 插入日志 operateLogMapper.insert(olog); return result; } // 示例方法，获取当前用户ID private int getCurrentUserId() { // 这里应该根据实际情况从认证信息中获取当前登录用户的ID return 1; // 示例返回值 } } 3). 在需要记录的日志的Controller层的方法上，加上注解@Log\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @RestController @RequestMapping(\u0026#34;/clazzs\u0026#34;) public class ClazzController { @Autowired private ClazzService clazzService; /* * 删除部门 * */ @Log @DeleteMapping public Result delete(Integer id){ //System.out.println(\u0026#34;根据ID删除部门: \u0026#34; + id); log.info(\u0026#34;根据ID删除部门: {}\u0026#34; ,id); deptService.deleteById(id); return Result.success(); } /* * 新增部门 * */ @Log @PostMapping public Result add(@RequestBody Dept dept){ //System.out.println(\u0026#34;添加部门：\u0026#34; + dept); log.info(\u0026#34;添加部门：{}\u0026#34; , dept); deptService.addDept(dept); return Result.success(); } /** * 修改部门 */ @Log @PutMapping public Result update(@RequestBody Dept dept){ //System.out.println(\u0026#34;修改部门, dept=\u0026#34; + dept); log.info(\u0026#34;修改部门, dept={}\u0026#34; , dept); deptService.update(dept); return Result.success(); } } 重启SpringBoot服务，测试操作日志记录功能：\n打开浏览器，针对于员工的数据、部门的数据进行增删改之后。我们打开数据库表结构可以来看一下：\n我们会看到，在数据库表中，就清晰的记录了谁、什么时间点、调用了哪个类的哪个方法、传入了什么参数、返回了什么数据，都清晰的记录在数据库中了。（此时id还没有进行优化）\n获取当前登录员工 员工登录成功后，哪里存储的有当前登录员工的信息？ 给客户端浏览器下发的jwt令牌中 如何从JWT令牌中获取当前登录用户的信息呢？ 获取请求头中传递的jwt令牌，并解析 TokenFilter 中已经解析了令牌的信息，如何传递给AOP程序、Controller、Service呢？ThreadLocal ThreadLocal ThreadLocal 并不是一个Thread，而是Thread的局部变量。 ThreadLocal为每个线程提供一份单独的存储空间，具有线程隔离的效果，不同的线程之间不会相互干扰。 常见方法： public void set(T value) 设置当前线程的线程局部变量的值 public T get() 返回当前线程所对应的线程局部变量的值 public void remove() 移除当前线程的线程局部变量 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.itheima; public class ThreadLocalTest { private static ThreadLocal\u0026lt;String\u0026gt; local = new ThreadLocal\u0026lt;\u0026gt;(); public static void main(String[] args) { //主线程 local.set(\u0026#34;Main Message\u0026#34;); //创建线程 //子线程 new Thread(new Runnable() { @Override public void run() { local.set(\u0026#34;Child Message\u0026#34;); System.out.println(Thread.currentThread().getName() + \u0026#34;: \u0026#34; + local.get()); } }).start(); System.out.println(Thread.currentThread().getName() + \u0026#34;: \u0026#34; + local.get()); local.remove(); System.out.println(Thread.currentThread().getName() + \u0026#34;: \u0026#34; + local.get()); } } 记录当前登录员工 增删改的执行流程：\n前端发起请求，请求到达我们的web服务器（Tomcat服务器），服务器接收到这次请求，它就会从线程池当中获取一个线程来处理这次请求，直到这次请求处理完成，也就意味着一次请求对应一个线程，每一次请求都会有一个独立的线程来处理这次请求\n我们可以来进行一个测试：在前端刷新一下部门管理页面（相当于查询部门）：\n这便是线程池当中的线程\n具体操作步骤：\n定义ThreadLocal操作的工具类，用于操作当前登录员工ID。 在 com.itheima.utils 引入工具类 CurrentHolder\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.itheima.utils; public class CurrentHolder { private static final ThreadLocal\u0026lt;Integer\u0026gt; CURRENT_LOCAL = new ThreadLocal\u0026lt;\u0026gt;(); public static void setCurrentId(Integer employeeId) { CURRENT_LOCAL.set(employeeId); } public static Integer getCurrentId() { return CURRENT_LOCAL.get(); } public static void remove() { CURRENT_LOCAL.remove(); } } 在TokenFilter中，解析完当前登录员工ID，将其存入ThreadLocal（用完之后需将其删除）。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.itheima.filter; import com.itheima.utils.CurrentHolder; import com.itheima.utils.JwtUtils; import io.jsonwebtoken.Claims; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @Slf4j @WebFilter(urlPatterns = \u0026#34;/*\u0026#34;) public class TokenFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; //1. 获取请求的url地址 String uri = request.getRequestURI(); // /employee/login //String url = request.getRequestURL().toString(); // http://localhost:8080/employee/login //2. 判断是否是登录请求, 如果url地址中包含 login, 则说明是登录请求, 放行 if (uri.contains(\u0026#34;login\u0026#34;)) { log.info(\u0026#34;登录请求, 放行\u0026#34;); filterChain.doFilter(request, response); return; } //3. 获取请求中的token String token = request.getHeader(\u0026#34;token\u0026#34;); //4. 判断token是否为空, 如果为空, 响应401状态码 if (token == null || token.isEmpty()) { log.info(\u0026#34;token为空, 响应401状态码\u0026#34;); response.setStatus(401); // 响应401状态码 return; } //5. 如果token不为空, 调用JWtUtils工具类的方法解析token, 如果解析失败, 响应401状态码 try { Claims claims = JwtUtils.parseJWT(token); Integer empId = Integer.valueOf(claims.get(\u0026#34;id\u0026#34;).toString()); CurrentHolder.setCurrentId(empId); log.info(\u0026#34;token解析成功, 放行\u0026#34;); } catch (Exception e) { log.info(\u0026#34;token解析失败, 响应401状态码\u0026#34;); response.setStatus(401); return; } //6. 放行 filterChain.doFilter(request, response); //7. 清空当前线程绑定的id CurrentHolder.remove(); } } 在AOP程序中，从ThreadLocal中获取当前登录员工的ID。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package com.itheima.aop; import com.itheima.anno.LogOperation; import com.itheima.mapper.OperateLogMapper; import com.itheima.pojo.OperateLog; import com.itheima.utils.CurrentHolder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Arrays; @Aspect @Component public class OperationLogAspect { @Autowired private OperateLogMapper operateLogMapper; // 环绕通知 @Around(\u0026#34;@annotation(log)\u0026#34;) public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable { // 记录开始时间 long startTime = System.currentTimeMillis(); // 执行方法 Object result = joinPoint.proceed(); // 当前时间 long endTime = System.currentTimeMillis(); // 耗时 long costTime = endTime - startTime; // 构建日志对象 OperateLog operateLog = new OperateLog(); operateLog.setOperateEmpId(getCurrentUserId()); // 需要实现 getCurrentUserId 方法 operateLog.setOperateTime(LocalDateTime.now()); operateLog.setClassName(joinPoint.getTarget().getClass().getName()); operateLog.setMethodName(joinPoint.getSignature().getName()); operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs())); operateLog.setReturnValue(result.toString()); operateLog.setCostTime(costTime); // 插入日志 operateLogMapper.insert(operateLog); return result; } // 示例方法，获取当前用户ID private int getCurrentUserId() { return CurrentHolder.getCurrentId(); } } 代码优化完毕之后，我们重新启动服务测试。就可以看到，可以获取到不同的登录用户信息了。\n在同一个线程/同一个请求中，进行数据共享就可以使用 ThreadLocal。\n新增日志管理 此节为完善日志管理功能模块\n接口描述 代码实现 OperateLogController：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.itheima.controller; import com.itheima.pojo.OperateLog; import com.itheima.pojo.PageResult; import com.itheima.pojo.Result; import com.itheima.service.OperateLogService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 操作日志 */ @Slf4j @RequestMapping(\u0026#34;/log\u0026#34;) @RestController public class OperateLogController { @Autowired private OperateLogService operateLogService; /* * 分页查询日志 * */ @GetMapping(\u0026#34;/page\u0026#34;) public Result page(@RequestParam(defaultValue = \u0026#34;1\u0026#34;) Integer page, @RequestParam(defaultValue = \u0026#34;10\u0026#34;) Integer pageSize) { log.info(\u0026#34;日志分页查询：page={}, pageSize={}\u0026#34;, page, pageSize); PageResult\u0026lt;OperateLog\u0026gt; pageResult = operateLogService.page(page, pageSize); return Result.success(pageResult); } } OperateLogService：\n1 2 3 4 5 6 7 8 9 10 11 12 package com.itheima.service; import com.itheima.pojo.OperateLog; import com.itheima.pojo.PageResult; public interface OperateLogService { /* * 分页查询日志 * */ PageResult\u0026lt;OperateLog\u0026gt; page(Integer page, Integer pageSize); } OperateLogServiceImpl：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.itheima.service.impl; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import com.itheima.mapper.OperateLogMapper; import com.itheima.pojo.OperateLog; import com.itheima.pojo.PageResult; import com.itheima.service.OperateLogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class OperateLogServiceImpl implements OperateLogService { @Autowired private OperateLogMapper operateLogMapper; @Override public PageResult\u0026lt;OperateLog\u0026gt; page(Integer page, Integer pageSize) { //1. 设置分页参数 PageHelper.startPage(page, pageSize); //2. 执行查询 List\u0026lt;OperateLog\u0026gt; logList = operateLogMapper.list(); //3. 封装分页结果 Page\u0026lt;OperateLog\u0026gt; p = (Page\u0026lt;OperateLog\u0026gt;) logList; return new PageResult\u0026lt;\u0026gt;(p.getTotal(), p.getResult()); } } OperateLogMapper接口：\n1 2 3 //分页查询日志数据 @Select(\u0026#34;select * from operate_log order by operate_time desc\u0026#34;) public List\u0026lt;OperateLog\u0026gt; list(); 前后端联调测试 功能测试 ","date":"2026-05-19T22:15:16+08:00","permalink":"https://aurora156.github.io/p/springaop/","title":"SpringAOP"},{"content":"Tlias智能学习辅助系统 通过前面的学习，我们已经实现了部门管理、员工管理的基本功能，但是会发现，我们并没有登录，就直接访问到了Tlias智能学习辅助系统的后台。 这是不安全的，所以我们今天的主题就是登录认证。最终要实现的效果是：\n如果用户名密码错误，不允许登录系统。\n如果用户名和密码都正确，则登录成功，可以访问系统。\n登录功能 需求 在登录界面中，我们可以输入用户的用户名以及密码，然后点击 \u0026ldquo;登录\u0026rdquo; 按钮就要请求服务器，服务端判断用户输入的用户名或者密码是否正确。如果正确，则返回成功结果，前端跳转至系统首页面。\n接口描述 地址：接口文档 - 飞书云文档\n我们参照接口文档中的 其他接口 -\u0026gt; 登录接口\n思路分析 怎么样才算登录成功了呢？\n用户名和密码都输入正确，登录成功 否则，登录失败 登录功能的本质是什么？\n查询 根据用户名和密码查询员工信息 三层架构\n​\n功能开发 AI提示词：\n你是一名java开发工程师，现需要基于 SpringBoot+Mybatis 实现员工登录的基本功能，开发一个基本的登录接口，基本信息如下：\n接口请求路径 /login ，请求方式post 接口请求参数有：用户名 username， 密码 password，为json格式的数据 {\u0026ldquo;username\u0026rdquo;:\u0026ldquo;admin\u0026rdquo;, \u0026ldquo;password\u0026rdquo;:\u0026ldquo;123456\u0026rdquo;} 接口响应数据：json格式，具体的数据格式如下： {\n​ \u0026ldquo;code\u0026rdquo;: 1,\n​ \u0026ldquo;msg\u0026rdquo;: \u0026ldquo;success\u0026rdquo;,\n​ \u0026ldquo;data\u0026rdquo;: {\n​ \u0026ldquo;id\u0026rdquo;: 1,\n​ \u0026ldquo;username\u0026rdquo;: \u0026ldquo;songjiang\u0026rdquo;,\n​ \u0026ldquo;name\u0026rdquo;: \u0026ldquo;宋江\u0026rdquo;,\n​ \u0026ldquo;token\u0026rdquo;: \u0026ldquo;\u0026hellip;\u0026rdquo;\n​ }\n}\n数据库表为 emp, 对应的实体类为Emp，已存在，对应的表结构为： create table emp (\n​ id int unsigned primary key auto_increment comment \u0026lsquo;ID,主键\u0026rsquo;,\n​ username varchar(20) not null comment \u0026lsquo;用户名\u0026rsquo;,\n​ password varchar(32) default \u0026lsquo;123456\u0026rsquo; not null comment \u0026lsquo;密码\u0026rsquo;,\n​ name varchar(10) not null comment \u0026lsquo;姓名\u0026rsquo;,\n​ gender tinyint unsigned not null comment \u0026lsquo;性别, 1:男, 2:女\u0026rsquo;,\n​ phone char(11) not null comment \u0026lsquo;手机号\u0026rsquo;,\n​ job tinyint unsigned null comment \u0026lsquo;职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师\u0026rsquo;,\n​ salary int unsigned null comment \u0026lsquo;薪资\u0026rsquo;,\n​ image varchar(300) null comment \u0026lsquo;头像\u0026rsquo;,\n​ entry_date date null comment \u0026lsquo;入职日期\u0026rsquo;,\n​ dept_id int unsigned null comment \u0026lsquo;关联的部门ID\u0026rsquo;,\n​ create_time datetime null comment \u0026lsquo;创建时间\u0026rsquo;,\n​ update_time datetime null comment \u0026lsquo;修改时间\u0026rsquo;,\n​ constraint emp_pk unique (phone),\n​ constraint username unique (username)\n) comment \u0026lsquo;员工表\u0026rsquo;;\n1). 准备实体类LoginInfo， 封装登录成功后， 返回给前端的数据。\n1 2 3 4 5 6 7 8 9 10 11 12 /** * 登录成功结果封装类 */ @Data @NoArgsConstructor @AllArgsConstructor public class LoginInfo { private Integer id; //员工ID private String username; //用户名 private String name; //姓名 private String token; //令牌 } 2). 定义LoginController\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Slf4j @RestController public class LoginController { @Autowired private EmpService empService; @PostMapping(\u0026#34;/login\u0026#34;) public Result login(@RequestBody Emp emp){ log.info(\u0026#34;登录 :{}\u0026#34;, emp); LoginInfo loginInfo = empService.login(emp); if(loginInfo != null){ return Result.success(loginInfo); } return Result.error(\u0026#34;用户名或密码错误~\u0026#34;); } } 3). EmpService接口中增加 login 登录方法\n1 2 3 4 /** * 登录 */ LoginInfo login(Emp emp); 4). EmpServiceImpl实现login方法\n1 2 3 4 5 6 7 8 9 @Override public LoginInfo login(Emp emp) { Emp empLogin = empMapper.getUsernameAndPassword(emp); if(empLogin != null){ LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), null); return loginInfo; } return null; } 5). EmpMapper增加接口方法\n1 2 3 4 5 /** * 根据用户名和密码查询员工信息 */ @Select(\u0026#34;select * from emp where username = #{username} and password = #{password}\u0026#34;) Emp getUsernameAndPassword(Emp emp); 功能测试 功能开发完毕后，我们就可以启动服务，打开 Apifox 进行测试了。\n发起POST请求，访问：http://localhost:8080/login\n前后端联调测试 错误密码登录：\n成功登录后进入到后台管理系统页面：\n我们已经完成了基础登录功能的开发与测试，在我们登录成功后就可以进入到后台管理系统中进行数据的操作。\n但是当我们在浏览器中新的页面上输入地址：http://localhost:90，发现没有登录仍然可以进入到后端管理系统页面。\n而真正的登录功能应该是：登陆后才能访问后端系统页面，不登陆则跳转登陆页面进行登陆。\n为什么会出现这个问题？其实原因很简单，就是因为针对于我们当前所开发的部门管理、员工管理以及文件上传等相关接口来说，我们在服务器端并没有做任何的判断，没有去判断用户是否登录了。所以无论用户是否登录，都可以访问部门管理以及员工管理的相关数据。所以我们目前所开发的登录功能，它只是徒有其表。而我们要想解决这个问题，我们就需要完成一步非常重要的操作：登录校验。\n登录校验 什么是登录校验？\n所谓登录校验，指的是我们在服务器端接收到浏览器发送过来的请求之后，首先我们要对请求进行校验。先要校验一下用户登录了没有，如果用户已经登录了，就直接执行对应的业务操作就可以了；如果用户没有登录，此时就不允许他执行相关的业务操作，直接给前端响应一个错误的结果，最终跳转到登录页面，要求他登录成功之后，再来访问对应的数据。\n思路 了解完什么是登录校验之后，接下来我们分析一下登录校验大概的实现思路。\n首先我们在宏观上先有一个认知：\n前面在讲解HTTP协议的时候，我们提到HTTP协议是无状态协议。什么又是无状态的协议？\n所谓无状态，指的是每一次请求都是独立的，下一次请求并不会携带上一次请求的数据。而浏览器与服务器之间进行交互，基于HTTP协议也就意味着现在我们通过浏览器来访问了登陆这个接口，实现了登陆的操作，接下来我们在执行其他业务操作时，服务器也并不知道这个员工到底登陆了没有。因为HTTP协议是无状态的，两次请求之间是独立的，所以是无法判断这个员工到底登陆了没有。\n那应该怎么来实现登录校验的操作呢？具体的实现思路可以分为两部分：\n在员工登录成功后，需要将用户登录成功的信息存起来，记录用户已经登录成功的标记。 在浏览器发起请求时，需要在服务端进行统一拦截，拦截后进行登录校验。 想要判断员工是否已经登录，我们需要在员工登录成功之后，存储一个登录成功的标记，接下来在每一个接口方法执行之前，先做一个条件判断，判断一下这个员工到底登录了没有。如果是登录了，就可以执行正常的业务操作，如果没有登录，会直接给前端返回一个错误的信息，前端拿到这个错误信息之后会自动的跳转到登录页面。\n我们程序中所开发的查询功能、删除功能、添加功能、修改功能，都需要使用以上套路进行登录校验。此时就会出现：相同代码逻辑，每个功能都需要编写，就会造成代码非常繁琐。\n为了简化这块操作，我们可以使用一种技术：统一拦截技术。\n通过统一拦截的技术，我们可以来拦截浏览器发送过来的所有的请求，拦截到这个请求之后，就可以通过请求来获取之前所存入的登录标记，在获取到登录标记且标记为登录成功，就说明员工已经登录了。如果已经登录，我们就直接放行(意思就是可以访问正常的业务接口了)。\n我们要完成以上操作，会涉及到web开发中的两个技术：\n会话技术 介绍 什么是会话？\n在我们日常生活当中，会话指的就是谈话、交谈。 在web开发当中，会话指的就是浏览器与服务器之间的一次连接，我们就称为一次会话。 在用户打开浏览器第一次访问服务器的时候，这个会话就建立了，直到有任何一方断开连接，此时会话就结束了。在一次会话当中，是可以包含多次请求和响应的。\n比如：打开了浏览器来访问web服务器上的资源（浏览器不能关闭、服务器不能断开）\n第1次：访问的是登录的接口，完成登录操作 第2次：访问的是部门管理接口，查询所有部门数据 第3次：访问的是员工管理接口，查询员工数据 只要浏览器和服务器都没有关闭，以上3次请求都属于一次会话当中完成的。\n需要注意的是：会话是和浏览器关联的，当有三个浏览器客户端和服务器建立了连接时，就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器，这多次请求是属于同一个会话。比如：1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后，这次会话就结束了。而如果我们是直接把web服务器关了，那么所有的会话就都结束了。\n知道了会话的概念了，接下来我们再来了解下会话跟踪。\n**会话跟踪：**一种维护浏览器状态的方法，服务器需要识别多次请求是否来自于同一浏览器，以便在同一次会话的多次请求间共享数据。\n比如在使用浏览器进行登录时的验证码校验就包含了两次请求，第一次是请求验证码，第二次是请求登录\n服务器会接收很多的请求，但是服务器是需要识别出这些请求是不是同一个浏览器发出来的。比如：1和2这两个请求是不是同一个浏览器发出来的，3和5这两个请求不是同一个浏览器发出来的。如果是同一个浏览器发出来的，就说明是同一个会话。如果是不同的浏览器发出来的，就说明是不同的会话。而识别多次请求是否来自于同一浏览器的过程，我们就称为会话跟踪。\n我们使用会话跟踪技术就是要完成在同一个会话中，多个请求之间进行共享数据。\n为什么要共享数据呢？\n由于HTTP是无状态协议，在后面请求中怎么拿到前一次请求生成的数据呢？此时就需要在一次会话的多次请求之间进行数据共享\n会话跟踪技术有三种：\nCookie（客户端会话跟踪技术）：数据存储在客户端浏览器当中 Session（服务端会话跟踪技术）：数据存储在储在服务端 令牌技术 会话跟踪方案 方案一：Cookie cookie 是客户端会话跟踪技术，它是存储在客户端浏览器的，我们使用 cookie 来跟踪会话，我们就可以在浏览器第一次发起请求来请求服务器的时候，我们在服务器端来设置一个cookie。\n比如第一次请求了登录接口，登录接口执行完成之后，我们就可以设置一个cookie，在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名，用户的ID。\n服务器端在给客户端在响应数据的时候，会自动的将 cookie 响应给浏览器，浏览器接收到响应回来的 cookie 之后，会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中，都会将浏览器本地所存储的 cookie 自动地携带到服务端。\n接下来在服务端我们就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在，如果不存在这个cookie，就说明客户端之前是没有访问登录接口的；如果存在 cookie 的值，就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。\n我刚才在介绍流程的时候，用了 3 个自动：\n服务器会 自动 的将 cookie 响应给浏览器。 浏览器接收到响应回来的数据之后，会 自动 的将 cookie 存储在浏览器本地。 在后续的请求当中，浏览器会 自动 的将 cookie 携带到服务器端。 为什么这一切都是自动化进行的？\n是因为 cookie 它是 HTP 协议当中所支持的技术，而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头：\n响应头 Set-Cookie ：设置Cookie数据的 请求头 Cookie：携带Cookie数据的 代码测试：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf4j @RestController public class SessionController { //设置Cookie @GetMapping(\u0026#34;/c1\u0026#34;) public Result cookie1(HttpServletResponse response){ response.addCookie(new Cookie(\u0026#34;login_username\u0026#34;,\u0026#34;itheima\u0026#34;)); //设置Cookie/响应Cookie return Result.success(); } //获取Cookie @GetMapping(\u0026#34;/c2\u0026#34;) public Result cookie2(HttpServletRequest request){ Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if(cookie.getName().equals(\u0026#34;login_username\u0026#34;)){ System.out.println(\u0026#34;login_username: \u0026#34;+cookie.getValue()); //输出name为login_username的cookie } } return Result.success(); } } A. 访问c1接口，设置Cookie，\n我们可以看到，设置的cookie，通过响应头Set-Cookie响应给浏览器，并且浏览器会将Cookie，存储在浏览器端。\nB. 访问c2接口 http://localhost:8080/c2，此时浏览器会自动的将Cookie携带到服务端，是通过请求头Cookie，携带的。\n可以看到响应的Cookie，注意：此时控制台的报错信息为没有favicon.ico这个静态资源，这是浏览器的图标，由于我现在并没有这个资源所以就会报错，这个问题可以不用管它\n优缺点：\n优点：HTTP协议中支持的技术（像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带，都是浏览器自动进行的，是无需我们手动操作的） 缺点： 移动端APP(Android、IOS)中无法使用Cookie 不安全，用户可以自己禁用Cookie Cookie不能跨域 跨域介绍：\n现在的项目，大部分都是前后端分离的，前后端最终也会分开部署，前端部署在服务器 192.168.150.200 上，端口 80，后端部署在 192.168.150.100上，端口 8080 我们打开浏览器直接访问前端工程，访问url：http://192.168.150.200/login.html 然后在该页面发起请求到服务端，而服务端所在地址不再是localhost，而是服务器的IP地址192.168.150.100，假设访问接口地址为：http://192.168.150.100:8080/login 那此时就存在跨域操作了，因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口 此时如果服务器设置了一个Cookie，这个Cookie是不能使用的，因为Cookie无法跨域 区分跨域的维度（三个维度有任何一个维度不同，那就是跨域操作）：\n协议 IP/协议 端口 举例：\nhttp://192.168.150.200/login.html \u0026mdash;\u0026mdash;\u0026mdash;-\u0026gt; https://192.168.150.200/login [协议不同，跨域] http://192.168.150.200/login.html \u0026mdash;\u0026mdash;\u0026mdash;-\u0026gt; http://192.168.150.100/login [IP不同，跨域] http://192.168.150.200/login.html \u0026mdash;\u0026mdash;\u0026mdash;-\u0026gt; http://192.168.150.200:8080/login [端口不同，跨域] http://192.168.150.200/login.html \u0026mdash;\u0026mdash;\u0026mdash;-\u0026gt; http://192.168.150.200/login 方案二：Session 前面介绍的时候，我们提到Session，它是服务器端会话跟踪技术，所以它是存储在服务器端的。而 Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。\n获取Session 如果我们现在要基于 Session 来进行会话跟踪，浏览器在第一次请求服务器的时候，我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求Session ，会话对象是不存在的，这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ，它都有一个ID（示意图中Session后面括号中的1，就表示ID），我们称之为 Session 的ID。\n响应Cookie (JSESSIONID) 接下来，服务器端在给浏览器响应数据的时候，它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie？ cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头，然后自动将Cookie存储在浏览器本地。\n查找Session 接下来，在后续的每一次请求当中，都会将 Cookie 的数据获取出来，并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值，也就是 Session 的ID。拿到 ID 之后，就会从众多的 Session 当中来找到当前请求对应的会话对象Session。\n这样我们是不是就可以通过 Session 会话对象在同一次会话的多次请求之间来共享数据了？好，这就是基于 Session 进行会话跟踪的流程。\n代码测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Slf4j @RestController public class SessionController { //存值 @GetMapping(\u0026#34;/s1\u0026#34;) public Result session1(HttpSession session){ log.info(\u0026#34;HttpSession-s1: {}\u0026#34;, session.hashCode()); session.setAttribute(\u0026#34;loginUser\u0026#34;, \u0026#34;tom\u0026#34;); //往session中存储数据 return Result.success(); } //取值 @GetMapping(\u0026#34;/s2\u0026#34;) public Result session2(HttpServletRequest request){ HttpSession session = request.getSession(); log.info(\u0026#34;HttpSession-s2: {}\u0026#34;, session.hashCode()); Object loginUser = session.getAttribute(\u0026#34;loginUser\u0026#34;); //从session中获取数据 log.info(\u0026#34;loginUser: {}\u0026#34;, loginUser); return Result.success(loginUser); } } A. 访问 s1 接口，http://localhost:8080/s1\n请求完成之后，在响应头中，就会看到有一个Set-Cookie的响应头，里面响应回来了一个Cookie，就是JSESSIONID，这个就是服务端会话对象 Session 的ID。\nB. 访问 s2 接口，http://localhost:8080/s2\n接下来，在后续的每次请求时，都会将Cookie的值，携带到服务端，那服务端呢，接收到Cookie之后，会自动的根据JSESSIONID的值，找到对应的会话对象Session。\n那经过这两步测试，大家也会看到，在控制台中输出如下日志：\n两次请求，获取到的Session会话对象的hashcode是一样的，就说明是同一个会话对象。而且，第一次请求时，往Session会话对象中存储的值，第二次请求时，也获取到了。 那这样，我们就可以通过Session会话对象，在同一个会话的多次请求之间来进行数据共享了。\n优缺点\n优点：Session是存储在服务端的，安全 缺点： 服务器集群环境下无法直接使用Session 移动端APP(Android、IOS)中无法使用Cookie 用户可以自己禁用Cookie Cookie不能跨域 PS：Session 底层是基于Cookie实现的会话跟踪，如果Cookie不可用，则该方案，也就失效了。\n服务器集群环境为何无法使用Session？\n首先第一点，我们现在所开发的项目，一般都不会只部署在一台服务器上，因为一台服务器会存在一个很大的问题，就是单点故障。所谓单点故障，指的就是一旦这台服务器挂了，整个应用都没法访问了。 所以在现在的企业项目开发当中，最终部署的时候都是以集群的形式来进行部署，也就是同一个项目它会部署多份。比如这个项目我们现在就部署了 3 份。 而用户在访问的时候，到底访问这三台其中的哪一台？其实用户在访问的时候，他会访问一台前置的服务器，我们叫负载均衡服务器,它的作用就是将前端发起的请求均匀的分发给后面的这三台服务器。 此时假如我们通过 session 来进行会话跟踪，可能就会存在这样一个问题。用户打开浏览器要进行登录操作，此时会发起登录请求。登录请求到达负载均衡服务器，将这个请求转给了第一台 Tomcat 服务器。 Tomcat 服务器接收到请求之后，要获取到会话对象session。获取到会话对象 session 之后，要给浏览器响应数据，最终在给浏览器响应数据的时候，就会携带这么一个 cookie 的名字，就是 JSESSIONID ，下一次再请求的时候，是不是又会将 Cookie 携带到服务端？\n好。此时假如又执行了一次查询操作，要查询部门的数据。这次请求到达负载均衡服务器之后，负载均衡服务器将这次请求转给了第二台 Tomcat 服务器，此时他就要到第二台 Tomcat 服务器当中。根据JSESSIONID 也就是对应的 session 的 ID 值，要找对应的 session 会话对象。\n我想请问在第二台服务器当中有没有这个ID的会话对象 Session， 是没有的。此时是不是就出现问题了？我同一个浏览器发起了 2 次请求，结果获取到的不是同一个会话对象，这就是Session这种会话跟踪方案它的缺点，在服务器集群环境下无法直接使用Session。\n可以看到上面这两种传统的会话技术，在现在的企业开发当中会存在很多的问题。 为了解决这些问题，在现在的企业开发当中，基本上都会采用第三种方案，通过令牌技术来进行会话跟踪。\n方案三：令牌技术 这里我们所提到的令牌，其实它就是一个用户身份的标识，看似很高大上，很神秘，其实本质就是一个字符串。\n如果通过令牌技术来跟踪会话，我们就可以在浏览器发起请求。在请求登录接口的时候，如果登录成功，我就可以生成一个令牌，令牌就是用户的合法身份凭证。接下来我在响应数据的时候，我就可以直接将令牌响应给前端。\n接下来我们在前端程序当中接收到令牌之后，就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中，也可以存储在其他的存储空间(比如：localStorage)当中。\n接下来，在后续的每一次请求当中，都需要将令牌携带到服务端。携带到服务端之后，接下来我们就需要来校验令牌的有效性。如果令牌是有效的，就说明用户已经执行了登录操作，如果令牌是无效的，就说明用户之前并未执行登录操作。\n此时，如果是在同一次会话的多次请求之间，我们想共享数据，我们就可以将共享的数据存储在令牌当中就可以了。\n优缺点\n优点： 支持PC端、移动端 解决集群环境下的认证问题 减轻服务器的存储压力（无需在服务器端存储） 缺点：需要自己实现（包括令牌的生成、令牌的传递、令牌的校验） 针对于这三种方案，现在企业开发当中使用的最多的就是第三种令牌技术进行会话跟踪。而前面的这两种传统的方案，现在企业项目开发当中已经很少使用了。所以在后续的学习当中，我也将会采用令牌技术来解决案例项目当中的会话跟踪问题。\n三种方案总结对比：\nJWT令牌最典型的应用场景就是登录认证：\n在浏览器发起请求来执行登录操作，此时会访问登录的接口，如果登录成功之后，我们需要生成一个jwt令牌，将生成的 jwt令牌返回给前端。 前端拿到jwt令牌之后，会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。 服务端统一拦截请求之后，先来判断一下这次请求有没有把令牌带过来，如果没有带过来，直接拒绝访问，如果带过来了，还要校验一下令牌是否是有效。如果有效，就直接放行进行请求的处理。 在JWT登录认证的场景中我们发现，整个流程当中涉及到两步操作：\n在登录成功之后，要生成令牌。 每一次请求当中，要接收令牌并对令牌进行校验。 JWT令牌 前面我们介绍了基于令牌技术来实现会话追踪。这里所提到的令牌就是用户身份的标识，其本质就是一个字符串。令牌的形式有很多，我们使用的是功能强大的 JWT令牌。\n介绍 JWT全称 JSON Web Token （官网：https://jwt.io/），定义了一种简洁的、自包含的格式，用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在，这些信息是可靠的。 简洁：是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。 自包含：指的是jwt令牌，看似是一个随机的字符串，但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如：可以直接在jwt令牌中存储用户的相关信息。 简单来讲，jwt就是将原始的json数据格式进行了安全的封装，这样就可以直接基于jwt在通信双方安全的进行信息传输了。 JWT的组成： （JWT令牌由三个部分组成，三个部分之间使用英文的点来分割）\n第一部分：Header(头）， 记录令牌类型、签名算法等。 例如：{\u0026ldquo;alg\u0026rdquo;:\u0026ldquo;HS256\u0026rdquo;,\u0026ldquo;type\u0026rdquo;:\u0026ldquo;JWT\u0026rdquo;} 第二部分：Payload(有效载荷），携带一些自定义信息、默认信息等。 例如：{\u0026ldquo;id\u0026rdquo;:\u0026ldquo;1\u0026rdquo;,\u0026ldquo;username\u0026rdquo;:\u0026ldquo;Tom\u0026rdquo;} 第三部分：Signature(签名），防止Token被篡改、确保安全性。将header、payload，并加入指定秘钥，通过指定签名算法计算而来。 签名的目的就是为了防jwt令牌被篡改，而正是因为jwt令牌最后一个部分数字签名的存在，所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了，整个令牌在校验的时候都会失败，所以它是非常安全可靠的。\nJWT是如何将原始的JSON格式数据，转变为字符串的呢？\n其实在生成JWT令牌时，会对JSON格式的数据进行一次编码：进行base64编码 Base64：是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码，那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9，一个加号，一个斜杠，加起来就是64个字符。任何数据经过base64编码之后，最终就会通过这64个字符来表示。当然还有一个符号，那就是等号。等号它是一个补位的符号 需要注意的是Base64是编码方式，而不是加密方式。 生成和校验 简单介绍了JWT令牌以及JWT令牌的组成之后，接下来我们就来学习基于Java代码如何生成和校验JWT令牌。\n1). 首先我们先来实现JWT令牌的生成。要想使用JWT令牌，需要先引入JWT的依赖：\n1 2 3 4 5 6 \u0026lt;!-- JWT依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.jsonwebtoken\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jjwt\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.9.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 在引入完JWT依赖后，就可以调用工具包中提供的API来完成JWT令牌的生成和校验。工具类：Jwts\n2). 生成JWT代码实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testGenJwt() { Map\u0026lt;String, Object\u0026gt; claims = new HashMap\u0026lt;\u0026gt;(); claims.put(\u0026#34;id\u0026#34;, 10); claims.put(\u0026#34;username\u0026#34;, \u0026#34;admin\u0026#34;); //生成JWT令牌 String jwt = Jwts.builder() //注意：此处\u0026#34; \u0026#34;里的内容是要去将你想要输入的内容转化为base64编码，此处是将字符串\u0026#34;itheima\u0026#34;进行base64编码 .signWith(SignatureAlgorithm.HS256, \u0026#34;aXRoZWltYQ==\u0026#34;) .addClaims(claims)//添加自定义信息 .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置令牌有效期:1h .compact(); System.out.println(jwt); } 运行测试方法：\n1 eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NzkwMjYxNzh9.kFW_awg_7DpJ-ld0Yk9N6WkItahYCPY6SxRpdZm_I6A 输出的结果就是生成的JWT令牌,，通过英文的点分割对三个部分进行分割，我们可以将生成的令牌复制一下，然后打开JWT的官网，将生成的令牌直接放在Encoded位置，此时就会自动的将令牌解析出来。\n第一部分解析出来，看到JSON格式的原始数据，所使用的签名算法为HS256。\n第二个部分是我们自定义的数据，之前我们自定义的数据就是id，还有一个exp代表的是我们所设置的过期时间。\n由于前两个部分是base64编码，所以是可以直接解码出来。但最后一个部分并不是base64编码，是经过签名算法计算出来的，所以最后一个部分是不会解析的。\n3). 实现了JWT令牌的生成，下面我们接着使用Java代码来校验JWT令牌(解析生成的令牌)：\n1 2 3 4 5 6 7 @Test public void testParseJwt() { Claims claims = Jwts.parser().setSigningKey(\u0026#34;aXRoZWltYQ==\u0026#34;) .parseClaimsJws(\u0026#34;eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NzkwMjY3NzV9.dpsge5KAa9d0MTFZmJaCjh78TW5H9BF1WWCpzBW1VpE\u0026#34;) .getBody(); System.out.println(claims); } 注意将这里的令牌替换为自己的令牌，如果一直报错可以尝试重新生成一个令牌再将生成的令牌放进来，必须一个字符都不能错\n运行测试方法：\n1 {id=10, username=admin, exp=1779026775} 令牌解析后，我们可以看到id和过期时间，如果在解析的过程当中没有报错，就说明解析成功了。\n下面我们做一个测试：把令牌header中的数字9变为8，还是随便将一个字母进行大小写互换，运行测试方法后都会发现报错：\n结论：篡改令牌中的任何一个字符，在对令牌进行解析时都会报错，所以JWT令牌是非常安全可靠的。\n我们继续测试：修改生成令牌的时指定的过期时间，修改为1分钟。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void genJwt(){ Map\u0026lt;String, Object\u0026gt; claims = new HashMap\u0026lt;\u0026gt;(); claims.put(\u0026#34;id\u0026#34;, 10); claims.put(\u0026#34;username\u0026#34;, \u0026#34;itheima\u0026#34;); String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, \u0026#34;aXRjYXN0\u0026#34;) .addClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000)) //有效期60s .compact(); System.out.println(jwt); //输出结果：eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NzkwMjM2OTB9.qLyt0ewcR06W58giSPRC_XXS71-Omt4ZSQ_1gVE0vYE } @Test public void parseJwt(){ Claims claims = Jwts.parser() .setSigningKey(\u0026#34;aXRjYXN0\u0026#34;)//指定签名密钥 .parseClaimsJws(\u0026#34;eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NzkwMjM2OTB9.qLyt0ewcR06W58giSPRC_XXS71-Omt4ZSQ_1gVE0vYE) .getBody(); System.out.println(claims); } 等待1分钟之后运行测试方法发现也报错了，说明：JWT令牌过期后，令牌就失效了，解析的为非法令牌。\n通过以上测试，我们在使用JWT令牌时需要注意：\nJWT校验时使用的签名秘钥，必须和生成JWT令牌时使用的秘钥是配套的。 如果JWT令牌解析校验时报错，则说明 JWT令牌被篡改或失效了（过期了），令牌非法。 登录时下发令牌 JWT令牌的生成和校验的基本操作我们已经学习完了，接下来我们就需要在案例当中通过JWT令牌技术来跟踪会话。具体的思路我们前面已经分析过了，主要就是两步操作：\n生成令牌 在登录成功之后来生成一个JWT令牌，并且把这个令牌直接返回给前端 校验令牌 拦截前端请求，从请求中获取到令牌，对令牌进行解析校验 那我们首先来完成：登录成功之后生成JWT令牌，并且把令牌返回给前端。\n实现步骤：\n引入JWT工具类：在项目工程下创建 com.itheima.util 包，并把提供JWT工具类复制到该包下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.itheima.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.Map; public class JwtUtils { private static final String SECRET_KEY = \u0026#34;aXRoZWltYQ==\u0026#34;; // 秘钥 private static final long EXPIRATION_TIME = 12 * 60 * 60 * 1000; // 12小时 /** * 生成JWT令牌 * @param claims 令牌中包含的信息 * @return 生成的JWT令牌字符串 */ public static String generateToken(Map\u0026lt;String, Object\u0026gt; claims) { return Jwts.builder() .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .addClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .compact(); } /** * 解析JWT令牌 * @param token 要解析的JWT令牌字符串 * @return 包含令牌信息的Claims对象 * @throws Exception 如果令牌无效或已过期，则抛出异常 */ public static Claims parseToken(String token) throws Exception { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } } 完善 EmpServiceImpl中的 login 方法逻辑， 登录成功，生成JWT令牌并返回 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public LoginInfo login(Emp emp) { Emp empLogin = empMapper.getUsernameAndPassword(emp); if(empLogin != null){ //1. 生成JWT令牌 Map\u0026lt;String,Object\u0026gt; dataMap = new HashMap\u0026lt;\u0026gt;(); dataMap.put(\u0026#34;id\u0026#34;, empLogin.getId()); dataMap.put(\u0026#34;username\u0026#34;, empLogin.getUsername()); String jwt = JwtUtils.generateJwt(dataMap); LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt); return loginInfo; } return null; } 重启服务，打开 Apifox 测试登录接口：\n在接口文档中有这么一个备注说明\n备注说明：\n用户登录成功后，系统会自动下发JWT令牌，然后在后续的每次请求中，都需要在请求头header中携带到服务端，请求头的名称为 token ，值为 登录时下发的JWT令牌。 如果检测到用户未登录，则直接响应 401 状态码 。\n打开浏览器完成前后端联调操作：利用开发者工具，抓取一下网络请求\n登录请求完成后，可以看到JWT令牌已经响应给了前端，此时前端就会将JWT令牌存储在浏览器本地。\n服务器响应的JWT令牌存储在本地浏览器哪里了呢？\n在当前案例中，JWT令牌存储在浏览器的本地存储空间 localstorage中了。 localstorage 是浏览器的本地存储，在移动端也是支持的。 我们在发起一个查询部门数据的请求，此时我们可以看到在请求头中包含一个token(JWT令牌)，后续的每一次请求当中，都会将这个令牌携带到服务端。\n过滤器Filter Filter快速入门 什么是Filter？\nFilter表示过滤器，是 JavaWeb三大组件(Servlet、Filter、Listener)之一。 过滤器可以把对资源的请求拦截下来，从而实现一些特殊的功能 使用了过滤器之后，要想访问web服务器上的资源，必须先经过滤器，过滤器处理完毕之后，才可以访问对应的资源。 过滤器一般完成一些通用的操作，比如：登录校验、统一编码处理、敏感字符处理等。 过滤器当中我们拦截到了请求之后，如果希望继续访问后面的web资源，就要执行放行操作，放行就是调用 FilterChain对象当中的doFilter()方法，在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。\n在放行后访问完 web 资源之后还会回到过滤器当中，回到过滤器之后如有需求还可以执行放行之后的逻辑，放行之后的逻辑我们写在doFilter()这行代码之后。\n定义Filter：定义一个类，实现Filter接口，并实现其所有方法 配置Filter：Filter类上加@WebFilter注解，配置拦截路径。引导类上加@ServletComponentScan 开启Servlet组件支持 1). 定义过滤器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @WebFilter(urlPatterns = \u0026#34;/*\u0026#34;) // 拦截所有请求 @Slf4j public class DemoFilter implements Filter { //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次 @Override public void init(FilterConfig filterConfig) throws ServletException { log. info(\u0026#34;init 初始化方法执行了\u0026#34;); } //核心方法,拦截到请求之后调用, 每一次请求都会调用 @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info(\u0026#34;拦截到了请求...\u0026#34;); //放行请求 filterChain.doFilter(servletRequest,servletResponse); } //销毁方法, web容器销毁时调用,只调用一次 @Override public void destroy() { log.info(\u0026#34;销毁方法执行了\u0026#34;); } } 当我们在Filter类上面加了@WebFilter注解之后，接下来我们还需要在启动类上面加上一个注解@ServletComponentScan，通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。\n1 2 3 4 5 6 7 8 @ServletComponentScan @SpringBootApplication class TilasWebManagementApplicationTests { public static void main(String[] args) { SpringApplication.run(TilasWebManagementApplication.class, args); } } 重新启动服务，打开浏览器，执行部门管理的请求，可以看到控制台输出了过滤器中的内容：\n这是未加放行请求时的输出结果，加入了放行请求后就可以在控制台看到信息了\n**注意事项：**在过滤器Filter中，如果不执行放行操作，将无法访问后面的资源。 放行操作：chain.doFilter(request, response);\n登录校验过滤器 分析 过滤器Filter的快速入门以及使用细节我们已经介绍完了，接下来最后一步，我们需要使用过滤器Filter来完成案例当中的登录校验功能。\n我们先来回顾下前面分析过的登录校验的基本流程：\n要进入到后台管理系统，我们必须先完成登录操作，此时就需要访问登录接口login。 登录成功之后，我们会在服务端生成一个JWT令牌，并且把JWT令牌返回给前端，前端会将JWT令牌存储下来。 在后续的每一次请求当中，都会将JWT令牌携带到服务端，请求到达服务端之后，要想去访问对应的业务功能，此时我们必须先要校验令牌的有效性。 对于校验令牌的这一块操作，我们使用登录校验的过滤器，在过滤器当中来校验令牌的有效性。如果令牌是无效的，就响应一个错误的信息，也不会再去放行访问对应的资源了。如果令牌存在，并且它是有效的，此时就会放行去访问对应的web资源，执行相应的业务操作。 大概清楚了在Filter过滤器的实现步骤了，那在正式开发登录校验过滤器之前，我们思考两个问题：\n所有的请求，拦截到了之后，都需要校验令牌吗 ？ 答案：注册、登录请求例外（当前系统不支持注册功能） 拦截到请求后，什么情况下才可以放行，执行业务操作 ？ 答案：有令牌，且令牌校验通过(合法)；否则都返回未登录错误结果(401) 具体流程 我们要完成登录校验，主要是利用Filter过滤器实现，而Filter过滤器的流程步骤：\n基于上面的业务流程，我们分析出具体的操作步骤：\n获取请求url 判断请求url中是否包含login，如果包含，说明是登录操作，放行 获取请求头中的令牌（token） 判断令牌是否存在，如果不存在，响应 401 解析token，如果解析失败，响应 401 放行 代码实现 在 com.itheima.filter 包下创建TokenFilter，具体代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.itheima.filter; import com.itheima.utils.JwtUtils; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpStatus; import java.io.IOException; /* 令牌校验过滤器 */ @Slf4j @WebFilter(urlPatterns = \u0026#34;/*\u0026#34;) public class TokenFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException { //获取请求和响应对象 HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; //1.获取到请求路径 String url = request.getRequestURL().toString(); //2.判断请求路径是否为登录路径，如果路径中包含了/login，说明是登录操作，放行 if(url.contains(\u0026#34;login\u0026#34;)){ //登录请求 log.info(\u0026#34;登录请求 , 直接放行\u0026#34;); filterChain.doFilter(request, response); return; } //3.获取请求头中的令牌（token） String token = request.getHeader(\u0026#34;token\u0026#34;); //4.判断令牌是否存在，如果存在，则通过，如果不存在，说明用户未登录，返回错误信息（响应401状态码） if(token == null || token.isEmpty()){ //jwt为空 log.info(\u0026#34;获取到token令牌为空, 返回401\u0026#34;); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } //5.如果令牌存在，则解析令牌，获取其中的用户名，如果校验失败 -\u0026gt; 返回错误信息（响应401状态码） try { JwtUtils.parseToken(token); } catch (Exception e) { e.printStackTrace(); log.info(\u0026#34;解析令牌失败, 返回401\u0026#34;); response.setStatus(HttpStatus.SC_UNAUTHORIZED); return; } //6.校验通过，放行 log.info(\u0026#34;令牌合法, 放行\u0026#34;); filterChain.doFilter(request , response); } } ps：此时可以将之前的DemoFilter中的@WebFilter(urlPatterns = \u0026quot;/*\u0026quot;) 注释了，原因在本节结尾\n登录校验的过滤器我们编写完成了，接下来我们就可以重新启动服务来做一个测试：\n我们可以在步骤1处打个断点进行测试\n测试1：进行登录操作 可以发现是登陆操作，此时再点击下一步即可返回对应的信息\n测试2：未登录是否可以访问部门管理页面 首先关闭浏览器，重新打开浏览器，在地址栏中输入：http://localhost:90/dept\n由于用户没有登录，登录校验过滤器返回错误信息，前端页面根据返回的错误信息结果，自动跳转到登录页面了\n测试3：先进行登录操作，再访问部门管理页面 登录校验成功之后，可以正常访问相关业务操作页面\n还可以进行其他的测试，比如随便找一个要登录以后才能使用的操作（如：员工性别统计），然后按照测试1步骤打断点进行测试，分别测试无token、token错误以及token正确的情况（该接口的token需要写到Headers中）。可以发现前两种情况都会返回401错误信息，只有当输入token且token正确的情况下才可以进入到相关页面\nFilter详解 Filter过滤器的快速入门程序我们已经完成了，接下来我们就要详细的介绍一下过滤器Filter在使用中的一些细节。主要介绍以下3个方面的细节：\n过滤器的执行流程 过滤器的拦截路径配置 过滤器链 执行流程 首先我们先来看下过滤器的执行流程：\n过滤器当中我们拦截到了请求之后，如果希望继续访问后面的web资源，就要执行放行操作，放行就是调用 FilterChain对象当中的doFilter()方法，在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。\n在放行后访问完 web 资源之后还会回到过滤器当中，回到过滤器之后如有需求还可以执行放行之后的逻辑，放行之后的逻辑我们写在doFilter()这行代码之后。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @WebFilter(urlPatterns = \u0026#34;/*\u0026#34;) public class DemoFilter implements Filter { @Override //初始化方法, 只调用一次 public void init(FilterConfig filterConfig) throws ServletException { System.out.println(\u0026#34;init 初始化方法执行了\u0026#34;); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println(\u0026#34;DemoFilter 放行前逻辑.....\u0026#34;); //放行请求 filterChain.doFilter(servletRequest,servletResponse); System.out.println(\u0026#34;DemoFilter 放行后逻辑.....\u0026#34;); } @Override //销毁方法, 只调用一次 public void destroy() { System.out.println(\u0026#34;destroy 销毁方法执行了\u0026#34;); } } 启动之后运行测试：\n也可以分别在放行前、放行请求、放行后以及LoginController中的log处打断点进行测试\n了解了Filter的执行流程之后，我们来总结两个问题\n问题1：放行后访问对应资源，资源访问完成后，还会回到Filter中吗？会 问题2：如果回到Filter中，是重新执行还是执行放行后的逻辑？执行放行后逻辑 拦截路径 执行流程我们搞清楚之后，接下来再来介绍一下过滤器的拦截路径，Filter可以根据需求，配置不同的拦截资源路径：\n拦截路径 urlPatterns值 含义 拦截具体路径 /login 只有访问 /login 路径时，才会被拦截 目录拦截 /emps/* 访问/emps下的所有资源，都会被拦截 拦截所有 /* 访问所有资源，都会被拦截 下面我们来测试\u0026quot;拦截具体路径\u0026quot;：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @WebFilter(urlPatterns = \u0026#34;/login\u0026#34;) //拦截/login具体路径 public class DemoFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println(\u0026#34;DemoFilter 放行前逻辑.....\u0026#34;); //放行请求 filterChain.doFilter(servletRequest,servletResponse); System.out.println(\u0026#34;DemoFilter 放行后逻辑.....\u0026#34;); } @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void destroy() { Filter.super.destroy(); } } 执行完后在apifox中发送登录请求可以发现正常输出了放行前、放行请求和放行后的日志\n此时再换成其他的接口（如：员工性别统计）进行测试，可以发现控制台并没有输出上述的三个日志\n说明没有拦截此次请求，因为我们此次拦截的是登录请求\n还可以测试/emps/*拦截，这里就不一一展示了，有兴趣的可以自行测试\n过滤器链 最后我们在来介绍下过滤器链，什么是过滤器链呢？所谓过滤器链指的是在一个web应用程序当中，可以配置多个过滤器，多个过滤器就形成了一个过滤器链。\n比如：在我们web服务器当中，定义了两个过滤器，这两个过滤器就形成了一个过滤器链。\n而这个链上的过滤器在执行的时候会一个一个的执行，会先执行第一个Filter，放行之后再来执行第二个Filter，如果执行到了最后一个过滤器放行之后，才会访问对应的web资源。\n访问完web资源之后，按照我们刚才所介绍的过滤器的执行流程，还会回到过滤器当中来执行过滤器放行后的逻辑，而在执行放行后的逻辑的时候，顺序是反着的。\n先要执行过滤器2放行之后的逻辑，再来执行过滤器1放行之后的逻辑，最后在给浏览器响应数据。\n过滤器链上过滤器的执行顺序：注解配置的Filter，优先级是按照过滤器类名（字符串）的自然排序。 比如：\nAbcFilter DemoFilter 这两个过滤器来说，AbcFilter 会先执行，DemoFilter会后执行。\n可以复制一份DemoFilter到AbcFilter中进行测试，可以发现AbcFilter是先于DemoFilter执行的\n在测试完成后可以将这两个测试类的过滤器注释掉，然后将项目中的过滤器打开，因为过滤器链中的过滤器越多，执行性能越低\n拦截器Interceptor 快速入门 什么是拦截器？\n是一种动态拦截方法调用的机制，类似于过滤器。 拦截器是Spring框架中提供的，用来动态拦截控制器方法的执行。 拦截器的作用：拦截请求，在指定方法调用前后，根据业务需要执行预先设定的代码。 在拦截器当中，我们通常也是做一些通用性的操作，比如：我们可以通过拦截器来拦截前端发起的请求，将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中，如发现用户登录了(携带JWT令牌且是合法令牌)，就可以直接放行，去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌，就可以直接给前端响应未登录的错误信息。\n下面我们通过快速入门程序，来学习下拦截器的基本使用。拦截器的使用步骤和过滤器类似，也分为两步：\n定义拦截器，实现HandleInterceptor接口，并实现其所有方法 注册配置拦截器 1). 自定义拦截器\n实现HandlerInterceptor接口，并重写其所有方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf4j @Component //自定义拦截器 public class DemoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //目标资源方法执行前执行。 返回true：放行 返回false：不放行 log.info(\u0026#34;preHandle...\u0026#34;); return true; } //目标资源方法执行后执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { log.info(\u0026#34;postHandle...\u0026#34;); } //视图渲染完毕后执行，最后执行 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { log.info(\u0026#34;afterCompletion...\u0026#34;); } } 注意：\npreHandle方法：目标资源方法执行前执行。 返回true：放行 返回false：不放行 postHandle方法：目标资源方法执行后执行 afterCompletion方法：视图渲染完毕后执行，最后执行 2). 注册配置拦截器\n在 com.itheima下创建一个包，然后创建一个配置类 WebConfig， 实现 WebMvcConfigurer 接口，并重写 addInterceptors 方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class WebConfig implements WebMvcConfigurer { //自定义的拦截器对象 @Autowired private DemoInterceptor demoInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 registry.addInterceptor(demoInterceptor).addPathPatterns(\u0026#34;/**\u0026#34;);//设置拦截器拦截的请求路径（ /** 表示拦截所有请求） } } 注意：使用拦截器时需要将过滤器全部注释掉~\n重新启动SpringBoot服务，打开Apifox测试：\n可以看到控制台输出的日志：\n接下来我们再来做一个测试：将拦截器中返回值改为false\n使用Apifox，再次点击send发送请求后，没有响应数据，说明请求被拦截了没有放行\n令牌校验Interceptor 讲解完了拦截器的基本操作之后，接下来我们需要完成最后一步操作：通过拦截器来完成案例当中的登录校验功能。\n登录校验的业务逻辑以及操作步骤我们前面已经分析过了，和登录校验Filter过滤器当中的逻辑是完全一致的。现在我们只需要把这个技术方案由原来的过滤器换成拦截器interceptor就可以了。\n1). TokenInterceptor\n在 com.itheima.interceptor 包下创建 TokenInterceptor\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Slf4j @Component public class TokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1. 获取请求url。 String url = request.getRequestURL().toString(); //2. 判断请求url中是否包含login，如果包含，说明是登录操作，放行。 if(url.contains(\u0026#34;login\u0026#34;)){ //登录请求 log.info(\u0026#34;登录请求 , 直接放行\u0026#34;); return true; } //3. 获取请求头中的令牌（token）。 String token = request.getHeader(\u0026#34;token\u0026#34;); //4. 判断令牌是否存在，如果不存在，返回错误结果（未登录）。 if(!StringUtils.hasLength(token)){ //token为空 log.info(\u0026#34;获取到token令牌为空, 返回错误结果\u0026#34;); response.setStatus(HttpStatus.SC_UNAUTHORIZED); return false; } //5. 解析token，如果解析失败，返回错误结果（未登录）。 try { JwtUtils.parseToken(token); } catch (Exception e) { e.printStackTrace(); log.info(\u0026#34;解析令牌失败, 返回错误结果\u0026#34;); response.setStatus(HttpStatus.SC_UNAUTHORIZED); return false; } //6. 放行。 log.info(\u0026#34;令牌合法, 放行\u0026#34;); return true; } } 2). 配置拦截器\n1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class WebConfig implements WebMvcConfigurer { //拦截器对象 @Autowired private TokenInterceptor tokenInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 registry.addInterceptor(tokenInterceptor).addPathPatterns(\u0026#34;/**\u0026#34;); } } 登录校验的拦截器编写完成后，接下来我们就可以重新启动服务来做一个测试： （关闭登录校验Filter过滤器）\n测试1：未登录是否可以访问部门管理页面 首先关闭浏览器，重新打开浏览器，在地址栏中输入：http://localhost:90/dept\n由于用户没有登录，校验机制返回错误信息，前端页面根据返回的错误信息结果，自动跳转到登录页面了\n测试2：先进行登录操作，再访问部门管理页面 登录校验成功之后，可以正常访问相关业务操作页面\n到此我们也就验证了所开发的登录校验的拦截器也是没问题的。登录校验的过滤器和拦截器，我们只需要使用其中的一种就可以了\n如果想要在apifox中直接测试其他接口未登录访问，可以先使用登录接口进行测试获取到token令牌，再将token令牌复制到其他接口中进行测试（先将Headers中的token置为空，然后输入错误，最后输入正确token），测试过程中可以分别在TokenInterceptor的步骤中打断点进行观察。\nInterceptor详解 拦截器的入门程序完成之后，接下来我们来介绍拦截器的使用细节。拦截器的使用细节我们主要介绍两个部分：\n拦截器的拦截路径配置 拦截器的执行流程 拦截路径 首先我们先来看拦截器的拦截路径的配置，在注册配置拦截器的时候，我们要指定拦截器的拦截路径，通过addPathPatterns(\u0026quot;要拦截路径\u0026quot;)方法，就可以指定要拦截哪些资源。\n在入门程序中我们配置的是/**，表示拦截所有资源，而在配置拦截器时，不仅可以指定要拦截哪些资源，还可以指定不拦截哪些资源，只需要调用excludePathPatterns(\u0026quot;不拦截路径\u0026quot;)方法，指定哪些资源不需要拦截。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /* * 拦截器配置类 * */ @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private TokenInterceptor tokenInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 registry.addInterceptor(tokenInterceptor) .addPathPatterns(\u0026#34;/**\u0026#34;)//设置拦截器拦截的请求路径（ /** 表示拦截所有请求） .excludePathPatterns(\u0026#34;/login\u0026#34;);//设置不拦截的请求路径 } } 在拦截器中除了可以设置/**拦截所有资源外，还有一些常见拦截路径设置：\n拦截路径 含义 举例 /* 一级路径 能匹配/depts，/emps，/login，不能匹配 /depts/1 /** 任意级路径 能匹配/depts，/depts/1，/depts/1/2 /depts/* /depts下的一级路径 能匹配/depts/1，不能匹配/depts/1/2，/depts /depts/** /depts下的任意级路径 能匹配/depts，/depts/1，/depts/1/2，不能匹配/emps/1 执行流程 介绍完拦截路径的配置之后，接下来我们再来介绍拦截器的执行流程。通过执行流程，大家就能够清晰的知道过滤器与拦截器的执行时机。\n当我们打开浏览器来访问部署在web服务器当中的web应用时，此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后，它会先执行放行前的逻辑，然后再执行放行操作。而由于我们当前是基于springboot开发的，所以放行之后是进入到了spring的环境当中，也就是要来访问我们所定义的controller当中的接口方法。 Tomcat并不识别所编写的Controller程序，但是它识别Servlet程序，所以在Spring的Web环境中提供了一个非常核心的Servlet：DispatcherServlet（前端控制器），所有请求都会先进行到DispatcherServlet，再将请求转给Controller。 当我们定义了拦截器后，会在执行Controller的方法之前，请求被拦截器拦截住。执行preHandle()方法，这个方法执行完成后需要返回一个布尔类型的值，如果返回true，就表示放行本次操作，才会继续访问controller中的方法；如果返回false，则不会放行（controller中的方法也不会执行）。 在controller当中的方法执行完毕之后，再回过来执行postHandle()这个方法以及afterCompletion() 方法，然后再返回给DispatcherServlet，最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后，最终给浏览器响应数据。 以上就是拦截器的执行流程。通过执行流程分析，大家应该已经清楚了过滤器和拦截器之间的区别，其实它们之间的区别主要是两点：\n接口规范不同：过滤器需要实现Filter接口，而拦截器需要实现HandlerInterceptor接口。\n拦截范围不同：过滤器Filter会拦截所有的资源，而Interceptor只会拦截Spring环境中的资源。\n将DemoInterceptor中的preHandle设置为false，\n通过同时打开DemoFilter和DemoInterceptor来进行测试：\n可以发现DemoFilter已经放行了，但是由于DemoInterceptor中的preHandle设置为false，所以并没有放行拦截到了请求\n现在将preHandles设置为true，可以在apifox中查询到了，而此时的拦截顺序就是DemoFilter先执行，但是放行顺序是反过来的\n注意：在学完这部分内容后记得关掉多余的拦截器或过滤器，只需要其中生效一个就可以了，我这里开的是TokenFilter过滤器\n【小tips】在本次学习过程中，我发现我没有登录直接访问http://localhost:90是能够直接进入到前端页面中的，但是进入到的是http://localhost:90/index页面，而这个页面似乎并没有接口来实现它（由于我没有学习前端课程，不知道前端页面的代码是怎样的），我在前面课程一键导入的apifox接口中没有发现index页面，我不确定是不是因为index页面不属于这个系统中的页面所以才能直接访问，而当我在这个页面中直接点击其他接口的页面就会直接跳转到登录页面并提示错误信息，这时候就会被/**给拦截了。\n","date":"2026-05-17T14:02:36+08:00","permalink":"https://aurora156.github.io/p/web%E5%90%8E%E7%AB%AF%E5%AE%9E%E6%88%986/","title":"Web后端实战（6）"},{"content":"Tlias智能学习辅助系统 数据准备 在数据库中，创建学生表 clazz , student，SQL如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 create table clazz( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, name varchar(30) not null unique comment \u0026#39;班级名称\u0026#39;, room varchar(20) comment \u0026#39;班级教室\u0026#39;, begin_date date not null comment \u0026#39;开课时间\u0026#39;, end_date date not null comment \u0026#39;结课时间\u0026#39;, master_id int unsigned null comment \u0026#39;班主任ID, 关联员工表ID\u0026#39;, subject tinyint unsigned not null comment \u0026#39;学科, 1:java, 2:前端, 3:大数据, 4:Python, 5:Go, 6: 嵌入式\u0026#39;, create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; )comment \u0026#39;班级表\u0026#39;; INSERT INTO clazz VALUES (1,\u0026#39;JavaEE就业163期\u0026#39;,\u0026#39;212\u0026#39;,\u0026#39;2024-04-30\u0026#39;,\u0026#39;2024-06-29\u0026#39;,10,1,\u0026#39;2024-06-01 17:08:23\u0026#39;,\u0026#39;2024-06-01 17:39:58\u0026#39;), (2,\u0026#39;前端就业90期\u0026#39;,\u0026#39;210\u0026#39;,\u0026#39;2024-07-10\u0026#39;,\u0026#39;2024-01-20\u0026#39;,3,2,\u0026#39;2024-06-01 17:45:12\u0026#39;,\u0026#39;2024-06-01 17:45:12\u0026#39;), (3,\u0026#39;JavaEE就业165期\u0026#39;,\u0026#39;108\u0026#39;,\u0026#39;2024-06-15\u0026#39;,\u0026#39;2024-12-25\u0026#39;,6,1,\u0026#39;2024-06-01 17:45:40\u0026#39;,\u0026#39;2024-06-01 17:45:40\u0026#39;), (4,\u0026#39;JavaEE就业166期\u0026#39;,\u0026#39;105\u0026#39;,\u0026#39;2024-07-20\u0026#39;,\u0026#39;2024-02-20\u0026#39;,20,1,\u0026#39;2024-06-01 17:46:10\u0026#39;,\u0026#39;2024-06-01 17:46:10\u0026#39;), (5,\u0026#39;大数据就业58期\u0026#39;,\u0026#39;209\u0026#39;,\u0026#39;2024-08-01\u0026#39;,\u0026#39;2024-02-15\u0026#39;,7,3,\u0026#39;2024-06-01 17:51:21\u0026#39;,\u0026#39;2024-06-01 17:51:21\u0026#39;), (6,\u0026#39;JavaEE就业167期\u0026#39;,\u0026#39;325\u0026#39;,\u0026#39;2024-11-20\u0026#39;,\u0026#39;2024-05-10\u0026#39;,36,1,\u0026#39;2024-11-15 11:35:46\u0026#39;,\u0026#39;2024-12-13 14:31:24\u0026#39;); create table student( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, name varchar(10) not null comment \u0026#39;姓名\u0026#39;, no char(10) not null unique comment \u0026#39;学号\u0026#39;, gender tinyint unsigned not null comment \u0026#39;性别, 1: 男, 2: 女\u0026#39;, phone varchar(11) not null unique comment \u0026#39;手机号\u0026#39;, id_card char(18) not null unique comment \u0026#39;身份证号\u0026#39;, is_college tinyint unsigned not null comment \u0026#39;是否来自于院校, 1:是, 0:否\u0026#39;, address varchar(100) comment \u0026#39;联系地址\u0026#39;, degree tinyint unsigned comment \u0026#39;最高学历, 1:初中, 2:高中, 3:大专, 4:本科, 5:硕士, 6:博士\u0026#39;, graduation_date date comment \u0026#39;毕业时间\u0026#39;, clazz_id int unsigned not null comment \u0026#39;班级ID, 关联班级表ID\u0026#39;, violation_count tinyint unsigned default \u0026#39;0\u0026#39; not null comment \u0026#39;违纪次数\u0026#39;, violation_score tinyint unsigned default \u0026#39;0\u0026#39; not null comment \u0026#39;违纪扣分\u0026#39;, create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; ) comment \u0026#39;学员表\u0026#39;; INSERT INTO student VALUES (1,\u0026#39;段誉\u0026#39;,\u0026#39;2022000001\u0026#39;,1,\u0026#39;18800000001\u0026#39;,\u0026#39;110120000300200001\u0026#39;,1,\u0026#39;北京市昌平区建材城西路1号\u0026#39;,1,\u0026#39;2021-07-01\u0026#39;,2,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-15 16:20:59\u0026#39;), (2,\u0026#39;萧峰\u0026#39;,\u0026#39;2022000002\u0026#39;,1,\u0026#39;18800210003\u0026#39;,\u0026#39;110120000300200002\u0026#39;,1,\u0026#39;北京市昌平区建材城西路2号\u0026#39;,2,\u0026#39;2022-07-01\u0026#39;,1,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (3,\u0026#39;虚竹\u0026#39;,\u0026#39;2022000003\u0026#39;,1,\u0026#39;18800013001\u0026#39;,\u0026#39;110120000300200003\u0026#39;,1,\u0026#39;北京市昌平区建材城西路3号\u0026#39;,2,\u0026#39;2024-07-01\u0026#39;,1,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (4,\u0026#39;萧远山\u0026#39;,\u0026#39;2022000004\u0026#39;,1,\u0026#39;18800003211\u0026#39;,\u0026#39;110120000300200004\u0026#39;,1,\u0026#39;北京市昌平区建材城西路4号\u0026#39;,3,\u0026#39;2024-07-01\u0026#39;,1,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (5,\u0026#39;阿朱\u0026#39;,\u0026#39;2022000005\u0026#39;,2,\u0026#39;18800160002\u0026#39;,\u0026#39;110120000300200005\u0026#39;,1,\u0026#39;北京市昌平区建材城西路5号\u0026#39;,4,\u0026#39;2020-07-01\u0026#39;,1,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (6,\u0026#39;阿紫\u0026#39;,\u0026#39;2022000006\u0026#39;,2,\u0026#39;18800000034\u0026#39;,\u0026#39;110120000300200006\u0026#39;,1,\u0026#39;北京市昌平区建材城西路6号\u0026#39;,4,\u0026#39;2021-07-01\u0026#39;,2,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (7,\u0026#39;游坦之\u0026#39;,\u0026#39;2022000007\u0026#39;,1,\u0026#39;18800000067\u0026#39;,\u0026#39;110120000300200007\u0026#39;,1,\u0026#39;北京市昌平区建材城西路7号\u0026#39;,4,\u0026#39;2022-07-01\u0026#39;,2,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (8,\u0026#39;康敏\u0026#39;,\u0026#39;2022000008\u0026#39;,2,\u0026#39;18800000077\u0026#39;,\u0026#39;110120000300200008\u0026#39;,1,\u0026#39;北京市昌平区建材城西路8号\u0026#39;,5,\u0026#39;2024-07-01\u0026#39;,2,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (9,\u0026#39;徐长老\u0026#39;,\u0026#39;2022000009\u0026#39;,1,\u0026#39;18800000341\u0026#39;,\u0026#39;110120000300200009\u0026#39;,1,\u0026#39;北京市昌平区建材城西路9号\u0026#39;,3,\u0026#39;2024-07-01\u0026#39;,2,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (10,\u0026#39;云中鹤\u0026#39;,\u0026#39;2022000010\u0026#39;,1,\u0026#39;18800006571\u0026#39;,\u0026#39;110120000300200010\u0026#39;,1,\u0026#39;北京市昌平区建材城西路10号\u0026#39;,2,\u0026#39;2020-07-01\u0026#39;,2,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (11,\u0026#39;钟万仇\u0026#39;,\u0026#39;2022000011\u0026#39;,1,\u0026#39;18800000391\u0026#39;,\u0026#39;110120000300200011\u0026#39;,1,\u0026#39;北京市昌平区建材城西路11号\u0026#39;,4,\u0026#39;2021-07-01\u0026#39;,1,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-15 16:21:24\u0026#39;), (12,\u0026#39;崔百泉\u0026#39;,\u0026#39;2022000012\u0026#39;,1,\u0026#39;18800000781\u0026#39;,\u0026#39;110120000300200018\u0026#39;,1,\u0026#39;北京市昌平区建材城西路12号\u0026#39;,4,\u0026#39;2022-07-05\u0026#39;,3,6,17,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-12-13 14:33:58\u0026#39;), (13,\u0026#39;耶律洪基\u0026#39;,\u0026#39;2022000013\u0026#39;,1,\u0026#39;18800008901\u0026#39;,\u0026#39;110120000300200013\u0026#39;,1,\u0026#39;北京市昌平区建材城西路13号\u0026#39;,4,\u0026#39;2024-07-01\u0026#39;,2,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-15 16:21:21\u0026#39;), (14,\u0026#39;天山童姥\u0026#39;,\u0026#39;2022000014\u0026#39;,2,\u0026#39;18800009201\u0026#39;,\u0026#39;110120000300200014\u0026#39;,1,\u0026#39;北京市昌平区建材城西路14号\u0026#39;,4,\u0026#39;2024-07-01\u0026#39;,1,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-15 16:21:17\u0026#39;), (15,\u0026#39;刘竹庄\u0026#39;,\u0026#39;2022000015\u0026#39;,1,\u0026#39;18800009401\u0026#39;,\u0026#39;110120000300200015\u0026#39;,1,\u0026#39;北京市昌平区建材城西路15号\u0026#39;,3,\u0026#39;2020-07-01\u0026#39;,4,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (16,\u0026#39;李春来\u0026#39;,\u0026#39;2022000016\u0026#39;,1,\u0026#39;18800008501\u0026#39;,\u0026#39;110120000300200016\u0026#39;,1,\u0026#39;北京市昌平区建材城西路16号\u0026#39;,4,\u0026#39;2021-07-01\u0026#39;,4,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (17,\u0026#39;王语嫣\u0026#39;,\u0026#39;2022000017\u0026#39;,2,\u0026#39;18800007601\u0026#39;,\u0026#39;110120000300200017\u0026#39;,1,\u0026#39;北京市昌平区建材城西路17号\u0026#39;,2,\u0026#39;2022-07-01\u0026#39;,4,0,0,\u0026#39;2024-11-14 21:22:19\u0026#39;,\u0026#39;2024-11-14 21:22:19\u0026#39;), (18,\u0026#39;郑成功\u0026#39;,\u0026#39;2024001101\u0026#39;,1,\u0026#39;13309092345\u0026#39;,\u0026#39;110110110110110110\u0026#39;,0,\u0026#39;北京市昌平区回龙观街道88号\u0026#39;,5,\u0026#39;2021-07-01\u0026#39;,3,2,7,\u0026#39;2024-11-15 16:26:18\u0026#39;,\u0026#39;2024-11-15 16:40:10\u0026#39;); 表结构关系说明：\n实体类Clazz：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @NoArgsConstructor @AllArgsConstructor public class Clazz { private Integer id; //ID private String name; //班级名称 private String room; //班级教室 private LocalDate beginDate; //开课时间 private LocalDate endDate; //结课时间 private Integer masterId; //班主任 private Integer subject; //学科 private LocalDateTime createTime; //创建时间 private LocalDateTime updateTime; //修改时间 private String masterName; //班主任姓名 private String status; //班级状态 - 未开班 , 在读 , 已结课 } 实体类Student：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data @NoArgsConstructor @AllArgsConstructor public class Student { private Integer id; //ID private String name; //姓名 private String no; //序号 private Integer gender; //性别 , 1: 男 , 2 : 女 private String phone; //手机号 private String idCard; //身份证号 private Integer isCollege; //是否来自于院校, 1: 是, 0: 否 private String address; //联系地址 private Integer degree; //最高学历, 1: 初中, 2: 高中 , 3: 大专 , 4: 本科 , 5: 硕士 , 6: 博士 private LocalDate graduationDate; //毕业时间 private Integer clazzId; //班级ID private Short violationCount; //违纪次数 private Short violationScore; //违纪扣分 private LocalDateTime createTime; //创建时间 private LocalDateTime updateTime; //修改时间 private String clazzName;//班级名称 } 需求：班级管理 需要开发如下几个接口，且开发的时候建议按照如下顺序开发 ：\n条件分页查询接口 地址：接口文档 - 飞书云文档\n参照接口文档 班级管理 -\u0026gt; 班级列表查询\n注意：班级状态，显示为：未开班、已结课、在读中 这三种。\n如果：\n当前时间 \u0026gt; 结课时间：状态未 已结课。 当前时间 \u0026lt; 开课时间：状态未 未开班。 否则，就是 在读中。 1).准备Clazz表对应的基础结构，包括Controller、Server、Mapper：\nClazzController：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Slf4j @RestController @RequestMapping(\u0026#34;/clazzs\u0026#34;) public class ClazzController { @Autowired private ClazzService clazzService; /** * 查询所有班级数据 */ @GetMapping public Result page(@RequestParam(defaultValue = \u0026#34;1\u0026#34;) Integer page , @RequestParam(defaultValue = \u0026#34;10\u0026#34;) Integer pageSize, String name, @DateTimeFormat(pattern = \u0026#34;yyyy-MM-dd\u0026#34;) LocalDate begin, @DateTimeFormat(pattern = \u0026#34;yyyy-MM-dd\u0026#34;) LocalDate end) { log.info(\u0026#34;分页查询：{}，{}，{}，{}，{}\u0026#34;,name,begin,end,page,pageSize); return Result.success(clazzService.page(page,pageSize,name,begin,end)); } } ClazzService：\n1 2 3 4 5 6 7 8 9 @Service public interface ClazzService { /* * 查询所有班级 * */ PageResult\u0026lt;Clazz\u0026gt; page(Integer page, Integer pageSize, String name, LocalDate begin,LocalDate end); } ClazzServiceImpl：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service public class ClazzServiceImpl implements ClazzService { @Autowired private ClazzMapper ClazzMapper; public PageResult\u0026lt;Clazz\u0026gt; page(Integer page, Integer pageSize, String name, LocalDate begin, LocalDate end) { PageHelper.startPage(page,pageSize); List\u0026lt;Clazz\u0026gt; clazzList = ClazzMapper.list(name,begin,end); Page\u0026lt;Clazz\u0026gt; p = (Page\u0026lt;Clazz\u0026gt;) clazzList; return new PageResult\u0026lt;\u0026gt;(p.getTotal(),p.getResult()); } } ClazzMapper：\n1 2 3 4 5 6 7 8 9 10 11 @Mapper public interface ClazzMapper { /* * 查询所有班级 * */ @Select(\u0026#34;select * from clazz;\u0026#34;) List\u0026lt;Clazz\u0026gt; findall(); List\u0026lt;Clazz\u0026gt; list(String name, LocalDate begin, LocalDate end); } 将对应的查询语句写在映射文件中\nClazzMapper.xml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;select id=\u0026#34;page\u0026#34; resultType=\u0026#34;com.itheima.pojo.Clazz\u0026#34;\u0026gt; SELECT c.id, c.name, c.room, c.begin_date, c.end_date, c.create_time, c.update_time, -- 重点：从 emp 表中获取 name，并起别名为 masterName e.name AS masterName, -- 之前的状态计算逻辑也可以保留在这里 CASE WHEN CURDATE() \u0026amp;gt; c.end_date THEN \u0026#39;已结课\u0026#39; WHEN CURDATE() \u0026amp;lt; c.begin_date THEN \u0026#39;未开班\u0026#39; ELSE \u0026#39;在读中\u0026#39; END AS status FROM clazz c LEFT JOIN emp e ON c.master_id = e.id \u0026lt;/select\u0026gt; XML 常用的转义字符：\n\u0026amp;lt;：\u0026lt;\n\u0026amp;gt;：：\u0026gt;\n\u0026amp;amp;：：\u0026amp;\n\u0026amp;quote;：\u0026quot;\n\u0026amp;apos;：'\n功能测试 前后端联调测试 （此处页面展示不同是因为前面代码写错了，没有将班主任和状态展示出来，这是修改以后的图，下面几张是修改前的图）\n查询所有员工接口 那其实，对于培训机构来说，班主任就是这个企业的员工。所以，班主任下拉列表中展示的就是所有的员工数据。\n地址：接口文档 - 飞书云文档\n参照接口文档 员工管理 -\u0026gt; 查询全部员工\nEmpController：\n1 2 3 4 5 6 7 8 9 10 /** * 查询所有员工 */ @GetMapping(\u0026#34;/list\u0026#34;) public Result list() { // 调用业务逻辑层（Service）的 findall 方法，获取部门列表数据 List\u0026lt;Emp\u0026gt; empList = empService.findAll(); // 将查询到的 deptList 封装在 Result 对象中返回，通常包含状态码、消息和数据。 return Result.success(empList); } EmpService：\n1 List\u0026lt;Emp\u0026gt; findAll(); EmpServiceImpl：\n1 2 3 4 @Override public List\u0026lt;Emp\u0026gt; findAll() { return empMapper.findAll(); } EmpMapper：\n1 2 //查询所有员工 List\u0026lt;Emp\u0026gt; findAll(); EmpMapper.xml：\n1 2 3 \u0026lt;select id=\u0026#34;findAll\u0026#34; resultType=\u0026#34;com.itheima.pojo.Emp\u0026#34;\u0026gt; select * from emp; \u0026lt;/select\u0026gt; 功能测试 前后端联调测试 新增班级信息接口 参照接口文档 班级管理 -\u0026gt; 添加班级\n功能测试 前后端联调测试 根据ID查询班级接口 参照接口文档 班级管理 -\u0026gt; 根据ID查询\nClazzController：\n1 2 3 4 5 6 7 8 9 /** * 查询回显 */ @GetMapping(\u0026#34;/{id}\u0026#34;) public Result getInfo(@PathVariable Integer id){ log.info(\u0026#34;根据id查询员工的详细信息\u0026#34;); Clazz clazz = clazzService.getInfo(id); return Result.success(clazz); } ClazzService：\n1 2 3 4 /* * 根据ID查询班级信息 * */ Clazz getInfo(Integer id); ClazzServiceImpl：\n1 2 3 4 @Override public Clazz getInfo(Integer id) { return clazzMapper.getById(id); } ClazzMapper：\n1 2 3 4 /* * 根据ID查询班级信息 * */ Clazz getById(Integer id); ClazzMapper.xml：\n1 2 3 \u0026lt;select id=\u0026#34;getById\u0026#34; resultType=\u0026#34;com.itheima.pojo.Clazz\u0026#34;\u0026gt; select * from clazz where id = #{id} \u0026lt;/select\u0026gt; 功能测试 前后端联调测试 选择与我们刚刚查询id相同的班级信息，点击编辑，发现班级的信息都展示出来了\n修改班级信息接口 参照接口文档 班级管理 -\u0026gt; 修改班级\nClazzController：\n1 2 3 4 5 6 7 8 9 /** * 更新班级信息 */ @PutMapping public Result update(@RequestBody Clazz clazz){ log.info(\u0026#34;修改部门信息, {}\u0026#34;, clazz); clazzService.update(clazz); return Result.success(); } ClazzService：\n1 2 3 4 /* * 修改班级信息 * */ void update(Clazz clazz); ClazzServiceImpl：\n1 2 3 4 5 6 7 @Override public void update(Clazz clazz) { //补全基础属性 - updateTime clazz.setUpdateTime(LocalDateTime.now()); //调用Mapper接口方法更新班级 clazzMapper.update(clazz); } ClazzMapper：\n1 2 3 4 5 6 /* * 修改班级信息 * */ @Update(\u0026#34;update clazz set name=#{name},room=#{room},begin_date=#{beginDate},end_date=#{endDate},master_id=#{masterId},subject=#{subject}\u0026#34; + \u0026#34; where id = #{id}\u0026#34;) void update(Clazz clazz); 功能测试 前后端联调测试 修改前：\n修改后：\n删除班级信息接口 参照接口文档 班级管理 -\u0026gt; 删除班级\nClazzController：\n1 2 3 4 5 6 7 8 9 /** * 删除班级 - List */ @DeleteMapping(\u0026#34;/{id}\u0026#34;) public Result delete(@PathVariable Integer id){ log.info(\u0026#34;删除班级: id={} \u0026#34;, id); clazzService.delete(id); return Result.success(\u0026#34;删除成功\u0026#34;); } ClazzService：\n1 2 3 4 /* * 删除班级 * */ void delete(Integer id); ClazzServiceImpl：\n1 2 3 4 5 6 7 8 9 10 11 12 @Transactional(rollbackFor = {Exception.class}) @Override public void delete(Integer id) { //1. 检查班级下是否有学生 int count = clazzMapper.countStudentsByClazzId(id); if (count \u0026gt; 0) { throw new ClassHasStudentsException(); } //2. 根据ID批量删除班级基本信息 clazzMapper.deleteById(id); } ClazzMapper：\n1 2 3 4 5 6 7 8 9 /* * 删除班级 * */ void deleteById(@Param(\u0026#34;id\u0026#34;) Integer id); /* * 根据班级ID统计学生数量 * */ int countStudentsByClazzId(@Param(\u0026#34;clazzId\u0026#34;) Integer clazzId); ClazzMapper.xml：\n1 2 3 \u0026lt;delete id=\u0026#34;deleteById\u0026#34;\u0026gt; DELETE FROM clazz WHERE id = #{id} \u0026lt;/delete\u0026gt; 自定义异常处理器：ClassHasStudentsException\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.exception; /** * 班级下有学生时抛出的异常 */ public class ClassHasStudentsException extends RuntimeException { public ClassHasStudentsException() { super(\u0026#34;对不起，该班级下有学生，不能直接删除\u0026#34;); } public ClassHasStudentsException(String message) { super(message); } } 全局异常处理器：GlobalExceptionHandler\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /* * 全局异常处理器 * */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { //处理异常 @ExceptionHandler public Result ex(Exception e){//方法形参中指定能够处理的异常类型 log.error(\u0026#34;程序出错啦~\u0026#34;,e); //e.printStackTrace();//打印堆栈中的异常信息 //捕获到异常之后，响应一个标准的Result return Result.error(\u0026#34;对不起,操作失败,请联系管理员\u0026#34;); } @ExceptionHandler //如果是DuplicateKeyException，那么这个处理器就会生效，如果是别的上面的就会生效 public Result handleDuplicateKeyException(DuplicateKeyException e){ log.error(\u0026#34;程序出错啦~\u0026#34;,e); // 获取异常的具体描述信息，通常包含 \u0026#34;Duplicate entry \u0026#39;xxx\u0026#39; for key \u0026#39;xxx\u0026#39;\u0026#34; String message = e.getMessage(); // 定位 \u0026#34;Duplicate entry\u0026#34; 关键字的位置，作为截取错误信息的起点 int i = message.indexOf(\u0026#34;Duplicate entry\u0026#34;); // 从关键字位置开始截取字符串，获取包含具体重复值的片段 String errMsg = message.substring(i); // 按照空格将截取的字符串分割成数组 String[] arr = errMsg.split(\u0026#34; \u0026#34;); // 数组下标为2的元素通常是具体的重复值（如 \u0026#39;admin\u0026#39;），拼接提示语后返回 // 注意：这里假设了异常信息的格式是固定的，且重复值在数组的第三个位置 return Result.error(arr[2] + \u0026#34;已存在\u0026#34;); } @ExceptionHandler public Result handleClassHasStudentsException(ClassHasStudentsException e){ return Result.error(e.getMessage()); } } 功能测试 前后端联调测试 删除无学生班级：\n删除有学生班级：\n需求：学员管理 由于该部分为学习了后面知识后回来补充的，而后面有些功能会影响到这个部分的测试（如测试时无Token令牌会导致报错，测试起来比较麻烦），所以该部分就不进行前后端联调测试了，仅为完成功能实现\n需要开发如下几个接口，建议按照如下顺序开发接口：\n查询所有班级接口 地址：接口文档 - 飞书云文档\n参照接口文档 班级管理 -\u0026gt; 查询所有班级\n在新增学员的时候，要展示出所有的班级信息。\nClazzController：新增 @GetMapping(\u0026quot;/list\u0026quot;) 端点，返回所有班级\n1 2 3 4 5 6 7 8 9 /** * 查询所有班级数据 */ @GetMapping(\u0026#34;/list\u0026#34;) public Result list() { log.info(\u0026#34;查询所有班级数据\u0026#34;); List\u0026lt;Clazz\u0026gt; clazzList = clazzService.findAll(); return Result.success(clazzList); } ClazzService：新增 List findAll() 接口方法\n1 2 3 4 /* * 查询所有班级 * */ List\u0026lt;Clazz\u0026gt; findAll(); ClazzServiceImpl：\n1 2 3 4 @Override public List\u0026lt;Clazz\u0026gt; findAll() { return clazzMapper.findall(); } 由于ClazzMapper.findall() 已经存在（@Select(\u0026ldquo;select * from clazz;\u0026quot;)），所以 Mapper 层无需改动。\n条件分页查询接口 接口描述：\n在pojo层创建StudentQueryParam.java ：\n1 2 3 4 5 6 7 8 9 10 11 12 package com.itheima.pojo; import lombok.Data; @Data public class StudentQueryParam { private Integer page = 1; //页码 private Integer pageSize = 10; //每页展示记录数 private String name; //姓名 private Integer degree; //最高学历 private Integer clazzId; //班级ID } 在mapper层创建StudentMapper.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.itheima.mapper; import com.itheima.pojo.Student; import com.itheima.pojo.StudentQueryParam; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface StudentMapper { /* * 条件分页查询学员列表 * */ List\u0026lt;Student\u0026gt; list(StudentQueryParam studentQueryParam); } 在resource目录下的mapper层创建StudentMapper.xml ，基于动态SQL，通过left join clazz 表获取班级名称\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;https://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.StudentMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;list\u0026#34; resultType=\u0026#34;com.itheima.pojo.Student\u0026#34;\u0026gt; select s.*, c.name clazzName from student s left join clazz c on s.clazz_id = c.id \u0026lt;where\u0026gt; \u0026lt;if test=\u0026#34;name != null and name != \u0026#39;\u0026#39;\u0026#34;\u0026gt; s.name like concat(\u0026#39;%\u0026#39;,#{name},\u0026#39;%\u0026#39;) \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;degree != null\u0026#34;\u0026gt; and s.degree = #{degree} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;clazzId != null\u0026#34;\u0026gt; and s.clazz_id = #{clazzId} \u0026lt;/if\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 在service层创建StudentService.java ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 package com.itheima.service; import com.itheima.pojo.PageResult; import com.itheima.pojo.Student; import com.itheima.pojo.StudentQueryParam; public interface StudentService { /* * 学员分页查询 * */ PageResult\u0026lt;Student\u0026gt; page(StudentQueryParam studentQueryParam); } 在service层创建StudentServiceImpl.java ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.itheima.service.impl; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import com.itheima.mapper.StudentMapper; import com.itheima.pojo.PageResult; import com.itheima.pojo.Student; import com.itheima.pojo.StudentQueryParam; import com.itheima.service.StudentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Override public PageResult\u0026lt;Student\u0026gt; page(StudentQueryParam studentQueryParam) { //1. 设置分页参数 PageHelper.startPage(studentQueryParam.getPage(), studentQueryParam.getPageSize()); //2. 执行查询 List\u0026lt;Student\u0026gt; studentList = studentMapper.list(studentQueryParam); //3. 封装分页结果 Page\u0026lt;Student\u0026gt; p = (Page\u0026lt;Student\u0026gt;) studentList; return new PageResult\u0026lt;\u0026gt;(p.getTotal(), p.getResult()); } } 在controller层创建StudentController.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.itheima.controller; import com.itheima.pojo.PageResult; import com.itheima.pojo.Result; import com.itheima.pojo.Student; import com.itheima.pojo.StudentQueryParam; import com.itheima.service.StudentService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 学员管理 */ @Slf4j @RequestMapping(\u0026#34;/students\u0026#34;) @RestController public class StudentController { @Autowired private StudentService studentService; /* * 条件分页查询学员列表 * */ @GetMapping public Result page(StudentQueryParam studentQueryParam) { log.info(\u0026#34;学员查询请求参数：{}\u0026#34;, studentQueryParam); PageResult\u0026lt;Student\u0026gt; pageResult = studentService.page(studentQueryParam); return Result.success(pageResult); } } 新增学生信息接口 接口文档：\nStudentController：\n1 2 3 4 5 6 7 8 9 /* * 新增学员 * */ @PostMapping public Result save(@RequestBody Student student) { log.info(\u0026#34;新增学员: {}\u0026#34;, student); studentService.save(student); return Result.success(); } StudentService：\n1 2 3 4 /* * 新增学员 * */ void save(Student student); StudentServiceImpl：\n1 2 3 4 5 6 7 8 @Override public void save(Student student) { //补全基础属性 student.setCreateTime(LocalDateTime.now()); student.setUpdateTime(LocalDateTime.now()); //调用Mapper接口方法插入数据 studentMapper.insert(student); } StudentMapper：\n1 2 3 4 5 6 7 * * 新增学员 * */ @Options(useGeneratedKeys = true, keyProperty = \u0026#34;id\u0026#34;) @Insert(\u0026#34;insert into student(name, no, gender, phone, id_card, is_college, address, degree, graduation_date, clazz_id, create_time, update_time) \u0026#34; + \u0026#34;values(#{name}, #{no}, #{gender}, #{phone}, #{idCard}, #{isCollege}, #{address}, #{degree}, #{graduationDate}, #{clazzId}, #{createTime}, #{updateTime})\u0026#34;) void insert(Student student); 根据ID查询学生接口 接口文档：\nStudentController：\n1 2 3 4 5 6 7 8 9 /* * 根据ID查询学员 * */ @GetMapping(\u0026#34;/{id}\u0026#34;) public Result getInfo(@PathVariable Integer id) { log.info(\u0026#34;根据id查询学员信息: {}\u0026#34;, id); Student student = studentService.getInfo(id); return Result.success(student); } StudentService：\n1 2 3 4 /* * 根据ID查询学员 * */ Student getInfo(Integer id); StudentServiceImpl：\n1 2 3 4 @Override public Student getInfo(Integer id) { return studentMapper.getById(id); } StudentMapper.java：\n1 2 3 4 /* * 根据ID查询学员 * */ Student getById(Integer id); StudentMapper.xml：\n1 2 3 4 5 \u0026lt;select id=\u0026#34;getById\u0026#34; resultType=\u0026#34;com.itheima.pojo.Student\u0026#34;\u0026gt; select s.*, c.name clazzName from student s left join clazz c on s.clazz_id = c.id where s.id = #{id} \u0026lt;/select\u0026gt; 修改学生信息接口 接口文档：\nStudentController：\n1 2 3 4 5 6 7 8 9 /* * 修改学员 * */ @PutMapping public Result update(@RequestBody Student student) { log.info(\u0026#34;修改学员信息: {}\u0026#34;, student); studentService.update(student); return Result.success(); } StudentService：\n1 2 3 4 /* * 修改学员 * */ void update(Student student); StudentServiceImpl：\n1 2 3 4 5 6 7 @Override public void update(Student student) { //补全基础属性 student.setUpdateTime(LocalDateTime.now()); //调用Mapper接口方法更新数据 studentMapper.update(student); } StudentMapper：\n1 2 3 4 5 6 /* * 修改学员 * */ @Update(\u0026#34;update student set name=#{name}, no=#{no}, gender=#{gender}, phone=#{phone}, id_card=#{idCard}, is_college=#{isCollege}, \u0026#34; + \u0026#34;address=#{address}, degree=#{degree}, graduation_date=#{graduationDate}, clazz_id=#{clazzId}, update_time=#{updateTime} where id = #{id}\u0026#34;) void update(Student student); 修改前：\n修改后：\n删除学生信息接口 接口文档：\nStudentController：\n1 2 3 4 5 6 7 8 9 /* * 删除学员 * */ @DeleteMapping(\u0026#34;/{ids}\u0026#34;) public Result delete(@PathVariable Integer ids) { log.info(\u0026#34;删除学员: id={}\u0026#34;, ids); studentService.delete(ids); return Result.success(); } StudentService：\n1 2 3 4 * * 删除学员 * */ void delete(Integer id); StudentServiceImpl：\n1 2 3 4 @Override public void delete(Integer id) { studentMapper.deleteById(id); } StudentMapper：\n1 2 3 4 5 * * 删除学员 * */ @Delete(\u0026#34;delete from student where id = #{id}\u0026#34;) void deleteById(Integer id); 删除前：\n删除后：\n违纪处理接口 接口文档：\n违纪处理一次，需要将违纪次数+1，违纪扣分+前端输入的分数。\nStudentController：\n1 2 3 4 5 6 7 8 9 /* * 违纪处理 * */ @PutMapping(\u0026#34;/violation/{id}/{score}\u0026#34;) public Result violation(@PathVariable Integer id, @PathVariable Integer score) { log.info(\u0026#34;违纪处理: id={}, score={}\u0026#34;, id, score); studentService.violation(id, score); return Result.success(); } StudentService：\n1 2 3 4 * * 违纪处理 * */ void violation(Integer id, Integer score); StudentServiceImpl：\n1 2 3 4 @Override public void violation(Integer id, Integer score) { studentMapper.updateViolation(id, score, LocalDateTime.now()); } StudentMapper：\n1 2 3 4 5 /* * 违纪处理 * */ @Update(\u0026#34;update student set violation_count = violation_count + 1, violation_score = violation_score + #{score}, update_time = #{updateTime} where id = #{id}\u0026#34;) void updateViolation(@Param(\u0026#34;id\u0026#34;) Integer id, @Param(\u0026#34;score\u0026#34;) Integer score, @Param(\u0026#34;updateTime\u0026#34;) LocalDateTime updateTime); 再添加一次违纪记录，分值为1分\n到此，关于学生管理的增删改查功能，我们就已经全部实现了。\n需求：学员信息统计 需要开发如下几个接口：\n班级人数统计接口开发：参照接口文档 数据统计 -\u0026gt; 班级人数统计 学员学历信息统计接口开发：参照接口文档 数据统计 -\u0026gt; 学员学历统计 班级人数统计接口： ClazzOption：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class ClazzOption { private List clazzList; // 班级列表 private List dataList; // 数据列表 } ReportController：\n1 2 3 4 5 6 7 8 9 /** * 统计班级人数 */ @GetMapping(\u0026#34;/studentCountData\u0026#34;) public Result getStudentCountData(){ log.info(\u0026#34;统计班级人数\u0026#34;); ClazzOption clazzOption = reportService.getStudentCountData(); return Result.success(clazzOption); } ReportService：\n1 2 3 4 /* * 统计班级人数 * */ List\u0026lt;Map\u0026gt; getStudentCountData(); ReportServiceImpl：\n1 2 3 4 5 6 7 8 9 @Override public ClazzOption getStudentCountData() { //1.调用mapper接口，获取统计数据 List\u0026lt;Map\u0026gt; list = studentMapper.countStudentByClazz(); // map：name 班级名称，value = 班级人数 //2.组装结果并返回 List\u0026lt;Object\u0026gt; clazzList = list.stream().map(dataMap -\u0026gt; dataMap.get(\u0026#34;name\u0026#34;)).toList(); List\u0026lt;Object\u0026gt; dataList = list.stream().map(dataMap -\u0026gt; dataMap.get(\u0026#34;value\u0026#34;)).toList(); return new ClazzOption(clazzList, dataList); } StudentMapper.java：\n1 2 3 4 /* * 统计班级人数 * */ List\u0026lt;Map\u0026gt; countStudentByClazz(); StudentMapper.xml：\n1 2 3 4 5 \u0026lt;select id=\u0026#34;countStudentByClazz\u0026#34; resultType=\u0026#34;java.util.Map\u0026#34;\u0026gt; select c.name as name, count(s.id) as value from clazz c left join student s on c.id = s.clazz_id group by c.id, c.name \u0026lt;/select\u0026gt; 结果与学历统计一起放在下面了\n学员学历信息统计接口： ReportController：\n1 2 3 4 5 6 7 8 9 /** * 统计学历分布 */ @GetMapping(\u0026#34;/studentDegreeData\u0026#34;) public Result getStudentDegreeData(){ log.info(\u0026#34;统计学历分布\u0026#34;); List\u0026lt;Map\u0026gt; degreeList = reportService.getStudentDegreeData(); return Result.success(degreeList); } ReportService：\n1 2 3 4 /* * 统计学历分布 * */ List\u0026lt;Map\u0026gt; getStudentDegreeData(); ReportServiceImpl：\n1 2 3 4 @Override public List\u0026lt;Map\u0026gt; getStudentDegreeData() { return studentMapper.countStudentByDegree(); } StudentMapper.java：\n1 2 3 4 /* * 统计学历分布 * */ List\u0026lt;Map\u0026gt; countStudentByDegree(); StudentMapper.xml：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;select id=\u0026#34;countStudentByDegree\u0026#34; resultType=\u0026#34;java.util.Map\u0026#34;\u0026gt; select case when degree = 1 then \u0026#39;初中\u0026#39; when degree = 2 then \u0026#39;高中\u0026#39; when degree = 3 then \u0026#39;大专\u0026#39; when degree = 4 then \u0026#39;本科\u0026#39; when degree = 5 then \u0026#39;硕士\u0026#39; when degree = 6 then \u0026#39;博士\u0026#39; end as name, count(*) as value from student group by degree order by degree \u0026lt;/select\u0026gt; 需求：功能完善 删除部门时：如果部门下有员工，则不允许删除该部门，并给前端提示错误信息：对不起，当前部门下有员工，不能直接删除！ DeptHasEmployeesException.java：新建，RuntimeException，提示消息：\u0026ldquo;对不起，当前部门下有员工，不能直接删除！\u0026rdquo;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.exception; /** * 部门下有员工时抛出的异常 */ public class DeptHasEmployeesException extends RuntimeException { public DeptHasEmployeesException() { super(\u0026#34;对不起，当前部门下有员工，不能直接删除！\u0026#34;); } public DeptHasEmployeesException(String message) { super(message); } } GlobalExceptionHandler.java：新增 handleDeptHasEmployeesException() 处理器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.exception; /** * 班级下有学生时抛出的异常 */ public class ClassHasStudentsException extends RuntimeException { public ClassHasStudentsException() { super(\u0026#34;对不起，该班级下有学生，不能直接删除\u0026#34;); } public ClassHasStudentsException(String message) { super(message); } } DeptMapper.java：新增 countEmployeesByDeptId(Integer deptId)\n1 2 3 4 5 6 7 8 9 10 11 /* * 根据部门ID统计员工数量 * */ @Select(\u0026#34;select count(*) from emp where dept_id = #{deptId}\u0026#34;) int countEmployeesByDeptId(Integer deptId); /* *删除部门 * */ @Delete(\u0026#34;delete from dept where id = #{id}\u0026#34;) void deleteById(Integer id); DeptServiceImpl.java：deleteById() 中新增检查逻辑\n1 2 3 4 5 6 7 8 9 10 @Override public void deleteById(Integer id) { //1. 检查部门下是否有员工 int count = deptMapper.countEmployeesByDeptId(id); if (count \u0026gt; 0) { throw new DeptHasEmployeesException(); } //2. 根据ID删除部门 deptMapper.deleteById(id); } ","date":"2026-05-15T14:34:35+08:00","permalink":"https://aurora156.github.io/p/web%E5%90%8E%E7%AB%AF%E5%AE%9E%E6%88%985/","title":"Web后端实战（5）"},{"content":"Tlias智能学习辅助系统 删除员工 需求 当我们勾选列表前面的复选框，然后点击 \u0026ldquo;批量删除\u0026rdquo; 按钮，就可以将这一批次的员工信息删除掉了。也可以只勾选一个复选框，仅删除一个员工信息。\n问题：我们需要开发两个功能接口吗？一个删除单个员工，一个删除多个员工\n答案：不需要。 只需要开发一个功能接口即可（删除多个员工包含只删除一个员工）\n接口文档 地址：接口文档 - 飞书云文档\n查看 员工管理 -\u0026gt; 删除员工 接口的描述。\nsql语句：\n1 2 3 4 -- 删除员工 delete from emp where id in (1,2,3); delete from emp_expr where emp_id in (1,2,3); 思路分析 功能开发 Controller接收参数 在 EmpController 中增加如下方法 delete ，来执行批量删除员工的操作。\n方式一：在Controller方法中通过数组来接收\n多个参数，默认可以将其封装到一个数组中，需要保证前端传递的参数名 与 方法形参名称保持一致。\n1 2 3 4 5 6 7 8 /** * 删除员工 - 数组 */ @DeleteMapping public Result delete(Integer[] ids){ log.info(\u0026#34;删除员工: ids={} \u0026#34;, Arrays.asList(ids)); return Result.success(); } 方式二：在Controller方法中通过集合来接收\n也可以将其封装到一个List 集合中，如果要将其封装到一个集合中，需要在集合前面加上 @RequestParam 注解。\n1 2 3 4 5 6 7 8 9 /** * 删除员工 - List */ @DeleteMapping public Result delete(@RequestParam List\u0026lt;Integer\u0026gt; ids){ log.info(\u0026#34;批量删除部门: ids={} \u0026#34;, ids); empService.delete(ids); return Result.success(); } 结果同方式一\n两种方式，选择其中一种就可以，我们一般推荐选择集合，因为基于集合操作其中的元素会更加方便。\nService 1). 在接口中 EmpService 中定义接口方法 deleteByIds\n1 2 3 4 /** * 批量删除员工 */ void deleteByIds(List\u0026lt;Integer\u0026gt; ids); 2). 在实现类 EmpServiceImpl 中实现接口方法 deleteByIds\n在删除员工信息时，既需要删除 emp 表中的员工基本信息，还需要删除 emp_expr 表中员工的工作经历信息\n1 2 3 4 5 6 7 8 9 10 //只有出现异常时才需要回滚 @Transactional(rollbackFor = {Exception.class}) @Override public void deleteByIds(List\u0026lt;Integer\u0026gt; ids) { //1. 根据ID批量删除员工基本信息 empMapper.deleteByIds(ids); //2. 根据员工的ID批量删除员工的工作经历信息 empExprMapper.deleteByEmpIds(ids); } 由于删除员工信息，既要删除员工基本信息，又要删除工作经历信息，操作多次数据库的删除，所以需要进行事务控制。\nMapper 1). 在 EmpMapper 接口中增加 deleteByIds 方法实现批量删除员工基本信息\n1 2 3 4 /** * 批量删除员工信息 */ void deleteByIds(List\u0026lt;Integer\u0026gt; ids); 2). 在 EmpMapper.xml 配置文件中, 配置对应的SQL语句\n1 2 3 4 5 6 7 \u0026lt;!--批量删除员工信息--\u0026gt; \u0026lt;delete id=\u0026#34;deleteByIds\u0026#34;\u0026gt; delete from emp where id in \u0026lt;foreach collection=\u0026#34;ids\u0026#34; item=\u0026#34;id\u0026#34; separator=\u0026#34;,\u0026#34; open=\u0026#34;(\u0026#34; close=\u0026#34;)\u0026#34;\u0026gt; #{id} \u0026lt;/foreach\u0026gt; \u0026lt;/delete\u0026gt; 3). 在 EmpExprMapper 接口中增加 deleteByEmpIds 方法实现根据员工ID批量删除员工的工作经历信息\n1 2 3 4 /** * 根据员工的ID批量删除工作经历信息 */ void deleteByEmpIds(List\u0026lt;Integer\u0026gt; empIds); 4). 在 EmpExprMapper.xml 配置文件中, 配置对应的SQL语句\n1 2 3 4 5 6 7 \u0026lt;!--根据员工的ID批量删除工作经历信息--\u0026gt; \u0026lt;delete id=\u0026#34;deleteByEmpIds\u0026#34;\u0026gt; delete from emp_expr where emp_id in \u0026lt;foreach collection=\u0026#34;empIds\u0026#34; item=\u0026#34;empId\u0026#34; open=\u0026#34;(\u0026#34; close=\u0026#34;)\u0026#34; separator=\u0026#34;,\u0026#34;\u0026gt; #{empId} \u0026lt;/foreach\u0026gt; \u0026lt;/delete\u0026gt; 功能测试 功能开发完成后，重启项目工程，打开 Apifox，发起DELETE请求：\n控制台SQL语句：\n数据库删除前：\n删除后：\n前后端联调 打开浏览器，测试后端功能接口：\n删除单条数据\n删除多条数据：\n修改员工 查询回显 接口文档 地址：接口文档 - 飞书云文档\n查看 员工管理 -\u0026gt; 根据ID查询 接口的描述。\n思路分析 在查询回显时，既需要查询出员工的基本信息，又需要查询出该员工的工作经历信息。\n我们可以先通过一条SQL语句（根据id进行查询员工信息），查询出指定员工的基本信息，及其员工的工作经历信息。SQL如下：\n1 2 3 4 5 6 7 select e.*, ee.id ee_id, ee.begin ee_begin, ee.end ee_end, ee.company ee_company, ee.job ee_job from emp e left join emp_expr ee on e.id = ee.emp_id where e.id = 39; 具体的实现思路如下：\n代码实现 1). EmpController 添加 getInfo 用来根据ID查询员工数据，用于页面回显\n1 2 3 4 5 6 7 8 9 /** * 查询回显 */ @GetMapping(\u0026#34;/{id}\u0026#34;) public Result getInfo(@PathVariable Integer id){ log.info(\u0026#34;根据id查询员工的详细信息\u0026#34;); Emp emp = empService.getInfo(id); return Result.success(emp); } 2). EmpService 接口中增加 getInfo 方法\n1 2 3 4 /** * 根据ID查询员工的详细信息 */ Emp getInfo(Integer id); 3). EmpServiceImpl 实现类中实现 getInfo 方法\n1 2 3 4 @Override public Emp getInfo(Integer id) { return empMapper.getById(id); } 4). EmpMapper 接口中增加 getById 方法\n1 2 3 4 /** * 根据ID查询员工详细信息 */ Emp getById(Integer id); 5). EmpMapper.xml 配置文件中定义对应的SQL\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 \u0026lt;!--自定义结果集ResultMap--\u0026gt; \u0026lt;!--通过resultMap进行手动结果封装--\u0026gt; \u0026lt;resultMap id=\u0026#34;empResultMap\u0026#34; type=\u0026#34;com.itheima.pojo.Emp\u0026#34;\u0026gt; \u0026lt;!-- id用于封装id --\u0026gt; \u0026lt;id column=\u0026#34;id\u0026#34; property=\u0026#34;id\u0026#34; /\u0026gt; \u0026lt;!-- result用于封装普通属性 --\u0026gt; \u0026lt;!-- property用于往这个字段里面进行封装 --\u0026gt; \u0026lt;result column=\u0026#34;username\u0026#34; property=\u0026#34;username\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;password\u0026#34; property=\u0026#34;password\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;name\u0026#34; property=\u0026#34;name\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;gender\u0026#34; property=\u0026#34;gender\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;phone\u0026#34; property=\u0026#34;phone\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;job\u0026#34; property=\u0026#34;job\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;salary\u0026#34; property=\u0026#34;salary\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;image\u0026#34; property=\u0026#34;image\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;entry_date\u0026#34; property=\u0026#34;entryDate\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;dept_id\u0026#34; property=\u0026#34;deptId\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;create_time\u0026#34; property=\u0026#34;createTime\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;update_time\u0026#34; property=\u0026#34;updateTime\u0026#34; /\u0026gt; \u0026lt;!--封装exprList--\u0026gt; \u0026lt;!-- collection用于封装集合 --\u0026gt; \u0026lt;collection property=\u0026#34;exprList\u0026#34; ofType=\u0026#34;com.itheima.pojo.EmpExpr\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;ee_id\u0026#34; property=\u0026#34;id\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;ee_company\u0026#34; property=\u0026#34;company\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;ee_job\u0026#34; property=\u0026#34;job\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;ee_begin\u0026#34; property=\u0026#34;begin\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;ee_end\u0026#34; property=\u0026#34;end\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;ee_empid\u0026#34; property=\u0026#34;empId\u0026#34;/\u0026gt; \u0026lt;/collection\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;!--根据ID查询员工的详细信息--\u0026gt; \u0026lt;select id=\u0026#34;getById\u0026#34; resultMap=\u0026#34;empResultMap\u0026#34;\u0026gt; select e.*, ee.id ee_id, ee.emp_id ee_empid, ee.begin ee_begin, ee.end ee_end, ee.company ee_company, ee.job ee_job from emp e left join emp_expr ee on e.id = ee.emp_id where e.id = #{id} \u0026lt;/select\u0026gt; 在这一步中我们先按之前的操作步骤只将sql语句复制粘贴进EmpMapper.xml 配置文件中，此时打开apifox就会发现报了500的错误，如果要解决这种步骤就需要按照上述步骤进行手动结果封装\n我们需要将结果封装到一个Emp对象当中，但是这条sql语句执行完后发现了两条数据，两条数据是没有办法往一个对象当中封装的，所以此时就报错了，但是根据id查询员工信息只能返回一个员工对象（id是主键，唯一的），之所以返回两条数据是因为这个员工有两段工作经历，最终结果应该封装到一个Emp员工对象当中，但是我们使用Mybatis将结果自动封装，那么两条数据就会封装到两个对象当中，如果返回的结果是单个对象，那么就会报错\n在这种一对多的查询中，我们要想成功的封装的结果，需要手动的基于 \u0026lt;resultMap\u0026gt; 来进行封装结果。\nMybatis中封装查询结果，什么时候用 resultType，什么时候用resultMap ？\n如果查询返回的字段名与实体的属性名可以直接对应上，用resultType 自动封装。 如果查询返回的字段名与实体的属性名对应不上，或实体属性比较复杂，可以通过resultMap手动封装 。 功能测试 重新启动服务，基于Apifox进行接口测试。\n前后端联调测试 打开浏览器，进行前后端联调测试。\n选择与我们刚刚查询id相同的员工信息，点击编辑，发现员工的信息都展示出来了\n修改员工 查询回显之后，就可以在页面上修改员工的信息了。\n当用户修改完数据之后，点击保存按钮，就需要将数据提交到服务端，然后服务端需要将修改后的数据更新到数据库中 。 而此次更新的时候，既需要更新员工的基本信息； 又需要更新员工的工作经历信息 。 接口文档 地址：接口文档 - 飞书云文档\n查看 员工管理 -\u0026gt; 修改员工 接口的描述。\n思路分析 在修改员工经历的操作时，有三种情况：增加员工信息，删除员工信息，修改员工信息，对于修改员工信息，我们可以先删再加\n代码实现 1). EmpController 增加 update 方法接收请求参数，响应数据\n1 2 3 4 5 6 7 8 9 /** * 更新员工信息 */ @PutMapping public Result update(@RequestBody Emp emp){ log.info(\u0026#34;修改员工信息, {}\u0026#34;, emp); empService.update(emp); return Result.success(); } 2). EmpService 接口增加 update 方法\n1 2 3 4 5 /** * 更新员工信息 * @param emp */ void update(Emp emp); 3). EmpServiceImpl实现类实现 update 方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Transactional(rollbackFor = {Exception.class})//只有一个元素可以省略{} @Override public void update(Emp emp) { //1. 根据ID更新员工基本信息 emp.setUpdateTime(LocalDateTime.now()); empMapper.updateById(emp); //2. 根据员工ID删除员工的工作经历信息 【先删除原有的】 //Arrays.asList：快速将一个数组（或一组元素）转换成一个 List 集合。 empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId())); //3. 新增员工的工作经历数据 【再新增新的】 Integer empId = emp.getId(); List\u0026lt;EmpExpr\u0026gt; exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)){ exprList.forEach(empExpr -\u0026gt; empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } 4). EmpMapper 接口中增加 updateById 方法\n1 2 3 4 /** * 更新员工基本信息 */ void updateById(Emp emp); 5). EmpMapper.xml 配置文件中定义对应的SQL语句，基于动态SQL更新员工信息\n程序优化后：基于动态SQL优化\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;!--根据ID更新员工信息--\u0026gt; \u0026lt;update id=\u0026#34;updateById\u0026#34;\u0026gt; update emp \u0026lt;!--set标签：会自动生成set关键字；会自动的删除更新后多余的\u0026#34;,\u0026#34;--\u0026gt; \u0026lt;set\u0026gt; \u0026lt;if test=\u0026#34;username != null and username != \u0026#39;\u0026#39;\u0026#34;\u0026gt;username = #{username},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;password != null and password != \u0026#39;\u0026#39;\u0026#34;\u0026gt;password = #{password},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;name != null and name != \u0026#39;\u0026#39;\u0026#34;\u0026gt;name = #{name},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;gender != null\u0026#34;\u0026gt;gender = #{gender},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;phone != null and phone != \u0026#39;\u0026#39;\u0026#34;\u0026gt;phone = #{phone},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;job != null\u0026#34;\u0026gt;job = #{job},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;salary != null\u0026#34;\u0026gt;salary = #{salary},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;image != null and image != \u0026#39;\u0026#39;\u0026#34;\u0026gt;image = #{image},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;entryDate != null\u0026#34;\u0026gt;entry_date = #{entryDate},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;deptId != null\u0026#34;\u0026gt;dept_id = #{deptId},\u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;updateTime != null\u0026#34;\u0026gt;update_time = #{updateTime},\u0026lt;/if\u0026gt; \u0026lt;/set\u0026gt; where id = #{id} \u0026lt;/update\u0026gt; 初版的配置文件如下：【不灵活、扩展性差】\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;update id=\u0026#34;updateById\u0026#34;\u0026gt; UPDATE emp SET username = \u0026#39;#{username}\u0026#39;, password = \u0026#39;#{password}\u0026#39;, name = \u0026#39;#{name}\u0026#39;, gender = #{gender}, phone = \u0026#39;#{phone}\u0026#39;, job = #{job}, salary = #{salary} image = #{image}, entry_date = #{entryDate}, dept_id = #{deptId},\tupdate_time = #{updateTime} where id = #{id} \u0026lt;/update\u0026gt; 功能测试 重新启动服务，打开 Apifox 进行接口测试。\n前后端联调测试 修改前：\n修改后再次点击编辑进行查看信息：\n异常处理 问题分析 当我们在修改部门数据的时候，如果输入一个在数据库表中已经存在的手机号，点击保存按钮之后，前端提示了错误信息，但是返回的结果并不是统一的响应结果，而是框架默认返回的错误结果 。\n状态码为500，表示服务器端异常，我们打开idea，来看一下，服务器端出了什么问题。\n上述错误信息的含义是，emp员工表的phone手机号字段的值重复了，因为在数据库表emp中已经有了13390901232这个手机号了，我们之前设计这张表时，为phone字段建议了唯一约束，所以该字段的值是不能重复的。\n而当我们再将该员工的手机号也设置为 13390901232，就违反了唯一约束，此时就会报错。\n我们来看一下出现异常之后，最终服务端给前端响应回来的数据长什么样。\n响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗？显然并不是。由于返回的数据不符合开发规范，所以前端并不能解析出响应的JSON数据 。\n接下来我们需要思考的是出现异常之后，当前案例项目的异常是怎么处理的？ 答案：没有做任何的异常处理\n当我们没有做任何的异常处理时，我们三层架构处理异常的方案：\nMapper接口在操作数据库的时候出错了，此时异常会往上抛(谁调用Mapper就抛给谁)，会抛给service。 service 中也存在异常了，会抛给controller。 而在controller当中，我们也没有做任何的异常处理，所以最终异常会再往上抛。最终抛给前端框架之后，框架就会返回一个JSON格式的数据，里面封装的就是错误的信息，但是框架返回的JSON格式的数据并不符合我们的开发规范。 解决方案 那么在三层构架项目中，出现了异常，该如何处理?\n方案一：在所有Controller的所有方法中进行try…catch处理 缺点：代码臃肿（不推荐）\n方案二：全局异常处理器 好处：简单、优雅（推荐）\n全局异常处理器 我们该怎么样定义全局异常处理器？\n定义全局异常处理器非常简单，就是定义一个类，在类上加上一个注解@RestControllerAdvice，加上这个注解就代表我们定义了一个全局异常处理器。 在全局异常处理器当中，需要定义一个方法来捕获异常，在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* * 全局异常处理器 * */ //记录日志 @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { //处理异常 @ExceptionHandler public Result ex(Exception e){//方法形参中指定能够处理的异常类型 //使用日志 log.error(\u0026#34;程序出错啦~\u0026#34;,e);//注意：这里的error方法要使用第二个带两个参数的才可以抛出异常 //e.printStackTrace();//打印堆栈中的异常信息 //捕获到异常之后，响应一个标准的Result return Result.error(\u0026#34;对不起,操作失败,请联系管理员\u0026#34;); } } @RestControllerAdvice = @ControllerAdvice + @ResponseBody\n处理异常的方法返回值会转换为json后再响应给前端\n重新启动SpringBoot服务，打开浏览器，再来测试一下 修改员工 这个操作，我们依然设置已存在的 13390901232这个手机号：\n此时，我们可以看到，出现异常之后，异常已经被全局异常处理器捕获了。然后返回的错误信息，被前端程序正常解析，然后提示出了对应的错误提示信息。\n以上就是全局异常处理器的使用，主要涉及到两个注解：\n@RestControllerAdvice //表示当前类为全局异常处理器 @ExceptionHandler //指定可以捕获哪种类型的异常进行处理 那么我们可不可以让这个异常捕获的更精准一点呢？具体是哪里出现异常了？ 答案是可以的\n打开控制台我们发现了下面这个异常，可以对其进行捕获，然后获取这个异常的堆栈信息，之后可以捕获到这个异常的手机号，再对其进行提示手机号重复了\n按照继承关系从下往上匹配\n根据这个异常进行编写代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @ExceptionHandler //如果是DuplicateKeyException，那么这个处理器就会生效，如果是别的上面的Exception就会生效 public Result handleDuplicateKeyException(DuplicateKeyException e){ log.error(\u0026#34;程序出错啦~\u0026#34;,e); // 获取异常的具体描述信息，通常包含 \u0026#34;Duplicate entry \u0026#39;xxx\u0026#39; for key \u0026#39;xxx\u0026#39;\u0026#34; String message = e.getMessage(); // 定位 \u0026#34;Duplicate entry\u0026#34; 关键字的位置，作为截取错误信息的起点 int i = message.indexOf(\u0026#34;Duplicate entry\u0026#34;); // 从关键字位置开始截取字符串，获取包含具体重复值的片段 String errMsg = message.substring(i); // 按照空格将截取的字符串分割成数组 String[] arr = errMsg.split(\u0026#34; \u0026#34;); // 数组下标为2的元素通常是具体的重复值（如 \u0026#39;admin\u0026#39;），拼接提示语后返回 // 注意：这里假设了异常信息的格式是固定的，且重复值在数组的第三个位置 return Result.error(arr[2] + \u0026#34;已存在\u0026#34;); } 此时再来测试上面的手机号就会出现以下报错信息：\n再来测试用户名重复：\n员工信息统计 员工管理的增删改查功能我们已开发完成，接下来，我们再来完成员工信息统计的接口开发。 对于这些图形报表的开发，其实呢，都是基于现成的一些图形报表的组件开发的，比如：Echarts、HighCharts等。\n而报表的制作，主要是前端人员开发，引入对应的组件（比如：ECharts）即可。 服务端开发人员仅为其提供数据即可。\n官网：https://echarts.apache.org/zh/index.html\n职位统计 需求 对于这类的图形报表，服务端要做的，就是为其提供数据即可。 我们可以通过官方的示例，看到提供的数据其实就是X轴展示的信息，和对应的数据。\n三层架构每层的职责：\n接口描述 地址：接口文档 - 飞书云文档\n查看 数据统计 -\u0026gt; 员工职位统计 接口的描述。\n代码实现 1). 定义封装结果对象JobOption\n在 com.itheima.pojo 包中定义实体类 JobOption\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class JobOption { private List jobList;\t// 职位列表 private List dataList;\t// 数据列表 } 2). 定义ReportController，并添加方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Slf4j @RequestMapping(\u0026#34;/report\u0026#34;) @RestController public class ReportController { @Autowired private ReportService reportService; /** * 统计各个职位的员工人数 */ @GetMapping(\u0026#34;/empJobData\u0026#34;) public Result getEmpJobData(){ log.info(\u0026#34;统计各个职位的员工人数\u0026#34;); JobOption jobOption = reportService.getEmpJobData(); return Result.success(jobOption); } } 2). 定义ReportService接口，并添加接口方法。\n1 2 3 4 5 6 7 public interface ReportService { /** * 统计各个职位的员工人数 * @return */ JobOption getEmpJobData(); } 3). 定义ReportServiceImpl实现类，并实现方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service public class ReportServiceImpl implements ReportService { @Autowired private EmpMapper empMapper; @Override public JobOption getEmpJobData() { List\u0026lt;Map\u0026lt;String,Object\u0026gt;\u0026gt; list = empMapper.countEmpJobData(); List\u0026lt;Object\u0026gt; jobList = list.stream().map(dataMap -\u0026gt; dataMap.get(\u0026#34;pos\u0026#34;)).toList(); List\u0026lt;Object\u0026gt; dataList = list.stream().map(dataMap -\u0026gt; dataMap.get(\u0026#34;total\u0026#34;)).toList(); return new JobOption(jobList, dataList); } } 4). 定义EmpMapper 接口\n统计的是员工的信息，所以需要操作的是员工表。 所以代码我们就写在 EmpMapper 接口中即可。\n1 2 3 4 5 6 /** * 统计各个职位的员工人数 */ @MapKey(\u0026#34;pos\u0026#34;) //先将集合封装到Map中，再将Map封装到集合当中 List\u0026lt;Map\u0026lt;String,Object\u0026gt;\u0026gt; countEmpJobData(); 如果查询的记录往Map中封装，可以通过@MapKey注解指定返回的map中的唯一标识是那个字段。【也可以不指定】\n5). 定义EmpMapper.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;!-- 统计各个职位的员工人数 --\u0026gt; \u0026lt;select id=\u0026#34;countEmpJobData\u0026#34; resultType=\u0026#34;java.util.Map\u0026#34;\u0026gt; select (case job when 1 then \u0026#39;班主任\u0026#39; when 2 then \u0026#39;讲师\u0026#39; when 3 then \u0026#39;学工主管\u0026#39; when 4 then \u0026#39;教研主管\u0026#39; when 5 then \u0026#39;咨询师\u0026#39; else \u0026#39;其他\u0026#39; end) pos, count(*) total from emp group by job order by total \u0026lt;/select\u0026gt; case流程控制函数：\n语法一：case when 表达式1 then res1 [ when 表达式2 then res2 ] else res end ;\n含义：如果 表达式1 成立， 取 res1。 如果 表达式2 成立，取 res2。 如果前面的条件都不成立，则取 res。\n语法二（仅适用于等值匹配）：case expr when val1 then res1 [ when val2 then res2 ] else res end ;\n含义：如果 expr 的值为 val1 ， 取 res1。 如果 expr 的值为 val2 ，取 res2。 如果前面的条件都不成立，则取 res。\n功能测试 重新启动服务，打开Apifox进行测试。\n前后端联调测试 性别统计 需求 对于这类的图形报表，服务端要做的，就是为其提供数据即可。 我们可以通过官方的示例，看到提供的数据就是一个json格式的数据。\n接口描述 地址：接口文档 - 飞书云文档\n查看 数据统计 -\u0026gt; 员工性别统计 接口的描述。\n代码实现 1). 在ReportController，添加方法。\n1 2 3 4 5 6 7 8 9 /** * 统计员工性别信息 */ @GetMapping(\u0026#34;/empGenderData\u0026#34;) public Result getEmpGenderData(){ log.info(\u0026#34;统计员工性别信息\u0026#34;); List\u0026lt;Map\u0026gt; genderList = reportService.getEmpGenderData(); return Result.success(genderList); } 2). 在ReportService接口，添加接口方法。\n1 2 3 4 /** * 统计员工性别信息 */ List\u0026lt;Map\u0026gt; getEmpGenderData(); 3). 在ReportServiceImpl实现类，实现方法\n1 2 3 4 @Override public List\u0026lt;Map\u0026gt; getEmpGenderData() { return empMapper.countEmpGenderData(); } 4). 定义EmpMapper 接口\n统计的是员工的信息，所以需要操作的是员工表。 所以代码我们就写在 EmpMapper 接口中即可。\n1 2 3 4 5 /** * 统计员工性别信息 */ @MapKey(\u0026#34;name\u0026#34;) List\u0026lt;Map\u0026gt; countEmpGenderData(); 5). 定义EmpMapper.xml\n1 2 3 4 5 6 7 \u0026lt;!-- 统计员工的性别信息 --\u0026gt; \u0026lt;select id=\u0026#34;countEmpGenderData\u0026#34; resultType=\u0026#34;java.util.Map\u0026#34;\u0026gt; select if(gender = 1, \u0026#39;男\u0026#39;, \u0026#39;女\u0026#39;) as name, count(*) as value from emp group by gender ; \u0026lt;/select\u0026gt; if函数语法：if(条件, 条件为true取值, 条件为false取值)\nifnull函数语法：ifnull(expr, val1) 如果expr不为null，取自身，否则取val1\n功能测试 前后端联调测试 ","date":"2026-05-08T20:59:55+08:00","permalink":"https://aurora156.github.io/p/web%E5%90%8E%E7%AB%AF%E5%AE%9E%E6%88%984/","title":"Web后端实战（4）"},{"content":"Tlias智能学习辅助系统 完成了员工管理的列表查询功能之后，接下来呢，我们再来完成新增员工的功能。 具体的需求如下：\n新增员工 在新增员工的时候，在表单中，我们既要录入员工的基本信息，又要录入员工的工作经历信息。 员工基本信息，对应的表结构是 emp表，员工工作经历信息，对应的表结构是 emp_expr 表，所以这里我们要操作两张表，往两张表中保存数据。\n在Dategrip新增员工\n1 2 3 4 5 6 7 8 9 -- 新增员工 -- 保存员工基本信息 - emp insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) values (\u0026#39;linpingzhi\u0026#39;,\u0026#39;林平之\u0026#39;,1,\u0026#39;13309091234\u0026#39;,1,6000,\u0026#39;1.jpg\u0026#39;,\u0026#39;2020-01-01\u0026#39;,1,\u0026#39;2024-10-01 00:00:00\u0026#39;,\u0026#39;2024-10-01 00:00:00\u0026#39;); -- 批量保存员工工作经历信息 - emp_expr -- 注意：此处的员工id是在上面员工基本信息运行完后生成的id insert into emp_expr(emp_id, begin, end, company, job) values(37,\u0026#39;2021-01-01\u0026#39;,\u0026#39;2022-01-01\u0026#39;,\u0026#39;百度\u0026#39;,\u0026#39;Java开发\u0026#39;),(37,\u0026#39;2023-01-01\u0026#39;,\u0026#39;2024-01-01\u0026#39;,\u0026#39;字节\u0026#39;,\u0026#39;Java开发\u0026#39;); 接口描述 参照前面提供的接口文档中 员工管理 -\u0026gt; 添加员工 接口的描述。\n地址：接口文档 - 飞书云文档\n接口文档规定： 请求路径：/emps 请求方式：POST 请求参数：Json格式数据 响应数据：Json格式数据 **问题1：**如何限定请求方式是POST？ @PostMapping 问题2：怎么在controller中接收json格式的请求参数？@RequestBody 功能开发 准备工作 准备EmpExprMapper接口及映射配置文件EmpExprMapper.xml，并准备实体类接收前端传递的json格式的请求参数。\n1). EmpExprMapper接口\n1 2 3 4 @Mapper public interface EmpExprMapper { } 2). EmpExprMapper.xml 配置文件\n1 2 3 4 5 6 7 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.EmpExprMapper\u0026#34;\u0026gt; \u0026lt;/mapper\u0026gt; 3). 需要在 Emp 员工实体类中增加属性 exprList 来封装工作经历数据。 最终完整代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data public class Emp { private Integer id; //ID,主键 private String username; //用户名 private String password; //密码 private String name; //姓名 private Integer gender; //性别, 1:男, 2:女 private String phone; //手机号 private Integer job; //职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师 private Integer salary; //薪资 private String image; //头像 private LocalDate entryDate; //入职日期 private Integer deptId; //关联的部门ID private LocalDateTime createTime; //创建时间 private LocalDateTime updateTime; //修改时间 //封装部门名称数 private String deptName; //部门名称 //封装员工工作经历信息 private List\u0026lt;EmpExpr\u0026gt; exprList; } 保存员工基本信息 1). EmpController\n在 EmpController 中增加save方法。\n1 2 3 4 5 6 7 8 9 /** * 添加员工 */ @PostMapping public Result save(@RequestBody Emp emp){ log.info(\u0026#34;请求参数emp: {}\u0026#34;, emp); empService.save(emp); return Result.success(); } 2). EmpService \u0026amp; EmpServiceImpl\n在 EmpService 中增加 save 方法\n1 2 3 4 5 /** * 添加员工 * @param emp */ void save(Emp emp); 在 EmpServiceImpl 中增加save方法 , 实现接口中的save方法\n1 2 3 4 5 6 7 8 9 10 11 12 @Override public void save(Emp emp) { //1.补全基础属性 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //2.保存员工基本信息 empMapper.insert(emp); //3. 保存员工的工作经历信息 - 批量 (稍后完成) } 3). EmpMapper\n在 EmpMapper 中增加insert方法，新增员工的基本信息。\n1 2 3 4 5 6 7 /** * 新增员工数据 */ @Options(useGeneratedKeys = true, keyProperty = \u0026#34;id\u0026#34;) @Insert(\u0026#34;insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) \u0026#34; + \u0026#34;values (#{username},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})\u0026#34;) void insert(Emp emp); 主键返回：@Options(useGeneratedKeys = true, keyProperty = \u0026ldquo;id\u0026rdquo;)`\n由于我们在保存工作经历信息的时候，需要记录是哪位员工的工作经历。 所以，保存完员工信息之后，是需要获取到员工的ID的，那这里就需要通过Mybatis中提供的主键返回功能来获取。\n批量保存工作经历 分析 一个员工，是可以有多段工作经历的，所以在页面上将来用户录入员工信息时，可以自己根据需要添加多段工作经历。页面原型展示如下：\n那如果员工只有一段工作经历，我们就需要往工作经历表中保存一条记录。 执行的SQL如下：\n如果员工有两段工作经历，我们就需要往工作经历表中保存两条记录。执行的SQL如下：\n以此类推，三段、四段\u0026hellip;\u0026hellip;\n所以，这里最终我们需要执行的是批量插入数据的insert语句。\n实现 1). EmpServiceImpl\n完善save方法中保存员工信息的逻辑。完整逻辑如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public void save(Emp emp) { //1.补全基础属性 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //2.保存员工基本信息 empMapper.insert(emp); //3. 保存员工的工作经历信息 - 批量 Integer empId = emp.getId(); List\u0026lt;EmpExpr\u0026gt; exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)){ exprList.forEach(empExpr -\u0026gt; empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } 2). EmpExprMapper\n1 2 3 4 5 6 7 8 @Mapper public interface EmpExprMapper { /** * 批量插入员工工作经历信息 */ public void insertBatch(List\u0026lt;EmpExpr\u0026gt; exprList); } 3). EmpExprMapper.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.EmpExprMapper\u0026#34;\u0026gt; \u0026lt;!--批量插入员工工作经历信息--\u0026gt; \u0026lt;insert id=\u0026#34;insertBatch\u0026#34;\u0026gt; insert into emp_expr (emp_id, begin, end, company, job) values \u0026lt;foreach collection=\u0026#34;exprList\u0026#34; item=\u0026#34;expr\u0026#34; separator=\u0026#34;,\u0026#34;\u0026gt; (#{expr.empId}, #{expr.begin}, #{expr.end}, #{expr.company}, #{expr.job}) \u0026lt;/foreach\u0026gt; \u0026lt;/insert\u0026gt; \u0026lt;/mapper\u0026gt; 这里用到Mybatis中的动态SQL里提供的 \u0026lt;foreach\u0026gt; 标签，改标签的作用，是用来遍历循环，常见的属性说明：\ncollection：集合名称 item：集合遍历出来的元素/项 separator：每一次遍历使用的分隔符 open：遍历开始前拼接的片段 close：遍历结束后拼接的片段 上述的属性，是可选的，并不是所有的都是必须的。 可以自己根据实际需求，来指定对应的属性。\n功能测试 代码开发完成后，重启服务器，打开 Apifox 发送 POST 请求，请求路径：http://localhost:8080/emps\n此时经过前面的学习，我们可以配置通用的环境来进行测试了\n点开项目设置 -\u0026gt; 导入数据 -\u0026gt; 将资料中的json格式数据导入 -\u0026gt; 进入到已导入的接口管理中 - \u0026gt; 在右上角中选择管理环境 -\u0026gt; 将开发环境中的默认模块改为：http://localhost:8080 -\u0026gt; 确定后选择要测试的模块就可以直接进行测试了\n请求完毕后，可以打开idea的控制台看到控制台输出的日志：\n前后端联调测试 功能测试通过后，我们再进行通过打开浏览器，测试后端功能接口：\n点击保存之后，可以看到列表中已经展示出了这条数据。\n事物管理 问题分析 目前我们实现的新增员工功能中，操作了两次数据库，执行了两次 insert 操作。\n第一次：保存员工的基本信息到 emp 表中。 第二次：保存员工的工作经历信息到 emp_expr 表中。 如果说，保存员工的基本信息成功了，而保存员工的工作经历信息出错了，会发生什么现象呢？那接下来，我们来做一个测试 。 我们可以在代码中，人为在保存员工的service层的save方法中，构造一个错误：\n当代码执行到这个异常的时候代码就会报错\n那接下来，我们就重启服务，打开浏览器，来做一个测试：\n此时点击保存会发现并没有响应，并且idea中也会报错\n我们再打开数据库，看看表结构中的数据是否正常。\n1). emp 员工表中是有 wangwu 这条数据的。\n2). emp_expr 表中没有该员工的工作经历信息。\n最终，我们看到，程序出现了异常 ，员工表 emp 数据保存成功了, 但是 emp_expr 员工工作经历信息表，数据保存失败了。 那是否允许这种情况发生呢？\n不允许 因为这属于一个业务操作，如果保存员工信息成功了，保存工作经历信息失败了，就会造成数据库数据的不完整、不一致。 那如何解决这个问题呢？ 这需要通过数据库中的事务来解决这个问题。\n介绍 概念： 事务是一组操作的集合，它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求，即这些操作要么同时成功，要么同时失败。\n就拿添加员工的这个业务为例，在这个业务操作中，包含了两个操作，那这两个操作是一个不可分割的工作单位。\n这两个操作，要么同时失败，要么同时成功。\n默认MySQL的事务是自动提交的，也就是说，当执行一条DML语句，MySQL会立即隐式的提交事务。\n操作 事务控制主要三步操作：开启事务、提交事务/回滚事务。\n需要在这组操作执行之前，先开启事务 ( start transaction; / begin;)。 所有操作如果全部都执行成功，则提交事务 ( commit; )。 如果这组操作中，有任何一个操作执行失败，都应该回滚事务 ( rollback )。 那接下来，我们就可以将添加员工的业务操作，进行事务管理。 具体的SQL如下：\n1.提交事务\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 开启事务 start transaction; / begin; -- 1. 保存员工基本信息 insert into emp(id,username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) values (41,\u0026#39;tom\u0026#39;,\u0026#39;汤姆\u0026#39;,1,\u0026#39;13309091235\u0026#39;,1,6000,\u0026#39;1.jpg\u0026#39;,\u0026#39;2020-01-01\u0026#39;,1,now(),now()); -- 2. 保存员工的工作经历信息 insert into emp_expr(emp_id, begin, end, company, job) values (41,\u0026#39;2019-01-01\u0026#39;, \u0026#39;2020-01-01\u0026#39;, \u0026#39;百度\u0026#39;, \u0026#39;开发\u0026#39;), (41,\u0026#39;2020-01-10\u0026#39;, \u0026#39;2022-02-01\u0026#39;, \u0026#39;阿里\u0026#39;, \u0026#39;架构\u0026#39;); -- 提交事务(全部成功) commit; -- 回滚事务(有一个失败) rollback; 此时运行第1步操作和第2步操作后发现控制台运行成功了，但是库中并没有更新数据\n这涉及到事物的隔离特性，在后续内容会学习到\n此时提交一下就可以看到库中已经添加好了数据\n2.回滚事物\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 开启事物 start transaction ; -- 新增员工 -- 1.保存员工基本信息 - emp insert into emp(id,username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) values (42,\u0026#39;jerry\u0026#39;,\u0026#39;杰瑞\u0026#39;,1,\u0026#39;13309091234\u0026#39;,1,6000,\u0026#39;1.jpg\u0026#39;,\u0026#39;2020-01-01\u0026#39;,1,now(),now()); -- 2.批量保存员工工作经历信息 - emp_expr insert into emp_expr(emp_id, begin, end, company, job) values(42,\u0026#39;2021-01-01\u0026#39;,\u0026#39;2022-01-01\u0026#39;,\u0026#39;百度\u0026#39;,\u0026#39;Java开发\u0026#39;), -- 测试时将,删除 (42,\u0026#39;2023-01-01\u0026#39;,\u0026#39;2024-01-01\u0026#39;,\u0026#39;字节\u0026#39;,\u0026#39;Java开发\u0026#39;); -- 提交事务 commit ; -- 回滚事物 rollback ; 直接运行第1步和第2步操作会发现员工基本信息新增成功，但是批量保存失败了\n但是我们直接查询这两张表中的所有元素的时候能够发现，在员工表中汤姆添加成功了，但是员工经历表中却没有添加成功\n此时就需要进行事务的回滚将id为42这条数据删除了\n场景：\n例如：\n银行转账 下单扣减库存 Spring事务管理 在上述实现的新增员工的功能中，一旦在保存员工基本信息后出现异常。 我们就会发现，员工信息保存成功，但是工作经历信息保存失败，造成了数据的不完整不一致。\n产生原因：\n先执行新增员工的操作，这步执行完毕，就已经往员工表 emp 插入了数据。 执行 1/0 操作，抛出异常 抛出异常之前，下面所有的代码都不会执行了，批量保存工作经历信息，这个操作也不会执行 。 此时就出现问题了，员工基本信息保存了，员工的工作经历信息未保存，业务操作前后数据不一致。\n而要想保证操作前后，数据的一致性，就需要让新增员工中涉及到的两个业务操作，要么全部成功，要么全部失败 。 那我们如何，让这两个操作要么全部成功，要么全部失败呢 ？\n那就可以通过事务来实现，因为一个事务中的多个业务操作，要么全部成功，要么全部失败。\n此时，我们就需要在新增员工功能中添加事务。\n在方法运行之前，开启事务，如果方法成功执行，就提交事务，如果方法执行的过程当中出现异常了，就回滚事务。\n**思考：**开发中所有的业务操作，一旦我们要进行控制事务，是不是都是这样的套路？\n**答案：**是的。\n所以在spring框架当中就已经把事务控制的代码都已经封装好了，并不需要我们手动实现。我们使用了spring框架，我们只需要通过一个简单的注解@Transactional就搞定了。\nTransactional注解 注解：@Transactional 作用：就是在当前这个方法执行开始之前来开启事务，方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常，就会进行事务的回滚操作。 位置：业务层的方法上、类上、接口上 方法上：当前方法交给spring进行事务管理 类上：当前类中所有的方法都交由spring进行事务管理 接口上：接口下所有的实现类当中所有的方法都交给spring 进行事务管理 接下来，我们就可以在业务方法save上加上 @Transactional 来控制事务 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Transactional @Override public void save(Emp emp) { //1.补全基础属性 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //2.保存员工基本信息 empMapper.insert(emp); int i = 1/0; //3. 保存员工的工作经历信息 - 批量 Integer empId = emp.getId(); List\u0026lt;EmpExpr\u0026gt; exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)){ exprList.forEach(empExpr -\u0026gt; empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } @Transactional注解：我们一般会在业务层当中来控制事务，因为在业务层当中，一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务，我们就可以将多个数据访问操作控制在一个事务范围内。\n说明：可以在application.yml配置文件中开启事务管理日志，这样就可以在控制看到和事务相关的日志信息了\n1 2 3 4 #spring事务管理日志 logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug 接下来，我们再次添加员工，看看控制台输出的日志信息。\n添加Spring事务管理后，由于服务端程序引发了异常，所以事务进行回滚。\n打开数据库，我们会看到 emp 表 与 emp_expr 表中都没有对应的数据信息，保证了数据的一致性、完整性。\n事物进阶 前面我们通过spring事务管理注解@Transactional已经控制了业务层方法的事务。接下来我们要来详细的介绍一下@Transactional事务管理注解的使用细节。我们这里主要介绍@Transactional注解当中的两个常见的属性：\n异常回滚的属性：rollbackFor 事务传播行为：propagation 我们先来学习下rollbackFor属性。\nrollbackFor rollbackFor属性用于控制出现何种异常类型，回滚事务。 我们在之前编写的业务方法上添加了@Transactional注解，来实现事务管理。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Transactional @Override public void save(Emp emp) { //1.补全基础属性 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //2.保存员工基本信息 empMapper.insert(emp); int i = 1/0; //3. 保存员工的工作经历信息 - 批量 Integer empId = emp.getId(); List\u0026lt;EmpExpr\u0026gt; exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)){ exprList.forEach(empExpr -\u0026gt; empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } 以上业务功能save方法在运行时，会引发除0的算术运算异常(运行时异常)，出现异常之后，由于我们在方法上加了@Transactional注解进行事务管理，所以发生异常会执行rollback回滚操作，从而保证事务操作前后数据是一致的。\n下面我们在做一个测试，我们修改业务功能代码，在模拟异常的位置上直接抛出Exception异常（编译时异常）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Transactional\t//默认出现RuntimeException才会回滚。 @Override public void save(Emp emp) { //1.补全基础属性 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //2.保存员工基本信息 empMapper.insert(emp); //模拟：异常发生 if(true){ throw new Exception(\u0026#34;出现异常了~~~\u0026#34;); } //3. 保存员工的工作经历信息 - 批量 Integer empId = emp.getId(); List\u0026lt;EmpExpr\u0026gt; exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)){ exprList.forEach(empExpr -\u0026gt; empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } 说明：在service中向上抛出一个Exception编译时异常之后，由于是controller调用service，所以在controller中要有异常处理代码，此时我们选择在controller中继续把异常向上抛。\n重新启动服务后，对前端进行测试，请求添加员工的接口：\n通过前端返回的结果，我们看到抛出异常了。然后我们在回到IDEA的控制台来看一下。\n我们看到数据库的事务居然提交了，并没有进行回滚。[员工表提交成功，员工经历表并没有]\n通过以上测试可以得出一个结论：默认情况下，只有出现RuntimeException(运行时异常)才会回滚事务。\n假如我们想让所有的异常都回滚，需要来配置@Transactional注解当中的rollbackFor属性，通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Transactional(rollbackFor = Exception.class) @Override public void save(Emp emp) throws Exception { //1.补全基础属性 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //2.保存员工基本信息 empMapper.insert(emp); //int i = 1/0; //为什么这里的1/0异常会回滚 //因为该异常属于RuntimeExceptiony异常 if(true){ throw new Exception(\u0026#34;出异常啦....\u0026#34;); } //3. 保存员工的工作经历信息 - 批量 Integer empId = emp.getId(); List\u0026lt;EmpExpr\u0026gt; exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)){ exprList.forEach(empExpr -\u0026gt; empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } 接下来我们重新启动服务，测试新增员工的操作：\n控制台日志，可以看到因为出现了异常又进行了事务回滚。\n结论：\n在Spring的事务管理中，默认只有运行时异常 RuntimeException才会回滚。 如果还需要回滚指定类型的异常，可以通过rollbackFor属性来指定。 propagation 介绍 我们接着继续学习@Transactional注解当中的第二个属性propagation，这个属性是用来配置事务的传播行为的。\n什么是事务的传播行为呢？\n就是当一个事务方法被另一个事务方法调用时，这个事务方法应该如何进行事务控制。 例如：两个事务方法，一个A方法，一个B方法。在这两个方法上都添加了@Transactional注解，就代表这两个方法都具有事务，而在A方法当中又去调用了B方法。\n所谓事务的传播行为，指的就是在A方法运行的时候，首先会开启一个事务，在A方法当中又调用了B方法， B方法自身也具有事务，那么B方法在运行的时候，到底是加入到A方法的事务当中来，还是B方法在运行的时候新建一个事务？这个就涉及到了事务的传播行为。\n我们要想控制事务的传播行为，在@Transactional注解的后面指定一个属性propagation，通过 propagation 属性来指定传播行为。接下来我们就来介绍一下常见的事务传播行为。\n属性值 含义 REQUIRED 【默认值】需要事务，有则加入，无则创建新事务 REQUIRES_NEW 需要新事务，无论有无，总是创建新事务 SUPPORTS 支持事务，有则加入，无则在无事务状态中运行 NOT_SUPPORTED 不支持事务，在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 MANDATORY 必须有事务，否则抛异常 NEVER 必须没事务，否则抛异常 对于这些事务传播行为，我们只需要关注以下两个就可以了：\nREQUIRED（默认值） REQUIRES_NEW 案例 接下来我们就通过一个案例来演示下事务传播行为propagation属性的使用。\n**需求：**在新增员工信息时，无论是成功还是失败，都要记录操作日志。\n步骤：\n准备日志表 emp_log、实体类EmpLog、Mapper接口EmpLogMapper 在新增员工时记录日志 准备工作：\n1). 创建数据库表 emp_log 日志表\n1 2 3 4 5 6 -- 创建员工日志表 create table emp_log( id int unsigned primary key auto_increment comment \u0026#39;ID, 主键\u0026#39;, operate_time datetime comment \u0026#39;操作时间\u0026#39;, info varchar(2000) comment \u0026#39;日志信息\u0026#39; ) comment \u0026#39;员工日志表\u0026#39;; 2). 引入资料中提供的实体类：EmpLog\n1 2 3 4 5 6 7 8 @Data @NoArgsConstructor @AllArgsConstructor public class EmpLog { private Integer id; //ID private LocalDateTime operateTime; //操作时间 private String info; //详细信息 } 3). 引入资料中提供的Mapper接口：EmpLogMapper\n1 2 3 4 5 6 @Mapper public interface EmpLogMapper { //插入日志 @Insert(\u0026#34;insert into emp_log (operate_time, info) values (#{operateTime}, #{info})\u0026#34;) public void insert(EmpLog empLog); } 4). 引入资料中提供的业务接口：EmpLogService\n1 2 3 4 public interface EmpLogService { //记录新增员工日志 public void insertLog(EmpLog empLog); } 5). 引入资料中提供的业务实现类：EmpLogServiceImpl\n1 2 3 4 5 6 7 8 9 10 11 12 @Service public class EmpLogServiceImpl implements EmpLogService { @Autowired private EmpLogMapper empLogMapper; @Transactional @Override public void insertLog(EmpLog empLog) { empLogMapper.insert(empLog); } } 代码实现:\n业务实现类：EmpServiceImpl\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Autowired private EmpMapper empMapper; @Autowired private EmpExprMapper empExprMapper; @Autowired private EmpLogService empLogService; @Transactional(rollbackFor = {Exception.class}) @Override public void save(Emp emp) { try { //1.补全基础属性 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //2.保存员工基本信息 empMapper.insert(emp); //测试失败时 //测试成功案例时需要将这行代码删去或注释 int i = 1/0; //3. 保存员工的工作经历信息 - 批量 Integer empId = emp.getId(); List\u0026lt;EmpExpr\u0026gt; exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)){ exprList.forEach(empExpr -\u0026gt; empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } finally { //记录操作日志 EmpLog empLog = new EmpLog(null, LocalDateTime.now(), emp.toString()); empLogService.insertLog(empLog); } } try-finally 结构的主要作用是确保代码的“收尾”工作一定会被执行，无论 try 代码块中的程序是正常运行结束，还是因为遇到了错误（异常）而中断。\nfinally 块中的代码具有“无论如何都会执行”的特性，这在以下场景中至关重要：\ntry 块正常执行完毕：finally 块会执行。 try 块中发生异常：无论异常是否被 catch 捕获，finally 块都会执行。 try 或 catch 块中有 return 语句：在方法真正返回之前，finally 块会抢先执行。 测试:\n成功时：\n失败时：\n重新启动SpringBoot服务，测试新增员工操作 。我们可以看到控制台中输出的日志：\n从日志中我们可以看到：\n执行了插入员工数据的操作 执行了插入日志操作 程序发生Exception异常 执行事务回滚（保存员工数据、插入操作日志 因为在一个事务范围内，两个操作都会被回滚） 然后在 emp_log 表中没有记录日志数据 。\n原因分析:\n接下来我们就需要来分析一下具体是什么原因导致的日志没有成功的记录。\n在执行 save 方法时开启了一个事务 当执行 empLogService.insertLog 操作时，insertLog设置的事务传播行是默认值REQUIRED，表示有事务就加入，没有则新建事务 此时：save 和 insertLog 操作使用了同一个事务，同一个事务中的多个操作，要么同时成功，要么同时失败，所以当异常发生时进行事务回滚，就会回滚 save 和 insertLog 操作 解决方案：\n在EmpLogServiceImpl类中insertLog方法上，添加 @Transactional(propagation = Propagation.REQUIRES_NEW)\nPropagation.REQUIRES_NEW ：不论是否有事务，都创建新事务 ，运行在一个独立的事务中。\n1 2 3 4 5 6 7 8 9 10 11 12 @Service public class EmpLogServiceImpl implements EmpLogService { @Autowired private EmpLogMapper empLogMapper; @Transactional(propagation = Propagation.REQUIRES_NEW)//需要在一个新的事务中运行 @Override public void insertLog(EmpLog empLog) { empLogMapper.insert(empLog); } } 重启SpringBoot服务，再次测试新增员工的操作 ：\n会看到具体的日志如下：\n那此时，EmpServiceImpl 中的 save 方法运行时，会开启一个事务。 当调用empLogService.insertLog(empLog) 时，也会创建一个新的事务，那此时，当 insertLog 方法运行完毕之后，事务就已经提交了。 即使外部的事务出现异常，内部已经提交的事务，也不会回滚了，因为是两个独立的事务。\n到此事务传播行为已演示完成，事务的传播行为我们只需要掌握两个：REQUIRED、REQUIRES_NEW。\n**REQUIRED：**大部分情况下都是用该传播行为即可。 **REQUIRES_NEW：**当我们不希望事务之间相互影响时，可以使用该传播行为。比如：下订单前需要记录日志，不论订单保存成功与否，都需要保证日志记录能够记录成功。 事务四大特性 事务有哪些特性？\n事务的四大特性简称为：ACID\n原子性（Atomicity） ：原子性是指事务包装的一组sql是一个不可分割的工作单元，事务中的操作要么全部成功，要么全部失败。 一致性（Consistency）：一个事务完成之后数据都必须处于一致性状态。 如果事务成功的完成，那么数据库的所有变化将生效。 如果事务执行出现错误，那么数据库的所有变化将会被回滚(撤销)，返回到原始状态。 隔离性（Isolation）：多个用户并发的访问数据库时，一个用户的事务不能被其他用户的事务干扰，多个并发的事务之间要相互隔离。 一个事务的成功或者失败对于其他的事务是没有影响。 持久性（Durability）：一个事务一旦被提交或回滚，它对数据库的改变将是永久性的，哪怕数据库发生异常，重启之后数据亦然存在。 文件上传 在我们完成的 新增员工 功能中，还存在一个问题：没有头像(图片缺失)\n上述问题，需要我们通过文件上传技术来解决。下面我们就进入到文件上传技术的学习。\n文件上传技术这块我们主要讲解三个方面：首先我们先对文件上传做一个整体的介绍，接着再学习文件上传的本地存储方式，最后学习云存储方式。\n接下来我们就先来学习下什么是文件上传。\n简介 文件上传，是指将本地图片、视频、音频等文件上传到服务器，供其他用户浏览或下载的过程。 文件上传在项目中应用非常广泛，我们经常发微博、发微信朋友圈都用到了文件上传功能。 在我们的案例中，在新增员工的时候，要上传员工的头像，此时就会涉及到文件上传的功能。在进行文件上传时，我们点击加号或者是点击图片，就可以选择手机或者是电脑本地的图片文件了。当我们选择了某一个图片文件之后，这个文件就会上传到服务器，从而完成文件上传的操作\n**AI提示词：**你是一名java开发工程师，现在正在学习文件上传功能，如何基于HTML+SpringBoot完成文件上传功能。\n1). 生成的前端代码形式如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;上传文件\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026#34;/upload\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; 姓名: \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34;\u0026gt;\u0026lt;br\u0026gt; 年龄: \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;br\u0026gt; 头像: \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;提交\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 上传文件的原始form表单，要求表单必须具备以下三点（上传文件页面三要素）：\n表单必须有file域，用于选择要上传的文件 表单提交方式必须为POST：通常上传的文件会比较大，所以需要使用 POST 提交方式 表单的编码类型enctype必须要设置为：multipart/form-data：普通默认的编码格式是不适合传输大型的二进制数据的，所以在文件上传时，表单的编码格式必须设置为multipart/form-data 前端页面的3要素我们了解后，接下来我们就来验证下所讲解的文件上传3要素。\n将课程资料中的文件上传中的upload.html 文件，复制到springboot项目工程下的static目录里面。\n2). 生成的服务端代码形式如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.itheima.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; @Slf4j @RestController public class UploadController { /** * 上传文件 - 参数名file */ @PostMapping(\u0026#34;/upload\u0026#34;) public Result upload(String username, Integer age , MultipartFile file) throws Exception { log.info(\u0026#34;上传文件：{}, {}, {}\u0026#34;, username, age, file); return Result.success(); } } 在定义的方法中接收提交过来的数据 （方法中的形参名和请求参数的名字保持一致）\n用户名：String name 年龄： Integer age 文件： MultipartFile file Spring中提供了一个API：MultipartFile，使用这个API就可以来接收到上传的文件\n问题1：如果表单项的名字和方法中形参名不一致，该怎么办？\n1 2 3 public Result upload(String username, Integer age, MultipartFile image) //image形参名和请求参数名file不一致 解决：使用@RequestParam注解进行参数绑定\n1 2 3 public Result upload(String username, Integer age, @RequestParam(\u0026#34;file\u0026#34;) MultipartFile image) 本地存储 上面我们已经完成了文件上传最基本的功能实现，已经可以在服务端接收到上传的文件，并将文件保存在本地服务器的磁盘目录中了。 但是我们测试的时候发现，如果上传的文件名相同，后面上传的会覆盖前面上传的文件，那接下来，我们就要来优化这一块的功能。\nAI提示词：请完善上述代码, 将接收到的文件存储在本地的磁盘目录中(D:/images)中, 并要保证上传的文件名不重复\n代码实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.itheima.controller; import com.itheima.pojo.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.util.UUID; @Slf4j @RestController public class UploadController { /** * 上传文件 - 参数名file */ @PostMapping(\u0026#34;/upload\u0026#34;) public Result upload(String name, Integer age , MultipartFile file) throws IOException { log.info(\u0026#34;上传文件：{}, {}, {}\u0026#34;, name, age, file); //获取原始文件名 String originalFilename = file.getOriginalFilename(); //extName：文件的扩展名 //.substring(originalFilename.lastIndexOf(\u0026#34;.\u0026#34;))从最后一个点进行截取， //例如：1.2.11.jpg //新的文件名 String extension = originalFilename.substring(originalFilename.lastIndexOf(\u0026#34;.\u0026#34;)); String newFileName = UUID.randomUUID().toString() + extension; //保存文件 file.transferTo(new File(\u0026#34;D:/images/\u0026#34; + newFileName)); return Result.success(); } } 如果上传文件过大，会出现上传失败的警告\n那么如果需要上传大文件，可以在 application.properties 进行如下配置：\n1 2 3 4 5 6 7 spring： servlet: multipart: #最大单个文件大小 max-file-size: 10MB #最大请求大小(包括所有文件和表单数据) max-request-size: 100MB MultipartFile 常见方法：\nString getOriginalFilename(); //获取原始文件名 void transferTo(File dest); //将接收的文件转存到磁盘文件中 long getSize(); //获取文件的大小，单位：字节 byte[] getBytes(); //获取文件内容的字节数组 InputStream getInputStream(); //获取接收到的文件内容的输入流 利用 Apifox 测试，注意：请求参数名和controller方法形参名保持一致。\n通过 Apifox 测试，我们发现文件上传是没有问题的。(其中名字为uuid)\n到时此，我们文件上传的本地存储方式已完成了。但是这种本地存储方式还存在一问题：\n如果直接存储在服务器的磁盘目录中，存在以下缺点：\n不安全：磁盘如果损坏，所有的文件就会丢失 容量有限：如果存储大量的图片，磁盘空间有限(磁盘不可能无限制扩容) 无法直接访问 为了解决上述问题呢，通常有两种解决方案：\n自己搭建存储服务器，如：fastDFS 、MinIO 使用现成的云服务，如：阿里云，腾讯云，华为云 阿里云OSS 准备 介绍 阿里云是阿里巴巴集团旗下全球领先的云计算公司，也是国内最大的云服务提供商 。\n云服务指的就是通过互联网对外提供的各种各样的服务，比如像：语音服务、短信服务、邮件服务、视频直播服务、文字识别服务、对象存储服务等等。\n当我们在项目开发时需要用到某个或某些服务，就不需要自己来开发了，可以直接使用阿里云提供好的这些现成服务就可以了。比如：在项目开发当中，我们要实现一个短信发送的功能，如果我们项目组自己实现，将会非常繁琐，因为你需要和各个运营商进行对接。而此时阿里云完成了和三大运营商对接，并对外提供了一个短信服务。我们项目组只需要调用阿里云提供的短信服务，就可以很方便的来发送短信了。这样就降低了我们项目的开发难度，同时也提高了项目的开发效率。（大白话：别人帮我们实现好了功能，我们只要调用即可）\n云服务提供商给我们提供的软件服务通常是需要收取一部分费用的。\n阿里云对象存储OSS（Object Storage Service），是一款海量、安全、低成本、高可靠的云存储服务。使用OSS，您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。\n在我们使用了阿里云OSS对象存储服务之后，我们的项目当中如果涉及到文件上传这样的业务，在前端进行文件上传并请求到服务端时，在服务器本地磁盘当中就不需要再来存储文件了。我们直接将接收到的文件上传到oss，由 oss帮我们存储和管理，同时阿里云的oss存储服务还保障了我们所存储内容的安全可靠。\n那我们学习使用这类云服务，我们主要学习什么呢？其实我们主要学习的是如何在项目当中来使用云服务完成具体的业务功能。而无论使用什么样的云服务，阿里云也好，腾讯云、华为云也罢，在使用第三方的服务时，操作的思路都是一样的。\nSDK：Software Development Kit 的缩写，软件开发工具包，包括辅助软件开发的依赖（jar包）、代码示例等，都可以叫做SDK。\n简单说，sdk中包含了我们使用第三方云服务时所需要的依赖，以及一些示例代码。我们可以参照sdk所提供的示例代码就可以完成入门程序。\n第三方服务使用的通用思路，我们做一个简单介绍之后，接下来我们就来介绍一下我们当前要使用的阿里云oss对象存储服务具体的使用步骤。\n充值根据自身存储的大小文件来判定，一般金额不会太多\n**Bucket：**存储空间是用户用于存储对象（Object，就是文件）的容器，所有的对象都必须隶属于某个存储空间。\n账号准备 下面我们根据之前介绍的使用步骤，完成准备工作：\n1). 注册阿里云账户（注册完成后需要实名认证）\nhttps://account.aliyun.com/login/login.htm?oauth_callback=https%3A%2F%2Fwww.aliyun.com%2F\n2). 注册完账号之后，就可以登录阿里云\n此处我已经注册登录完毕，就不进行展示了\n开通OSS云服务 1). 通过控制台找到对象存储OSS服务\n选择要开通的服务\n如果是第一次访问，还需要开通对象存储服务OSS\n2). 开通OSS服务之后，就可以进入到阿里云对象存储的控制台\n3). 点击左侧的 \u0026ldquo;Bucket列表\u0026rdquo;，创建一个Bucket\n输入Bucket的相关信息.\n初次使用，阻止公共访问和读写权限都无法进行操作，可以等创建以后进行关闭\n其他的信息，配置项使用默认的即可。\n读写权限也在该页面，就在阻止公共访问后面，可自行修改\n配置AccessKey 1). 创建AccessKey\n点击 \u0026ldquo;AccessKey管理\u0026rdquo;，进入到管理页面。\n选择后点击继续使用AccessKey，进入到下面页面\n点击 \u0026ldquo;创建AccessKey\u0026rdquo;。\n2). 配置AccessKey\n以管理员身份打开CMD命令行，执行如下命令，配置系统的环境变量。\n1 2 set OSS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx set OSS_ACCESS_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 注意：将上述的ACCESS_KEY_ID 与 ACCESS_KEY_SECRET 的值一定一定一定一定一定一定要替换成自己的 。\n执行如下命令，让更改生效。\n1 2 setx OSS_ACCESS_KEY_ID \u0026#34;%OSS_ACCESS_KEY_ID%\u0026#34; setx OSS_ACCESS_KEY_SECRET \u0026#34;%OSS_ACCESS_KEY_SECRET%\u0026#34; 执行如下命令，验证环境变量是否生效。\n1 2 echo %OSS_ACCESS_KEY_ID% echo %OSS_ACCESS_KEY_SECRET% 入门 阿里云oss 对象存储服务的准备工作我们已经完成了，接下来我们就来完成第二步操作：参照官方所提供的sdk示例来编写入门程序。\n首先我们需要来打开阿里云OSS的官方文档，在官方文档中找到 SDK 的示例代码：\n参照文档，引入依赖：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;!--阿里云OSS依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.aliyun.oss\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aliyun-sdk-oss\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.17.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.xml.bind\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.activation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activation\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- no more than 2.3.3--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.glassfish.jaxb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxb-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 参照文档，编写入门程序：\n将官方提供的入门程序，复制过来，将里面的参数值改造成我们自己的即可。代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import com.aliyun.oss.*; import com.aliyun.oss.common.auth.*; import com.aliyun.oss.common.comm.SignVersion; import com.aliyun.oss.model.PutObjectRequest; import com.aliyun.oss.model.PutObjectResult; import java.io.File; public class Demo { public static void main(String[] args) throws Exception { // Endpoint以华东1（杭州）为例，其它Region请按实际情况填写。 String endpoint = \u0026#34;https://oss-cn-hangzhou.aliyuncs.com\u0026#34;; // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 填写Bucket名称，例如examplebucket。 String bucketName = \u0026#34;examplebucket\u0026#34;; // 填写Object完整路径，完整路径中不能包含Bucket名称，例如exampledir/exampleobject.txt。 String objectName = \u0026#34;exampledir/exampleobject.txt\u0026#34;; // 填写本地文件的完整路径，例如D:\\\\localpath\\\\examplefile.txt。 // 如果未指定本地路径，则默认从示例程序所属项目对应本地路径中上传文件。 String filePath= \u0026#34;D:\\\\localpath\\\\examplefile.txt\u0026#34;; // 填写Bucket所在地域。以华东1（杭州）为例，Region填写为cn-hangzhou。 String region = \u0026#34;cn-hangzhou\u0026#34;; // 创建OSSClient实例。 // 当OSSClient实例不再使用时，调用shutdown方法以释放资源。 ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration(); clientBuilderConfiguration.setSignatureVersion(SignVersion.V4); OSS ossClient = OSSClientBuilder.create() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .clientConfiguration(clientBuilderConfiguration) .region(region) .build(); try { // 创建PutObjectRequest对象。 PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(filePath)); // 如果需要上传时设置存储类型和访问权限，请参考以下示例代码。 // ObjectMetadata metadata = new ObjectMetadata(); // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); // metadata.setObjectAcl(CannedAccessControlList.Private); // putObjectRequest.setMetadata(metadata); // 上传文件。 PutObjectResult result = ossClient.putObject(putObjectRequest); } catch (OSSException oe) { System.out.println(\u0026#34;Caught an OSSException, which means your request made it to OSS, \u0026#34; + \u0026#34;but was rejected with an error response for some reason.\u0026#34;); System.out.println(\u0026#34;Error Message:\u0026#34; + oe.getErrorMessage()); System.out.println(\u0026#34;Error Code:\u0026#34; + oe.getErrorCode()); System.out.println(\u0026#34;Request ID:\u0026#34; + oe.getRequestId()); System.out.println(\u0026#34;Host ID:\u0026#34; + oe.getHostId()); } catch (ClientException ce) { System.out.println(\u0026#34;Caught an ClientException, which means the client encountered \u0026#34; + \u0026#34;a serious internal problem while trying to communicate with OSS, \u0026#34; + \u0026#34;such as not being able to access the network.\u0026#34;); System.out.println(\u0026#34;Error Message:\u0026#34; + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } } } 切记，大家需要将上面的 endpoint ，bucketName，objectName，file 都需要改成自己的。\n在以上代码中，需要替换的内容为：\nendpoint：阿里云OSS中的bucket对应的域名 bucketName：Bucket名称 objectName：对象名称，在Bucket中存储的对象的名称 region：bucket所属区域 这是我的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.itheima; import com.aliyun.oss.*; import com.aliyun.oss.common.auth.*; import com.aliyun.oss.common.comm.SignVersion; import com.aliyun.oss.model.PutObjectRequest; import com.aliyun.oss.model.PutObjectResult; import java.io.ByteArrayInputStream; import java.io.File; import java.nio.file.Files; public class Demo { public static void main(String[] args) throws Exception { String endpoint = \u0026#34;https://oss-cn-beijing.aliyuncs.com\u0026#34;; EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); String bucketName = \u0026#34;java-ai-26-5-07\u0026#34;; String objectName = \u0026#34;111.jpg\u0026#34;; String region = \u0026#34;cn-beijing\u0026#34;; ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration(); clientBuilderConfiguration.setSignatureVersion(SignVersion.V4); OSS ossClient = OSSClientBuilder.create() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .clientConfiguration(clientBuilderConfiguration) .region(region) .build(); try { File file = new File(\u0026#34;D:\\\\picture\\\\111.jpg\u0026#34;); byte[] content = Files.readAllBytes(file.toPath()); ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content)); } catch (OSSException oe) { System.out.println(\u0026#34;Caught an OSSException, which means your request made it to OSS, \u0026#34; + \u0026#34;but was rejected with an error response for some reason.\u0026#34;); System.out.println(\u0026#34;Error Message:\u0026#34; + oe.getErrorMessage()); System.out.println(\u0026#34;Error Code:\u0026#34; + oe.getErrorCode()); System.out.println(\u0026#34;Request ID:\u0026#34; + oe.getRequestId()); System.out.println(\u0026#34;Host ID:\u0026#34; + oe.getHostId()); } catch (ClientException ce) { System.out.println(\u0026#34;Caught an ClientException, which means the client encountered \u0026#34; + \u0026#34;a serious internal problem while trying to communicate with OSS, \u0026#34; + \u0026#34;such as not being able to access the network.\u0026#34;); System.out.println(\u0026#34;Error Message:\u0026#34; + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } } } 运行以上程序后，会把本地的文件上传到阿里云OSS服务器上。\n集成 介绍 阿里云oss对象存储服务的准备工作以及入门程序我们都已经完成了，接下来我们就需要在案例当中集成oss对象存储服务，来存储和管理案例中上传的图片。\n在新增员工的时候，上传员工的图像，而之所以需要上传员工的图像，是因为将来我们需要在系统页面当中访问并展示员工的图像。而要想完成这个操作，需要做两件事：\n需要上传员工的图像，并把图像保存起来（存储到阿里云OSS） 访问员工图像（通过图像在阿里云OSS的存储地址访问图像） OSS中的每一个文件都会分配一个访问的url，通过这个url就可以访问到存储在阿里云上的图片。所以需要把url返回给前端，这样前端就可以通过url获取到图像。 我们参照接口文档来开发文件上传功能：\n基本信息\n请求路径：/upload\n请求方式：POST\n接口描述：上传图片接口\n请求参数\n参数格式：multipart/form-data\n参数说明：\n参数名称 参数类型 是否必须 image file 是 响应数据\n参数格式：application/json 参数说明： 参数名 类型 是否必须 备注 code number 必须 响应码，1代表成功，0代表失败 msg string 非必须 提示信息 data object 非必须 返回的数据，上传图片的访问路径 响应数据样例：\n1 2 3 4 5 { \u0026#34;code\u0026#34;: 1, \u0026#34;msg\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: \u0026#34;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-0400.jpg\u0026#34; } 实现 1). 引入阿里云OSS上传文件工具类（由官方的示例代码改造而来）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.itheima.utils; import com.aliyun.oss.*; import com.aliyun.oss.common.auth.CredentialsProviderFactory; import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider; import com.aliyun.oss.common.comm.SignVersion; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.UUID; @Component public class AliyunOSSOperator { private String endpoint = \u0026#34;https://oss-cn-beijing.aliyuncs.com\u0026#34;; private String bucketName = \u0026#34;java-ai\u0026#34;; private String region = \u0026#34;cn-beijing\u0026#34;; public String upload(byte[] content, String originalFilename) throws Exception { // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 填写Object完整路径，例如202406/1.png。Object完整路径中不能包含Bucket名称。 //获取当前系统日期的字符串,格式为 yyyy/MM String dir = LocalDate.now().format(DateTimeFormatter.ofPattern(\u0026#34;yyyy/MM\u0026#34;)); //生成一个新的不重复的文件名 String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf(\u0026#34;.\u0026#34;)); String objectName = dir + \u0026#34;/\u0026#34; + newFileName; // 创建OSSClient实例。 ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration(); clientBuilderConfiguration.setSignatureVersion(SignVersion.V4); OSS ossClient = OSSClientBuilder.create() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .clientConfiguration(clientBuilderConfiguration) .region(region) .build(); try { ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content)); } finally { ossClient.shutdown(); } return endpoint.split(\u0026#34;//\u0026#34;)[0] + \u0026#34;//\u0026#34; + bucketName + \u0026#34;.\u0026#34; + endpoint.split(\u0026#34;//\u0026#34;)[1] + \u0026#34;/\u0026#34; + objectName; } } 2). 修改UploadController代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.itheima.controller; import com.itheima.pojo.Result; import com.itheima.utils.AliyunOSSOperator; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.util.UUID; @Slf4j @RestController public class UploadController { @Autowired private AliyunOSSOperator aliyunOSSOperator; @PostMapping(\u0026#34;/upload\u0026#34;) public Result upload(MultipartFile file) throws Exception { log.info(\u0026#34;上传文件：{}\u0026#34;, file); if (!file.isEmpty()) { // 生成唯一文件名 String originalFilename = file.getOriginalFilename(); String extName = originalFilename.substring(originalFilename.lastIndexOf(\u0026#34;.\u0026#34;)); String uniqueFileName = UUID.randomUUID().toString().replace(\u0026#34;-\u0026#34;, \u0026#34;\u0026#34;) + extName; // 上传文件 String url = aliyunOSSOperator.upload(file.getBytes(), uniqueFileName); return Result.success(url); } return Result.error(\u0026#34;上传失败\u0026#34;); } } 使用 Apifox 测试：\n上传成功后就可以在浏览器中输入图片地址进行下载了\n打开阿里云可以发现已经上传成功了\n此时进行前后端联调测试发现测试成功了\n功能优化 员工管理的新增功能我们已开发完成，但在我们所开发的程序中还有一些小问题，下面我们就来分析一下当前案例中存在的问题以及如何优化解决。\n在刚才我们制作的AliyunOSS操作的工具类中，我们直接将 endpoint、bucketName参数直接在java文件中写死了。如下所示：\n如果后续，项目要部署到测试环境、上生产环境，我们需要来修改这两个参数。 而如果开发一个大型项目，所有用到的技术涉及到的这些个参数全部写死在java代码中，是非常不便于维护和管理的。\n那么对于这些容易变动的参数，我们可以将其配置在配置文件中，然后通过 @Value 注解来注解外部配置的属性。如下所示：\n具体实现代码如下：\n1). application.yml\n1 2 3 4 5 6 #阿里云OSS aliyun: oss: endpoint: https://oss-cn-beijing.aliyuncs.com bucketName: java-ai-26-5-07 region: cn-beijing 2). AliyunOSSOperator\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package com.itheima.utils; import com.aliyun.oss.*; import com.aliyun.oss.common.auth.CredentialsProviderFactory; import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider; import com.aliyun.oss.common.comm.SignVersion; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.UUID; @Component public class AliyunOSSOperator { //方式一: 通过@Value注解一个属性一个属性的注入 @Value(\u0026#34;${aliyun.oss.endpoint}\u0026#34;) private String endpoint; @Value(\u0026#34;${aliyun.oss.bucketName}\u0026#34;) private String bucketName; @Value(\u0026#34;${aliyun.oss.region}\u0026#34;) private String region; public String upload(byte[] content, String originalFilename) throws Exception { // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 填写Object完整路径，例如2024/06/1.png。Object完整路径中不能包含Bucket名称。 //获取当前系统日期的字符串,格式为 yyyy/MM String dir = LocalDate.now().format(DateTimeFormatter.ofPattern(\u0026#34;yyyy/MM\u0026#34;)); //生成一个新的不重复的文件名 String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf(\u0026#34;.\u0026#34;)); String objectName = dir + \u0026#34;/\u0026#34; + newFileName; // 创建OSSClient实例。 ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration(); clientBuilderConfiguration.setSignatureVersion(SignVersion.V4); OSS ossClient = OSSClientBuilder.create() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .clientConfiguration(clientBuilderConfiguration) .region(region) .build(); try { ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content)); } finally { ossClient.shutdown(); } return endpoint.split(\u0026#34;//\u0026#34;)[0] + \u0026#34;//\u0026#34; + bucketName + \u0026#34;.\u0026#34; + endpoint.split(\u0026#34;//\u0026#34;)[1] + \u0026#34;/\u0026#34; + objectName; } } 如果只有一两个属性需要注入，而且不需要考虑复用性，使用@Value注解就可以了。@Value(\u0026quot;${key}\u0026quot;)\n但是使用@Value注解注入配置文件的配置项，**如果配置项多，注入繁琐，不便于维护管理和复用。**如下所示:\n那么有没有一种方式可以简化这些配置参数的注入呢？答案是肯定有，在Spring中给我们提供了一种简化方式，可以直接将配置文件中配置项的值自动的注入到对象的属性中。\nSpring提供的简化方式套路：\n1). 需要创建一个实现类，且实体类中的属性名和配置文件当中key的名字必须要一致\n​ 比如：配置文件当中叫endpoint，实体类当中的属性也得叫endpoint，另外实体类当中的属性还需要提供 getter / setter方法\n2). 需要将实体类交给Spring的IOC容器管理，成为IOC容器当中的bean对象\n3). 在实体类上添加@ConfigurationProperties注解，并通过prefix属性来指定配置参数项的前缀\n具体实现步骤:\n1). 定义实体类AliyunOSSProperties ，并交给IOC容器管理\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.itheima.utils; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = \u0026#34;aliyun.oss\u0026#34;) public class AliyunOSSProperties { private String endpoint; private String bucketName; private String region; } 2). 修改AliyunOSSOperator\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.itheima.utils; import com.aliyun.oss.*; import com.aliyun.oss.common.auth.CredentialsProviderFactory; import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider; import com.aliyun.oss.common.comm.SignVersion; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.UUID; @Component public class AliyunOSSOperator { //方式一: 通过@Value注解一个属性一个属性的注入 //@Value(\u0026#34;${aliyun.oss.endpoint}\u0026#34;) //private String endpoint; //@Value(\u0026#34;${aliyun.oss.bucketName}\u0026#34;) //private String bucketName; //@Value(\u0026#34;${aliyun.oss.region}\u0026#34;) //private String region; @Autowired private AliyunOSSProperties aliyunOSSProperties; public String upload(byte[] content, String originalFilename) throws Exception { String endpoint = aliyunOSSProperties.getEndpoint(); String bucketName = aliyunOSSProperties.getBucketName(); String region = aliyunOSSProperties.getRegion(); // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 填写Object完整路径，例如2024/06/1.png。Object完整路径中不能包含Bucket名称。 //获取当前系统日期的字符串,格式为 yyyy/MM String dir = LocalDate.now().format(DateTimeFormatter.ofPattern(\u0026#34;yyyy/MM\u0026#34;)); //生成一个新的不重复的文件名 String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf(\u0026#34;.\u0026#34;)); String objectName = dir + \u0026#34;/\u0026#34; + newFileName; // 创建OSSClient实例。 ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration(); clientBuilderConfiguration.setSignatureVersion(SignVersion.V4); OSS ossClient = OSSClientBuilder.create() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .clientConfiguration(clientBuilderConfiguration) .region(region) .build(); try { ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content)); } finally { ossClient.shutdown(); } return endpoint.split(\u0026#34;//\u0026#34;)[0] + \u0026#34;//\u0026#34; + bucketName + \u0026#34;.\u0026#34; + endpoint.split(\u0026#34;//\u0026#34;)[1] + \u0026#34;/\u0026#34; + objectName; } } ","date":"2026-05-06T19:58:41+08:00","permalink":"https://aurora156.github.io/p/web%E5%90%8E%E7%AB%AF%E5%AE%9E%E6%88%983/","title":"Web后端实战（3）"},{"content":"Tlias智能学习辅助系统 实现了部门管理的功能之后，接下来我们再来实现员工管理的功能。\n从页面原型中，我们可以看到，在查询员工信息的时候，除了要展示 姓名、性别、头像、职位、入职日期、最后操作时间这些员工信息外，还要展示出所属部门，那此时就需要从两张表中查询数据，一张是部门表，一张是员工表，此时就会涉及到多表操作。 所以这章的内容如下：\n多表关系 多表查询 员工列表查询 多表关系 概述 项目开发中，在进行数据库表结构设计时，会根据业务需求及业务模块之间的关系，分析并设计表结构，由于业务之间相互关联，所以各个表结构之间也存在着各种联系，基本上分为三种：\n一对多(多对一) 多对多 一对一 一对多(多对一) 场景：部门与员工的关系（一个部门下有多个员工）。 部门管理的页面原型： 员工管理的页面原型： 由于一个部门下，会关联多个员工。 而一个员工，是归属于某一个部门的 。那么此时，我们就需要在 emp 表中增加一个字段 dept_id 来标识这个员工属于哪一个部门，dept_id 关联的是 dept 的 id 。 如下所示：\n上述的 emp 员工表的 dept_id 字段，关联的是 dept 部门表的 id 。部门表是一的一方，也称为父表，员工表是多的一方，称之为子表。\n一对多的关系如何实现？在数据库表中多的一方，添加字段，来关联一的一方的主键 那接下来，我们就可以将上述的两张表创建出来。具体的SQL语句如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 CREATE TABLE dept ( id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT \u0026#39;ID, 主键\u0026#39;, name varchar(10) NOT NULL UNIQUE COMMENT \u0026#39;部门名称\u0026#39;, create_time datetime DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, update_time datetime DEFAULT NULL COMMENT \u0026#39;修改时间\u0026#39; ) COMMENT \u0026#39;部门表\u0026#39;; INSERT INTO dept VALUES (1,\u0026#39;学工部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2023-04-25 09:47:40\u0026#39;), (2,\u0026#39;教研部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-10-09 15:17:04\u0026#39;), (3,\u0026#39;咨询部2\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-11-30 21:26:24\u0026#39;), (4,\u0026#39;就业部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;), (5,\u0026#39;人事部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;), (6,\u0026#39;行政部\u0026#39;,\u0026#39;2024-11-30 20:56:37\u0026#39;,\u0026#39;2024-11-30 20:56:37\u0026#39;); create table emp( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, username varchar(20) not null unique comment \u0026#39;用户名\u0026#39;, password varchar(50) default \u0026#39;123456\u0026#39; comment \u0026#39;密码\u0026#39;, name varchar(10) not null comment \u0026#39;姓名\u0026#39;, gender tinyint unsigned not null comment \u0026#39;性别, 1:男, 2:女\u0026#39;, phone char(11) not null unique comment \u0026#39;手机号\u0026#39;, job tinyint unsigned comment \u0026#39;职位, 1 班主任, 2 讲师 , 3 学工主管, 4 教研主管, 5 咨询师\u0026#39;, salary int unsigned comment \u0026#39;薪资\u0026#39;, image varchar(300) comment \u0026#39;头像\u0026#39;, entry_date date comment \u0026#39;入职日期\u0026#39;, dept_id int unsigned comment \u0026#39;部门ID\u0026#39;, create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; ) comment \u0026#39;员工表\u0026#39;; INSERT INTO emp VALUES (1,\u0026#39;shinaian\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;施耐庵\u0026#39;,1,\u0026#39;13309090001\u0026#39;,4,15000,\u0026#39;5.png\u0026#39;,\u0026#39;2000-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-16 16:11:26\u0026#39;), (2,\u0026#39;songjiang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;宋江\u0026#39;,1,\u0026#39;13309090002\u0026#39;,2,8600,\u0026#39;01.png\u0026#39;,\u0026#39;2015-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:37\u0026#39;), (3,\u0026#39;lujunyi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;卢俊义\u0026#39;,1,\u0026#39;13309090003\u0026#39;,2,8900,\u0026#39;01.png\u0026#39;,\u0026#39;2008-05-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:39\u0026#39;), (4,\u0026#39;wuyong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;吴用\u0026#39;,1,\u0026#39;13309090004\u0026#39;,2,9200,\u0026#39;01.png\u0026#39;,\u0026#39;2007-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:41\u0026#39;), (5,\u0026#39;gongsunsheng\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;公孙胜\u0026#39;,1,\u0026#39;13309090005\u0026#39;,2,9500,\u0026#39;01.png\u0026#39;,\u0026#39;2012-12-05\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:43\u0026#39;), (6,\u0026#39;huosanniang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;扈三娘\u0026#39;,2,\u0026#39;13309090006\u0026#39;,3,6500,\u0026#39;01.png\u0026#39;,\u0026#39;2013-09-05\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:45\u0026#39;), (7,\u0026#39;chaijin\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;柴进\u0026#39;,1,\u0026#39;13309090007\u0026#39;,1,4700,\u0026#39;01.png\u0026#39;,\u0026#39;2005-08-01\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:47\u0026#39;), (8,\u0026#39;likui\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李逵\u0026#39;,1,\u0026#39;13309090008\u0026#39;,1,4800,\u0026#39;01.png\u0026#39;,\u0026#39;2014-11-09\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:49\u0026#39;), (9,\u0026#39;wusong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;武松\u0026#39;,1,\u0026#39;13309090009\u0026#39;,1,4900,\u0026#39;01.png\u0026#39;,\u0026#39;2011-03-11\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:51\u0026#39;), (10,\u0026#39;linchong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;林冲\u0026#39;,1,\u0026#39;13309090010\u0026#39;,1,5000,\u0026#39;01.png\u0026#39;,\u0026#39;2013-09-05\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:53\u0026#39;), (11,\u0026#39;huyanzhuo\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;呼延灼\u0026#39;,1,\u0026#39;13309090011\u0026#39;,2,9700,\u0026#39;01.png\u0026#39;,\u0026#39;2007-02-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:55\u0026#39;), (12,\u0026#39;xiaoliguang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;小李广\u0026#39;,1,\u0026#39;13309090012\u0026#39;,2,10000,\u0026#39;01.png\u0026#39;,\u0026#39;2008-08-18\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:57\u0026#39;), (13,\u0026#39;yangzhi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;杨志\u0026#39;,1,\u0026#39;13309090013\u0026#39;,1,5300,\u0026#39;01.png\u0026#39;,\u0026#39;2012-11-01\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:59\u0026#39;), (14,\u0026#39;shijin\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;史进\u0026#39;,1,\u0026#39;13309090014\u0026#39;,2,10600,\u0026#39;01.png\u0026#39;,\u0026#39;2002-08-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:01\u0026#39;), (15,\u0026#39;sunerniang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;孙二娘\u0026#39;,2,\u0026#39;13309090015\u0026#39;,2,10900,\u0026#39;01.png\u0026#39;,\u0026#39;2011-05-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:03\u0026#39;), (16,\u0026#39;luzhishen\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;鲁智深\u0026#39;,1,\u0026#39;13309090016\u0026#39;,2,9600,\u0026#39;01.png\u0026#39;,\u0026#39;2010-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:05\u0026#39;), (17,\u0026#39;liying\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;李应\u0026#39;,1,\u0026#39;13309090017\u0026#39;,1,5800,\u0026#39;01.png\u0026#39;,\u0026#39;2015-03-21\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:07\u0026#39;), (18,\u0026#39;shiqian\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;时迁\u0026#39;,1,\u0026#39;13309090018\u0026#39;,2,10200,\u0026#39;01.png\u0026#39;,\u0026#39;2015-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:09\u0026#39;), (19,\u0026#39;gudasao\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;顾大嫂\u0026#39;,2,\u0026#39;13309090019\u0026#39;,2,10500,\u0026#39;01.png\u0026#39;,\u0026#39;2008-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:11\u0026#39;), (20,\u0026#39;ruanxiaoer\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小二\u0026#39;,1,\u0026#39;13309090020\u0026#39;,2,10800,\u0026#39;01.png\u0026#39;,\u0026#39;2018-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:13\u0026#39;), (21,\u0026#39;ruanxiaowu\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小五\u0026#39;,1,\u0026#39;13309090021\u0026#39;,5,5200,\u0026#39;01.png\u0026#39;,\u0026#39;2015-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:15\u0026#39;), (22,\u0026#39;ruanxiaoqi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小七\u0026#39;,1,\u0026#39;13309090022\u0026#39;,5,5500,\u0026#39;01.png\u0026#39;,\u0026#39;2016-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:17\u0026#39;), (23,\u0026#39;ruanji\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮籍\u0026#39;,1,\u0026#39;13309090023\u0026#39;,5,5800,\u0026#39;01.png\u0026#39;,\u0026#39;2012-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:19\u0026#39;), (24,\u0026#39;tongwei\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;童威\u0026#39;,1,\u0026#39;13309090024\u0026#39;,5,5000,\u0026#39;01.png\u0026#39;,\u0026#39;2006-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:21\u0026#39;), (25,\u0026#39;tongmeng\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;童猛\u0026#39;,1,\u0026#39;13309090025\u0026#39;,5,4800,\u0026#39;01.png\u0026#39;,\u0026#39;2002-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:23\u0026#39;), (26,\u0026#39;yanshun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;燕顺\u0026#39;,1,\u0026#39;13309090026\u0026#39;,5,5400,\u0026#39;01.png\u0026#39;,\u0026#39;2011-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-08 22:12:46\u0026#39;), (27,\u0026#39;lijun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李俊\u0026#39;,1,\u0026#39;13309090027\u0026#39;,2,6600,\u0026#39;8.png\u0026#39;,\u0026#39;2004-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-16 17:56:59\u0026#39;), (28,\u0026#39;lizhong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李忠\u0026#39;,1,\u0026#39;13309090028\u0026#39;,5,5000,\u0026#39;6.png\u0026#39;,\u0026#39;2007-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-17 16:34:22\u0026#39;), (30,\u0026#39;liyun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李云\u0026#39;,1,\u0026#39;13309090030\u0026#39;,NULL,NULL,\u0026#39;01.png\u0026#39;,\u0026#39;2020-03-01\u0026#39;,NULL,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:31\u0026#39;), (36,\u0026#39;linghuchong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;令狐冲\u0026#39;,1,\u0026#39;18809091212\u0026#39;,2,6800,\u0026#39;1.png\u0026#39;,\u0026#39;2023-10-19\u0026#39;,2,\u0026#39;2023-10-20 20:44:54\u0026#39;,\u0026#39;2023-11-09 09:41:04\u0026#39;); 多表问题分析 问题 表结构创建完毕后，我们看到两张表的数据分别为：\n我们看到，在1号部门下，是关联的有6个员工。 当删除了1号部门后，数据变为：\n1号部门被删除了，但是依然还有6个员工是属于1号部门的。 此时：就出现数据的不完整、不一致了。\n此时这些一号部门的员工就属于脏数据了\n分析 现象：部门数据可以直接删除，然而还有部分员工归属于该部门下，此时就出现了数据的不完整、不一致问题 。 原因：目前上述的两张表，在数据库层面，并未建立关联，所以是无法保证数据的一致性和完整性的 。 解决方案：想解决上述的问题呢，我们就可以通过数据库中的 外键约束 来解决。 外键约束：让两张表的数据建立连接，保证数据的一致性和完整性。\n对应的关键字：foreign key\n外键约束的语法：\n1 2 3 4 5 6 7 8 9 10 -- 创建表时指定 create table 表名( 字段名 数据类型, ... [constraint] [外键名称] foreign key (外键字段名) references 主表 (主表列名) ); -- 建完表后，添加外键 alter table 表名 add constraint 外键名称 foreign key(外键字段名) references 主表(主表列名); 那接下来，我们就为员工表的dept_id 建立外键约束，来关联部门表的主键。\n当我们添加外键约束时，我们得保证当前数据库表中的数据是完整的。 所以，我们需要将之前删除掉的数据再添加回来。\n方式1：通过SQL语句操作\n1 2 -- 修改表： 添加外键约束 alter table emp add constraint fk_dept_id foreign key (dept_id) references dept(id); 我们可以打开图形化界面查看\n此时就可以清晰的查看哪些表是关联的了\n此时在去删除父表，就会发现已经报错了\n但此时还是可以删除没有外键约束的部门表：\n例如删除员工表中没有的id：15\n可以发现并没有报错\n方式2：图形化界面操作\n在左侧菜单栏，在emp表上右键，选择 modify Table... (old UI)\n外键约束 注意：在现在的企业开发中，很少会使用物理外键，都是使用逻辑外键。 甚至在一些数据库开发规范中，会明确指出禁止使用物理外键 foreign key\n一对一 案例: 用户与身份证信息 的关系 关系: 一对一关系，多用于单表拆分，将一张表的基础字段放在一张表中，其他字段放在另一张表中，以提升操作效率 实现: 在任意一方加入外键，关联另外一方的主键，并且设置外键为唯一的(UNIQUE) 一对一的应用场景： 用户表(基本信息+身份信息)\n基本信息：用户的ID、姓名、性别、手机号、学历 身份信息：民族、生日、身份证号、身份证签发机关，身份证的有效期(开始时间、结束时间) 如果在业务系统当中，对用户的基本信息查询频率特别的高，但是对于用户的身份信息查询频率很低，此时出于提高查询效率的考虑，我就可以将这张大表拆分成两张小表，第一张表存放的是用户的基本信息，而第二张表存放的就是用户的身份信息。他们两者之间一对一的关系，一个用户只能对应一个身份证，而一个身份证也只能关联一个用户。\n那么在数据库层面怎么去体现上述两者之间是一对一的关系呢？\n其实一对一我们可以看成一种特殊的一对多。一对多我们是怎么设计表关系的？是不是在多的一方添加外键。同样我们也可以通过外键来体现一对一之间的关系，我们只需要在任意一方来添加一个外键就可以了。\nSQL脚本：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 -- 用户基本信息表 create table tb_user( id int unsigned primary key auto_increment comment \u0026#39;ID\u0026#39;, name varchar(10) not null comment \u0026#39;姓名\u0026#39;, gender tinyint unsigned not null comment \u0026#39;性别, 1 男 2 女\u0026#39;, phone char(11) comment \u0026#39;手机号\u0026#39;, degree varchar(10) comment \u0026#39;学历\u0026#39; ) comment \u0026#39;用户基本信息表\u0026#39;; -- 测试数据 insert into tb_user values (1,\u0026#39;白眉鹰王\u0026#39;,1,\u0026#39;18812340001\u0026#39;,\u0026#39;初中\u0026#39;), (2,\u0026#39;青翼蝠王\u0026#39;,1,\u0026#39;18812340002\u0026#39;,\u0026#39;大专\u0026#39;), (3,\u0026#39;金毛狮王\u0026#39;,1,\u0026#39;18812340003\u0026#39;,\u0026#39;初中\u0026#39;), (4,\u0026#39;紫衫龙王\u0026#39;,2,\u0026#39;18812340004\u0026#39;,\u0026#39;硕士\u0026#39;); -- 用户身份信息表 create table tb_user_card( id int unsigned primary key auto_increment comment \u0026#39;ID\u0026#39;, nationality varchar(10) not null comment \u0026#39;民族\u0026#39;, birthday date not null comment \u0026#39;生日\u0026#39;, idcard char(18) not null comment \u0026#39;身份证号\u0026#39;, issued varchar(20) not null comment \u0026#39;签发机关\u0026#39;, expire_begin date not null comment \u0026#39;有效期限-开始\u0026#39;, expire_end date comment \u0026#39;有效期限-结束\u0026#39;, user_id int unsigned not null unique comment \u0026#39;用户ID\u0026#39;, constraint fk_user_id foreign key (user_id) references tb_user(id) ) comment \u0026#39;用户身份信息表\u0026#39;; -- 测试数据 insert into tb_user_card values (1,\u0026#39;汉\u0026#39;,\u0026#39;1960-11-06\u0026#39;,\u0026#39;100000100000100001\u0026#39;,\u0026#39;朝阳区公安局\u0026#39;,\u0026#39;2000-06-10\u0026#39;,null,1), (2,\u0026#39;汉\u0026#39;,\u0026#39;1971-11-06\u0026#39;,\u0026#39;100000100000100002\u0026#39;,\u0026#39;静安区公安局\u0026#39;,\u0026#39;2005-06-10\u0026#39;,\u0026#39;2025-06-10\u0026#39;,2), (3,\u0026#39;汉\u0026#39;,\u0026#39;1963-11-06\u0026#39;,\u0026#39;100000100000100003\u0026#39;,\u0026#39;昌平区公安局\u0026#39;,\u0026#39;2006-06-10\u0026#39;,null,3), (4,\u0026#39;回\u0026#39;,\u0026#39;1980-11-06\u0026#39;,\u0026#39;100000100000100004\u0026#39;,\u0026#39;海淀区公安局\u0026#39;,\u0026#39;2008-06-10\u0026#39;,\u0026#39;2028-06-10\u0026#39;,4); 多对多 案例: 学生 与 课程的关系 关系: 一个学生可以选修多门课程，一门课程也可以供多个学生选择 实现: 建立第三张中间表，中间表至少包含两个外键，分别关联两方主键 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 -- 学生表 create table tb_student( id int auto_increment primary key comment \u0026#39;主键ID\u0026#39;, name varchar(10) comment \u0026#39;姓名\u0026#39;, no varchar(10) comment \u0026#39;学号\u0026#39; ) comment \u0026#39;学生表\u0026#39;; -- 学生表测试数据 insert into tb_student(name, no) values (\u0026#39;黛绮丝\u0026#39;, \u0026#39;2000100101\u0026#39;), (\u0026#39;谢逊\u0026#39;, \u0026#39;2000100102\u0026#39;), (\u0026#39;殷天正\u0026#39;, \u0026#39;2000100103\u0026#39;), (\u0026#39;韦一笑\u0026#39;, \u0026#39;2000100104\u0026#39;); -- 课程表 create table tb_course( id int auto_increment primary key comment \u0026#39;主键ID\u0026#39;, name varchar(10) comment \u0026#39;课程名称\u0026#39; ) comment \u0026#39;课程表\u0026#39;; -- 课程表测试数据 insert into tb_course (name) values (\u0026#39;Java\u0026#39;), (\u0026#39;PHP\u0026#39;), (\u0026#39;MySQL\u0026#39;) , (\u0026#39;Hadoop\u0026#39;); -- 学生课程表（中间表） create table tb_student_course( id int auto_increment comment \u0026#39;主键\u0026#39; primary key, student_id int not null comment \u0026#39;学生ID\u0026#39;, course_id int not null comment \u0026#39;课程ID\u0026#39;, constraint fk_courseid foreign key (course_id) references tb_course (id), constraint fk_studentid foreign key (student_id) references tb_student (id) )comment \u0026#39;学生课程中间表\u0026#39;; -- 学生课程表测试数据 insert into tb_student_course(student_id, course_id) values (1,1),(1,2),(1,3),(2,2),(2,3),(3,4); 案例 需求：请根据资料中提供的页面原型，设计员工模块涉及到的表结构。\n步骤：\n阅读页面原型及需求文档，分析各个模块涉及到的表结构，及表结构之间的关系。 根据页面原型及需求文档，分析各个表结构中具体的字段及约束。 分析：\n涉及到的模块有：\n部门管理 部门管理涉及到一张部门表，这个前面我们都已经设计过了。 无需再进行设计了。\n员工管理 上述在员工列表查询的页面原型，当我们点击 \u0026ldquo;新增员工\u0026rdquo; 按钮时，会弹出一个新增员工的表单，表单展示形式如下：\n​\t在上述的页面原型中，我们可以看到，每一个员工是归属于某一个部门的，而一个部门下可以有多个员工，所以部门与员工之间的关系是一对多的关系。\n​\t从页面员工中，我们可以看到，员工还有工作经历的信息。而每一个员工，是可以添加多个工作经历的。 所以，工作经历我们可以再设计一张表，而员工与员工的工作经历之间的关系，是一对多的关系。\n​\t那么，这里总共会涉及三张表，分别是：部门表、员工表、员工工作经历表。\n​\t最终，具体的表结构如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 -- 部门表 create table dept ( id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT \u0026#39;ID, 主键\u0026#39;, name varchar(10) NOT NULL UNIQUE COMMENT \u0026#39;部门名称\u0026#39;, create_time datetime COMMENT \u0026#39;创建时间\u0026#39;, update_time datetime COMMENT \u0026#39;修改时间\u0026#39; ) COMMENT \u0026#39;部门表\u0026#39;; -- 员工表 create table emp( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, username varchar(20) not null unique comment \u0026#39;用户名\u0026#39;, password varchar(50) default \u0026#39;123456\u0026#39; comment \u0026#39;密码\u0026#39;, name varchar(10) not null comment \u0026#39;姓名\u0026#39;, gender tinyint unsigned not null comment \u0026#39;性别, 1:男, 2:女\u0026#39;, phone char(11) not null unique comment \u0026#39;手机号\u0026#39;, job tinyint unsigned comment \u0026#39;职位, 1 班主任, 2 讲师 , 3 学工主管, 4 教研主管, 5 咨询师\u0026#39;, salary int unsigned comment \u0026#39;薪资\u0026#39;, image varchar(300) comment \u0026#39;头像\u0026#39;, entry_date date comment \u0026#39;入职日期\u0026#39;, dept_id int unsigned comment \u0026#39;部门ID\u0026#39;, -- 关联的是dept部门表的ID create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; ) comment \u0026#39;员工表\u0026#39;; -- 员工工作经历表 create table emp_expr( id int unsigned primary key auto_increment comment \u0026#39;ID, 主键\u0026#39;, emp_id int unsigned null comment \u0026#39;员工ID\u0026#39;, -- 关联的是emp员工表的ID begin date null comment \u0026#39;开始时间\u0026#39;, end date null comment \u0026#39;结束时间\u0026#39;, company varchar(50) null comment \u0026#39;公司名称\u0026#39;, job varchar(50) null comment \u0026#39;职位\u0026#39; ) comment \u0026#39;工作经历\u0026#39;; 注意：在上述的表结构设计中，我们使用的都是逻辑外键。\n多表查询 概述 数据准备 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 -- 部门管理 create table dept( id int unsigned primary key auto_increment comment \u0026#39;ID, 主键\u0026#39;, name varchar(10) not null unique comment \u0026#39;部门名称\u0026#39;, create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; ) comment \u0026#39;部门表\u0026#39; ; insert into dept (id, name, create_time, update_time) values (1,\u0026#39;学工部\u0026#39;,now(),now()), (2,\u0026#39;教研部\u0026#39;,now(),now()), (3,\u0026#39;咨询部\u0026#39;,now(),now()), (4,\u0026#39;就业部\u0026#39;,now(),now()), (5,\u0026#39;人事部\u0026#39;,now(),now()); -- 员工管理 create table emp( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, username varchar(20) not null unique comment \u0026#39;用户名\u0026#39;, password varchar(32) not null comment \u0026#39;密码\u0026#39;, name varchar(10) not null comment \u0026#39;姓名\u0026#39;, gender tinyint unsigned not null comment \u0026#39;性别, 1:男, 2:女\u0026#39;, phone char(11) not null unique comment \u0026#39;手机号\u0026#39;, job tinyint unsigned comment \u0026#39;职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师\u0026#39;, salary int unsigned comment \u0026#39;薪资\u0026#39;, image varchar(255) comment \u0026#39;头像\u0026#39;, entry_date date comment \u0026#39;入职日期\u0026#39;, dept_id int unsigned COMMENT \u0026#39;关联的部门ID\u0026#39;, create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; ) comment \u0026#39;员工表\u0026#39;; -- 准备测试数据 INSERT INTO `emp` VALUES (1,\u0026#39;shinaian\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;施耐庵\u0026#39;,1,\u0026#39;13309090001\u0026#39;,4,15000,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2000-01-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:35\u0026#39;), (2,\u0026#39;songjiang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;宋江\u0026#39;,1,\u0026#39;13309090002\u0026#39;,2,8600,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2015-01-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:37\u0026#39;), (3,\u0026#39;lujunyi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;卢俊义\u0026#39;,1,\u0026#39;13309090003\u0026#39;,2,8900,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2008-05-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:39\u0026#39;), (4,\u0026#39;wuyong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;吴用\u0026#39;,1,\u0026#39;13309090004\u0026#39;,2,9200,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2007-01-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:41\u0026#39;), (5,\u0026#39;gongsunsheng\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;公孙胜\u0026#39;,1,\u0026#39;13309090005\u0026#39;,2,9500,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2012-12-05\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:43\u0026#39;), (6,\u0026#39;huosanniang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;扈三娘\u0026#39;,2,\u0026#39;13309090006\u0026#39;,3,6500,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2013-09-05\u0026#39;,1,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:45\u0026#39;), (7,\u0026#39;chaijin\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;柴进\u0026#39;,1,\u0026#39;13309090007\u0026#39;,1,4700,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2005-08-01\u0026#39;,1,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:47\u0026#39;), (8,\u0026#39;likui\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李逵\u0026#39;,1,\u0026#39;13309090008\u0026#39;,1,4800,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2014-11-09\u0026#39;,1,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:49\u0026#39;), (9,\u0026#39;wusong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;武松\u0026#39;,1,\u0026#39;13309090009\u0026#39;,1,4900,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2011-03-11\u0026#39;,1,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:51\u0026#39;), (10,\u0026#39;lichong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;林冲\u0026#39;,1,\u0026#39;13309090010\u0026#39;,1,5000,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2013-09-05\u0026#39;,1,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:53\u0026#39;), (11,\u0026#39;huyanzhuo\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;呼延灼\u0026#39;,1,\u0026#39;13309090011\u0026#39;,2,9700,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2007-02-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:55\u0026#39;), (12,\u0026#39;xiaoliguang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;小李广\u0026#39;,1,\u0026#39;13309090012\u0026#39;,2,10000,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2008-08-18\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:57\u0026#39;), (13,\u0026#39;yangzhi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;杨志\u0026#39;,1,\u0026#39;13309090013\u0026#39;,1,5300,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2012-11-01\u0026#39;,1,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:35:59\u0026#39;), (14,\u0026#39;shijin\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;史进\u0026#39;,1,\u0026#39;13309090014\u0026#39;,2,10600,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2002-08-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:01\u0026#39;), (15,\u0026#39;sunerniang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;孙二娘\u0026#39;,2,\u0026#39;13309090015\u0026#39;,2,10900,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2011-05-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:03\u0026#39;), (16,\u0026#39;luzhishen\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;鲁智深\u0026#39;,1,\u0026#39;13309090016\u0026#39;,2,9600,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2010-01-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:05\u0026#39;), (17,\u0026#39;liying\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;李应\u0026#39;,1,\u0026#39;13309090017\u0026#39;,1,5800,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2015-03-21\u0026#39;,1,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:07\u0026#39;), (18,\u0026#39;shiqian\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;时迁\u0026#39;,1,\u0026#39;13309090018\u0026#39;,2,10200,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2015-01-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:09\u0026#39;), (19,\u0026#39;gudasao\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;顾大嫂\u0026#39;,2,\u0026#39;13309090019\u0026#39;,2,10500,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2008-01-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:11\u0026#39;), (20,\u0026#39;ruanxiaoer\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小二\u0026#39;,1,\u0026#39;13309090020\u0026#39;,2,10800,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2018-01-01\u0026#39;,2,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:13\u0026#39;), (21,\u0026#39;ruanxiaowu\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小五\u0026#39;,1,\u0026#39;13309090021\u0026#39;,5,5200,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2015-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:15\u0026#39;), (22,\u0026#39;ruanxiaoqi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小七\u0026#39;,1,\u0026#39;13309090022\u0026#39;,5,5500,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2016-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:17\u0026#39;), (23,\u0026#39;ruanji\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮籍\u0026#39;,1,\u0026#39;13309090023\u0026#39;,5,5800,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2012-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:19\u0026#39;), (24,\u0026#39;tongwei\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;童威\u0026#39;,1,\u0026#39;13309090024\u0026#39;,5,5000,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2006-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:21\u0026#39;), (25,\u0026#39;tongmeng\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;童猛\u0026#39;,1,\u0026#39;13309090025\u0026#39;,5,4800,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2002-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:23\u0026#39;), (26,\u0026#39;yanshun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;燕顺\u0026#39;,1,\u0026#39;13309090026\u0026#39;,5,5400,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2011-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:25\u0026#39;), (27,\u0026#39;lijun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李俊\u0026#39;,1,\u0026#39;13309090027\u0026#39;,5,6600,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2004-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:27\u0026#39;), (28,\u0026#39;lizhong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李忠\u0026#39;,1,\u0026#39;13309090028\u0026#39;,5,5000,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2007-01-01\u0026#39;,3,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:29\u0026#39;), (29,\u0026#39;songqing\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;宋清\u0026#39;,1,\u0026#39;13309090029\u0026#39;,NULL,5100,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2020-01-01\u0026#39;,NULL,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:31\u0026#39;), (30,\u0026#39;liyun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李云\u0026#39;,1,\u0026#39;13309090030\u0026#39;,NULL,NULL,\u0026#39;https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png\u0026#39;,\u0026#39;2020-03-01\u0026#39;,NULL,\u0026#39;2024-10-27 16:35:33\u0026#39;,\u0026#39;2024-10-27 16:36:31\u0026#39;); 介绍 多表查询：查询时从多张表中获取所需数据 单表查询的SQL语句：select 字段列表 from 表名;\n那么要执行多表查询，只需要使用逗号分隔多张表即可，如： select 字段列表 from 表1, 表2;\n1 select * from emp , dept; 此时，我们看到查询结果中包含了大量的结果集，总共150条记录，而这其实就是员工表所有的记录(30行)与部门表所有记录(5行)的所有组合情况，这种现象称之为笛卡尔积。\n笛卡尔积：笛卡尔乘积是指在数学中，两个集合(A集合和B集合)的所有组合情况。\n在多表查询时，需要消除无效的笛卡尔积，只保留表关联部分的数据。\n在SQL语句中，如何去除无效的笛卡尔积呢？只需要给多表查询加上连接查询的条件即可。\n1 select * from emp , dept where emp.dept_id = dept.id ; 即让员工表中的dept_id字段等于部门表中的主键id即可\n这样，我们就查询出来了所有的员工，及其这个员工所属的部门信息。 而由于id为29、30的员工，没有dept_id字段值，所以在多表查询时，根据连接查询的条件并没有查询到。\n分类 多表查询可以分为：\n连接查询 内连接：相当于查询A、B交集部分数据【A∩B深绿色】 外连接 左外连接：查询左表所有数据(包括两张表交集部分数据)【A中蓝色】 右外连接：查询右表所有数据(包括两张表交集部分数据)【B中浅绿】 子查询 内连接 内连接查询：查询两表或多表中交集部分数据。\n内连接从语法上可以分为：\n隐式内连接(常见) 显式内连接 隐式内连接语法：\n1 select 字段列表 from 表1 , 表2 where 条件 ... ; 显式内连接语法：\n1 select 字段列表 from 表1 [ inner ] join 表2 on 连接条件 ... ; 案例1. 查询所有员工的ID, 姓名 , 及所属的部门名称 (隐式、显式内连接实现)\n1 2 3 4 5 -- 隐式 select emp.id,emp.name from emp,dept where emp.dept_id = dept.id; -- 显示 select emp.id,emp.name from emp inner join dept on emp.dept_id = dept.id; 案例2：查询 性别为男, 且工资 高于8000 的员工的ID, 姓名, 及所属的部门名称(隐式、显式内连接实现)\n1 2 3 4 5 -- 隐式 select emp.id,emp.name,dept.name from emp,dept where emp.dept_id = dept.id and emp.gender = 1 and salary \u0026gt; 8000; -- 显式 select emp.id,emp.name,dept.name from emp join dept on emp.dept_id = dept.id where emp.gender = 1 and salary \u0026gt; 8000; 在多表联查时，我们指定字段时，需要在字段名前面加上表名，来指定具体是哪一张的字段。 如：emp.dept_id\n给表起别名简化书写：\n1 2 3 select 字段列表 from 表1 as 别名1 , 表2 as 别名2 where 条件 ... ; select 字段列表 from 表1 别名1 , 表2 别名2 where 条件 ... ; -- as 可以省略 使用了别名的多表查询：\n1 select e.id,e.name,d.name from emp e join dept d on e.dept_id = d.id where e.gender = 1 and salary \u0026gt; 8000; 注意事项: 一旦为表起了别名，就不能再使用表名来指定对应的字段了，此时只能够使用别名来指定字段。\n外连接 外连接分为两种：左外连接和右外连接。 左外连接语法：\n1 select 字段列表 from 表1 left [ outer ] join 表2 on 连接条件 ... ; 左外连接相当于查询表1(左表)的所有数据，当然也包含表1和表2交集部分的数据。\n右外连接语法：\n1 select 字段列表 from 表1 right [ outer ] join 表2 on 连接条件 ... ; 右外连接相当于查询表2(右表)的所有数据，当然也包含表1和表2交集部分的数据。\n案例1：查询员工表所有员工的姓名, 和对应的部门名称 (左外连接)\n1 select e.name,d.name from emp e left outer join dept d on e.dept_id = d.id 案例2：查询部门表所有部门的名称, 和对应的员工名称 (右外连接)\n1 select d.name,e.name from emp e right join dept d on e.dept_id = d.id 案例3：查询工资高于8000的所有员工的姓名, 和对应的部门名称 (左外连接)\n1 select e.name,d.name from emp e left join dept d on e.dept_id = d.id where e.salary \u0026gt; 8000; 注意事项：\n左外连接和右外连接是可以相互替换的，只需要调整连接查询时SQL语句中表的先后顺序就可以了。而我们在日常开发使用时，更偏向于左外连接。\n子查询 介绍：SQL语句中嵌套select语句，称为嵌套查询，又称子查询。 形式：select * from t1 where column1 = (select column1 from t2 …); 说明：子查询外部的语句可以是insert / update / delete / select 的任何一个，最常见的是 select。 分类： 标量子查询：子查询返回的结果为单个值 列子查询：子查询返回的结果为一列 行子查询：子查询返回的结果为一行 表子查询：子查询返回的结果为多行多列 提示：子查询的要点是，先对需求做拆分，明确具体的步骤，然后再逐步编写SQL语句。\n标量子查询\n子查询返回的结果是单个值(数字、字符串、日期等)，最简单的形式，这种子查询称为标量子查询。\n常用的操作符： = \u0026lt;\u0026gt; \u0026gt; \u0026gt;= \u0026lt; \u0026lt;=\n案例1：查询最早入职的员工信息\n1 2 3 4 5 6 7 -- a.获取最早入职时间 select min(entry_date) from emp; -- b.查询最早入职的员工信息 select * from emp where entry_date = \u0026#39;2000-01-01\u0026#39;; select * from emp where entry_date = (select min(entry_date) from emp); 案例2：询在\u0026quot;阮小五\u0026quot;入职之后入职的员工信息\n1 2 3 4 5 6 select emp.entry_date from emp where name = \u0026#39;阮小五\u0026#39;; -- b.查询在该时间之后入职的员工信息 select * from emp where entry_date \u0026gt; \u0026#39;2015-01-01\u0026#39;; select * from emp where entry_date \u0026gt; (select emp.entry_date from emp where name = \u0026#39;阮小五\u0026#39;); 列子查询\n子查询返回的结果是一列(可以是多行)，这种子查询称为列子查询。\n常用的操作符：\n操作符 描述 in 在指定的范围之内，多选一 not in 不在指定的集合范围之内 案例：查询\u0026quot;教研部\u0026quot;和\u0026quot;咨询部\u0026quot;的所有员工信息\n1 2 3 4 5 6 -- a.查询\u0026#34;教研部\u0026#34;和\u0026#34;咨询部\u0026#34;的部门ID select dept.id from dept where name =\u0026#39;教研部\u0026#39; or name = \u0026#39;咨询部\u0026#39;; -- b.查询指定部门ID的员工信息 select * from emp,dept where dept.id in(2,3); select * from emp,dept where dept.id in(select dept.id from dept where name =\u0026#39;教研部\u0026#39; or name = \u0026#39;咨询部\u0026#39;); 行子查询\n子查询返回的结果是一行(可以是多列)，这种子查询称为行子查询。\n常用的操作符：= 、\u0026lt;\u0026gt; 、IN 、NOT IN\n案例：查询与\u0026quot;李忠\u0026quot;的薪资及职位都相同的员工信息\n1 2 3 4 5 6 -- a. 查询\u0026#34;李忠\u0026#34;的薪资和职位 select salary,job from emp where name = \u0026#39;李忠\u0026#39;; -- b. 查询指定薪资和职位的员工信息 select * from emp where salary = 5000 and job = 5; select * from emp where salary = (select salary from emp where name = \u0026#39;李忠\u0026#39;) and job = (select job from emp where name = \u0026#39;李忠\u0026#39;); 1 2 3 4 5 -- 优化： select salary,job from emp where name = \u0026#39;李忠\u0026#39;; select * from emp where (salary, job) = (5000,5); select * from emp where (salary, job) = (select salary , job from emp where name = \u0026#39;李忠\u0026#39;); 表子查询\n子查询返回的结果是多行多列，常作为临时表，这种子查询称为表子查询。\n案例：获取每个部门中薪资最高的员工信息\n1 2 3 4 5 6 -- a.获取每个部门的最高薪资 select dept_id,max(salary) from emp group by dept_id; -- b.查询每个部门中薪资最高的员工信息 select * from emp e,(select dept_id,max(salary) max_sal from emp group by dept_id) a where e.dept_id = a.dept_id and e.salary = a.max_sal; 在完成a后，将a的查询结果视为一张新的表，再去完成b\n案例 根据需求，完成多表查询的SQL语句的编写。\n查询 \u0026ldquo;教研部\u0026rdquo; 性别为 男，且在 \u0026ldquo;2011-05-01\u0026rdquo; 之后入职的员工信息 。 1 2 -- 表：dept,emp select e.* from emp e,dept d where e.dept_id = d.id and d.name = \u0026#39;教研部\u0026#39; and gender = 1 and entry_date \u0026gt; \u0026#39;2011-05-01\u0026#39; ; 查询工资 低于公司平均工资的且性别为男 的员工信息 。 1 2 3 4 5 6 7 8 -- 表：emp -- 2.1 查询公司平均工资 select avg(salary) from emp; -- 2.2 查询工资低于公司平均工资的且性别为男的员工信息 select * from emp where salary \u0026lt; 7548.2759 and gender = 1; select * from emp where salary \u0026lt; (select avg(salary) from emp) and gender = 1; 查询部门人数超过10人的部门名称 。 1 2 -- 表：dept,emp select d.name,count(*) from emp e,dept d where e.dept_id = d.id group by d.name having count(*) \u0026gt; 10; 查询在 \u0026ldquo;2010-05-01\u0026rdquo; 后入职，且薪资高于 10000 的 \u0026ldquo;教研部\u0026rdquo; 员工信息，并根据薪资倒序排序。 1 2 select e.* from emp e,dept d where e.dept_id = d.id and entry_date \u0026gt; \u0026#39;2010-05-01\u0026#39; and e.salary \u0026gt; 10000 and d.name = \u0026#39;教研部\u0026#39; order by salary desc; 查询工资 低于本部门平均工资的员工信息 。【难】(类似于上面的表子查询) 1 2 select e.* from emp e,(select dept_id,avg(salary) avg_sal from emp group by dept_id) as a where e.dept_id = a.dept_id and e.salary \u0026lt; a.avg_sal 员工列表查询 具体的需求如下：\n在查询员工列表数据时，既需要查询 员工的基本信息，还需要查询员工所属的部门名称，所以这里呢，会涉及到多表查询的操作。\n而且，在查询员工列表数据时，既要考虑搜索栏中的查询条件，还要考虑对查询的结果进行分页处理。\n那么接下来，我们在实现这个功能时，将会分为三个部分来逐一实现：\n准备工作 需求：查询所有员工信息，并查询出部门名称。（涉及到的表：emp、dept）\n基础代码准备 1). 创建员工管理相关表结构\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 -- 员工表 create table emp( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, username varchar(20) not null unique comment \u0026#39;用户名\u0026#39;, password varchar(50) default \u0026#39;123456\u0026#39; comment \u0026#39;密码\u0026#39;, name varchar(10) not null comment \u0026#39;姓名\u0026#39;, gender tinyint unsigned not null comment \u0026#39;性别, 1:男, 2:女\u0026#39;, phone char(11) not null unique comment \u0026#39;手机号\u0026#39;, job tinyint unsigned comment \u0026#39;职位, 1 班主任, 2 讲师 , 3 学工主管, 4 教研主管, 5 咨询师\u0026#39;, salary int unsigned comment \u0026#39;薪资\u0026#39;, image varchar(300) comment \u0026#39;头像\u0026#39;, entry_date date comment \u0026#39;入职日期\u0026#39;, dept_id int unsigned comment \u0026#39;部门ID\u0026#39;, create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; ) comment \u0026#39;员工表\u0026#39;; INSERT INTO emp VALUES (1,\u0026#39;shinaian\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;施耐庵\u0026#39;,1,\u0026#39;13309090001\u0026#39;,4,15000,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2000-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-16 16:11:26\u0026#39;), (2,\u0026#39;songjiang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;宋江\u0026#39;,1,\u0026#39;13309090002\u0026#39;,2,8600,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2015-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:37\u0026#39;), (3,\u0026#39;lujunyi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;卢俊义\u0026#39;,1,\u0026#39;13309090003\u0026#39;,2,8900,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2008-05-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:39\u0026#39;), (4,\u0026#39;wuyong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;吴用\u0026#39;,1,\u0026#39;13309090004\u0026#39;,2,9200,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2007-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:41\u0026#39;), (5,\u0026#39;gongsunsheng\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;公孙胜\u0026#39;,1,\u0026#39;13309090005\u0026#39;,2,9500,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2012-12-05\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:43\u0026#39;), (6,\u0026#39;huosanniang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;扈三娘\u0026#39;,2,\u0026#39;13309090006\u0026#39;,3,6500,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2013-09-05\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:45\u0026#39;), (7,\u0026#39;chaijin\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;柴进\u0026#39;,1,\u0026#39;13309090007\u0026#39;,1,4700,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2005-08-01\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:47\u0026#39;), (8,\u0026#39;likui\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李逵\u0026#39;,1,\u0026#39;13309090008\u0026#39;,1,4800,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2014-11-09\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:49\u0026#39;), (9,\u0026#39;wusong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;武松\u0026#39;,1,\u0026#39;13309090009\u0026#39;,1,4900,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2011-03-11\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:51\u0026#39;), (10,\u0026#39;linchong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;林冲\u0026#39;,1,\u0026#39;13309090010\u0026#39;,1,5000,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2013-09-05\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:53\u0026#39;), (11,\u0026#39;huyanzhuo\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;呼延灼\u0026#39;,1,\u0026#39;13309090011\u0026#39;,2,9700,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2007-02-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:55\u0026#39;), (12,\u0026#39;xiaoliguang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;小李广\u0026#39;,1,\u0026#39;13309090012\u0026#39;,2,10000,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2008-08-18\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:57\u0026#39;), (13,\u0026#39;yangzhi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;杨志\u0026#39;,1,\u0026#39;13309090013\u0026#39;,1,5300,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2012-11-01\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:35:59\u0026#39;), (14,\u0026#39;shijin\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;史进\u0026#39;,1,\u0026#39;13309090014\u0026#39;,2,10600,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2002-08-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:01\u0026#39;), (15,\u0026#39;sunerniang\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;孙二娘\u0026#39;,2,\u0026#39;13309090015\u0026#39;,2,10900,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2011-05-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:03\u0026#39;), (16,\u0026#39;luzhishen\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;鲁智深\u0026#39;,1,\u0026#39;13309090016\u0026#39;,2,9600,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2010-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:05\u0026#39;), (17,\u0026#39;liying\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;李应\u0026#39;,1,\u0026#39;13309090017\u0026#39;,1,5800,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2015-03-21\u0026#39;,1,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:07\u0026#39;), (18,\u0026#39;shiqian\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;时迁\u0026#39;,1,\u0026#39;13309090018\u0026#39;,2,10200,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2015-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:09\u0026#39;), (19,\u0026#39;gudasao\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;顾大嫂\u0026#39;,2,\u0026#39;13309090019\u0026#39;,2,10500,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2008-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:11\u0026#39;), (20,\u0026#39;ruanxiaoer\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小二\u0026#39;,1,\u0026#39;13309090020\u0026#39;,2,10800,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2018-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:13\u0026#39;), (21,\u0026#39;ruanxiaowu\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小五\u0026#39;,1,\u0026#39;13309090021\u0026#39;,5,5200,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2015-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:15\u0026#39;), (22,\u0026#39;ruanxiaoqi\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮小七\u0026#39;,1,\u0026#39;13309090022\u0026#39;,5,5500,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2016-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:17\u0026#39;), (23,\u0026#39;ruanji\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;阮籍\u0026#39;,1,\u0026#39;13309090023\u0026#39;,5,5800,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2012-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:19\u0026#39;), (24,\u0026#39;tongwei\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;童威\u0026#39;,1,\u0026#39;13309090024\u0026#39;,5,5000,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2006-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:21\u0026#39;), (25,\u0026#39;tongmeng\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;童猛\u0026#39;,1,\u0026#39;13309090025\u0026#39;,5,4800,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2002-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:23\u0026#39;), (26,\u0026#39;yanshun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;燕顺\u0026#39;,1,\u0026#39;13309090026\u0026#39;,5,5400,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2011-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-08 22:12:46\u0026#39;), (27,\u0026#39;lijun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李俊\u0026#39;,1,\u0026#39;13309090027\u0026#39;,2,6600,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2004-01-01\u0026#39;,2,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-16 17:56:59\u0026#39;), (28,\u0026#39;lizhong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李忠\u0026#39;,1,\u0026#39;13309090028\u0026#39;,5,5000,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2007-01-01\u0026#39;,3,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-11-17 16:34:22\u0026#39;), (30,\u0026#39;liyun\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;李云\u0026#39;,1,\u0026#39;13309090030\u0026#39;,NULL,NULL,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2020-03-01\u0026#39;,NULL,\u0026#39;2023-10-20 16:35:33\u0026#39;,\u0026#39;2023-10-20 16:36:31\u0026#39;), (36,\u0026#39;linghuchong\u0026#39;,\u0026#39;123456\u0026#39;,\u0026#39;令狐冲\u0026#39;,1,\u0026#39;18809091212\u0026#39;,2,6800,\u0026#39;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg\u0026#39;,\u0026#39;2023-10-19\u0026#39;,2,\u0026#39;2023-10-20 20:44:54\u0026#39;,\u0026#39;2023-11-09 09:41:04\u0026#39;); -- 员工工作经历信息 create table emp_expr( id int unsigned primary key auto_increment comment \u0026#39;ID, 主键\u0026#39;, emp_id int unsigned comment \u0026#39;员工ID\u0026#39;, begin date comment \u0026#39;开始时间\u0026#39;, end date comment \u0026#39;结束时间\u0026#39;, company varchar(50) comment \u0026#39;公司名称\u0026#39;, job varchar(50) comment \u0026#39;职位\u0026#39; )comment \u0026#39;工作经历\u0026#39;; 2). 准备emp表对应的实体类Emp、EmpExpr\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.itheima.pojo; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @Data public class Emp { private Integer id; //ID,主键 private String username; //用户名 private String password; //密码 private String name; //姓名 private Integer gender; //性别, 1:男, 2:女 private String phone; //手机号 private Integer job; //职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师 private Integer salary; //薪资 private String image; //头像 private LocalDate entryDate; //入职日期 private Integer deptId; //关联的部门ID private LocalDateTime createTime; //创建时间 private LocalDateTime updateTime; //修改时间 //封装部门名称数 private String deptName; //部门名称 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.itheima.pojo; import lombok.Data; import java.time.LocalDate; /** * 工作经历 */ @Data public class EmpExpr { private Integer id; //ID private Integer empId; //员工ID private LocalDate begin; //开始时间 private LocalDate end; //结束时间 private String company; //公司名称 private String job; //职位 } 3). 准备Emp员工管理的基础结构，包括Controller、Service、Mapper\nEmpMapper：\n1 2 3 4 5 6 7 8 9 10 package com.itheima.mapper; import com.itheima.pojo.Emp; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface EmpMapper { } EmpService:\n1 2 3 4 package com.itheima.service; public interface EmpService { } EmpServiceImpl:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.itheima.service.impl; import com.itheima.mapper.EmpMapper; import com.itheima.service.EmpService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 员工管理 */ @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; } EmpController:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.itheima.controller; import com.itheima.service.EmpService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RestController; /** * 员工管理 */ @Slf4j @RestController public class EmpController { @Autowired private EmpService empService; } 分页查询 分析 每次只展示一页的数据，比如：一页展示10条数据，如果还想看其他的数据，可以通过点击页码进行查询。\n在员工管理的需求中，就要求我们进行分页查询，展示出对应的数据。 具体的页面原型如下：\n要想从数据库中进行分页查询，我们要使用LIMIT关键字，格式为：limit 开始索引 每页显示的条数。\n1). 查询第1页数据的SQL语句是：\n1 select * from emp limit 0,10; 2). 查询第2页数据的SQL语句是：\n1 select * from emp limit 10,10; 3). 查询第3页的数据的SQL语句是：\n1 select * from emp limit 20,10; 观察以上SQL语句，发现： 开始索引一直在改变 ， 每页显示条数是固定的\n开始索引的计算公式： 开始索引 = (当前页码 - 1) * 每页显示条数\n我们继续基于页面原型，继续分析，得出以下结论：\n前端在请求服务端时，传递的参数 当前页码 page 每页显示条数 pageSize 后端需要响应什么数据给前端 所查询到的数据列表（存储到List 集合中） 总记录数 后台给前端返回的数据包含：List集合(数据列表)、total(总记录数)\n而这两部分我们通常封装到PageResult对象中，并将该对象转换为json格式的数据响应回给浏览器。\n1 2 3 4 5 6 7 @Data @NoArgsConstructor @AllArgsConstructor public class PageResult { private Long total; //总记录数 private List rows; //当前页数据列表 } 接口描述 1). 基本信息\n请求路径：/emps\n请求方式：GET\n接口描述：该接口用于员工列表数据的条件分页查询\n2). 请求参数\n请求数据样例：\n1 /emps?name=张\u0026amp;gender=1\u0026amp;begin=2007-09-01\u0026amp;end=2022-09-01\u0026amp;page=1\u0026amp;pageSize=10 3). 响应数据\n参数格式：application/json\n参数说明：\n响应数据样例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 { \u0026#34;code\u0026#34;: 1, \u0026#34;msg\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;total\u0026#34;: 2, \u0026#34;rows\u0026#34;: [ { \u0026#34;id\u0026#34;: 1, \u0026#34;username\u0026#34;: \u0026#34;jinyong\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;金庸\u0026#34;, \u0026#34;gender\u0026#34;: 1, \u0026#34;image\u0026#34;: \u0026#34;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg\u0026#34;, \u0026#34;job\u0026#34;: 2, \u0026#34;salary\u0026#34;: 8000, \u0026#34;entryDate\u0026#34;: \u0026#34;2015-01-01\u0026#34;, \u0026#34;deptId\u0026#34;: 2, \u0026#34;deptName\u0026#34;: \u0026#34;教研部\u0026#34;, \u0026#34;createTime\u0026#34;: \u0026#34;2022-09-01T23:06:30\u0026#34;, \u0026#34;updateTime\u0026#34;: \u0026#34;2022-09-02T00:29:04\u0026#34; }, { \u0026#34;id\u0026#34;: 2, \u0026#34;username\u0026#34;: \u0026#34;zhangwuji\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;张无忌\u0026#34;, \u0026#34;gender\u0026#34;: 1, \u0026#34;image\u0026#34;: \u0026#34;https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg\u0026#34;, \u0026#34;job\u0026#34;: 2, \u0026#34;salary\u0026#34;: 6000, \u0026#34;entryDate\u0026#34;: \u0026#34;2015-01-01\u0026#34;, \u0026#34;deptId\u0026#34;: 2, \u0026#34;deptName\u0026#34;: \u0026#34;教研部\u0026#34;, \u0026#34;createTime\u0026#34;: \u0026#34;2022-09-01T23:06:30\u0026#34;, \u0026#34;updateTime\u0026#34;: \u0026#34;2022-09-02T00:29:04\u0026#34; } ] } } 目前我们只考虑分页查询，先不考虑查询条件，而上述的接口文档中，与分页查询相关的参数就两个，一个是page，一个是pageSize。\n原始方式 代码实现\n通过查看接口文档：员工列表查询\n请求路径：/emps\n请求方式：GET\n请求参数：跟随在请求路径后的参数字符串。 例：/emps?page=1\u0026amp;pageSize=10\n响应数据：json格式\n1). EmpController\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.itheima.controller; import com.itheima.pojo.Emp; import com.itheima.pojo.PageResult; import com.itheima.pojo.Result; import com.itheima.service.EmpService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 员工管理 */ @Slf4j @RequestMapping(\u0026#34;/emps\u0026#34;) @RestController public class EmpController { @Autowired private EmpService empService; @GetMapping public Result page(@RequestParam(defaultValue = \u0026#34;1\u0026#34;) Integer page , @RequestParam(defaultValue = \u0026#34;10\u0026#34;) Integer pageSize){ log.info(\u0026#34;查询员工信息, page={}, pageSize={}\u0026#34;, page, pageSize); PageResult\u0026lt;Emp\u0026gt; pageResult = empService.page(page, pageSize); return Result.success(pageResult); } } @RequestParam(defaultValue=\u0026ldquo;默认值\u0026rdquo;) //设置请求参数默认值\n2). EmpService\n1 2 3 4 5 6 7 8 9 10 11 12 13 package com.itheima.service; import com.itheima.pojo.Emp; import com.itheima.pojo.PageResult; public interface EmpService { /* * 分页查询 * @param page 页码 * @param pageSize 每页记录数 * */ PageResult\u0026lt;Emp\u0026gt; page(Integer page, Integer pageSize); } 3). EmpServiceImpl\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.itheima.service.impl; import com.itheima.mapper.EmpMapper; import com.itheima.pojo.Emp; import com.itheima.pojo.PageResult; import com.itheima.service.EmpService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * 员工管理 */ @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override public PageResult page(Integer page, Integer pageSize) { //1. 获取总记录数 Long total = empMapper.count(); //2. 获取结果列表 Integer start = (page - 1) * pageSize; List\u0026lt;Emp\u0026gt; rows = empMapper.list(start, pageSize); //3. 封装结果 return new PageResult\u0026lt;Emp\u0026gt;(total,rows); } } 4). EmpMapper\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.itheima.mapper; import com.itheima.pojo.Emp; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; @Mapper public interface EmpMapper { /* * 查询总记录数 * */ @Select(\u0026#34;select count(*) from emp e left join dept d on e.dept_id = d.id\u0026#34;) Long count(); /* * 分页查询 * */ @Select(\u0026#34;select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id \u0026#34; + \u0026#34;order by e.update_time desc limit #{start},#{pageSize}\u0026#34;) public List\u0026lt;Emp\u0026gt; list(Integer start, Integer pageSize); } 功能测试：\n功能开发完成后，重新启动项目，使用Apifox，发起GET请求：\n前后端联调测试：\n打开浏览器，测试后端功能接口：\n点击下面的页码，可以正常的查询出对应的数据 。\nPageHelper分页插件 PageHelper是第三方提供的Mybatis框架中的一款功能强大、方便易用的分页插件，支持任何形式的单标、多表的分页查询。\n官网：https://pagehelper.github.io/\n那接下来，我们可以对比一下，使用PageHelper分页插件进行分页 与 原始方式进行分页代码实现的上的差别。\nMapper接口层： 原始的分页查询功能中，我们需要在Mapper接口中定义两条SQL语句。 PageHelper实现分页查询之后，只需要编写一条SQL语句，而且不需要考虑分页操作，就是一条正常的查询语句。 Service层： 需要根据页码、每页展示记录数，手动的计算起始索引。 无需手动计算起始索引，直接告诉PageHelper需要查询那一页的数据，每页展示多少条记录即可。 使用步骤：引入PageHelper的依赖 定义Mapper接口的查询方法(无需考虑分页) 在Service方法中实现分页查询 代码实现：\n当使用了PageHelper分页插件进行分页，就无需再Mapper中进行手动分页了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中，调用Mapper的方法之前设置分页参数，在调用Mapper方法执行查询之后，解析分页结果，并将结果封装到PageResult对象中返回。\n1). 在pom.xml引入依赖\n1 2 3 4 5 6 \u0026lt;!--分页插件PageHelper--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github.pagehelper\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pagehelper-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2). EmpMapper\n1 2 3 4 5 /** * 查询所有的员工及其对应的部门名称 */ @Select(\u0026#34;select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id\u0026#34;) public List\u0026lt;Emp\u0026gt; list(); 3). EmpServiceImpl\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public PageResult page(Integer page, Integer pageSize) { //1. 设置分页参数 PageHelper.startPage(page,pageSize); //2. 执行查询 //此处能够将List集合强转为page原因是empList实际类型就是page //为什么实际类型是page能拿List来接收呢？ //原因是因为Page\u0026lt;E\u0026gt;是ArrayList\u0026lt;E\u0026gt;的子类，而ArrayList\u0026lt;E\u0026gt;又实现了List\u0026lt;E\u0026gt;的接口【如上面图示】 List\u0026lt;Emp\u0026gt; empList = empMapper.list(); //3. 封装结果 Page\u0026lt;Emp\u0026gt; p = (Page\u0026lt;Emp\u0026gt;) empList; return new PageResult(p.getTotal(), p.getResult()); } 功能测试\n功能开发完成后，我们重启项目工程，打开Apifox，发起GET请求，访问 ：http://localhost:8080/emps?page=1\u0026amp;pageSize=5\n我们可以看到数据可以正常查询返回，是可以正常实现分页查询的。\n实现机制\n我们打开Idea的控制台，可以看到在进行分页查询时，输出的SQL语句。\n我们看到执行了两条SQL语句，而这两条SQL语句，其实是从我们在Mapper接口中定义的SQL演变而来的。\n第一条SQL语句，用来查询总记录数。 其实就是将我们编写的SQL语句进行的改造增强，将查询返回的字段列表替换成了 count(0) 来统计总记录数。\n第二条SQL语句，用来进行分页查询，查询指定页码对应 的数据列表。\n其实就是将我们编写的SQL语句进行的改造增强，在SQL语句之后拼接上了limit进行分页查询，而由于测试时查询的是第一页，起始索引是0，所以简写为limit ？。\n而PageHelper在进行分页查询时，会执行上述两条SQL语句，并将查询到的总记录数，与数据列表封装到了 Page\u0026lt;Emp\u0026gt; 对象中，我们再获取查询结果时，只需要调用Page对象的方法就可以获取。\n注意：\nPageHelper实现分页查询时，SQL语句的结尾一定一定一定不要加分号(;).。 PageHelper只会对紧跟在其后的第一条SQL语句进行分页处理。 条件分页查询 完了分页查询后，下面我们需要在分页查询的基础上，添加条件。\n需求 通过员工管理的页面原型我们可以看到，员工列表页面的查询，不仅仅需要考虑分页，还需要考虑查询条件。 分页查询我们已经实现了，接下来，我们需要考虑在分页查询的基础上，再加上查询条件。\n我们看到页面原型及需求中描述，搜索栏的搜索条件有三个，分别是：\n姓名：模糊匹配 性别：精确匹配 入职日期：范围匹配 接口描述 查看上一小节的接口文档\n思路 三层架构职责：\n功能开发\n1). 在EmpController方法中通过多个方法形参，依次接收这几个参数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Slf4j @RequestMapping(\u0026#34;/emps\u0026#34;) @RestController public class EmpController { @Autowired private EmpService empService; @GetMapping public Result page(@RequestParam(defaultValue = \u0026#34;1\u0026#34;) Integer page , @RequestParam(defaultValue = \u0026#34;10\u0026#34;) Integer pageSize, String name, Integer gender, @DateTimeFormat(pattern = \u0026#34;yyyy-MM-dd\u0026#34;) LocalDate begin, @DateTimeFormat(pattern = \u0026#34;yyyy-MM-dd\u0026#34;) LocalDate end){ log.info(\u0026#34;分页查询：{}, {}, {}, {}, {}, {}\u0026#34;, page, pageSize, name, gender, begin, end); PageResult\u0026lt;Emp\u0026gt; pageResult = empService.page(page, pageSize, name, gender, begin, end); return Result.success(pageResult); } } 日期时间类型参数接收时，需要通过 @DateTimeFormat 注解指定前端传递的日期格式\n2). 修改EmpService及EmpServiceImpl中的代码逻辑\nEmpService：\n1 2 3 4 5 6 7 8 public interface EmpService { /* * 分页查询 * @param page 页码 * @param pageSize 每页记录数 * */ PageResult\u0026lt;Emp\u0026gt; page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin,LocalDate end); } EmpServiceImpl:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Service public class EmpServiceImpl implements EmpService { /* * PageHelper分页查询 * @param page 页码 * @param pageSize 每页记录数 * @return * */ @Autowired private EmpMapper empMapper; @Override public PageResult\u0026lt;Emp\u0026gt; page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end){ //1.设置分页参数 PageHelper.startPage(page,pageSize); //2.调佣Mapper接口方法 List\u0026lt;Emp\u0026gt; empList = empMapper.list(name, gender, begin, end); //3.解析并封装结果 Page\u0026lt;Emp\u0026gt; p = (Page\u0026lt;Emp\u0026gt;) empList; return new PageResult\u0026lt;Emp\u0026gt;(p.getTotal(),p.getResult()); } } 3). 调整EmpMapper接口方法\n1 2 3 4 5 6 7 8 9 @Mapper public interface EmpMapper { /** * 查询所有的员工及其对应的部门名称 */ public List\u0026lt;Emp\u0026gt; list(String name, Integer gender, LocalDate begin, LocalDate end); } 由于SQL语句比较复杂，建议将SQL语句配置在XML映射文件中。\n4). 新增Mapper映射文件EmpMapper.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;!--定义Mapper映射文件的约束和基本结构--\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.EmpMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;list\u0026#34; resultType=\u0026#34;com.itheima.pojo.Emp\u0026#34;\u0026gt; select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id //注意不能像下面注释这行这样书写，否则会变成\u0026#39;%?%\u0026#39;，就会报错 //where e.name like (\u0026#39;%#{name},%\u0026#39;) //此时就可以用到concat函数进行字符串的拼接 where e.name like concat(\u0026#39;%\u0026#39;,#{name},\u0026#39;%\u0026#39;) and e.gender = #{gender} and e.entry_date between #{begin} and #{end} \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 功能测试：\n程序优化1 在上述分页条件查询中，请求参数比较多，有6个，如下所示：\n请求参数：/emps?name=张\u0026amp;gender=1\u0026amp;begin=2007-09-01\u0026amp;end=2022-09-01\u0026amp;page=1\u0026amp;pageSize=10 那我们在controller层方法中，接收请求参数的时候，直接在controller方法中声明这样6个参数即可，这样做，功能可以实现，但是不方便维护和管理。\n如果controller方法的参数较多，且未来可能继续增加，这会使得方法签名变得复杂难以维护，此时可以考虑将多个请求参数封装为一个对象。 请求参数：/emps?name=张\u0026amp;gender=1\u0026amp;begin=2007-09-01\u0026amp;end=2022-09-01\u0026amp;page=1\u0026amp;pageSize=10 优化思路：定义一个实体类，来封装这几个请求参数。 【需要保证，前端传递的请求参数和实体类的属性名是一样的】\n1). 定义实体类：EmpQueryParam\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.itheima.pojo; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; @Data public class EmpQueryParam { private Integer page = 1; //页码 private Integer pageSize = 10; //每页展示记录数 private String name; //姓名 private Integer gender; //性别 @DateTimeFormat(pattern = \u0026#34;yyyy-MM-dd\u0026#34;) private LocalDate begin; //入职开始时间 @DateTimeFormat(pattern = \u0026#34;yyyy-MM-dd\u0026#34;) private LocalDate end; //入职结束时间 } 2). EmpController接收请求参数\n1 2 3 4 5 6 @GetMapping public Result page(EmpQueryParam empQueryParam) { log.info(\u0026#34;查询请求参数： {}\u0026#34;, empQueryParam); PageResult pageResult = empService.page(empQueryParam); return Result.success(pageResult); } 3). 修改EmpService接口方法\n1 2 3 4 5 6 7 public interface EmpService { /** * 分页查询 */ //PageResult page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end); PageResult page(EmpQueryParam empQueryParam); } 4). 修改EmpServiceImpl中的page方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; /*@Override public PageResult page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end) { //1. 设置PageHelper分页参数 PageHelper.startPage(page, pageSize); //2. 执行查询 List\u0026lt;Emp\u0026gt; empList = empMapper.list(name, gender, begin, end); //3. 封装分页结果 Page\u0026lt;Emp\u0026gt; p = (Page\u0026lt;Emp\u0026gt;) empList; return new PageResult(p.getTotal(), p.getResult()); }*/ public PageResult page(EmpQueryParam empQueryParam) { //1. 设置PageHelper分页参数 PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize()); //2. 执行查询 List\u0026lt;Emp\u0026gt; empList = empMapper.list(empQueryParam); //3. 封装分页结果 Page\u0026lt;Emp\u0026gt; p = (Page\u0026lt;Emp\u0026gt;)empList; return new PageResult(p.getTotal(), p.getResult()); } } 5). 修改EmpMapper接口方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Mapper public interface EmpMapper { /** * 查询所有的员工及其对应的部门名称 */ // @Select(\u0026#34;select e.*, d.name as deptName from emp e left join dept d on e.dept_id = d.id\u0026#34;) // public List\u0026lt;Emp\u0026gt; list(String name, Integer gender, LocalDate begin, LocalDate end); /** * 根据查询条件查询员工 */ List\u0026lt;Emp\u0026gt; list(EmpQueryParam empQueryParam); } EmpMapper.xml 中的配置无需修改。\n代码优化完毕之后，重新启动运行测试，依然正常运行：\n此时我们如果想要单独根据某一条件进行查询，就会发现条件被写死了\n这时就需要用到动态SQL\n程序优化2 动态SQL 随着用户的输入或外部条件的变化而变化的SQL语句，我们称为动态SQL。\n\u0026lt; if \u0026gt;：判断条件是否成立，如果条件为true，则拼接SQL。\n\u0026lt; where \u0026gt;：根据查询条件，来生成where关键字，并会自动去除条件前面多余的and或or。\n​\n如果只输入 姓名 这个查询条件，则SQL语句中只根据name字段查询，SQL如下：\n1 select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id where e.name like concat(\u0026#39;%\u0026#39;,#{name},\u0026#39;%\u0026#39;)； 如果只输入 性别 这个查询条件，则SQL语句中只根据gender字段查询，SQL如下： 1 select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id where e.gender = #{gender}； 如果输入 姓名 和 性别 这两个查询条件，则SQL语句中要根据name、gender两个字段查询，SQL如下： 1 2 select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id where e.name like concat(\u0026#39;%\u0026#39;,#{name},\u0026#39;%\u0026#39;) and e.gender = #{gender}； 具体的代码实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;!--定义Mapper映射文件的约束和基本结构--\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.EmpMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;list\u0026#34; resultType=\u0026#34;com.itheima.pojo.Emp\u0026#34;\u0026gt; select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id where \u0026lt;if test=\u0026#34;name != null and name != \u0026#39;\u0026#39;\u0026#34;\u0026gt; e.name like concat(\u0026#39;%\u0026#39;,#{name},\u0026#39;%\u0026#39;) \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;gender != null\u0026#34;\u0026gt; and e.gender = #{gender} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;begin != null and end != null\u0026#34;\u0026gt; and e.entry_date between #{begin} and #{end} \u0026lt;/if\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 此时取消勾选Apifox中的某些参数会发现依旧能查询成功，并且在idea控制台中也会显示根据哪几个条件进行模糊匹配\n但是此时如果将name删除，就会报出500的错误\n所以就需要用到上面介绍的where标签\n完整代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;!--定义Mapper映射文件的约束和基本结构--\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.EmpMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;list\u0026#34; resultType=\u0026#34;com.itheima.pojo.Emp\u0026#34;\u0026gt; select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id \u0026lt;where\u0026gt; \u0026lt;if test=\u0026#34;name != null and name != \u0026#39;\u0026#39;\u0026#34;\u0026gt; e.name like concat(\u0026#39;%\u0026#39;,#{name},\u0026#39;%\u0026#39;) \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;gender != null\u0026#34;\u0026gt; and e.gender = #{gender} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;begin != null and end != null\u0026#34;\u0026gt; and e.entry_date between #{begin} and #{end} \u0026lt;/if\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; ","date":"2026-04-19T22:07:05+08:00","permalink":"https://aurora156.github.io/p/web%E5%90%8E%E7%AB%AF%E5%AE%9E%E6%88%982/","title":"Web后端实战（2）"},{"content":"Tlias智能学习辅助系统 需求：\n部门管理：\n查询、新增、修改、删除 员工管理：\n查询、新增、修改、删除 文件上传 报表统计\n登录认证\n日志管理\n班级、学员管理(实战内容)\n准备工作 开发规范 - 开发模式 前后端混合开发\n前后端分离开发\n当前最为主流的开发模式：前后端分离 此处的接口为功能接口，接口是依据原型+ 需求进行设计的\n该项目的接口文档：接口文档 - 飞书云文档\n基本的开发流程为：\n开发规范 - Restful风格 REST（REpresentational State Transfer），表述性状态转换，它是一种软件架构风格。 传统风格：不规范、难维护\nREST风格：URL定位资源，HTTP动词描述操作\n注意：\n1.REST是风格，是约定方式，约定不是规定，可以打破\n2.描述功能模块通常使用复数形式(加s)，表示此类资源，而非单个资源。如：users、books\u0026hellip;\n思考：\n前后端都在并行开发，后端开发完对应的接口之后，如何对接口进行请求测试呢？\n浏览器中发送的请求都是GET方式的请求，无法发送其他三个请求\n前后端都在并行开发，前端开发过程中，如何获取到数据，测试页面的渲染展示呢？\n此时需要借助两个工具(选其一即可)：\n此处使用Apifox工具：\n介绍：Apifox是一款集成了Api文档、Api调试、Api Mock、Api测试的一体化协作平台\n作用：接口文档管理、接口请求测试、Mock服务\nMock服务：（Apifox为前端提供的模拟的URL地址，访问这个地址，就会给你返回对应的数据，以便前端测试）\n官网： https://apifox.com/\n安装完成之后，就可以扫码登录Apifox，就可以使用了\n此处是打开了之前写的Springboot的入门程序的代码进行测试的\n在输入URL地址后，就可以看到接受的数据了\n工程搭建 创建SpringBoot工程，并引入web开发起步依赖、mybatis、mysql驱动、lombok。 创建数据库表dept，并在application.yml中配置数据库的基本信息。 准备基础代码结构，并引入实体类Dept及统一的响应结果封装类 Result。 创建项目： 创建tlias数据库，并准备dept部门表。 1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE dept ( id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT \u0026#39;ID, 主键\u0026#39;, name varchar(10) NOT NULL UNIQUE COMMENT \u0026#39;部门名称\u0026#39;, create_time datetime DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, update_time datetime DEFAULT NULL COMMENT \u0026#39;修改时间\u0026#39; ) COMMENT \u0026#39;部门表\u0026#39;; INSERT INTO dept VALUES (1,\u0026#39;学工部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;), (2,\u0026#39;教研部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-09-09 15:17:04\u0026#39;), (3,\u0026#39;咨询部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-09-30 21:26:24\u0026#39;), (4,\u0026#39;就业部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;), (5,\u0026#39;人事部\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;,\u0026#39;2024-09-25 09:47:40\u0026#39;), (6,\u0026#39;行政部\u0026#39;,\u0026#39;2024-11-30 20:56:37\u0026#39;,\u0026#39;2024-09-30 20:56:37\u0026#39;); 在 application.yml 配置文件中配置数据库的连接信息。 1 2 3 4 5 6 7 8 9 10 11 12 application: name: tlias-web-management # 配置数据库连接信息 datasource: url: jdbc:mysql://localhost:3306/tlias username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: #配置Mybatis的日志输出 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 准备基础包结构 实体类Dept 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.itheima.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor public class Dept { private Integer id; private String name; private LocalDateTime createTime; private LocalDateTime updateTime; } 统一响应结果Result 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.itheima.pojo; import lombok.Data; import java.io.Serializable; /** * 后端统一返回结果 */ @Data public class Result { private Integer code; //编码：1成功，0为失败 private String msg; //错误信息 private Object data; //数据 public static Result success() { Result result = new Result(); result.code = 1; result.msg = \u0026#34;success\u0026#34;; return result; } public static Result success(Object object) { Result result = new Result(); result.data = object; result.code = 1; result.msg = \u0026#34;success\u0026#34;; return result; } public static Result error(String msg) { Result result = new Result(); result.msg = msg; result.code = 0; return result; } } 基础代码结构 DeptMapper 1 2 3 4 5 6 7 package com.itheima.mapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface DeptMapper { } DeptService 1 2 3 4 package com.itheima.service; public interface DeptService { } DeptServiceImpl 1 2 3 4 package com.itheima.service; public interface DeptService { } DeptServiceImpl 1 2 3 4 5 6 7 8 package com.itheima.service.impl; import com.itheima.service.DeptService; import org.springframework.stereotype.Service; @Service public class DeptServiceImpl implements DeptService { } DeptController 1 2 3 4 5 6 7 8 9 10 package com.itheima.controller; import org.springframework.web.bind.annotation.RestController; /** * 部门管理控制器 */ @RestController public class DeptController { } 查询部门 基本实现 需求 查询所有的部门数据，查询出来展示在部门管理的页面中。页面原型效果如下：\n接口描述 参照课程资料中提供的接口文档。 部门管理 -\u0026gt; 部门列表查询\n地址：接口文档 - 飞书云文档\n思路分析 明确了查询部门的需求之后，再来梳理一下实现该功能时，三层架构每一次的职责：\nController层，负责接收前端发起的请求，并调用service查询部门数据，然后响应结果。 Service层，负责调用Mapper接口方法，查询所有部门数据。 Mapper层，执行查询所有部门数据的操作。 代码实现 实现顺序：\n1). Controller层\n在 DeptController 中，增加 list 方法，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /** * 部门管理控制器 */ @RestController public class DeptController { //依赖注入，Spring 容器会自动将 DeptService 的实现类注入到这里，实现业务逻辑层的调用。 @Autowired private DeptService deptService; /** * 查询部门列表 */ // 映射 HTTP 请求路径 // 这里表示该方法处理访问路径为 /depts 的请求（默认支持所有 HTTP 方法，如 GET, POST 等）。 @RequestMapping(\u0026#34;/depts\u0026#34;) public Result list(){ // 调用业务逻辑层（Service）的 findall 方法，获取部门列表数据 List\u0026lt;Dept\u0026gt; deptList = deptService.findAll(); // 将查询到的 deptList 封装在 Result 对象中返回，通常包含状态码、消息和数据。 return Result.success(deptList); } } 快捷操作：当写完deptService.findAll();报红时，将光标放在此处，按alt + enter即可自动跳转至相应代码文件中\n2). Service层\n在 DeptService 中，增加 findAll方法，代码如下：\n1 2 3 4 5 6 public interface DeptService { /** * 查询所有部门 */ public List\u0026lt;Dept\u0026gt; findAll(); } 在 DeptServiceImpl 中，增加 findAll方法，代码如下：\n1 2 3 4 5 6 7 8 9 10 @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; public List\u0026lt;Dept\u0026gt; findAll() { return deptMapper.findAll(); } } 3). Mapper层\n在 DeptMapper 中，增加 findAll方法，代码如下：\n1 2 3 4 5 6 7 8 9 @Mapper public interface DeptMapper { /** * 查询所有部门 */ @Select(\u0026#34;select * from dept\u0026#34;) public List\u0026lt;Dept\u0026gt; findAll(); } 接口测试 启动项目，然后我们就可以打开Apifox进行测试了。\n我们发现，已经查询出了所有的部门数据，并且响应回来的就是json格式的数据，与接口文档一致。 那接下来，我们再来测试一下，这个查询操作，我们使用post、put、delete方式来请求，是否可以获取到数据。\n经过测试，我们发现，现在我们其实是可以通过任何方式的请求来访问查询部门的这个接口的。 而在接口文档中，明确要求该接口的请求方式为GET，那么如何限制请求方式呢？\n方式一：在controller方法的@RequestMapping注解中通过method属性来限定。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController public class DeptController { @Autowired private DeptService deptService; /** * 查询部门列表 */ @RequestMapping(value = \u0026#34;/depts\u0026#34;, method = RequestMethod.GET) public Result list(){ List\u0026lt;Dept\u0026gt; deptList = deptService.findAll(); return Result.success(deptList); } } 此时在去查看Apifox就会发现请求失败了，错误信息为“请求不被允许”\n方式二：在controller方法上使用，@RequestMapping的衍生注解 @GetMapping。 该注解就是标识当前方法，必须以GET方式请求。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController public class DeptController { @Autowired private DeptService deptService; /** * 查询部门列表 */ @GetMapping(\u0026#34;/depts\u0026#34;) public Result list(){ List\u0026lt;Dept\u0026gt; deptList = deptService.findAll(); return Result.success(deptList); } } 上述两种方式，在项目开发中，推荐使用第二种方式，简洁、优雅。\nGET方式：@GetMapping POST方式：@PostMapping PUT方式：@PutMapping DELETE方式：@DeleteMapping 小结 功能实现：\n数据封装 在上述测试中，我们发现部门的数据中，id、name两个属性是有值的，但是createTime、updateTime两个字段值并未成功封装(前端页面均返回null)，而数据库中是有对应的字段值的，这是为什么呢？\n原因如下：\n实体类属性名和数据库表查询返回的字段名一致，mybatis会自动封装。 如果实体类属性名和数据库表查询返回的字段名不一致，不能自动封装。 解决方案：\n手动结果映射：通过@Results及@Result进行手动结果映射 起别名：在SQL语句中，对不一样的列名起别名，别名和实体类属性名一样 开启驼峰命名：如果字段名与属性名符合驼峰命名规则，Mybatis会自动通过驼峰命名规则映射 手动结果映射： 起别名 结果同上\n开启驼峰命名(要求：xxx_abc -\u0026gt; xxxAbc) 结果同上\n前后端联调测试 联调测试 完成了查询部门的功能，我们也通过 Apifox 工具测试通过了，下面我们再基于前后端分离的方式进行接口联调。具体操作如下：\n下载nginx\n启动nginx，访问测试：http://localhost:90\n双击 nginx.exe 启动Nginx，一闪而过，就说明nginx已启动完成。\n如果在任务管理器中，能看到上述两个进程，就说明nginx已经启动成功。\n请求访问流程 前端工程请求服务器的地址为 http://localhost:90/api/depts，是如何访问到后端的tomcat服务器的？\n其实这里，是通过前端服务Nginx中提供的反向代理功能实现的。\n1). 浏览器发起请求，请求的是localhost:90 ，那其实请求的是nginx服务器。\n2). 在nginx服务器中呢，并没有对请求直接进行处理，而是将请求转发给了后端的tomcat服务器，最终由tomcat服务器来处理该请求。\n这个过程就是通过nginx的反向代理实现的。 那为什么浏览器不直接请求后端的tomcat服务器，而是直接请求nginx服务器呢，主要有以下几点原因：\n1). 安全：由于后端的tomcat服务器一般都会搭建集群，会有很多的服务器，把所有的tomcat暴露给前端，让前端直接请求tomcat，对于后端服务器是比较危险的。\n2). 灵活：基于nginx的反向代理实现，更加灵活，后端想增加、减少服务器，对于前端来说是无感知的，只需要在nginx中配置即可。\n3). 负载均衡：基于nginx的反向代理，可以很方便的实现后端tomcat的负载均衡操作。\n反向代理是一种网络架构，通过代理服务器为后端的服务器做代理，客户端的请求直接请求代理服务器，然后转发给后端的服务器。\n具体的访问流程如下：\n**location：**用于定义匹配特定uri请求的规则。 **^~ /api/：**表示精确匹配，即只匹配以/api/开头的路径。 补充：^ $：正则的开始与结束；(.*)：.代表单个任意字符，*代表出现的次数为任意次数 **rewrite：**该指令用于重写匹配到的uri路径。 说明：此处的意思就是将路径/api/depts重写为/depts **proxy_pass：**该指令用于代理转发，它将匹配到的请求转发给位于后端的指令服务器。 我们可以打开Nginx的配置文件看看是不是这样的\n选择图中文件打开\n删除部门 需求 查询所有的部门数据，查询出来展示在部门管理的页面中。页面原型效果如下：\n接口文档：删除部门\n思路分析 明确了删除部门的需求之后，再来梳理一下实现该功能时，三层架构每一次的职责： 方式一：通过原始的 HttpServletRequest 对象获取请求参数 1 2 3 4 5 6 7 8 9 10 11 /** * 根据ID删除部门 - 简单参数接收: 方式一 (HttpServletRequest) */ @DeleteMapping(\u0026#34;/depts\u0026#34;) public Result delete(HttpServletRequest request){ String idStr = request.getParameter(\u0026#34;id\u0026#34;); int id = Integer.parseInt(idStr); System.out.println(\u0026#34;根据ID删除部门: \u0026#34; + id); return Result.success(); } 由于Date是给前端返回的数据，但是删除不需要给前端响应数据，所以返回值置空就可以\n这种方案实现较为繁琐，而且还需要进行手动类型转换。【项目开发很少用】\n方案二：通过Spring提供的 @RequestParam 注解，将请求参数绑定给方法形参 1 2 3 4 5 @DeleteMapping(\u0026#34;/depts\u0026#34;) public Result delete(@RequestParam(\u0026#34;id\u0026#34;) Integer deptId){ System.out.println(\u0026#34;根据ID删除部门: \u0026#34; + deptId); return Result.success(); } 结果同上\n@RequestParam 注解的value属性，需要与前端传递的参数名保持一致 。\n@RequestParam注解required属性默认为true，代表该参数必须传递，如果不传递将报错。 如果参数可选，可以将属性设置为false。\n1 2 3 4 5 @DeleteMapping(\u0026#34;/depts\u0026#34;) public Result delete(@RequestParam(value = \u0026#34;id\u0026#34;,required = false) Integer deptId){ System.out.println(\u0026#34;根据ID删除部门: \u0026#34; + deptId); return Result.success(); } 此时就算继续传递参数也不会报错\n方案三：如果请求参数名与形参变量名相同，直接定义方法形参即可接收。（省略@RequestParam） 【推荐】 1 2 3 4 5 @DeleteMapping(\u0026#34;/depts\u0026#34;)java public Result delete(Integer id){ System.out.println(\u0026#34;根据ID删除部门: \u0026#34; + id); return Result.success(); } 结果同上\n代码实现 1). Controller层\n在 DeptMapper 中，增加 delete 方法，代码实现如下：\n1 2 3 4 5 6 7 8 9 /** * 根据id删除部门 - delete http://localhost:8080/depts?id=1 */ @DeleteMapping(\u0026#34;/depts\u0026#34;) public Result delete(Integer id){ System.out.println(\u0026#34;根据id删除部门, id=\u0026#34; + id); deptService.deleteById(id); return Result.success(); } 2). Service层\n在 DeptService 中，增加 deleteById 方法，代码实现如下：\n1 2 3 4 /** * 根据id删除部门 */ void deleteById(Integer id); 3). Mapper层\n在 DeptMapper 中，增加 deleteById 方法，代码实现如下：\n1 2 3 4 5 /** * 根据id删除部门 */ @Delete(\u0026#34;delete from dept where id = #{id}\u0026#34;) void deleteById(Integer id); 如果mapper接口方法形参只有一个普通类型的参数，#{…} 里面的属性名可以随便写，如：#{id}、#{value}。\n对于 DML 语句来说，执行完毕，也是有返回值的，返回值代表的是增删改操作，影响的记录数，所以可以将执行 DML 语句的方法返回值设置为 Integer。 但是一般开发时，是不需要这个返回值的，所以也可以设置为void。\n代码编写完毕之后，我们就可以启动服务，进行测试了。\n初始数据库：\n用上面的方法在Apifox中删除id=6的数据(此处就不放页面展示了)\n更新后数据库：\n前后端联调测试 此时可以发现已经删除成功了\n现在测试前端删除：\n数据库中删除成功\n测试成功\n新增部门 需求 点击 \u0026ldquo;新增部门\u0026rdquo; 的按钮之后，弹出新增部门表单，填写部门名称之后，点击确定之后，保存部门数据。\n接口文档：添加部门\n思路分析 明确了删除部门的需求之后，再来梳理一下实现该功能时，三层架构每一次的职责：\njson参数接收 我们看到，在controller中，需要接收前端传递的请求参数。 那接下来，我们就先来看看在服务器端的Controller程序中，如何获取json格式的参数。\nJSON格式的参数，通常会使用一个实体对象进行接收 。 规则：JSON数据的键名与方法形参对象的属性名相同，并需要使用@RequestBody注解标识。 前端传递的请求参数格式为json，内容如下：{\u0026quot;name\u0026quot;:\u0026quot;研发部\u0026quot;}。这里，我们可以通过一个对象(dept)来接收，只需要保证对象中有name属性即可。\n代码实现 1). Controller层\n在DeptController中增加方法save，具体代码如下：\n1 2 3 4 5 6 7 8 9 /* * 新增部门 * */ @PostMapping(\u0026#34;/depts\u0026#34;) public Result add(@RequestBody Dept dept){ System.out.println(\u0026#34;添加部门：\u0026#34; + dept); deptService.addDept(dept); return Result.success(); } 2). Service层\n在DeptService中增加接口方法save，具体代码如下：\n1 2 3 4 /** * 新增部门 */ void addDept(Dept dept); 在DeptServiceImpl中增加save方法，完成添加部门的操作，具体代码如下：\n1 2 3 4 5 6 7 8 public void addDept(Dept dept) { //补全基础属性 - createTime,updateTime dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); //调用Mapper接口方法插入数据 deptMapper.insert(dept); } 3). Mapper层\n1 2 3 4 5 6 /** * 新增部门 */ //注意：此处的values的后两个写的是属性名(驼峰式命名)，而不是字段名(下划线) @Insert(\u0026#34;insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})\u0026#34;) void insert(Dept dept); 此处序号为9是因为前面代码有误，删除了前面的序号后重新添加的\n前后端联调测试 此时刷新前端页面发现校验部添加成功\n再测试前端添加\n由于我们查询是倒序查询，所以新增的部门排在最上面\n发现数据库中添加成功\n修改部门 对于任何业务的修改功能来说，一般都会分为两步进行：查询回显、修改数据。\n查询回显 需求 当我们点击 \u0026ldquo;编辑\u0026rdquo; 的时候，需要根据ID查询部门数据，然后用于页面回显展示。\n接口描述：根据id查询部门\n了解了需求之后，我们再看看接口文档中，关于根据ID查询部门的接口的描述，然后根据接口文档进行服务端接口的开发 。\n/depts/1，/depts/2 这种在url中传递的参数，我们称之为路径参数。 那么如何接收这样的路径参数呢 ？\n路径参数：通过请求URL直接传递参数，使用{…}来标识该路径参数，需要使用 @PathVariable获取路径参数。如下所示：\n如果路径参数名与controller方法形参名称一致，@PathVariable注解的value属性是可以省略的。\n代码实现 1). Controller层\n在 DeptController 中增加 getById方法，具体代码如下：\n1 2 3 4 5 6 7 8 9 /** * 根据ID查询 - GET http://localhost:8080/depts/1 */ @GetMapping(\u0026#34;/depts/{id}\u0026#34;) public Result getById(@PathVariable Integer id){ System.out.println(\u0026#34;根据ID查询, id=\u0026#34; + id); Dept dept = deptService.getById(id); return Result.success(dept); } 2). Service层\n在 DeptService 中增加 getById方法，具体代码如下：\n1 2 3 4 /** * 根据id查询部门 */ Dept getById(Integer id); 在 DeptServiceImpl 中增加 getById方法，具体代码如下：\n1 2 3 public Dept getById(Integer id) { return deptMapper.getById(id); } 3). Mapper层\n在 DeptMapper 中增加 getById 方法，具体代码如下：\n1 2 3 4 5 /** * 根据ID查询部门数据 */ @Select(\u0026#34;select id, name, create_time, update_time from dept where id = #{id}\u0026#34;) Dept getById(Integer id); 代码编写完毕之后，我们就可以启动服务，进行测试了。\n前后端分离测试 点击修改，发现部门已经回显回来了，说明测试成功\n小结 修改数据 需求 查询回显回来之后，就可以对部门的信息进行修改了，修改完毕之后，点击确定，此时，就需要根据ID修改部门的数据。\n接口文档：修改部门\n思路分析 明确了删除部门的需求之后，再来梳理一下实现该功能时，三层架构每一次的职责： 通过接口文档，我们可以看到前端传递的请求参数是json格式的请求参数，在Controller的方法中，我们可以通过 @RequestBody 注解来接收，并将其封装到一个对象中。\n代码实现 1). Controller层\n在 DeptController 中增加 update 方法，具体代码如下：\n1 2 3 4 5 6 7 8 9 /** * 修改部门 - PUT http://localhost:8080/depts 请求参数：{\u0026#34;id\u0026#34;:1,\u0026#34;name\u0026#34;:\u0026#34;研发部\u0026#34;} */ @PutMapping(\u0026#34;/depts\u0026#34;) public Result update(@RequestBody Dept dept){ System.out.println(\u0026#34;修改部门, dept=\u0026#34; + dept); deptService.update(dept); return Result.success(); } 2). Service层\n在 DeptService 中增加 update 方法。\n1 2 3 4 /** * 修改部门 */ void update(Dept dept); 在 DeptServiceImpl 中增加 update 方法。 由于是修改操作，每一次修改数据，都需要更新updateTime。所以，具体代码如下：\n1 2 3 4 5 6 public void update(Dept dept) { //补全基础属性 - updateTime dept.setUpdateTime(LocalDateTime.now()); //调用Mapper接口方法更新部门 deptMapper.update(dept); } 3). Mapper层\n在 DeptMapper 中增加 update 方法，具体代码如下：\n1 2 3 4 5 /** * 更新部门 */ @Update(\u0026#34;update dept set name = #{name},update_time = #{updateTime} where id = #{id}\u0026#34;) void update(Dept dept); 代码编写完毕之后，我们就可以启动服务，进行测试了。\n前后端分离测试 前端修改名字\n数据库更新成功\n@RequestMapping 到此呢，关于基本的部门的增删改查功能，我们已经实现了。 我们会发现，我们在 DeptController 中所定义的方法，所有的请求路径，都是 /depts 开头的，只要操作的是部门数据，请求路径都是 /depts 开头。\n那么这个时候，我们其实是可以把这个公共的路径 /depts 抽取到类上的，那在各个方法上，就可以省略了这个 /depts 路径。 代码如下：\n一个完整的请求路径，应该是类上的 @RequestMapping 的value属性 + 方法上的 @RequestMapping的value属性。\n日志技术 思考 程序中的日志，是用来记录应用程序的运行信息、状态信息、错误信息等\n为什么要在程序中记录日志呢？\n便于追踪应用程序中的数据信息、程序的执行过程。 便于对应用程序的性能进行优化。 便于应用程序出现问题之后，排查问题，解决问题。 便于监控系统的运行状态。 之前我们编写程序时，也可以通过 System.out.println(...) 来输出日志，为什么我们还要学习单独的日志技术呢？\n​ 这是因为，如果通过 System.out.println(...) 来记录日志，会存在以下几点问题：\n硬编码。所有的记录日志的代码，都是硬编码，没有办法做到灵活控制，要想不输出这个日志了，只能删\t除掉记录日志的代码。 只能输出日志到控制台。 不便于程序的扩展、维护。 所以，在现在的项目开发中，我们一般都会使用专业的日志框架，来解决这些问题。\n日志框架 JUL：这是JavaSE平台提供的官方日志框架，也被称为JUL。配置相对简单，但不够灵活，性能较差。 Log4j：一个流行的日志框架，提供了灵活的配置选项，支持多种输出目标。 Logback：基于Log4j升级而来，提供了更多的功能和配置选项，性能由于Log4j。(推荐) Slf4j：（Simple Logging Facade for Java）简单日志门面，提供了一套日志操作的标准接口及抽象类，允许应用程序使用不同的底层日志框架。 LogBack入门 准备工作：引入logback的依赖**(springboot项目中该依赖已传递)**、配置文件logback.xml。 记录日志：定义日志记录对象Logger，记录日志。 1).springboot中无需引入，在springboot中已经传递了此依赖\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.11\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2). 引入配置文件 logback.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 控制台输出 --\u0026gt; \u0026lt;appender name=\u0026#34;STDOUT\u0026#34; class=\u0026#34;ch.qos.logback.core.ConsoleAppender\u0026#34;\u0026gt; \u0026lt;encoder class=\u0026#34;ch.qos.logback.classic.encoder.PatternLayoutEncoder\u0026#34;\u0026gt; \u0026lt;!--格式化输出：%d表示日期，%thread表示线程名，%-5level：级别从左显示5个字符宽度 %msg：日志消息，%n是换行符 --\u0026gt; \u0026lt;pattern\u0026gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n\u0026lt;/pattern\u0026gt; \u0026lt;/encoder\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;!-- 日志输出级别 --\u0026gt; \u0026lt;root level=\u0026#34;ALL\u0026#34;\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34; /\u0026gt; \u0026lt;/root\u0026gt; \u0026lt;/configuration\u0026gt; 3). 记录日志：定义日志记录对象Logger，记录日志\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class LogTest { //定义日志记录对象 private static final Logger log = LoggerFactory.getLogger(LogTest.class); @Test public void testLog(){ log.debug(\u0026#34;开始计算...\u0026#34;); int sum = 0; int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23}; for (int i = 0; i \u0026lt; nums.length; i++) { sum += nums[i]; } log.info(\u0026#34;计算结果为: \u0026#34;+sum); log.debug(\u0026#34;结束计算...\u0026#34;); } } 运行单元测试，可以在控制台中看到输出的日志，如下所示：\nLogback配置文件详解 配置文件名：logback.xml 该配置文件是对Logback日志框架输出的日志进行控制的，可以来配置输出的格式、位置及日志开关等。 常用的两种输出日志的位置：控制台、系统文件 开启日志（ALL），关闭日志（OFF） 1 2 3 4 \u0026lt;!-- 日志输出级别 --\u0026gt; \u0026lt;root level=\u0026#34;off\u0026#34;\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34; /\u0026gt; \u0026lt;/root\u0026gt; 完整版日志文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 控制台输出 --\u0026gt; \u0026lt;appender name=\u0026#34;STDOUT\u0026#34; class=\u0026#34;ch.qos.logback.core.ConsoleAppender\u0026#34;\u0026gt; \u0026lt;encoder class=\u0026#34;ch.qos.logback.classic.encoder.PatternLayoutEncoder\u0026#34;\u0026gt; \u0026lt;!--格式化输出：%d 表示日期，%thread 表示线程名，%-5level表示级别从左显示5个字符宽度，%logger显示日志记录器的名称， %msg表示日志消息，%n表示换行符 --\u0026gt; \u0026lt;pattern\u0026gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n\u0026lt;/pattern\u0026gt; \u0026lt;/encoder\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;!-- 系统文件输出 --\u0026gt; \u0026lt;appender name=\u0026#34;FILE\u0026#34; class=\u0026#34;ch.qos.logback.core.rolling.RollingFileAppender\u0026#34;\u0026gt; \u0026lt;rollingPolicy class=\u0026#34;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy\u0026#34;\u0026gt; \u0026lt;!-- 日志文件输出的文件名, %i表示序号 --\u0026gt; \u0026lt;FileNamePattern\u0026gt;D:/tlias-%d{yyyy-MM-dd}-%i.log\u0026lt;/FileNamePattern\u0026gt; \u0026lt;!-- 最多保留的历史日志文件数量 --\u0026gt; \u0026lt;MaxHistory\u0026gt;30\u0026lt;/MaxHistory\u0026gt; \u0026lt;!-- 最大文件大小，超过这个大小会触发滚动到新文件，默认为 10MB --\u0026gt; \u0026lt;maxFileSize\u0026gt;10MB\u0026lt;/maxFileSize\u0026gt; \u0026lt;/rollingPolicy\u0026gt; \u0026lt;encoder class=\u0026#34;ch.qos.logback.classic.encoder.PatternLayoutEncoder\u0026#34;\u0026gt; \u0026lt;!--格式化输出：%d 表示日期，%thread 表示线程名，%-5level表示级别从左显示5个字符宽度，%msg表示日志消息，%n表示换行符 --\u0026gt; \u0026lt;pattern\u0026gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n\u0026lt;/pattern\u0026gt; \u0026lt;/encoder\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;!-- 日志输出级别 --\u0026gt; \u0026lt;!-- 区分大小写--\u0026gt; \u0026lt;root level=\u0026#34;ALL\u0026#34;\u0026gt; \u0026lt;!--输出到控制台--\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34;/\u0026gt; \u0026lt;!--输出到文件--\u0026gt; \u0026lt;appender-ref ref=\u0026#34;FILE\u0026#34;/\u0026gt; \u0026lt;/root\u0026gt; \u0026lt;/configuration\u0026gt; Logback日志级别 日志级别指的是日志信息的类型，日志都会分级别，常见的日志级别如下：(级别由高到低)： 可以在配置文件中，灵活的控制输出那些类型的日志**(大于等于配置的日志级别的日志才会输出)** 例如：在上面的root中输入：info，那么warn，error级别的日志都会输出，trace和debug则不会输出 现在就可以将上面Controller层的代码替换为以下的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 /** * 部门管理控制器 */ @Slf4j @RequestMapping(\u0026#34;/depts\u0026#34;) @RestController public class DeptController { @Autowired private DeptService deptService; /** * 查询部门列表 */ //@RequestMapping(value = \u0026#34;/depts\u0026#34;, method = RequestMethod.GET) @GetMapping public Result list(){ //System.out.println(\u0026#34;查询部门列表\u0026#34;); log.info(\u0026#34;查询部门列表\u0026#34;); List\u0026lt;Dept\u0026gt; deptList = deptService.findAll(); return Result.success(deptList); } /** * 根据id删除部门 - delete http://localhost:8080/depts?id=1 */ @DeleteMapping public Result delete(Integer id){ //System.out.println(\u0026#34;根据id删除部门, id=\u0026#34; + id); log.info(\u0026#34;根据id删除部门, id: {}\u0026#34; , id); deptService.deleteById(id); return Result.success(); } /** * 新增部门 - POST http://localhost:8080/depts 请求参数：{\u0026#34;name\u0026#34;:\u0026#34;研发部\u0026#34;} */ @PostMapping public Result save(@RequestBody Dept dept){ //System.out.println(\u0026#34;新增部门, dept=\u0026#34; + dept); log.info(\u0026#34;新增部门, dept: {}\u0026#34; , dept); deptService.save(dept); return Result.success(); } /** * 根据ID查询 - GET http://localhost:8080/depts/1 */ @GetMapping(\u0026#34;/{id}\u0026#34;) public Result getById(@PathVariable Integer id){ //System.out.println(\u0026#34;根据ID查询, id=\u0026#34; + id); log.info(\u0026#34;根据ID查询, id: {}\u0026#34; , id); Dept dept = deptService.getById(id); return Result.success(dept); } /** * 修改部门 - PUT http://localhost:8080/depts 请求参数：{\u0026#34;id\u0026#34;:1,\u0026#34;name\u0026#34;:\u0026#34;研发部\u0026#34;} */ @PutMapping public Result update(@RequestBody Dept dept){ //System.out.println(\u0026#34;修改部门, dept=\u0026#34; + dept); log.info(\u0026#34;修改部门, dept: {}\u0026#34; , dept); deptService.update(dept); return Result.success(); } } 注意：如果上面写了注解@Slf4j，那么下面就不用在类中再写：\nprivate static final Logger log = LoggerFactory.getLogger(DeptController.class);\n日志代码的两种写法：(以删除部门为例)\n1.log.info(\u0026ldquo;根据id删除部门, id: {}\u0026rdquo; , id);(推荐)\n2.log.info(\u0026ldquo;根据id删除部门, id: \u0026quot; + id);\n","date":"2026-04-15T20:28:12+08:00","permalink":"https://aurora156.github.io/p/web%E5%90%8E%E7%AB%AF%E5%AE%9E%E6%88%98/","title":"Web后端实战"},{"content":"Mybatis 什么是Mybatis？\nMybatis是一款优秀的持久层框架，用于简化JDBC的开发 JDBC与Mybatis代码的对比： 入门程序 准备工作 创建SpringBoot工程、引入Mybatis相关依赖 准备数据库表user、实体类User 配置Mybatis（在application.properties中数据库连接信息） 编写Mybatis程序：编写Mybatis的持久层接口，定义SQL(注解/XML) 提示：Mybatis的持久层接口命名规范为 XxxMapper，也称Mapper接口\n准备工作：\n1.创建springboot工程，并导入 mybatis的起步依赖、mysql的驱动包、lombok。\n项目工程创建完成后，自动在pom.xml文件中，导入Mybatis依赖和MySQL驱动依赖。如下所示：\n2.数据准备：创建用户表user，并创建对应的实体类User。\n用户表 user（如果已经存在，就不用创建了）\n1 2 3 4 5 6 7 8 9 10 11 12 13 create table user( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, username varchar(20) comment \u0026#39;用户名\u0026#39;, password varchar(32) comment \u0026#39;密码\u0026#39;, name varchar(10) comment \u0026#39;姓名\u0026#39;, age tinyint unsigned comment \u0026#39;年龄\u0026#39; ) comment \u0026#39;用户表\u0026#39;; insert into user(id, username, password, name, age) values (1, \u0026#39;daqiao\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;大乔\u0026#39;, 22), (2, \u0026#39;xiaoqiao\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;小乔\u0026#39;, 18), (3, \u0026#39;diaochan\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;貂蝉\u0026#39;, 24), (4, \u0026#39;lvbu\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;吕布\u0026#39;, 28), (5, \u0026#39;zhaoyun\u0026#39;, \u0026#39;12345678\u0026#39;, \u0026#39;赵云\u0026#39;, 27); 实体类：实体类的属性名与表中的字段名一一对应。 实体类放在 com.itheima.pojo 包下。\n1 2 3 4 5 6 7 8 9 10 @Data @NoArgsConstructor @AllArgsConstructor public class User { private Integer id; //ID private String username; //用户名 private String password; //密码 private String name; //姓名 private Integer age; //年龄 } 3.配置Mybatis\n1 2 3 4 5 6 7 8 #数据库访问的url地址 spring.datasource.url=jdbc:mysql://localhost:3306/web #数据库驱动类类名 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #访问数据库-用户名 spring.datasource.username=root #访问数据库-密码 spring.datasource.password=123456 上述的配置，可以直接复制过去，不要敲错了。 全部都是 spring.datasource.xxxx 开头。\n具体内容根据自己的设置进行更改\n4.编写Mybatis程序：编写Mybatis的持久层接口，定义SQL语句（注解）\n在创建出来的springboot工程中，在引导类所在包下，在创建一个包 mapper 。在 mapper 包下创建一个接口 UserMapper ，这是一个持久层接口（Mybatis的持久层接口规范一般都叫 XxxMapper）。\nUserMapper接口的内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.itheima.mapper; import com.itheima.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; @Mapper //应用程序在运行时，汇总的为该接口创建一个实现类对象(代理对象)，并且会自动将该实现类对象存入IOC容器 - bean public interface UserMapper { /* * 查询所有用户 * */ @Select(\u0026#34;select * from user\u0026#34;) public List\u0026lt;User\u0026gt; finAll(); } 注解说明：\n@Mapper注解：表示是mybatis中的Mapper接口程序运行时，框架会自动生成接口的实现类对象(代理对象)，并给交Spring的IOC容器管理\n@Select注解：代表的就是select查询，用于书写select查询语句\n5.单元测试\n在创建出来的SpringBoot工程中，在src下的test目录下，已经自动帮我们创建好了测试类 ，并且在测试类上已经添加了注解 @SpringBootTest，代表该测试类已经与SpringBoot整合。\n该测试类在运行时，会自动通过引导类加载Spring的环境（IOC容器）。我们要测试那个bean对象，就可以直接通过@Autowired注解直接将其注入进行，然后就可以测试了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.itheima; import com.itheima.mapper.UserMapper; import com.itheima.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest //SpringBoot单元测试的注解 - 当前测试类中的测试方法运行时，会启动Springboot项目 - IOC容器 class SpringbootMybatisQuickstartApplicationTests { @Autowired private UserMapper userMapper; @Test public void testFindAll() { List\u0026lt;User\u0026gt; userList = userMapper.finAll(); userList.forEach(System.out::println); } } 运行结果：\n辅助配置 默认在Mybatis中配置的SQL语句是不识别的。可以做如下配置：\n先点击图中所示按钮：\n然后直接搜索MySQL：\n此时编译器就会给这条语句高亮提示，但是会报错：\n产生原因：idea和数据库没有建立连接，不识别信息 解决方式：在idea中配置MySQL数据库连接 连上之后就有SQL提示了\n默认情况喜爱，在Mybatis中，SQL语句执行是，我们并看不到SQL语句的执行日志。加入如下配置，即可查看配置： 扩展：可以将application.properties文件替换为application.yaml(需将原本的文件注释掉)\n下文中有详细叙述\n文件的优先级：properties \u0026gt; yml \u0026gt; yaml\n此时文件内容需要更改为：(因为文件格式不一致)\n1 2 3 4 5 6 7 8 9 10 # 更简洁，但是要注意: 后面一定一定要加个空格 spring: datasource: url: jdbc:mysql://localhost:3306/web01 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl JDBC VS Mybatis 数据库连接池 数据库连接池是个容器，负责分配、管理数据库连接(Connection)\n它允许应用程序重复使用一个现有的数据库连接，而不是重新再建立一个\n释放空闲时间超过最大空闲时间的连接，来避免因为没有释放资源而引起的数据库连接遗漏\n优势：\n资源重用 提升系统响应速度 避免数据库连接遗漏 标准接口：DataSource\n官方(sun)提供的数据库连接池接口，由第三方组织实现此接口。 功能：获取连接 Connection getConnection() throws SQLException; 常见产品：\n切换数据库连接池：\n增删改查操作 删除用户 - delete 需求：根据id删除用户信息\nSQL：delete from user where id = 5;\nMapper接口：\n在执行这条SQL语句时，会将#{}替换为问号，即预编译SQL\n1 2 @Delete(\u0026#34;delete from user where id = ${id}\u0026#34;) public Integer deleteById(Integer id); 1 2 3 4 @Test public void testDeleteById() { userMapper.deleteById(5); } 如果想要接收返回值，就将void替换为返回值类型\n注意：DML语句执行完毕的返回值，表示该DML语句执行完毕影响的次数\n1 2 3 4 public void testDeleteById() { Integer i = userMapper.deleteById(5); System.out.println(\u0026#34;执行完毕，影响的记录数：\u0026#34; + i); } Mybatis中的 # 号与 $ 号： 1 2 @Delete(\u0026#34;delete from user where id = ${id}\u0026#34;) public Integer deleteById(Integer id); 新增用户 - insert 需求：添加一个用户 SQL：insert into user(username,password,name,age) values(\u0026lsquo;xxx\u0026rsquo;,\u0026lsquo;xxx\u0026rsquo;,\u0026lsquo;xxx\u0026rsquo;,xx) 1 2 @Insert(\u0026#34;insert into user(username, password, name, age) VALUES (#{username},#{password},#{name},#{age})\u0026#34;) public void insert(User user);//将对象值封装到对象当中 1 2 3 4 @Test public void testInsert() { User user = new User(null,\u0026#34;gaoyuanyuan\u0026#34;,\u0026#34;666888\u0026#34;,\u0026#34;高圆圆\u0026#34;,18); userMapper.insert(user); 原本：\n现在：此时的id为6是因为主键是自增的，就算删除也是存在过，由于4和5存在过，所以自增为6\n修改用户 - update 需求：根据ID更新用户信息\nSQL：update user set username = \u0026rsquo; \u0026lsquo;,password = \u0026rsquo; \u0026rsquo; , name = \u0026rsquo; \u0026lsquo;,age = xx where id = x;\nMapper接口：\n1 2 @Update(\u0026#34;update user set username = #{username},password = #{password},name = #{name},age = #{age} where id = #{id}\u0026#34;) public void update(User user); 1 2 3 4 5 @Test public void testUpdate() { User user = new User(1,\u0026#34;zhouyu\u0026#34;,\u0026#34;666888\u0026#34;,\u0026#34;周瑜\u0026#34;,18); userMapper.update(user); } 查询用户 - select 需求：根据用户名和密码查询用户信息 SQL：select * from user where username = \u0026rsquo; \u0026rsquo; and \u0026rsquo; \u0026rsquo; and password = \u0026rsquo; \u0026lsquo;; Mapper接口： @Param注解的作用是为接口的方法形参起名字的\n为什么删除和新增用户不需要起名字？\n1 2 3 @Select(\u0026#34;select * from user where username = #{username} and password = #{password}\u0026#34;) //{}中写的是我们起的名字 public User findByUsernameAndpassword(@Param(\u0026#34;username\u0026#34;) String username, @Param(\u0026#34;password\u0026#34;) String password); 1 2 3 4 5 @Test public void testFindByUsernameAndPassword() { User user = userMapper.findByUsernameAndpassword(\u0026#34;zhouyu\u0026#34;,\u0026#34;666888\u0026#34;); System.out.println(user); } 说明：基于官方骨架创建的Springboot项目中，接口编译时会保留方法形参名，@Param注解可以省略(#{形参名}) XML映射配置 在Mybatis中，既可以通过注解配置SQL语句，也可以通过XML配置文件配置SQL语句。 默认规则： XML映射文件的名称与Mapper接口名称一致，并且将XML映射文件和Mapper接口放置在相同包下**（同包同名）**。 XML映射文件的namespace属性为Mapper接口全限定名一致。 XML映射文件中sql语句的id与Mapper 接口中的方法名一致，并保持返回类型一致。 注意：在创建UserMapper文件时，要用/创建目录，例如此处则是com/itheima/mapper\n创建好目录结构后，从官网引入XML文件\nMybatis中文网：MyBatis 中文网 官网\n1 2 3 4 5 6 7 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;https://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; //(头信息)约束(不用动) \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.UserMapper\u0026#34;\u0026gt; \u0026lt;/mapper\u0026gt; 这就是XML映射文件的基本结构\n然后添加查询语句：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;https://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.itheima.mapper.UserMapper\u0026#34;\u0026gt; \u0026lt;!--resultType：查询返回的单条记录所封装的类型--\u0026gt; \u0026lt;select id=\u0026#34;findAll\u0026#34; resultType=\u0026#34;com.itheima.pojo.User\u0026#34;\u0026gt; select id, username, password, name, age from user \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 记得要把原来的注解给注释了（二选一），如果不注释就不知道要执行哪个语句了导致报错\n那在Mybatis的开发中，到底使用注解开发还是使用XML开发呢 ？ 使用Mybatis的注解，主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能，建议使用XML来配置映射语句。\n官方说明：mybatis – MyBatis 3 | 入门\nXML映射文件 - 辅助配置 配置XML映射文件的位置： MybatisX 是一款基于 IDEA 的快速开发Mybatis的插件，为效率而生。 注意：在使用了这个插件以后，可能会让你原先的代码被警告(不是报错)\n此时将项目中同包同名的文件remove即可，不移除也可以照常运行，但是项目真正开发项目中一般不会出现这种情况\nSpringboot配置文件 Springboot项目提供了多种属性配置方式(properties、yaml、yml) 格式： 数值前边必须有空格，作为分隔符 使用缩进表示层级关系，缩进时，不允许使用Tab键，只能用空格（idea中会自动将Tab转换为空格） 缩进的空格数目不重要，只要相同层级的元素左侧对齐即可 # 表示注释，从这个字符一直到行尾，都会被解析器忽略 定义对象/Map集合： 定义数组/List/Set集合：记得加 - ，- 后面要加空格 注意：在yml格式的配置文件中，如果配置项的值是以0开头的，值需要使用 **\u0026rsquo; \u0026rsquo; ** 引起来，因为0开头在yml中表示8进制的数据\n例如：输入一个0开头的Password就需要设置为：\u0026lsquo;0123456\u0026rsquo;\n所以前面的application.properties就可以改为yml文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: application: name: springboot-mybatis-quickstart # 配置数据库连接信息 datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/web01 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: #配置Mybatis的日志输出 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #指定XML映射文件的位置 mapper-locations: classpath:mapper/*.xml ","date":"2026-04-13T10:46:52+08:00","permalink":"https://aurora156.github.io/p/mybatis/","title":"Mybatis"},{"content":"引言 Java程序操作数据库\nJDBC 本质： sun公司官方定义的一套操作所有关系型数据库的规范，即接口。 各个数据库厂商去实现这套接口，提供数据库驱动jar包。 我们可以使用这套接口（JDBC）编程，真正执行的代码是驱动jar包中的实现类。 例如：MySQL中的jar包依赖\n入门程序 需求：基于JDBC程序，执行update语句 (update user set age = 25 where id = 1) 步骤： 准备工作：创建一个maven项目，引入依赖；并准备数据库表user。 代码实现：编写JDBC程序，操作数据库 1.创建项目，引入依赖\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.33\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.jupiter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-jupiter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.9.3\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.30\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 2.准备数据库表user\n1 2 3 4 5 6 7 8 9 10 11 12 13 create table user( id int unsigned primary key auto_increment comment \u0026#39;ID,主键\u0026#39;, username varchar(20) comment \u0026#39;用户名\u0026#39;, password varchar(32) comment \u0026#39;密码\u0026#39;, name varchar(10) comment \u0026#39;姓名\u0026#39;, age tinyint unsigned comment \u0026#39;年龄\u0026#39; ) comment \u0026#39;用户表\u0026#39;; insert into user(id, username, password, name, age) values (1, \u0026#39;daqiao\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;大乔\u0026#39;, 22), (2, \u0026#39;xiaoqiao\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;小乔\u0026#39;, 18), (3, \u0026#39;diaochan\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;貂蝉\u0026#39;, 24), (4, \u0026#39;lvbu\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;吕布\u0026#39;, 28), (5, \u0026#39;zhaoyun\u0026#39;, \u0026#39;12345678\u0026#39;, \u0026#39;赵云\u0026#39;, 27); 3.编写测试类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.itheima; import org.testng.annotations.Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; public class JdbcTest { /* JDBC入门程序 */ @Test public void testUpdate() throws Exception { //1.注册驱动 Class.forName(\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;); //2.获取数据库连接 String url = \u0026#34;jdbc:mysql://localhost:3306/web01\u0026#34;; String username = \u0026#34;root\u0026#34;; String password = \u0026#34;123456\u0026#34;; Connection connection = DriverManager.getConnection(url,username,password); //3.获取SQL语句执行对象 Statement statement = connection.createStatement(); //4.执行SQL statement.executeUpdate(\u0026#34;update user set age = 25 where id = 1\u0026#34;); //5.释放资源 statement.close(); connection.close(); } } 注册驱动时：先双击shift，然后在搜索框输入Driver，记得点击右上角的include\u0026hellip;，找到后按照以下操作就可以找到驱动的路径(复制粘贴即可)\n获取数据库连接时url固定的格式为\u0026rsquo;\u0026lsquo;jdbc:mysql://\u0026rsquo;\u0026rsquo;，后面加上指定的数据库ip地址端口号\n4.查看结果：\n更新前：\n更新后：\n执行DQL语句 需求：基于JDBC执行如下select语句，将查询结果封装到User对象中 SQL：select * from user where username = \u0026lsquo;daqiao\u0026rsquo; and password = \u0026lsquo;123456\u0026rsquo; ResultSet（结果集对象）：ResultSet rs = statement.executeQuery() next()：将光标从当前位置向前移动一行，并判断当前行是否为有效行，返回值为boolean。 true：有效行，当前行有数据(解析各个字段的值，调用result当中get方法) false：无效行，当前行没有数据(代表数据已经解析完了) getXxx(\u0026hellip;)：获取数据，可以根据列的编号获取，也可以根据列名获取(推荐)。 结果解析步骤： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Test public void testSelect() throws Exception { String url = \u0026#34;jdbc:mysql://localhost:3306/web01\u0026#34;; String USER = \u0026#34;root\u0026#34;; String PASSWORD = \u0026#34;123456\u0026#34;; Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; //封装查询返回的结果 try { //1.注册JDBC驱动 Class.forName(\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;); //2.获取数据库连接 conn = DriverManager.getConnection(url, USER, PASSWORD); //3.执行查询 String sql = \u0026#34;select id,username,password,name,age from user where username = ? and password = ?\u0026#34;; //预编译SQL(推荐)【 ？：占位符】 stmt = conn.prepareStatement(sql); stmt.setString(1, \u0026#34;daqiao\u0026#34;); stmt.setString(2, \u0026#34;123456\u0026#34;); //将结果集封装在ResultSet对象中 rs = stmt.executeQuery(); //4.处理结果 while (rs.next()) { User user = new User( rs.getInt(\u0026#34;id\u0026#34;), rs.getString(\u0026#34;username\u0026#34;), rs.getString(\u0026#34;password\u0026#34;), rs.getString(\u0026#34;name\u0026#34;), rs.getInt(\u0026#34;age\u0026#34;) ); System.out.println(user);//使用Lombok的 @Data 自动生成 toString() 方法 } }catch (SQLException se){ //捕获SQLException异常 //Handle errors for JDBC se.printStackTrace(); }catch (Exception e){ //捕获Exception异常 //Handle errors for Class.forName e.printStackTrace(); }finally { //5.释放资源 try{ if(rs != null) rs.close(); if(stmt != null) stmt.close(); if(conn != null) conn.close(); }catch (SQLException se){ //捕获SQLException异常 se.printStackTrace(); } } } 小结：\nJDBC程序执行\nDML语句：int rowsAffected = statement.executeUpdate(); DQL语句：ResultSet rs = statement.executeQuery(); DQL语句执行完毕结果集ResultSet解析？\nresultSet.next()：光标往下一行 resultSet.getXxx()：获取字段数据 预编译SQL 1.静态SQL(参数硬编码)\n2.预编译SQL(参数动态传递)\n优势一：可以防止SQL注入，更安全\nSQL注入：通过控制输入来修改实现定义好的SQL语句，以达到执行代码对服务器进行攻击的方法\n典型案例：\n演示：\n1.静态SQL\n准备一个静态SQL文件，在其控制台输入java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar\n(这个文件和下面的那个文件都是事先准备的)\n接着在浏览器输入其端口号：localhost：xxxx\n在上述页面中输入账号密码后就会在控制台输出账号密码，将用户名密码直接写死在了SQL语句中\n此时在用户名随意书写：sjashakjf，然后在密码输入\u0026rsquo;or'1\u0026rsquo; = \u0026lsquo;1\n此时在SQL语句中就会变成如下的代码\n1 2 select count(*) from emp where username = \u0026#39;songjiang\u0026#39; and password = \u0026#39;123456\u0026#39;\t--原本 select count(*) from emp where username = \u0026#39;sjashakjf\u0026#39; and password = \u0026#39;\u0026#39;or\u0026#39;1\u0026#39; = \u0026#39;1\u0026#39; \u0026lsquo;\u0026lsquo;or'1\u0026rsquo; = \u0026lsquo;1\u0026rsquo;因为1 = 1是恒等，所以该语句不会报错(改变了原有的SQL语句的含义)，就会登录成功\n2.预编译SQL\n准备一个静态SQL文件，在其控制台输入java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar\n接着在浏览器输入其端口号：localhost：xxxx\n使用这种方法后，就会将第一个字符串替换第一个字符，第二个字符串替换第二个字符，此时就不会改变原有的SQL语句的含义\n优势二：性能更高\n","date":"2026-04-12T15:59:16+08:00","permalink":"https://aurora156.github.io/p/jdbc/","title":"JDBC"},{"content":"MySQL 定义 数据库：DataBase(DB)，是存储和管理数据的仓库 数据库管理系统：DataBase Management System(DBMS),操作和管理数据库的大型软件 SQL：Structured Query Language，操作关系型数据库的编程语言，定义了一套操作关系型数据库统一标准。 数据库产品 以下均属于关系型数据库\nMySQL概述 MySQL连接：mysql -hxxx -Pxxx -uxxx -pxxx\n-h：连接的ip地址(可选){未选，默认本机的3306接口}\n-P：连接的端口号(可选)\n-u：指定的用户名\n-p：指定的密码\n数据模型 关系型数据库：建立在关系模型基础上，由多张相互连接的二维表(有行有列)组成的数据库(RDBMS)\n特点：\n使用表存储数据，格式统一，便于维护\n使用SQL语言操作，标准统一，使用方便，可用于复杂查询\n数据库存储方式：\n数据库 -\u0026gt; 表 -\u0026gt; 数据(记录) SQL 分类： 重点关注前三个\nSQL语句 DDL 数据库 操作语法\t[ ]：可选(如果要使用里面的代码，[ ]不要敲) 数据库名后的[ ]作用是指定字符集(默认utf8mb4)\n在命令行敲：mysql -u root -p进入mysql\n系统自带数据库：\n注意：上述语法中的database，也可以替换成schema(复数要加s)。如：Create schema db01；MySQL8版本中，默认字符集为utfmb4\nMySQL客户端工具 - 图形化工具 介绍：DataGrip是JetBrains旗下的一款数据库管理工具，是管理和开发MySQL、Oracle、PostgreSQL的理想解决方案 官网：https://www.jetbrains.com/zh-cn/datagrip/ 连接数据库：\n创建project\n创建连接\n下载驱动, 可能会比较耗时, 耐心等待一会儿。\n测试连接 下载完驱动之后，可以点击 Test Connection 来测试一下是否可以正常的连接数据库。\n然后点击 OK ， 就已经连接上了MySQL数据库了。\n连接上以后点击图中所示按钮可以选择要展示的数据库\n此时右边的控制台会出现一个console，在这里就可以编写数据库(如果没有就自行点击左侧的类似于终端按钮的图标即可显示)\n注解：\u0026ndash;\n找上一次编辑的文件： 如果觉得名字不好记可以自行更改\n表结构-创建 创建表的语法： 1 2 3 4 5 6 7 8 --创建表 create table user( id int comment\u0026#39;ID,唯一标识\u0026#39;, username varchar(50) comment\u0026#39;用户名\u0026#39;, name varchar(50) comment\u0026#39;姓名\u0026#39;, age int comment\u0026#39;年龄\u0026#39;, gender char(1) comment\u0026#39;性别\u0026#39; ) comment \u0026#39;用户信息表\u0026#39;; 选中该字段运行：左侧就会出现相关信息\n双击user：就可以查看表\n约束：约束是作用于表中字段上的规则，用于限制存储在表中的数据 目的：保证数据库中数据的正确性、有效性和完整性 1 2 3 4 5 6 7 8 -- 创建表（约束） create table user( id int primary key auto_increment comment\u0026#39;ID,唯一标识\u0026#39;, -- 主键约束 auto_increment username varchar(50) not null unique comment\u0026#39;用户名\u0026#39;, -- 不为空且唯一 name varchar(50) not null comment\u0026#39;姓名\u0026#39;, -- 非空 age int comment\u0026#39;年龄\u0026#39;, gender char(1) default\u0026#39;男\u0026#39; comment\u0026#39;性别\u0026#39; ) comment \u0026#39;用户信息表\u0026#39;; 1 auto_increment：自增值(从最大值开始自增) 数据类型 MySQL中的数据类型有很多，主要分为三类：数值类型、字符串类型、日期时间类型\n数值类型：\n字符串类型：\n日期时间类型：\n案例 根据页面原型，设计员工表\n步骤：\n①阅读并分析页面原型及需求\n②分析表中包含哪些字段，以及字段的类型、约束\n③创建表结构\n需求：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 案例：设计员工表 emp -- 基础字段：id 主键； create_time 创建时间； update_time 修改时间； create table emp( id int unsigned primary key auto_increment comment \u0026#39;id,主键\u0026#39;, username varchar(20) not null unique comment \u0026#39;用户名\u0026#39;, password varchar(32) default \u0026#39;123456\u0026#39; comment \u0026#39;密码\u0026#39;, name varchar(10) not null comment \u0026#39;姓名\u0026#39;, gender tinyint unsigned not null comment \u0026#39;性别 ,1 男；2 女\u0026#39;, phone char(11) not null unique comment \u0026#39;手机号\u0026#39;, job tinyint unsigned comment \u0026#39;职位,1 班主任；2 讲师；3 学工主管；4 教研主管；5 咨询师\u0026#39;, salary int unsigned comment \u0026#39;薪资\u0026#39;, entry_date date comment \u0026#39;入职日期\u0026#39;, image varchar(255) comment \u0026#39;图像\u0026#39;, create_time datetime comment \u0026#39;创建时间\u0026#39;, update_time datetime comment \u0026#39;修改时间\u0026#39; )comment \u0026#39;员工表\u0026#39;; 表结构-查询、修改、删除 表结构的查询、修改、删除相关语法如下： 注意：在删除表时，表中的全部数据也会被删除\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 -- 查询当前数据库所有表 show tables ; -- 查看表结构 desc emp ; -- 查询建表语句 show create table emp; -- 字段：添加字段 qq varchar(13) alter table emp add qq varchar(13) comment \u0026#39;qq号码\u0026#39;; -- 字段：修改字段类型 qq varchar(15) alter table emp modify qq varchar(15) comment \u0026#39;qq号码\u0026#39;; -- 字段：修改字段名 qq -\u0026gt; qq_num varchar(15) alter table emp change qq qq_num varchar(15) comment \u0026#39;qq号码\u0026#39;; -- 字段：删除字段 qq_num alter table emp drop column qq_num; -- 修改表名 alter table emp rename to employee; -- 删除表 drop table user; 也可以通过以下操作添加/修改，删除字段选择\u0026quot; - \u0026ldquo;，删除表结构选择drop,改名选择Rename\nDML DML英文全称是Data Manipulation Language(数据操作语言)，用来对数据库中表的数据记录进行增、删、改操作。\n添加数据(INSERT) 修改数据(UPDATE) 删除数据(DELETE) 添加语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- DML : 插入数据 - insert -- 1. 为 emp 表的 username, password, name, gender, phone 字段插入值 insert into emp(username,password,name,gender,phone) values (\u0026#39;songjiang\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;宋江\u0026#39;,1,\u0026#39;11122233345\u0026#39;); -- 2. 为 emp 表的 所有字段插入值 -- 方式1： insert into emp (id, username, password, name, gender, phone, job, salary, entry_date, image, create_time, update_time) values (null,\u0026#39;linchong\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;林冲\u0026#39;,\u0026#39;1\u0026#39;,\u0026#39;13300012254\u0026#39;,1,6000,\u0026#39;2020-01-01\u0026#39;,\u0026#39;1.jpg\u0026#39;,now(),now()); -- 方式2： insert into emp values (null,\u0026#39;likui\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;李逵\u0026#39;,\u0026#39;1\u0026#39;,\u0026#39;13300012256\u0026#39;,1,6000,\u0026#39;2020-01-01\u0026#39;,\u0026#39;1.jpg\u0026#39;,now(),now()); -- 3. 批量为 emp 表的 username, password, name, gender, phone 字段插入数据 insert into emp (username,password,name,gender,phone) values (\u0026#39;ruanxiaoer\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;阮小二\u0026#39;,1,\u0026#39;11122233346\u0026#39;), (\u0026#39;ruanxiaowu\u0026#39;,\u0026#39;12345678\u0026#39;,\u0026#39;阮小五\u0026#39;,1,\u0026#39;11122233347\u0026#39;); 注意： 1.插入数据时，指定的字段顺序需要与值的顺序是一一对应的 2.字符串和日期型数据应该包含在引号中（单引号、双引号都可以） (使用双引号可能会报红，但是不妨碍正常运行，建议使用单引号) 3.插入的数据大小/长度，应该在字段的规定范围内\n修改语句 注意：修改语句的条件可以有，也可以没有，如果没有条件，则会修改整张表的所有数据\n1 2 3 4 5 6 7 -- 1. 将 emp 表的ID为1员工 用户名更新为 \u0026#39;zhangsan\u0026#39;, 姓名name字段更新为 \u0026#39;张三\u0026#39; update emp set username = \u0026#39;zhangsan\u0026#39; ,name = \u0026#39;张三\u0026#39; where id = 1; -- 2. 将 emp 表的所有员工的入职日期更新为 \u0026#39;2010-01-01\u0026#39; update emp set entry_date = \u0026#39;2010-01-01\u0026#39;; 如果不添加条件，会报出以下警告，此时点击Execute则会继续运行\n删除语句 注意：\n1.DELETE 语句的条件可以有，也可以没有，如果没有条件，则会删除整张表的所有数据。\n2.DELETE 语句不能删除某一个字段的值(如果要操作，可以使用UPDATE，将该字段的值置为NULL)。\n1 2 3 4 5 6 -- DML : 删除数据 - delete -- 1. 删除 emp 表中 ID为1的员工 delete from emp where id = 1; -- 2. 删除 emp 表中的所有员工 delete from emp; DQL DQL英文全称是Data Query Language(数据查询语言)，用来查询数据库表中的记录 关键字：SELECT 基本查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 -- =================== DQL: 基本查询 ====================== -- 1. 查询指定字段 name,entry_date 并返回 select name,entry_date from emp; -- 2. 查询返回所有字段 -- 方式1：推荐 select id, username, password, name, gender, phone, job, salary, entry_date, image, create_time, update_time from emp; -- 方式2：不推荐 select * from emp; -- 3. 查询所有员工的 name,entry_date, 并起别名(姓名、入职日期) -- 当别名之间有空格时需要加引号，其他情况则可以不加 -- as 关键字也可以省略 select name as 姓名,entry_date as 入职日期 from emp; select name 姓名,entry_date 入职日期 from emp; -- 4. 查询已有的员工关联了哪几种职位(不要重复) select distinct job from emp 注意：* 号代表查询所有字段，在实际开发中尽量少用(不直观、影响效率)\n条件查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 -- =================== DQL: 条件查询 ====================== -- 1. 查询 姓名 为 柴进 的员工 select * from emp where name=\u0026#39;柴进\u0026#39;; -- 2. 查询 薪资小于等于5000 的员工信息 select * from emp where salary \u0026lt;= 5000; -- 3. 查询 没有分配职位 的员工信息 select * from emp where job is null ; -- 4. 查询 有职位 的员工信息 select * from emp where job is not null; -- 5. 查询 密码不等于 \u0026#39;123456\u0026#39; 的员工信息 select * from emp where password != \u0026#39;123456\u0026#39;; select * from emp where password \u0026lt;\u0026gt; \u0026#39;123456\u0026#39;;-- 不推荐 -- 6. 查询 入职日期 在 \u0026#39;2000-01-01\u0026#39; (包含) 到 \u0026#39;2010-01-01\u0026#39;(包含) 之间的员工信息 select * from emp where entry_date between \u0026#39;2000-01-01\u0026#39; and \u0026#39;2010-01-01\u0026#39;;-- 注意：between之后要写‘最小值’ and之后要写‘最大值’，否则会报错 -- 7. 查询 入职时间 在 \u0026#39;2000-01-01\u0026#39; (包含) 到 \u0026#39;2010-01-01\u0026#39;(包含) 之间 且 性别为女 的员工信息 select * from emp where (entry_date between \u0026#39;2000-01-01\u0026#39; and \u0026#39;2010-01-01\u0026#39;) and gender = 2;-- 括号可写(更清晰)可不写 -- 8. 查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息 select * from emp where job = 2 or job = 3 or job = 4; select * from emp where job in(2,3,4); -- 9. 查询 姓名 为两个字的员工信息 select * from emp where name like \u0026#39;__\u0026#39;; -- 10. 查询 姓 \u0026#39;阮\u0026#39; 的员工信息 select * from emp where name like \u0026#39;阮%\u0026#39;; -- 11. 查询 姓名中包含 \u0026#39;二\u0026#39; 的员工信息 select * from emp where name like\u0026#39;%二%\u0026#39;; 语句太多，结果就不一一展示了\n分组查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -- =================== DQL: 分组查询 ====================== -- 聚合函数 -- 注意：所有的聚合函数不参与null统计 -- 1. 统计该企业员工数量 -- count(字段) select count(id) from emp; -- count(*)：推荐1 select count(*) from emp; -- count(常量) 有数据就标1 推荐2 select count(1) from emp; -- 2. 统计该企业员工的平均薪资 select avg(salary) from emp; -- 3. 统计该企业员工的最低薪资 select min(salary) from emp; -- 4. 统计该企业员工的最高薪资 select max(salary) from emp; -- 5. 统计该企业每月要给员工发放的薪资总额(薪资之和) select sum(salary) from emp; 注意：\n1.null值不参与所有聚合函数的运算\n2.统计数量可以使用：count() count(字段) count(常量)，推荐使用count()\n1 2 3 4 5 6 7 -- 分组 -- 注意：分组之后，select后的字段列表不能随意书写，能写的一般是 分组字段 + 聚合函数 -- 1. 根据性别分组 , 统计男性和女性员工的数量 select gender,count(*) from emp group by gender ; -- 2. 先查询入职时间在 \u0026#39;2015-01-01\u0026#39; (包含) 以前的员工 , 并对结果根据职位分组 , 获取员工数量大于等于2的职位 select job,count(*) from emp where entry_date \u0026lt;= \u0026#39;2015-01-01\u0026#39; group by job having count(*) \u0026gt;= 2; 注意：\n1.分组之后，查询的字段一般为聚合函数和分组字段，查询其他字段无任何意义\n2.执行顺序: where \u0026gt; 聚合函数(group by) \u0026gt; having\nhaving后可以使用聚合函数，where不可以\n排序查询 排序方式：升序（asc），降序（desc）；默认为升序asc，是可以不写的。 注意：如果是多字段排序，当第一个字段值相同时，才会根据第二个字段进行排序\n1 2 3 4 5 6 7 8 9 10 -- =================== 排序查询 ====================== -- 1. 根据入职时间, 对员工进行升序排序 select * from emp order by entry_date asc; select * from emp order by entry_date; -- 2. 根据入职时间, 对员工进行降序排序 select * from emp order by entry_date desc; -- 3. 根据 入职时间 对公司的员工进行 升序排序 ， 入职时间相同 , 再按照 更新时间 进行降序排序 select * from emp order by entry_date , update_time desc; 3.\n分页查询 1 2 3 4 5 6 7 8 9 10 11 12 -- =================== 分页查询 ====================== -- 1. 从起始索引0开始查询员工数据, 每页展示5条记录 select * from emp limit 5; -- 2. 查询 第1页 员工数据, 每页展示5条记录 select * from emp limit 0,5; -- 3. 查询 第2页 员工数据, 每页展示5条记录 select * from emp limit 5,5; -- 4. 查询 第3页 员工数据, 每页展示5条记录 select * from emp limit 10,5; 注意：\n项目开发中，前段传递过来的是页码，需要转化为起始索引\n起始索引 = (页码 - 1) * 每页展示记录数\n","date":"2026-04-11T12:36:46+08:00","permalink":"https://aurora156.github.io/p/mysql/","title":"MySQL"},{"content":"Web后端开发 Web基础知识 静态资源：服务器上存储的不会改变的数据，通常不会根据用户的请求而变化。比如：HTML、CSS、JS、图片、视频等(负责页面展示) 动态资源：服务器端根据用户请求和其他数据动态生成的，内容可能会在每次请求时都发生变化。比如：Servlet、JSP（Spring框架）等(负责逻辑处理) B/S架构：Browser/Server，浏览器/服务器架构模式。客户只需浏览器，应用程序的逻辑和数据都存在服务器端。(维护方便 体验一般) C/S架构：Client/Server，客户端/服务器架构模式。需要单独开发维护客户端。(体验不错 开发维护麻烦) Spring 官网：https://spring.io Spring发展到今天已经形成了一种开发生态圈，Spring提供了若干个子项目，每个项目用于完成特定的功能。 Spring Boot【官方推荐、企业主流】可以帮助我们非常快速的构架应用程序、简化开发、提高效率\n入门程序 需求：基于SpringBoot开发一个Web应用，浏览器发起请求/hello之后，给浏览器返回一个字符串“Hello Xxx”\n步骤：\n①创建springboot工程，并勾选web开发相关依赖\n②定义HelloController类，添加方法hello，并添加注解（定义请求处理方法）\n③运行启动类，测试\n1 2 3 4 5 6 7 8 9 10 @RestController //表示当前类是一个请求处理类 public class HelloController { @RequestMapping(\u0026#34;/hello\u0026#34;)//标识请求路径 public String hello(String name){ System.out.println(\u0026#34;name\u0026#34; + name); //给前端响应数据 return \u0026#34;Hello\u0026#34; + name + \u0026#34;~\u0026#34;; } 然后到Springboot启动类中运行debug：若出现以下界面则表示运行成功了\n然后在浏览器输入localhost:8080/hello?name=itheima\n出现以上界面则说明成功完成了\nSpring官方脚手架连接不上解决方案 如果出现上面情况，可以将URL换成阿里云的\n后面的和上面的入门程序的步骤一样\n入门程序剖析 为什么一个main方法就将web应用启动类了？ 起步依赖：\nspring-boot-starter-web：包含了web应用开发所需要的常见依赖 spring-boot-starter-test：包含了单元测试所需要的常见依赖 官方提供的starter： https://docs.spring.io/spring-boot/docs/3.1.3/reference/htmlsingle/#using.build-systems.starters\n其中的tomcat服务器是内嵌的，一个main方法就可以将tomcat服务器启动起来\nHTTP协议 请求协议 请求数据格式 概念：Hyper Text Transfer Protocol，超文本传输协议，规定了浏览器和服务器之间数据传输的规则。\n超文本：可以互相链接、跳转的文本，也就是包含超链接的文本。\n普通文本：只能从头到尾读，不能跳转。 超文本：里面有链接，点一下就能跳到另一篇文档、图片、网页。 HTTP 就是专门用来在网络上传输这种带链接的超文本内容的协议，所以叫 “超文本传输协议”。 特点：\n1.基于TCP协议的：面向连接，安全\n2.基于请求-响应模型的：一次请求对应一次响应\n3.HTTP协议是无状态的协议：对于事物处理没有记忆能力。每次请求-响应都是独立的\n缺点：多次请求间不能共享数据 优点：速度快 常见的请求头：\n请求数据获取 Web服务器(Tomcat)对HTTP协议的请求数据进行解析，并进行了封装(HttpServletRequest)，在调用Controller方法的时候传递给了该方法。这样，就使得程序员不必直接对协议进行操作，让Web开发更加便捷 通过HttpServerletRequest对象获取请求数据，里面封装了所有的请求信息 @RestController //表示当前类是一个请求处理类\n@RequestMapping(\u0026quot;/hello\u0026quot;) //标识请求路径\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @RestController public class RequestController { @RequestMapping(\u0026#34;/request\u0026#34;) public String request(HttpServletRequest request){ //1.获取请求方式 String method = request.getMethod(); System.out.println(\u0026#34;请求方式\u0026#34; + method); //2.获取请求url地址 String url = request.getRequestURL().toString(); //http://localhost:8080/request System.out.println(\u0026#34;请求地址\u0026#34; + url); String uri = request.getRequestURI(); System.out.println(\u0026#34;请求URI\u0026#34; + uri); //3.获取请求协议 String protocol = request.getProtocol(); System.out.println(\u0026#34;请求协议\u0026#34; + protocol); //4.获取请求参数 - name String name = request.getParameter(\u0026#34;name\u0026#34;); String age = request.getParameter(\u0026#34;age\u0026#34;); System.out.println(\u0026#34;请求参数 - name\u0026#34; + name + \u0026#34; age\u0026#34; + age); //5.获取请求头 - Accept(如果要获取其他请求头，将Accept替换即可) String accept = request.getHeader(\u0026#34;Accept\u0026#34;); System.out.println(\u0026#34;请求头 - Accept\u0026#34; + accept); return \u0026#34;OK\u0026#34;; } } 在浏览器输入：localhost:8080/request?name=itheima\u0026amp;age=18即可得到下方结果\n响应协议 响应数据格式 常见的响应状态码：\n重定向(3xx)：底层会获取到多次请求\n例如：在网页输入http://www.baidu.com/\n经过重定向再次请求：https://www.baidu.com/\n常见状态码：\n响应数据设置 Web服务器对HTTP协议的响应数据进行了封装(HttpServletResponse)，并在调用Controller方法的时候传递给了该方法。这样，就使得程序员不必直接对协议进行操作，让Web开发更加便捷。 封装方式一：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController public class ResponseController { /* * 方式一：使用HttpServletResponse对象 * */ @RequestMapping(\u0026#34;/response\u0026#34;) public void response(HttpServletResponse response) throws IOException { //1.设置响应状态码 response.setStatus(401); //2.设置响应头 response.setHeader(\u0026#34;name\u0026#34;, \u0026#34;itheima\u0026#34;); //3.设置响应体（数据） response.getWriter().write(\u0026#34;\u0026lt;h1\u0026gt;hello response\u0026lt;/h1\u0026gt;\u0026#34;); } } 封装方式二：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController public class ResponseController { /* 方式二：使用ResponseEntity对象 */ @RequestMapping(\u0026#34;/response2\u0026#34;) public ResponseEntity\u0026lt;String\u0026gt; response2() { return ResponseEntity.status(401)//响应状态码 .header(\u0026#34;name\u0026#34;, \u0026#34;javaweb-ai\u0026#34;)//响应头 .body(\u0026#34;\u0026lt;h1\u0026gt;hello response\u0026lt;/h1\u0026gt;\u0026#34;);//响应体 } } 结果：\n注意：响应状态码 和 响应头如果没有特殊要求的话，通常不手动设定。服务器会根据请求处理的逻辑，自动设置响应状态码和响应头。\nSpringBootWeb案例 需求：基于SpringBoot开发web程序，完成用户列表的渲染展示\n当在浏览器地址栏，访问前端静态页面（http://localhost:8080/usre.html）后，在前端页面上，会发送ajax请求，请求服务端（http://localhost:8080/list），服务端程序加载 user.txt 文件中的数据，读取出来后最终给前端页面响应json格式的数据，前端页面再将数据渲染展示在表格中。\n1.准备工作：\n创建一个SpringBoot工程，并勾选web依赖、lombok 引入资料中准备好的用户数据文件(user.txt)，及前端静态页面 定义一个实体类，用来封装用户信息 在 com.itheima 下再定义一个包 pojo，专门用来存放实体类。 在该包下定义一个实体类User：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.itheima.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; /* * 用户信息 * */ @Data //目的：自动生成getter和setter方法 @NoArgsConstructor //无参构造 @AllArgsConstructor //全参构造 public class User { private Integer id; //在项目中，建议使用包装类（目的：不要默认值） private String username; private String password; private String name; private Integer age; private LocalDateTime updateTime; } 2.开发服务端程序，接收请求，读取文本数据并响应\n由于在案例中，需要读取文本中的数据，并且还需要将对象转为json格式，所以这里呢，我们在项目中再引入一个非常常用的工具包hutool。 然后调用里面的工具类，就可以非常方便快捷的完成业务操作。\npom.xml中引入依赖 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;cn.hutool\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hutool-all\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.8.27\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 在com.itheima包下新建一个子包controller，在其中创建一个UserController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.itheima.controller; import cn.hutool.core.io.IoUtil; import com.itheima.pojo.User; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /* * 用户信息controller * */ @RestController //封装了@ResponseBody -\u0026gt; 作用：将controller返回值直接作为响应体的数据直接响应；返回值是对象/集合-\u0026gt;json-\u0026gt; 响应 public class UserController { @RequestMapping(\u0026#34;/list\u0026#34;) public List\u0026lt;User\u0026gt; list() throws Exception { //1.加载并读取user.txt文件，获取用户数据 //通过字节码对象来获取到类加载器 InputStream in = this.getClass().getClassLoader().getResourceAsStream(\u0026#34;user.txt\u0026#34;); List\u0026lt;String\u0026gt; lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList\u0026lt;\u0026gt;()); //2.解析用户信息，封装为User对象 -\u0026gt; list集合 //遍历集合 List\u0026lt;User\u0026gt; userList = lines.stream().map(line -\u0026gt; { // 根据“，”进行截取 String[] parts = line.split(\u0026#34;,\u0026#34;); //获取信息 //5,zhaoyun,1234567890,赵云,27,2024-07-16 11:03:28 Integer id = Integer.parseInt(parts[0]); String username = parts[1]; String password = parts[2]; String name = parts[3]; Integer age = Integer.parseInt(parts[4]); LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;));//注意：parts[5]和DTF是写在一个一个()里的，如果没有写在一个括号里，虽然不会报红，但是运行时会报错 return new User(id, username, password, name, age, updateTime); }).collect(Collectors.toList());//封装到集合当中 //3.返回数据(json) return userList; } } 3.启动服务测试，访问：http://localhost:8080/user.html\n@ResponseBody注解的作用\n将controller方法的返回值直接写入HTTP响应体 如果是对象或集合，会先转为json，再响应 @RestController = @Controller + @ResponseBody 分层解耦 由于上一个项目的代码有着复用性差、难以维护的缺点，所以引入了该小节的内容\n三层架构 为什么要进行拆分：遵循单一职责原则，便于复用、后期维护\ncontroller：控制层，接收前端发送的请求，对请求进行处理，并响应数据 service：业务逻辑层，处理具体的业务逻辑 dao：数据访问层（Data Access Object）(持久层)，负责数据访问操作，包括数据的增、删、改、查 流程：\n对比：\n分层解耦 耦合：衡量软件中各个层/各个模块的依赖关联程度 内聚：软件中各个功能模块内部的功能联系 软件设计原则：高内聚低耦合 控制反转：Inversion Of Control，简称IOC。对象的创建控制权由程序自身转移到外部(容器)，这种思想成为控制反转 依赖注入：Dependency Injection，简称DI。容器为应用程序提供运行时，所依赖的资源，称之为依赖注入 Bean对象：IOC容器中创建、管理的对象，称之为Bean IOC \u0026amp; DI入门 1.将Dao及Service层的实现类，交给IOC容器管理\n(@Component：将当前类交给IOC容器管理)\n注意：是加在实现类上，而非接口上\n2.为Controller及Service注入运行时所依赖的对象\n(@Autowired：应用程序运行时，会自动的查询该类型的Bean对象，并赋值给改成员变量)\n①修改rController层：\n②分别修改Dao层和Service层：\n③Controller层和Server层注入对象：\nIOC详解 要把某个对象交给IOC容器管理，需要在对应的类上加上如下注解之一： 注意：声明bean的时候，可以通过注解的value属性指定bean的名字，如果没有指定，默认为类名首字母小写\n前面声明的bean的四大注解，想要生效，还需要被组件扫描注解**@ComponentScan**扫描 该注解虽然没有显示配置，但是实际上已经包含在了启动类声明注解**@SpringBootApplication中，默认扫描的范围是启动类所在包及其子包** 按住ctrl键点击@SpringBootApplication即可看到@ComponentScan\nDI详解 实际开发中，前两种使用较多\nAutowired注解，默认是按照类型进行注入的 如果存在多个相同类型的bean，将会报出如下错误： 解决方案：\n注意：方案二中的@Qualifier中要写这个类的名字(默认是首字母小写，其他不变)\n@Aurowired是Spring框架提供的注解，而@Resource是JavaEE规范提供的\n@Aurowired默认是按照类型注入的，而@Resource默认是按照名称注入的\n","date":"2026-04-08T22:38:33+08:00","permalink":"https://aurora156.github.io/p/web%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/","title":"Web后端开发"},{"content":"Maven 概述 1.基本信息 项目对象模型：\n①\n②\n依赖管理模型：\ndependency\n2.安装步骤 ①解压Maven文件：\n这里我解压到的位置是：D:\\develop（注意：目录不要带空格）\n②配置本地仓库：\n在你的maven目录中新建一个mvn_repo(指定为Maven的本地仓库)\n然后修改maven的配置文件，打开conf文件中的settings.xml(打开方式不要选记事本，此处我选择的是vs code)，然后找到\n1 \u0026lt;localRepository\u0026gt;D:\\develop.m2\\apache-maven-3.9.11\\mvn_repo\u0026lt;/localRepository\u0026gt; 将其复制粘贴到注释之外，接着往下拉，找到mirror标签(不是mirrors)，将原本的内容删除，配置阿里云的私服地址：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;alimaven\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;aliyun maven\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;http://maven.aliyun.com/nexus/content/groups/public/\u0026lt;/url\u0026gt; \u0026lt;mirrorOf\u0026gt;central\u0026lt;/mirrorOf\u0026gt; \u0026lt;/mirror\u0026gt; ③配置环境变量\n打开系统属性 -\u0026gt; 环境变量 -\u0026gt; 新建 -\u0026gt; 变量名：MAVEN_HOME，值为你所下载的maven的位置，接着在PATH路径中添加%MAVEN_HOME%\\bin\n全部点击确定以后打开cmd输入mvn -v,若出现以下样式，则说明安装成功了\n![](assets/mvn- v.png)\nIDEA集成Maven项目 1.创建Maven项目 完成以上全局配置(注意：是全局配置)：首先点击close-project，再点击Customizs，接着点击All settings即可进入到全局配置\n创建模块，选择new module，填写模块信息，选择构建工具为Maven，点击create，创建完成\n2.Maven坐标 概念：是资源(jar)的唯一标识，通过该坐标可以唯一定位资源位置\n使用坐标来定义项目或引入项目中需要的依赖\n主要组成：\ngroupId：定义当前Maven项目隶属组织名称(通常是域名反写) artifacId:定义当前Maven项目名称(通常是模块名称) version:定义当前版本号 SNAPSHOT：功能不稳定、尚处于开发中的版本，即快照版本 RELEASE：功能趋于稳定、当前更新停止，可以用于发行的版本 3.导入Maven项目 方式一：File -\u0026gt; Project Structure -\u0026gt; Modules -\u0026gt; Import Module -\u0026gt; 选择maven项目的pom.xml\n方式二：Maven面板 -\u0026gt; (Add Maven Projects) -\u0026gt; 选择maven项目的pom.xml\n建议将要导入的maven项目复制到你的项目目录下 建议选择maven项目的pom.xml文件进行导入 依赖管理 1.依赖配置 ①依赖：指当前项目运行所需要的jar包，一个项目中可以引入多个依赖\n②配置：\n1.在pom.xml中编写\u0026lt;dependencies\u0026gt;标签\n2.在\u0026lt;dependencies\u0026gt;标签中使用\u0026lt;dependency\u0026gt;标签\n3.定义坐标的groupId,artifactId,version\n4.点击刷新按钮，引入最新加入的坐标\n依赖坐标信息：https://mvnrepository.com/\n③排除依赖：指主动断开依赖的资源，被排除的资源无需指定版本\n\u0026lt;exclusions\u0026gt;\u0026lt;/exclusions\u0026gt;\n2.生命周期 Maven的生命周期就是为了对所有的maven项目构建过程进行抽象和统一\nMaven中有三套相互独立的生命周期(Lifecycle)：\nclean:清理工作 default：核心工作，如：编译，测试，打包，安装，部署等 site：生成报告、发布站点等 每套生命周期包含一些阶段，阶段是有顺序的，后面的阶段依赖于前面的阶段\n只需要知道标红的五个阶段即可\nclean:移除上一次构建生成的文件 compile：编译项目源代码 test：使用合适的单元测试框架运行测试(junit) package：将编译后的文件打包，如：jar、war等 install：安装项目到本地仓库 注意：在同一套生命周期中，当运行后面的阶段时，前面的阶段都会运行\n执行指定生命周期的两种方式：\n在idea中，右侧的Maven工具栏，选中对应的生命周期，双击执行 在命令行中，通过命令执行 单元测试 1.测试 测试：是一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程 阶段划分：单元测试、集成测试、系统测试、验收测试 测试方法：白盒测试、黑盒测试及灰盒测试\n单-\u0026gt;白、集-\u0026gt;灰、系-\u0026gt;黑、验-\u0026gt;黑\n2.单元测试 单元测试：就是针对最小的功能单元(方法)，编写测试代码及其正确性进行测试 JUnit：最流行的java测试框架之一，提供了一些功能，方便程序进行单元测试(第三方公司提供) main方法测试的弊端： ①测试代码与源代码未分开，难维护 ②一个方法测试失败，影响后面的方法 ③无法自动化测试，得到测试报告\nJunit方法测试的好处：(推荐) ①测试代码与源代码分开，便于维护 ②可根据需要进行自动化测试 ③可自动化分析测试结果，产出测试报告\n①在pom.xml中，引入JUnit的依赖\n②在test/java目录下，创建测试类，并编写对应的测试方法，并在方法上声明@Test注解\n③运行单元测试(测试通过：绿色，测试失败：红色)\n注意：JUnit单元测试类命名规范为：XxxxxTest【规范】。JUnit单元测试的方法，必须声明为Public void【规定】：public void xxxx(){\u0026hellip;}\n断言 String msg:提示错误信息\n最后一个用来判断抛出的异常与我期望的异常是否是一致的（出现异常时使用的断言）\n注解 JUnit单元测试时通过参数化测试进行声明方法形参(@P+@V)\n其中：第二个和第三个(P、V)搭配使用，两B和两A之间的区别：BE、AE的执行次数为：有N个测试方法就执行N次，BA和AA是：整个测试类只执行1次，BE、AE的核心作用：初始化/清理每个测试独有的资源，BA、AA：初始化/清理全局共享的静态资源\nBA：\nPV：\n在测试过程中可以在测试前加上@DisplayName()，为测试添加说明\n(下图为在@parameterizedTest前加上此注解的运行结果)\n企业开发规范 原则：编写测试方法时，要尽可能的覆盖业务方法中所有可能的情况(尤其是边界值)。 依赖范围 在Maven项目中，test目录存放单元测试的代码可以放在main目录下，但是不规范，所以引入了依赖范围\n依赖的jar包，默认情况下，可以在任何地方使用，可以通过\u0026hellip;设置其作用范围。 作用范围： 主程序范围有效（main文件夹内） 测试范围有效（test文件夹范围内） 是否参与打包运行（package指令范围内) 例如：\n如果在测试中想要跳过test测试，只需按图中所示禁用即可\nMaven常见问题 Maven下载不下来 产生原因：由于网络不好，依赖没有下载完整导致的，在maven仓库中生成了xxx.lastupdate文件，该文件不删除，不会重新下载\n解决方案：\n1.根据maven依赖的坐标，找到仓库中对应的xxx.lastupdate文件，删除，删除之后重新加载项目即可\n2.通过命令(del /s *.lastupdate)批量递归删除指定目录下的xxx.lastupdate文件，删除之后重新加载项目即可（建议）（在maven结构目录下）\n（如果以上方法仍旧不行，那么重新打开项目，基本不会有问题）\n","date":"2026-04-01T17:51:36+08:00","permalink":"https://aurora156.github.io/p/maven/","title":"Maven"},{"content":"Hugo+Github搭建个人博客 参考视频：【【雷】Hugo + Github免费搭建博客，并实现自动化部署】 https://www.bilibili.com/video/BV1bovfeaEtQ/?share_source=copy_web\u0026vd_source=b5c399783868e9dbf7a8c84cca665abf\n下载HUGO HUGO官网：https://gohugo.io/\n4.找到自己电脑对应的版本\n5.解压后在文件夹中打开cmd\n6.在cmd中输入hugo new site 要创建文件夹的名字\n7.若配置了hugo的全局环境变量则无需执行copy命令，若不知道是否配置，则与图中操作一样\n下载网页主题并启动 8.配置完成后先看看启动后的样子是怎样的\n10.这时打开的网址什么都没有，但服务其实已经搭好了\n11.查看完后先把cmd窗口放到后台，别关掉，重新打开hugo官网\n12.\n15.选择版本随意，但此处建议使用3.30.0（因为视频教程中用的是这个，使用其他版本在后续的配置中可能会无法按照教程继续进行）\n17.下载好主题后，把主题压缩包解压到hugo的theme文件夹中\n18.打开下载的hugo-theme-stack-3.30.0文件夹，再打开其中的exampleSite文件夹\n20.粘贴到hugo的dev文件夹下\n21.删掉hugo.toml，因为刚刚复制的hugo.yaml与其作用是一致的，相当于已经配置好了的hugo.toml\n22.然后打开刚刚复制过来的content文件夹，再打开之中的post文件夹\n23.因为rich-content中有引用国外网站，可能会导致之后无法启动网页\n24.回到dev文件夹\n26.此时已经可以启动网页了，点开步骤8时放在后台的cmd窗口，输入hugo server -D\n27.注意别关闭cmd窗口，关闭后就打不开下方网页了\n创建第一个博客 28.网页启动后可以在cmd窗口用快捷键Ctrl+C结束网页，此时就可以继续输入命令了。\n输入hugo new content post/myFirstBlog/index.md创建新内容\n29.在cmd窗口输入hugo server -D，重新启动网页\n31.Hugo的默认根路径在在static文件夹，所以要注意修改笔记中图片的路径(以下为我存放图片的位置)\nstack主题配置 见参考视频【【雷】Hugo + Github免费搭建博客，并实现自动化部署】 https://www.bilibili.com/video/BV1bovfeaEtQ/?p=2\u0026share_source=copy_web\u0026vd_source=b5c399783868e9dbf7a8c84cca665abf\nGithub常规部署 32.下载Git：https://git-scm.com/downloads/win\n33.登录Github：https://github.com/\n34.若Github要2FA验证，可以下载下方插件\n35.创建仓库\n37.修改hugo.yaml中baseurl\n38.\n打开dev中的public文件夹，在路径处输入cmd\n补充（非必要，可不看）：可以先删掉public文件夹，再在cmd窗口输入hugo -D重新生成一个public文件夹。因为在配置主题的时候可能会把很多没必要的地方删掉，而删掉的部分仍留在public中，这时就可以用hugo -D重新生成一次静态页面\n39.然后在cmd窗口依次输入git init、git add .、git commit -m \u0026quot;first commit\u0026quot;、git branch -M main、git remote add origin https://github.com/xxx/xxx.github.io.git（这里填你自己的仓库地址）、git push -u origin main\n40.如果和图中一样说明部署完成了\n","date":"2026-03-05T21:31:04+08:00","permalink":"https://aurora156.github.io/p/myfirstblog/","title":"MyFirstBlog"}]