[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"\u002Fresource\u002Fdocument\u002Flist?undefined":3,"\u002Fresource\u002Fdocument\u002Fquery\u002Feve8gq72qmdb46sg?undefined":462,"\u002Fresource\u002Fadvertise\u002Flist?type=all?undefined":465},{"data":4,"status":460,"success":461},[5,148,202,291,332,370,420],{"books":6,"desc":145,"id":8,"image":146,"title":147},[7,40,63,78,93,105,117],{"cateId":8,"chapters":9,"desc":36,"id":11,"time":37,"title":38,"video":39},1,[10,15,18,21,24,27,30,33],{"bookId":11,"id":12,"indexOrder":13,"name":14},24,"8egfulw98v3h680j",0,"JavaSE 笔记（一）走进Java语言",{"bookId":11,"id":16,"indexOrder":13,"name":17},"pew6po6wrou23pk3","JavaSE 笔记（二）面向过程编程",{"bookId":11,"id":19,"indexOrder":13,"name":20},"eldst1fgrbdkmfs7","JavaSE 笔记（三）面向对象基础",{"bookId":11,"id":22,"indexOrder":13,"name":23},"48zphgkpjto8cath","JavaSE 笔记（四）面向对象高级篇",{"bookId":11,"id":25,"indexOrder":13,"name":26},"6r4llai92yc15j98","JavaSE 笔记（五）泛型程序设计",{"bookId":11,"id":28,"indexOrder":13,"name":29},"k6fmxd6qabgkwm9i","JavaSE 笔记（六）集合类与IO",{"bookId":11,"id":31,"indexOrder":13,"name":32},"qrd0xfttsz32gpqg","JavaSE 笔记（七）多线程与反射",{"bookId":11,"id":34,"indexOrder":13,"name":35},"td5tgn04nqmkrryt","JavaSE 笔记（八）GUI程序开发","基于Java25全新录制的SE课程",2025,"JavaSE 核心内容","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV163GGz2E8c",{"cateId":8,"chapters":41,"desc":59,"id":8,"time":60,"title":61,"video":62},[42,44,46,49,51,53,55,57],{"bookId":8,"id":43,"indexOrder":13,"name":14},"ibeeuwsbbi00undq",{"bookId":8,"id":45,"indexOrder":13,"name":17},"dncxjecdv4wciqcp",{"bookId":8,"id":47,"indexOrder":13,"name":48},"jviyz2hsht9ete5k","JavaSE 笔记（三）面向对象基础篇",{"bookId":8,"id":50,"indexOrder":13,"name":23},"qb9i6q9fap7bg1cc",{"bookId":8,"id":52,"indexOrder":13,"name":26},"hnkrjrkm3hjzeq6s",{"bookId":8,"id":54,"indexOrder":13,"name":29},"erpm32wduoaaqmrx",{"bookId":8,"id":56,"indexOrder":13,"name":32},"lfqtvxr7azumcwja",{"bookId":8,"id":58,"indexOrder":13,"name":35},"qs7gqok56gzc6idr","2022年制作的JavaSE版本",2022,"JavaSE 22年旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1YP4y1o75f\u002F",{"cateId":8,"chapters":64,"desc":75,"id":66,"time":60,"title":76,"video":77},[65,69,72],{"bookId":66,"id":67,"indexOrder":13,"name":68},2,"g96k66kczovvbm1i","JVM 笔记（一）走进JVM",{"bookId":66,"id":70,"indexOrder":13,"name":71},"ydd7n3jg8unc3clg","JVM 笔记（二）内存管理",{"bookId":66,"id":73,"indexOrder":13,"name":74},"r9dq37de0kaeauoi","JVM 笔记（三）类与类加载","了解Java的底层运作机制","Java JVM 虚拟机","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Er4y1r7as\u002F",{"cateId":8,"chapters":79,"desc":90,"id":81,"time":60,"title":91,"video":92},[80,84,87],{"bookId":81,"id":82,"indexOrder":13,"name":83},3,"asncyye9ya18gfar","JUC 笔记（一）再谈多线程",{"bookId":81,"id":85,"indexOrder":13,"name":86},"5tr1sm4ho6ygpt9q","JUC 笔记（二）并发编程核心",{"bookId":81,"id":88,"indexOrder":13,"name":89},"1scf51z5300mzxkh","JUC 笔记（三）并发编程进阶","你也可以成为多线程的主宰者","Java JUC 并发编程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1JT4y1S7K8\u002F",{"cateId":8,"chapters":94,"desc":102,"id":96,"time":60,"title":103,"video":104},[95,99],{"bookId":96,"id":97,"indexOrder":13,"name":98},4,"eedesc445ygiqhil","NIO 笔记（一）基础内容",{"bookId":96,"id":100,"indexOrder":13,"name":101},"ndz9t0uunrmfmv4n","NIO 笔记（二）Netty框架专题","编写畅快的高性能网络服务器","Java NIO 网络编程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1ar4y1J7mC\u002F",{"cateId":8,"chapters":106,"desc":114,"id":108,"time":60,"title":115,"video":116},[107,111],{"bookId":108,"id":109,"indexOrder":13,"name":110},5,"9890i8ofuadpwy2b","[扩展篇] Java 9-17新特性介绍",{"bookId":108,"id":112,"indexOrder":13,"name":113},"tsrkqvb6zpmtwh0n","[扩展篇] JavaSE关键字总结 笔记","精彩仍在继续，不要停止脚步","其他内容","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1tU4y1y7Fg\u002F",{"cateId":8,"chapters":118,"desc":141,"id":120,"time":142,"title":143,"video":144},[119,123,126,129,132,135,138],{"bookId":120,"id":121,"indexOrder":13,"name":122},6,"4db9h32opv7imszh","JavaSE 笔记（一）面向过程编程",{"bookId":120,"id":124,"indexOrder":13,"name":125},"c93u3v37br7hgn1q","JavaSE 笔记（二）面向对象基础篇",{"bookId":120,"id":127,"indexOrder":13,"name":128},"yglsjde9gi1jxkcb","JavaSE 笔记（三）泛型与集合类",{"bookId":120,"id":130,"indexOrder":13,"name":131},"ilhi987n986rmvo3","JavaSE 笔记（四）异常机制",{"bookId":120,"id":133,"indexOrder":13,"name":134},"pqv38vexmenglk4k","JavaSE 笔记（五）IO",{"bookId":120,"id":136,"indexOrder":13,"name":137},"jiq41n87i9ia7ilw","JavaSE 笔记（六）多线程",{"bookId":120,"id":139,"indexOrder":13,"name":140},"wn7x2mge9ws79zps","JavaSE 笔记（七）反射","此版本为早期录制的旧版本",2021,"JavaSE 21年旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Gv411T7pi\u002F","包含JavaSE基础路线全部教程笔记，打下坚实的基础","https:\u002F\u002Fpic2.zhimg.com\u002F80\u002Fv2-bf1a927f037a79f4d57d9ae543430a0d_1440w.webp","JavaSE 系列笔记 ☕️",{"books":149,"desc":199,"id":66,"image":200,"title":201},[150,166,178],{"cateId":66,"chapters":151,"desc":162,"id":153,"time":163,"title":164,"video":165},[152,156,159],{"bookId":153,"id":154,"indexOrder":13,"name":155},21,"iqbc2haub31bwqtz","Lombok 极速上手",{"bookId":153,"id":157,"indexOrder":13,"name":158},"ijay2hay19kn1k031","Mybatis 快速上手",{"bookId":153,"id":160,"indexOrder":13,"name":161},"ru4ogh2waocpn4jo","Maven 快速上手","JavaWeb阶段必须扩展知识点",2024,"常用知识讲解","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1gb421J7ok\u002F",{"cateId":66,"chapters":167,"desc":175,"id":169,"time":163,"title":176,"video":177},[168,172],{"bookId":169,"id":170,"indexOrder":13,"name":171},22,"ek20yvb6huhxizx7","JavaWeb 笔记（一）计算机网络基础",{"bookId":169,"id":173,"indexOrder":13,"name":174},"pgevws6w2krkffa4","JavaWeb笔记（二）Java与数据库","全面升级的JavaWeb课程","JavaWeb 网站开发","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1kS421X7rq\u002F",{"cateId":66,"chapters":179,"desc":196,"id":181,"time":142,"title":197,"video":198},[180,184,187,190,193],{"bookId":181,"id":182,"indexOrder":13,"name":183},7,"ggwwj09j2vkfftvd","JavaWeb 笔记（一）Java网络编程",{"bookId":181,"id":185,"indexOrder":13,"name":186},"sauvq105istskjaz","JavaWeb 笔记（二）数据库基础",{"bookId":181,"id":188,"indexOrder":13,"name":189},"xgbeasmvrhxx9tn4","JavaWeb 笔记（三）Java与数据库",{"bookId":181,"id":191,"indexOrder":13,"name":192},"k7dfwua3bsezvw9q","JavaWeb 笔记（四）前端基础",{"bookId":181,"id":194,"indexOrder":13,"name":195},"ycpagby2v7j4p728","JavaWeb 笔记（五）后端开发","搭建属于自己的Web网站","JavaWeb 旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1CL4y1i7qR\u002F","包含JavaWeb路线全套笔记，从零开始搭建自己的网站！","https:\u002F\u002Fpic3.zhimg.com\u002F80\u002Fv2-df3b38e3012258ed70c23b586309e3f6_1440w.webp","JavaWeb 系列笔记 🚛",{"books":203,"desc":288,"id":81,"image":289,"title":290},[204,220,235,255,273],{"cateId":81,"chapters":205,"desc":216,"id":207,"time":217,"title":218,"video":219},[206,210,213],{"bookId":207,"id":208,"indexOrder":13,"name":209},8,"h7sjo5oy0l03607e","SSM笔记（一）Spring基础",{"bookId":207,"id":211,"indexOrder":13,"name":212},"eve8gq72qmdb46sg","SSM笔记（二）SpringMvc基础",{"bookId":207,"id":214,"indexOrder":13,"name":215},"63v73g0zh1qlr6fk","SSM笔记（三）SpringSecurity基础","Spring的探索之路从这里开始",2023,"JavaSSM 基础部分","[\"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Kv4y1x7is\u002F\", \"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Lh4y1M7kx\u002F\", \"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1fV411M7aS\u002F\"]",{"cateId":81,"chapters":221,"desc":232,"id":223,"time":217,"title":233,"video":234},[222,226,229],{"bookId":223,"id":224,"indexOrder":13,"name":225},16,"0k66v5r6slsfuog4","SpringBoot笔记（一）核心内容",{"bookId":223,"id":227,"indexOrder":13,"name":228},"bqlrnc2yvkaxo8s1","SpringBoot笔记（二）数据交互",{"bookId":223,"id":230,"indexOrder":13,"name":231},"wci9lb9tgea866jt","SpringBoot笔记（三）前后端分离","SpringBoot全新重制版","SpringBoot 新版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1xu4y1m7UP\u002F",{"cateId":81,"chapters":236,"desc":252,"id":238,"time":60,"title":253,"video":254},[237,240,243,246,249],{"bookId":238,"id":239,"indexOrder":13,"name":225},9,"e43gl1ilygps032v",{"bookId":238,"id":241,"indexOrder":13,"name":242},"emnmd8nzfdb3hr50","SpringBoot笔记（二）Git版本控制",{"bookId":238,"id":244,"indexOrder":13,"name":245},"jjlolj5igvttvyhv","SpringBoot笔记（三）Redis数据库",{"bookId":238,"id":247,"indexOrder":13,"name":248},"skgr4ivb5curdoux","SpringBoot笔记（四）其他框架介绍",{"bookId":238,"id":250,"indexOrder":13,"name":251},"le91fqhu4dqui1k4","SpringBoot笔记（五）Linux系统","逐步走向企业级开发","SpringBoot 旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1UL411V7f3\u002F",{"cateId":81,"chapters":256,"desc":270,"id":258,"time":60,"title":271,"video":272},[257,261,264,267],{"bookId":258,"id":259,"indexOrder":13,"name":260},10,"oejzo0l77zeb6a7e","SpringCloud笔记（一）微服务基础",{"bookId":258,"id":262,"indexOrder":13,"name":263},"f6eya9taaelsl35p","SpringCloud笔记（二）微服务进阶",{"bookId":258,"id":265,"indexOrder":13,"name":266},"35v1hbsfcdgagdnw","SpringCloud笔记（三）微服务应用",{"bookId":258,"id":268,"indexOrder":13,"name":269},"a782u84512tyuo1m","SpringCloud笔记（四）消息队列","体验微服务架构带来的魅力","SpringCloud 进阶","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1AL4y1j7RY\u002F",{"cateId":81,"chapters":274,"desc":285,"id":276,"time":142,"title":286,"video":287},[275,278,280,282],{"bookId":276,"id":277,"indexOrder":13,"name":209},11,"efjw75u8a251qxk5",{"bookId":276,"id":279,"indexOrder":13,"name":212},"guc134xb7sl78vju",{"bookId":276,"id":281,"indexOrder":13,"name":215},"u8ekxxucowr2b1tm",{"bookId":276,"id":283,"indexOrder":13,"name":284},"vkpmw9wbej21nei6","SSM笔记（四）MySQL进阶","此教程为2021年旧版教程","JavaSSM 旧版","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1xL4y1H7Tq\u002F","包含Spring全套框架笔记，从开始到Spring Boot，以及众多运维小知识。","https:\u002F\u002Fpic4.zhimg.com\u002F80\u002Fv2-28c3144421220d7c048703281bc34f63_1440w.webp","Spring 系列笔记 🍏",{"books":292,"desc":329,"id":96,"image":330,"title":331},[293,308],{"cateId":96,"chapters":294,"desc":305,"id":296,"time":60,"title":306,"video":307},[295,299,302],{"bookId":296,"id":297,"indexOrder":13,"name":298},12,"jd3e8u5cmvx5gco6","C语言（一）计算机思维导论",{"bookId":296,"id":300,"indexOrder":13,"name":301},"lqv77apvx82nkkio","C语言（二）基础语法",{"bookId":296,"id":303,"indexOrder":13,"name":304},"xb0b9t37gyv96xns","C语言（三）高级特性","包含高等院校需要教授的全部内容","C语言程序设计","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Cr4y137os\u002F",{"cateId":96,"chapters":309,"desc":326,"id":311,"time":60,"title":327,"video":328},[310,314,317,320,323],{"bookId":311,"id":312,"indexOrder":13,"name":313},13,"8a046ps2e4w6k4py","数据结构与算法（一）线性结构篇",{"bookId":311,"id":315,"indexOrder":13,"name":316},"3ma8db91f9zrnkja","数据结构与算法（二）树形结构篇",{"bookId":311,"id":318,"indexOrder":13,"name":319},"0lsjm59k7cgu4tpr","数据结构与算法（三）散列表篇",{"bookId":311,"id":321,"indexOrder":13,"name":322},"0qzy7bogo0g2pusa","数据结构与算法（四）图结构篇",{"bookId":311,"id":324,"indexOrder":13,"name":325},"6gmcxcikcilyxblj","数据结构与算法（五）排序算法篇","虽然很难，但是它是考研必学科目","数据结构与算法","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV13W4y127Ey\u002F","你的内心一直有一个坚定的声音在告诉你，一定要考上一名研究生，向着未来前进吧！","https:\u002F\u002Fpic2.zhimg.com\u002F80\u002Fv2-ac128404efb29ce1c9d1ccc61024f1d1_1440w.webp","C语言 系列笔记 🥬",{"books":333,"desc":367,"id":108,"image":368,"title":369},[334,349,358],{"cateId":108,"chapters":335,"desc":346,"id":337,"time":163,"title":347,"video":348},[336,340,343],{"bookId":337,"id":338,"indexOrder":13,"name":339},17,"urw2e6gg1lprv65w","Kotlin（一）基础语法",{"bookId":337,"id":341,"indexOrder":13,"name":342},"t7lnl87f74f3v1ju","Kotlin（二）类与对象",{"bookId":337,"id":344,"indexOrder":13,"name":345},"v1zzvki0knb1xvml","Kotlin（三）高级特性","包含Kotlin语言完整基础部分","Kotlin程序设计基础","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1P94y1c7tV\u002F",{"cateId":108,"chapters":350,"desc":355,"id":352,"time":163,"title":356,"video":357},[351],{"bookId":352,"id":353,"indexOrder":13,"name":354},18,"ovbzpe7065bye1st","Kotlin扩展（一）","包含Kotlin额外扩展知识","Kotlin扩展篇","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Hg4y1m7Ca\u002F",{"cateId":108,"chapters":359,"desc":364,"id":361,"time":163,"title":365,"video":366},[360],{"bookId":361,"id":362,"indexOrder":13,"name":363},19,"3at7ybv04dmjc0wp","Gradle基础教程","Gradle配置教程（Kotlin）","Gradle教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1Fc411x7xF\u002F","Kotlin让JVM平台焕发新的生机，让语言的表达更加优美","https:\u002F\u002Fpic2.zhimg.com\u002F80\u002Fv2-be815568f7c79c64cdaa171b0409786d_1440w.webp","Kotlin 系列笔记 ☘️",{"books":371,"desc":418,"id":120,"title":419},[372,391,403],{"cateId":120,"chapters":373,"desc":387,"id":375,"time":388,"title":389,"video":390},[374,378,381,384],{"bookId":375,"id":376,"indexOrder":13,"name":377},26,"zjf5qapwqtqiohcn","JavaScript笔记（一）基础语法",{"bookId":375,"id":379,"indexOrder":13,"name":380},"95jc6sjyjwcp9pvp","JavaScript笔记（二）核心知识",{"bookId":375,"id":382,"indexOrder":13,"name":383},"j35cdc1qz8dzq7pn","JavaScript笔记（三）进阶知识",{"bookId":375,"id":385,"indexOrder":13,"name":386},"sdhodlihphnpcg37","JavaScript笔记（四）前端基础","包含JavaScript最新语法规范讲解",2026,"JavaScript教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1xq6gBgESU",{"cateId":120,"chapters":392,"desc":400,"id":394,"time":37,"title":401,"video":402},[393,397],{"bookId":394,"id":395,"indexOrder":13,"name":396},23,"bsisgazdftiz3o9c","HTML5笔记（一）基础内容",{"bookId":394,"id":398,"indexOrder":13,"name":399},"njol93fs34gfwuzf","HTML5笔记（二）高级内容","包含HTML基础内容和相关知识点","HTML5核心教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1BrBiYNEWg",{"cateId":120,"chapters":404,"desc":415,"id":406,"time":37,"title":416,"video":417},[405,409,412],{"bookId":406,"id":407,"indexOrder":13,"name":408},25,"jo74ciirtg8wh90y","CSS笔记（一）基础入门",{"bookId":406,"id":410,"indexOrder":13,"name":411},"ap5ixyomoejuw4ue","CSS笔记（二）盒模型和布局",{"bookId":406,"id":413,"indexOrder":13,"name":414},"4djgk5xy1lzpiuf2","CSS笔记（三）变换和过渡","包含CSS3基础内容和相关知识点","CSS3核心教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1sQeEzFEKi","包含Web前端学习路径全部教程笔记，打下坚实的基础","Web前端 系列笔记",{"books":421,"desc":458,"id":423,"image":368,"title":459},[422,432,450],{"cateId":423,"chapters":424,"desc":429,"id":426,"time":163,"title":430,"video":431},100,[425],{"bookId":426,"id":427,"indexOrder":13,"name":428},20,"o0ab271mkdsas87","Markdown基础语法","编写简洁而又优美的文档","Markdown教程","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1eJ4m157kC",{"cateId":423,"chapters":433,"desc":447,"id":435,"time":60,"title":448,"video":449},[434,438,441,444],{"bookId":435,"id":436,"indexOrder":13,"name":437},14,"6386mh7anqt4tzyv","设计模式（一）面向对象设计原则",{"bookId":435,"id":439,"indexOrder":13,"name":440},"8ftkb38wfn6ox0ug","设计模式（二）创建型",{"bookId":435,"id":442,"indexOrder":13,"name":443},"i1msql1k8y70etey","设计模式（三）结构型",{"bookId":435,"id":445,"indexOrder":13,"name":446},"5434a3cyyjvwhs8s","设计模式（四）行为型","使你的编码水平得到质的飞跃","设计模式系列","https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1u3411P7Na\u002F",{"cateId":423,"chapters":451,"desc":456,"id":453,"time":60,"title":457},[452],{"bookId":453,"id":454,"indexOrder":13,"name":455},15,"zj9uvg0sp3b0sok8","Docker 容器技术 笔记","这里包含其他中间件课程笔记","其他中间件笔记","我们对知识的探索从未停止，只有不断地学习，才能走向美好的未来！","其他笔记分类 🌽",200,true,{"data":463,"status":460,"success":461},{"bookId":207,"content":464,"id":211,"indexOrder":66,"name":212},"# SpringMVC基础\n\n**进入之前：** 《Spring核心内容》《JavaWeb》《JDK9-17新特性篇》\n\n在前面学习完Spring框架技术之后，差不多会出现两批人：一批是听得云里雾里，依然不明白这个东西是干嘛的；还有一批就是差不多理解了核心思想，但是不知道这些东西该如何去发挥它的作用。不过没有关系，在SpringMVC阶段，你一定能逐渐够体会到Spring框架为我们带来的便捷之处。\n\n此阶段，我们将再次回到Tomcat的Web应用程序开发中，去感受Spring框架为我们带来的巨大便捷。\n\n## MVC理论基础\n\n在之前，我们给大家讲解了三层架构，包括：\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F18\u002F2IiK8YrfhF4zyU1.jpg)\n\n每一层都有着各自的职责，其中最关键的当属表示层，因为它相当于就是直接与用户的浏览器打交道的一层，并且所有的请求都会经过它进行解析，然后再告知业务层进行处理，任何页面的返回和数据填充也全靠表示层来完成，因此它实际上是整个三层架构中最关键的一层，而在之前的实战开发中，我们编写了大量的Servlet（也就是表示层实现）来处理来自浏览器的各种请求，但是我们发现，仅仅是几个很小的功能，以及几个很基本的页面，我们都要编写将近十个Servlet，如果是更加大型的网站系统，比如淘宝、B站，光是一个页面中可能就包含了几十甚至上百个功能，想想那样的话写起来得多恐怖。\n\n因此，SpringMVC正是为了解决这种问题而生的，它是一个非常优秀的表示层框架，采用MVC思想设计实现。\n\nMVC详细解释如下：\n\n- M是指业务模型（Model）：通俗的讲就是我们之前用于封装数据传递的实体类。\n- V是指用户界面（View）：一般指的是前端页面。\n- C则是控制器（Controller）：控制器就相当于Servlet的基本功能，处理请求，返回响应。\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F18\u002Fvoy7HYrIbJuw9R3.jpg)\n\nSpringMVC正是希望这三者之间进行解耦，实现各干各的，更加精细地划分对应的职责。最后再将View和Model进行渲染，得到最终的页面并返回给前端（就像之前使用Thymeleaf那样，把实体数据对象和前端页面都给到Thymeleaf，然后它会将其进行整合渲染得到最终有数据的页面，而本教程也会使用Thymeleaf作为视图解析器进行讲解）\n\n***\n\n## 配置环境并搭建项目\n\n这里我们继续使用之前的Tomcat10服务器，Spring6之后要求必须使用Tomcat10或更高版本，跟之前一样，我们直接创建一个新的JakartaEE项目。\n\n![image-20230219162053172](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F19\u002F4IucyfBKsLzASNJ.png)\n\n创建完成后会自动生成相关文件，但是还是请注意检查运行配置中的URL和应用程序上下文名称是否一致。\n\n### 传统XML配置形式\n\nSpringMvc项目依然支持多种配置形式，这里我们首先讲解最传统的XML配置形式。\n\n首先我们需要添加Mvc相关依赖：\n\n```xml\n\u003Cdependency>\n    \u003CgroupId>org.springframework\u003C\u002FgroupId>\n    \u003CartifactId>spring-webmvc\u003C\u002FartifactId>\n    \u003Cversion>6.0.10\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n接着我们需要配置一下web.xml，将DispatcherServlet替换掉Tomcat自带的Servlet，这里url-pattern需要写为`\u002F`，即可完成替换：\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003Cweb-app xmlns=\"https:\u002F\u002Fjakarta.ee\u002Fxml\u002Fns\u002Fjakartaee\"\n         xmlns:xsi=\"http:\u002F\u002Fwww.w3.org\u002F2001\u002FXMLSchema-instance\"\n         xsi:schemaLocation=\"https:\u002F\u002Fjakarta.ee\u002Fxml\u002Fns\u002Fjakartaee https:\u002F\u002Fjakarta.ee\u002Fxml\u002Fns\u002Fjakartaee\u002Fweb-app_5_0.xsd\"\n         version=\"5.0\">\n    \u003Cservlet>\n        \u003Cservlet-name>mvc\u003C\u002Fservlet-name>\n        \u003Cservlet-class>org.springframework.web.servlet.DispatcherServlet\u003C\u002Fservlet-class>\n    \u003C\u002Fservlet>\n    \u003Cservlet-mapping>\n        \u003Cservlet-name>mvc\u003C\u002Fservlet-name>\n        \u003Curl-pattern>\u002F\u003C\u002Furl-pattern>\n    \u003C\u002Fservlet-mapping>\n\u003C\u002Fweb-app>\n```\n\n接着需要为整个Web应用程序配置一个Spring上下文环境（也就是容器），因为SpringMVC是基于Spring开发的，它直接利用Spring提供的容器来实现各种功能，那么第一步依然跟之前一样，需要编写一个配置文件：\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003Cbeans xmlns=\"http:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fbeans\"\n       xmlns:xsi=\"http:\u002F\u002Fwww.w3.org\u002F2001\u002FXMLSchema-instance\"\n       xsi:schemaLocation=\"http:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fbeans\n        https:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fbeans\u002Fspring-beans.xsd\">\n\u003C\u002Fbeans>\n```\n\n接着我们需要为DispatcherServlet配置一些初始化参数来指定刚刚创建的配置文件：\n\n```xml\n\u003Cservlet>\n    \u003Cservlet-name>mvc\u003C\u002Fservlet-name>\n    \u003Cservlet-class>org.springframework.web.servlet.DispatcherServlet\u003C\u002Fservlet-class>\n    \u003Cinit-param>\n      \t\u003C!--     指定我们刚刚创建在类路径下的XML配置文件       -->\n        \u003Cparam-name>contextConfigLocation\u003C\u002Fparam-name>\n        \u003Cparam-value>classpath:application.xml\u003C\u002Fparam-value>\n    \u003C\u002Finit-param>\n\u003C\u002Fservlet>\n```\n\n这样我们就完成了基本的配置，现在我们可以来测试一下是否配置正确，我们删除项目自带的Servlet类，创建一个Mvc中使用的Controller类，现在还没学没关系，跟着写就行了，这里我们只是测试一下：\n\n```java\n@Controller\npublic class HelloController {\n    @ResponseBody\n    @RequestMapping(\"\u002F\")\n    public String hello(){\n        return \"HelloWorld!\";\n    }\n}\n```\n\n接着我们需要将这个类注册为Bean才能正常使用，我们来编写一下Spring的配置文件，这里我们直接配置包扫描，XML下的包扫描需要这样开启：\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003Cbeans xmlns=\"http:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fbeans\"\n       xmlns:xsi=\"http:\u002F\u002Fwww.w3.org\u002F2001\u002FXMLSchema-instance\"\n       xmlns:context=\"http:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fcontext\"\n       xsi:schemaLocation=\"http:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fbeans\n        https:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fbeans\u002Fspring-beans.xsd http:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fcontext https:\u002F\u002Fwww.springframework.org\u002Fschema\u002Fcontext\u002Fspring-context.xsd\">\n  \t\u003C!-- 需要先引入context命名空间，然后直接配置base-package属性就可以了 -->\n    \u003Ccontext:component-scan base-package=\"com.example\"\u002F>\n\u003C\u002Fbeans>\n```\n\n如果可以成功在浏览器中出现HelloWorld则说明配置成功：\n\n![image-20230219170637540](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F19\u002FD1sAFePzj7d49VL.png)\n\n聪明的小伙伴可能已经发现了，实际上我们上面编写的Controller就是负责Servlet基本功能的，比如这里我们返回的是HelloWorld字符串，那么我们在访问这个地址的时候，的到的就是这里返回的字符串，可以看到写法非常简洁，至于这是怎么做到的的，怎么使用，我们会在本章进行详细介绍。\n\n### 全注解配置形式\n\n如果你希望完完全全丢弃配置文件，使用纯注解开发，可以直接添加一个类，Tomcat会在类路径中查找实现ServletContainerInitializer 接口的类，如果发现的话，就用它来配置Servlet容器，Spring提供了这个接口的实现类 SpringServletContainerInitializer , 通过@HandlesTypes(WebApplicationInitializer.class)设置，这个类反过来会查找实现WebApplicationInitializer 的类，并将配置的任务交给他们来完成，因此直接实现接口即可：\n\n```java\npublic class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {\n\n    @Override\n    protected Class\u003C?>[] getRootConfigClasses() {\n        return new Class[]{WebConfiguration.class};   \u002F\u002F基本的Spring配置类，一般用于业务层配置\n    }\n\n    @Override\n    protected Class\u003C?>[] getServletConfigClasses() {\n        return new Class[0];  \u002F\u002F配置DispatcherServlet的配置类、主要用于Controller等配置，这里为了教学简单，就不分这么详细了，只使用上面的基本配置类\n    }\n\n    @Override\n    protected String[] getServletMappings() {\n        return new String[]{\"\u002F\"};    \u002F\u002F匹配路径，与上面一致\n    }\n}\n```\n\n接着我们需要再配置类中添加一些必要的注解：\n\n```java\n@Configuration\n@EnableWebMvc   \u002F\u002F快速配置SpringMvc注解，如果不添加此注解会导致后续无法通过实现WebMvcConfigurer接口进行自定义配置\n@ComponentScan(\"com.example.controller\")\npublic class WebConfiguration {\n}\n```\n\n这样我们同样可以正常访问：\n\n![image-20230219170637540](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F19\u002FD1sAFePzj7d49VL.png)\n\n之后为了方便，我们就统一使用全注解形式编写。\n\n如果日志科技有报错无法显示Mvc相关的日志，请添加以下依赖：\n\n```xml\n\u003Cdependency>\n      \u003CgroupId>org.slf4j\u003C\u002FgroupId>\n      \u003CartifactId>slf4j-api\u003C\u002FartifactId>\n      \u003Cversion>1.7.33\u003C\u002Fversion>\n\u003C\u002Fdependency>\n\u003Cdependency>\n      \u003CgroupId>org.slf4j\u003C\u002FgroupId>\n      \u003CartifactId>slf4j-jdk14\u003C\u002FartifactId>\n      \u003Cversion>1.7.33\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n添加后就可以正常打印日志了：\n\n![image-20230630162821105](https:\u002F\u002Fs2.loli.net\u002F2023\u002F06\u002F30\u002F7eti1wuU8Bd4RqZ.png)\n\n## Controller控制器\n\n有了SpringMVC之后，我们不必再像之前那样一个请求地址创建一个Servlet了，它使用`DispatcherServlet`替代Tomcat为我们提供的默认的静态资源Servlet，也就是说，现在所有的请求（除了jsp，因为Tomcat还提供了一个jsp的Servlet）都会经过`DispatcherServlet`进行处理。\n\n那么`DispatcherServlet`会帮助我们做什么呢？\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F18\u002FSQNnl3yFjhHbp1G.jpg)\n\n根据图片我们可以了解，我们的请求到达Tomcat服务器之后，会交给当前的Web应用程序进行处理，而SpringMVC使用`DispatcherServlet`来处理所有的请求，也就是说它被作为一个统一的访问点，所有的请求全部由它来进行调度。\n\n当一个请求经过`DispatcherServlet`之后，会先走`HandlerMapping`，它会将请求映射为`HandlerExecutionChain`，依次经过`HandlerInterceptor`有点类似于之前我们所学的过滤器，不过在SpringMVC中我们使用的是拦截器，然后再交给`HandlerAdapter`，根据请求的路径选择合适的控制器进行处理，控制器处理完成之后，会返回一个`ModelAndView`对象，包括数据模型和视图，通俗的讲就是页面中数据和页面本身（只包含视图名称即可）。\n\n返回`ModelAndView`之后，会交给`ViewResolver`（视图解析器）进行处理，视图解析器会对整个视图页面进行解析，SpringMVC自带了一些视图解析器，但是只适用于JSP页面，我们也可以像之前一样使用Thymeleaf作为视图解析器，这样我们就可以根据给定的视图名称，直接读取HTML编写的页面，解析为一个真正的View。\n\n解析完成后，就需要将页面中的数据全部渲染到View中，最后返回给`DispatcherServlet`一个包含所有数据的成形页面，再响应给浏览器，完成整个过程。\n\n因此，实际上整个过程我们只需要编写对应请求路径的的Controller以及配置好我们需要的ViewResolver即可，之后还可以继续补充添加拦截器，而其他的流程已经由SpringMVC帮助我们完成了。\n\n### 配置视图解析器和控制器\n\n首先我们需要实现最基本的页面解析并返回，第一步就是配置视图解析器，这里我们使用Thymeleaf为我们提供的视图解析器，导入需要的依赖：\n\n```xml\n\u003Cdependency>\n    \u003CgroupId>org.thymeleaf\u003C\u002FgroupId>\n    \u003CartifactId>thymeleaf-spring6\u003C\u002FartifactId>\n    \u003Cversion>3.1.1.RELEASE\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n配置视图解析器非常简单，我们只需要将对应的`ViewResolver`注册为Bean即可，这里我们直接在配置类中编写：\n\n```java\n@Configuration\n@EnableWebMvc\n@ComponentScan(\"com.example.controller\")\npublic class WebConfiguration {\n    \u002F\u002F我们需要使用ThymeleafViewResolver作为视图解析器，并解析我们的HTML页面\n    @Bean\n    public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine springTemplateEngine){\n        ThymeleafViewResolver resolver = new ThymeleafViewResolver();\n        resolver.setOrder(1);   \u002F\u002F可以存在多个视图解析器，并且可以为他们设定解析顺序\n        resolver.setCharacterEncoding(\"UTF-8\");   \u002F\u002F编码格式是重中之重\n        resolver.setTemplateEngine(springTemplateEngine);   \u002F\u002F和之前JavaWeb阶段一样，需要使用模板引擎进行解析，所以这里也需要设定一下模板引擎\n        return resolver;\n    }\n\n    \u002F\u002F配置模板解析器\n    @Bean\n    public SpringResourceTemplateResolver templateResolver(){\n        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();\n        resolver.setSuffix(\".html\");   \u002F\u002F需要解析的后缀名称\n        resolver.setPrefix(\"\u002F\");   \u002F\u002F需要解析的HTML页面文件存放的位置，默认是webapp目录下，如果是类路径下需要添加classpath:前缀\n        return resolver;\n    }\n\n    \u002F\u002F配置模板引擎Bean\n    @Bean\n    public SpringTemplateEngine springTemplateEngine(ITemplateResolver resolver){\n        SpringTemplateEngine engine = new SpringTemplateEngine();\n        engine.setTemplateResolver(resolver);   \u002F\u002F模板解析器，默认即可\n        return engine;\n    }\n}\n```\n\n现在我们就完成了视图解析器的配置，我们接着来创建一个Controller，创建Controller也非常简单，只需在一个类上添加一个`@Controller`注解即可，它会被Spring扫描并自动注册为Controller类型的Bean，然后我们只需要在类中编写方法用于处理对应地址的请求即可：\n\n```java\n@Controller   \u002F\u002F直接添加注解即可\npublic class HelloController {\n\n    @RequestMapping(\"\u002Findex\")   \u002F\u002F直接填写访问路径\n    public ModelAndView index(){\n        return new ModelAndView(\"index\");  \u002F\u002F返回ModelAndView对象，这里填入了视图的名称\n      \t\u002F\u002F返回后会经过视图解析器进行处理\n    }\n}\n```\n\n接着我们在类路径根目录下创建一个简单html文件：\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Ctitle>测试\u003C\u002Ftitle>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cp>欢迎来到GayHub全球最大同性交友网站\u003C\u002Fp>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n我们会发现，打开浏览器之后就可以直接访问我们的HTML页面了：\n\n![image-20230220150905300](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F20\u002Fru4pBgI75JZxG6F.png)\n\n我们在之前，使用Thymeleaf解析后端的一些数据时，需要通过Context进行传递，而使用SpringMvc后，数据我们可以直接向Model模型层进行提供：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(){\n    ModelAndView modelAndView = new ModelAndView(\"index\");\n    modelAndView.getModel().put(\"name\", \"啊这\");   \u002F\u002F将name传递给Model\n    return modelAndView;\n}\n```\n\n这样Thymeleaf就能收到我们传递的数据进行解析：\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\" xmlns:th=\"http:\u002F\u002Fwww.thymeleaf.org\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Ctitle>Title\u003C\u002Ftitle>\n    \u003Cscript src=\"static\u002Ftest.js\">\u003C\u002Fscript>\n\u003C\u002Fhead>\n\u003Cbody>\n    HelloWorld！\n    \u003Cdiv th:text=\"${name}\">\u003C\u002Fdiv>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n当然，为了简便，我们可以直接返回View名称，SpringMVC会将其自动包装为ModelAndView对象：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic String index(){\n    return \"index\";\n}\n```\n\n我们还可以单独添加一个Model作为形参进行设置，SpringMVC通过依赖注入会自动帮助我们传递实例对象：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic String index(Model model){  \u002F\u002F这里不仅仅可以是Model，还可以是Map、ModelMap\n    model.addAttribute(\"name\", \"yyds\");\n    return \"index\";\n}\n```\n\n有了Spring框架的加持，相比我们之前编写的Web应用程序，简直方便了一个层次，你就说你爱不爱吧，你爱不爱。\n\n注意，一定要保证视图名称下面出现横线并且按住Ctrl可以跳转，配置才是正确的（最新版IDEA）\n\n我们的页面中可能还会包含一些静态资源，比如js、css，因此这里我们还需要配置一下，让静态资源通过Tomcat提供的默认Servlet进行解析，我们需要让配置类实现一下`WebMvcConfigurer`接口，这样在Web应用程序启动时，会根据我们重写方法里面的内容进行进一步的配置：\n\n```java\n@Override\npublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {\n    configurer.enable();   \u002F\u002F开启默认的Servlet\n}\n\n@Override\npublic void addResourceHandlers(ResourceHandlerRegistry registry) {\n    registry.addResourceHandler(\"\u002Fstatic\u002F**\").addResourceLocations(\"\u002Fstatic\u002F\");\n    \u002F\u002F配置静态资源的访问路径\n}\n```\n\n我们编写一下前端内容：\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\" xmlns:th=\"http:\u002F\u002Fwww.thymeleaf.org\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Ctitle>测试\u003C\u002Ftitle>\n    \u003C!-- 引用静态资源，这里使用Thymeleaf的网址链接表达式，Thymeleaf会自动添加web应用程序的名称到链接前面 -->\n    \u003Cscript th:src=\"@{\u002Fstatic\u002Ftest.js}\">\u003C\u002Fscript>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cp>欢迎来到GayHub全球最大同性交友网站\u003C\u002Fp>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n创建`test.js`并编写如下内容：\n\n```javascript\nwindow.alert(\"欢迎来到GayHub全球最大同性交友网站\")\n```\n\n最后访问页面，页面在加载时就会显示一个弹窗，这样我们就完成了最基本的页面配置。相比之前的方式，这样就简单很多了，直接避免了编写大量的Servlet来处理请求。\n\n### @RequestMapping详解\n\n前面我们已经了解了如何创建一个控制器来处理我们的请求，接着我们只需要在控制器添加一个方法用于处理对应的请求即可，之前我们需要完整地编写一个Servlet来实现，而现在我们只需要添加一个`@RequestMapping`即可实现，其实从它的名字我们也能得知，此注解就是将请求和处理请求的方法建立一个映射关系，当收到请求时就可以根据映射关系调用对应的请求处理方法，那么我们就来先聊聊`@RequestMapping`吧，注解定义如下：\n\n```java\n@Mapping\npublic @interface RequestMapping {\n    String name() default \"\";\n\n    @AliasFor(\"path\")\n    String[] value() default {};\n\n    @AliasFor(\"value\")\n    String[] path() default {};\n\n    RequestMethod[] method() default {};\n\n    String[] params() default {};\n\n    String[] headers() default {};\n\n    String[] consumes() default {};\n\n    String[] produces() default {};\n}\n```\n\n其中最关键的是path属性（等价于value），它决定了当前方法处理的请求路径，注意路径必须全局唯一，任何路径只能有一个方法进行处理，它是一个数组，也就是说此方法不仅仅可以只用于处理某一个请求路径，我们可以使用此方法处理多个请求路径：\n\n```java\n@RequestMapping({\"\u002Findex\", \"\u002Ftest\"})\npublic ModelAndView index(){\n    return new ModelAndView(\"index\");\n}\n```\n\n现在我们访问\u002Findex或是\u002Ftest都会经过此方法进行处理。\n\n我们也可以直接将`@RequestMapping`添加到类名上，表示为此类中的所有请求映射添加一个路径前缀，比如：\n\n```java\n@Controller\n@RequestMapping(\"\u002Fyyds\")\npublic class MainController {\n\n    @RequestMapping({\"\u002Findex\", \"\u002Ftest\"})\n    public ModelAndView index(){\n        return new ModelAndView(\"index\");\n    }\n}\n```\n\n那么现在我们需要访问`\u002Fyyds\u002Findex`或是`\u002Fyyds\u002Ftest`才可以得到此页面。我们可以直接在IDEA下方的端点板块中查看当前Web应用程序定义的所有请求映射，并且可以通过IDEA为我们提供的内置Web客户端直接访问某个路径。\n\n路径还支持使用通配符进行匹配：\n\n- ?：表示任意一个字符，比如`@RequestMapping(\"\u002Findex\u002Fx?\")`可以匹配\u002Findex\u002Fxa、\u002Findex\u002Fxb等等。\n- *：表示任意0-n个字符，比如`@RequestMapping(\"\u002Findex\u002F*\")`可以匹配\u002Findex\u002Flbwnb、\u002Findex\u002Fyyds等。\n- **：表示当前目录或基于当前目录的多级目录，比如`@RequestMapping(\"\u002Findex\u002F**\")`可以匹配\u002Findex、\u002Findex\u002Fxxx等。\n\n我们接着来看下一个method属性，顾名思义，它就是请求的方法类型，我们可以限定请求方式，比如：\n\n```java\n@RequestMapping(value = \"\u002Findex\", method = RequestMethod.POST)\npublic ModelAndView index(){\n    return new ModelAndView(\"index\");\n}\n```\n\n现在我们如果直接使用浏览器访问此页面，会显示405方法不支持，因为浏览器默认是直接使用GET方法获取页面，而我们这里指定为POST方法访问此地址，所以访问失败，我们现在再去端点中用POST方式去访问，成功得到页面。\n\n![image-20230220152559862](https:\u002F\u002Fs2.loli.net\u002F2023\u002F02\u002F20\u002FJVwN2MhrWBAGni9.png)\n\n我们也可以使用衍生注解直接设定为指定类型的请求映射：\n\n```java\n@PostMapping(value = \"\u002Findex\")\npublic ModelAndView index(){\n    return new ModelAndView(\"index\");\n}\n```\n\n这里使用了`@PostMapping`直接指定为POST请求类型的请求映射，同样的，还有`@GetMapping`可以直接指定为GET请求方式，这里就不一一列举了。\n\n我们可以使用`params`属性来指定请求必须携带哪些请求参数，比如：\n\n```java\n@RequestMapping(value = \"\u002Findex\", params = {\"username\", \"password\"})\npublic ModelAndView index(){\n    return new ModelAndView(\"index\");\n}\n```\n\n比如这里我们要求请求中必须携带`username`和`password`属性，否则无法访问。它还支持表达式，比如我们可以这样编写：\n\n```java\n@RequestMapping(value = \"\u002Findex\", params = {\"!username\", \"password\"})\npublic ModelAndView index(){\n    return new ModelAndView(\"index\");\n}\n```\n\n在username之前添加一个感叹号表示请求的不允许携带此参数，否则无法访问，我们甚至可以直接设定一个固定值：\n\n```java\n@RequestMapping(value = \"\u002Findex\", params = {\"username!=test\", \"password=123\"})\npublic ModelAndView index(){\n    return new ModelAndView(\"index\");\n}\n```\n\n这样，请求参数username不允许为test，并且password必须为123，否则无法访问。\n\n`header`属性用法与`params`一致，但是它要求的是请求头中需要携带什么内容，比如：\n\n```java\n@RequestMapping(value = \"\u002Findex\", headers = \"!Connection\")\npublic ModelAndView index(){\n    return new ModelAndView(\"index\");\n}\n```\n\n那么，如果请求头中携带了`Connection`属性，将无法访问。其他两个属性：\n\n- consumes： 指定处理请求的提交内容类型（Content-Type），例如application\u002Fjson, text\u002Fhtml;\n- produces:  指定返回的内容类型，仅当request请求头中的(Accept)类型中包含该指定类型才返回；\n\n### @RequestParam和@RequestHeader详解\n\n我们接着来看，如何获取到请求中的参数。\n\n我们只需要为方法添加一个形式参数，并在形式参数前面添加`@RequestParam`注解即可：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(@RequestParam(\"username\") String username){\n    System.out.println(\"接受到请求参数：\"+username);\n    return new ModelAndView(\"index\");\n}\n```\n\n我们需要在`@RequestParam`中填写参数名称，参数的值会自动传递给形式参数，我们可以直接在方法中使用，注意，如果参数名称与形式参数名称相同，即使不添加`@RequestParam`也能获取到参数值。\n\n一旦添加`@RequestParam`，那么此请求必须携带指定参数，我们也可以将require属性设定为false来将属性设定为非必须：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(@RequestParam(value = \"username\", required = false) String username){\n    System.out.println(\"接受到请求参数：\"+username);\n    return new ModelAndView(\"index\");\n}\n```\n\n我们还可以直接设定一个默认值，当请求参数缺失时，可以直接使用默认值：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(@RequestParam(value = \"username\", required = false, defaultValue = \"伞兵一号\") String username){\n    System.out.println(\"接受到请求参数：\"+username);\n    return new ModelAndView(\"index\");\n}\n```\n\n如果需要使用Servlet原本的一些类，比如：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(HttpServletRequest request){\n    System.out.println(\"接受到请求参数：\"+request.getParameterMap().keySet());\n    return new ModelAndView(\"index\");\n}\n```\n\n直接添加`HttpServletRequest`为形式参数即可，SpringMVC会自动传递该请求原本的`HttpServletRequest`对象，同理，我们也可以添加`HttpServletResponse`作为形式参数，甚至可以直接将HttpSession也作为参数传递：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(HttpSession session){\n    System.out.println(session.getAttribute(\"test\"));\n    session.setAttribute(\"test\", \"鸡你太美\");\n    return new ModelAndView(\"index\");\n}\n```\n\n我们还可以直接将请求参数传递给一个实体类：\n\n```java\n@Data\npublic class User {\n    String username;\n    String password;\n}\n```\n\n注意必须携带set方法或是构造方法中包含所有参数，请求参数会自动根据类中的字段名称进行匹配：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(User user){\n    System.out.println(\"获取到cookie值为：\"+user);\n    return new ModelAndView(\"index\");\n}\n```\n\n`@RequestHeader`与`@RequestParam`用法一致，不过它是用于获取请求头参数的，这里就不再演示了。\n\n### @CookieValue和@SessionAttrbutie\n\n通过使用`@CookieValue`注解，我们也可以快速获取请求携带的Cookie信息：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(HttpServletResponse response,\n                          @CookieValue(value = \"test\", required = false) String test){\n    System.out.println(\"获取到cookie值为：\"+test);\n    response.addCookie(new Cookie(\"test\", \"lbwnb\"));\n    return new ModelAndView(\"index\");\n}\n```\n\n同样的，Session也能使用注解快速获取：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic ModelAndView index(@SessionAttribute(value = \"test\", required = false) String test,\n                          HttpSession session){\n    session.setAttribute(\"test\", \"xxxx\");\n    System.out.println(test);\n    return new ModelAndView(\"index\");\n}\n```\n\n可以发现，通过使用SpringMVC框架，整个Web应用程序的开发变得非常简单，大部分功能只需要一个注解就可以搞定了，正是得益于Spring框架，SpringMVC才能大显身手。\n\n### 重定向和请求转发\n\n重定向和请求转发也非常简单，我们只需要在视图名称前面添加一个前缀即可，比如重定向：\n\n```java\n@RequestMapping(\"\u002Findex\")\npublic String index(){\n    return \"redirect:home\";\n}\n\n@RequestMapping(\"\u002Fhome\")\npublic String home(){\n    return \"home\";\n}\n```\n\n通过添加`redirect:`前缀，就可以很方便地实现重定向，那么请求转发呢，其实也是一样的，使用`forward:`前缀表示转发给其他请求映射：\n\n```java\n@RequestMapping(\"\u002Findex\")\npublic String index(){\n    return \"forward:home\";\n}\n\n@RequestMapping(\"\u002Fhome\")\npublic String home(){\n    return \"home\";\n}\n```\n\n使用SpringMVC，只需要一个前缀就可以实现重定向和请求转发，非常方便。\n\n### Bean的Web作用域\n\n在学习Spring时我们讲解了Bean的作用域，包括`singleton`和`prototype`，Bean分别会以单例和多例模式进行创建，而在SpringMVC中，它的作用域被继续细分：\n\n- request：对于每次HTTP请求，使用request作用域定义的Bean都将产生一个新实例，请求结束后Bean也消失。\n- session：对于每一个会话，使用session作用域定义的Bean都将产生一个新实例，会话过期后Bean也消失。\n- global session：不常用，不做讲解。\n\n这里我们创建一个测试类来试试看：\n\n```java\npublic class TestBean {\n\n}\n```\n\n接着将其注册为Bean，注意这里需要添加`@RequestScope`或是`@SessionScope`表示此Bean的Web作用域：\n\n```java\n@Bean\n@RequestScope\npublic TestBean testBean(){\n    return new TestBean();\n}\n```\n\n接着我们将其自动注入到Controller中：\n\n```java\n@Controller\npublic class MainController {\n\n    @Resource\n    TestBean bean;\n\n    @RequestMapping(value = \"\u002Findex\")\n    public ModelAndView index(){\n        System.out.println(bean);\n        return new ModelAndView(\"index\");\n    }\n}\n```\n\n我们发现，每次发起得到的Bean实例都不同，接着我们将其作用域修改为`@SessionScope`，这样作用域就上升到Session，只要清理浏览器的Cookie，那么都会被认为是同一个会话，只要是同一个会话，那么Bean实例始终不变。\n\n实际上，它也是通过代理实现的，我们调用Bean中的方法会被转发到真正的Bean对象去执行。\n\n***\n\n## RestFul风格\n\n中文释义为**“表现层状态转换”**（名字挺高大上的），它不是一种标准，而是一种设计风格。它的主要作用是充分并正确利用HTTP协议的特性，规范资源获取的URI路径。通俗的讲，RESTful风格的设计允许将参数通过URL拼接传到服务端，目的是让URL看起来更简洁实用，并且我们可以充分使用多种HTTP请求方式（POST\u002FGET\u002FPUT\u002FDELETE），来执行相同请求地址的不同类型操作。\n\n因此，这种风格的连接，我们就可以直接从请求路径中读取参数，比如：\n\n```\nhttp:\u002F\u002Flocalhost:8080\u002Fmvc\u002Findex\u002F123456\n```\n\n我们可以直接将index的下一级路径作为请求参数进行处理，也就是说现在的请求参数包含在了请求路径中：\n\n```java\n@RequestMapping(\"\u002Findex\u002F{str}\")\npublic String index(@PathVariable String str) {\n    System.out.println(str);\n    return \"index\";\n}\n```\n\n注意请求路径我们可以手动添加类似占位符一样的信息，这样占位符位置的所有内容都会被作为请求参数，而方法的形参列表中必须包括一个与占位符同名的并且添加了`@PathVariable`注解的参数，或是由`@PathVariable`注解指定为占位符名称：\n\n```java\n@RequestMapping(\"\u002Findex\u002F{str}\")\npublic String index(@PathVariable(\"str\") String text){\n    System.out.println(text);\n    return \"index\";\n}\n```\n\n如果没有配置正确，方法名称上会出现黄线。\n\n我们可以按照不同功能进行划分：\n\n- POST http:\u002F\u002Flocalhost:8080\u002Fmvc\u002Findex -  添加用户信息，携带表单数据\n- GET http:\u002F\u002Flocalhost:8080\u002Fmvc\u002Findex\u002F{id} -  获取用户信息，id直接放在请求路径中\n- PUT http:\u002F\u002Flocalhost:8080\u002Fmvc\u002Findex -  修改用户信息，携带表单数据\n- DELETE http:\u002F\u002Flocalhost:8080\u002Fmvc\u002Findex\u002F{id} -  删除用户信息，id直接放在请求路径中\n\n我们分别编写四个请求映射：\n\n```java\n@Controller\npublic class MainController {\n\n    @RequestMapping(value = \"\u002Findex\u002F{id}\", method = RequestMethod.GET)\n    public String get(@PathVariable(\"id\") String text){\n        System.out.println(\"获取用户：\"+text);\n        return \"index\";\n    }\n\n    @RequestMapping(value = \"\u002Findex\", method = RequestMethod.POST)\n    public String post(String username){\n        System.out.println(\"添加用户：\"+username);\n        return \"index\";\n    }\n\n    @RequestMapping(value = \"\u002Findex\u002F{id}\", method = RequestMethod.DELETE)\n    public String delete(@PathVariable(\"id\") String text){\n        System.out.println(\"删除用户：\"+text);\n        return \"index\";\n    }\n\n    @RequestMapping(value = \"\u002Findex\", method = RequestMethod.PUT)\n    public String put(String username){\n        System.out.println(\"修改用户：\"+username);\n        return \"index\";\n    }\n}\n```\n\n这只是一种设计风格而已，各位小伙伴了解即可。\n\n------\n\n## Interceptor拦截器\n\n拦截器是整个SpringMVC的一个重要内容，拦截器与过滤器类似，都是用于拦截一些非法请求，但是我们之前讲解的过滤器是作用于Servlet之前，只有经过层层的过滤器才可以成功到达Servlet，而拦截器并不是在Servlet之前，它在Servlet与RequestMapping之间，相当于DispatcherServlet在将请求交给对应Controller中的方法之前进行拦截处理，它只会拦截所有Controller中定义的请求映射对应的请求（不会拦截静态资源），这里一定要区分两者的不同。\n\n![image-20230630194651686](https:\u002F\u002Fs2.loli.net\u002F2023\u002F06\u002F30\u002F6J3D98HdkawAOVK.png)\n\n### 创建拦截器\n\n创建一个拦截器我们需要实现一个`HandlerInterceptor`接口：\n\n```java\npublic class MainInterceptor implements HandlerInterceptor {\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        System.out.println(\"我是处理之前！\");\n        return true;   \u002F\u002F只有返回true才会继续，否则直接结束\n    }\n\n    @Override\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\n        System.out.println(\"我是处理之后！\");\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n      \t\u002F\u002F在DispatcherServlet完全处理完请求后被调用\n        System.out.println(\"我是完成之后！\");\n    }\n}\n```\n\n接着我们需要在配置类中进行注册：\n\n```java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n    registry.addInterceptor(new MainInterceptor())\n      .addPathPatterns(\"\u002F**\")    \u002F\u002F添加拦截器的匹配路径，只要匹配一律拦截\n      .excludePathPatterns(\"\u002Fhome\");   \u002F\u002F拦截器不进行拦截的路径\n}\n```\n\n现在我们在浏览器中访问index页面，拦截器已经生效。\n\n得到整理拦截器的执行顺序：\n\n```\n我是处理之前！\n我是处理！\n我是处理之后！\n我是完成之后！\n```\n\n也就是说，处理前和处理后，包含了真正的请求映射的处理，在整个流程结束后还执行了一次`afterCompletion`方法，其实整个过程与我们之前所认识的Filter类似，不过在处理前，我们只需要返回true或是false表示是否被拦截即可，而不是再去使用FilterChain进行向下传递。\n\n那么我们就来看看，如果处理前返回false，会怎么样：\n\n```\n我是处理之前！\n```\n\n通过结果发现一旦返回false，之后的所有流程全部取消，那么如果是在处理中发生异常了呢？\n\n```java\n@RequestMapping(\"\u002Findex\")\npublic String index(){\n    System.out.println(\"我是处理！\");\n    if(true) throw new RuntimeException(\"\");\n    return \"index\";\n}\n```\n\n结果为：\n\n```\n我是处理之前！\n我是处理！\n我是完成之后！\n```\n\n我们发现如果处理过程中抛出异常，那么久不会执行处理后`postHandle`方法，但是会执行`afterCompletion`方法，我们可以在此方法中获取到抛出的异常。\n\n### 多级拦截器\n\n前面介绍了仅仅只有一个拦截器的情况，我们接着来看如果存在多个拦截器会如何执行，我们以同样的方式创建二号拦截器：\n\n```java\npublic class SubInterceptor implements HandlerInterceptor {\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        System.out.println(\"二号拦截器：我是处理之前！\");\n        return true;\n    }\n\n    @Override\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\n        System.out.println(\"二号拦截器：我是处理之后！\");\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n        System.out.println(\"二号拦截器：我是完成之后！\");\n    }\n}\n```\n\n注册二号拦截器：\n\n```java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n  \t\u002F\u002F一号拦截器\n    registry.addInterceptor(new MainInterceptor()).addPathPatterns(\"\u002F**\").excludePathPatterns(\"\u002Fhome\");\n  \t\u002F\u002F二号拦截器\n    registry.addInterceptor(new SubInterceptor()).addPathPatterns(\"\u002F**\");\n}\n```\n\n注意拦截顺序就是注册的顺序，因此拦截器会根据注册顺序依次执行，我们可以打开浏览器运行一次：\n\n```\n一号拦截器：我是处理之前！\n二号拦截器：我是处理之前！\n我是处理！\n二号拦截器：我是处理之后！\n一号拦截器：我是处理之后！\n二号拦截器：我是完成之后！\n一号拦截器：我是完成之后！\n```\n\n和多级Filter相同，在处理之前，是按照顺序从前向后进行拦截的，但是处理完成之后，就按照倒序执行处理后方法，而完成后是在所有的`postHandle`执行之后再同样的以倒序方式执行。\n\n那么如果这时一号拦截器在处理前就返回了false呢？\n\n```java\n@Override\npublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n    System.out.println(\"一号拦截器：我是处理之前！\");\n    return false;\n}\n```\n\n得到结果如下：\n\n```\n一号拦截器：我是处理之前！\n```\n\n我们发现，与单个拦截器的情况一样，一旦拦截器返回false，那么之后无论有无拦截器，都不再继续。\n\n## 异常处理\n\n当我们的请求映射方法中出现异常时，会直接展示在前端页面，这是因为SpringMVC为我们提供了默认的异常处理页面，当出现异常时，我们的请求会被直接转交给专门用于异常处理的控制器进行处理。\n\n我们可以自定义一个异常处理控制器，一旦出现指定异常，就会转接到此控制器执行：\n\n```java\n@ControllerAdvice\npublic class ErrorController {\n\n    @ExceptionHandler(Exception.class)\n    public String error(Exception e, Model model){  \u002F\u002F可以直接添加形参来获取异常\n        e.printStackTrace();\n        model.addAttribute(\"e\", e);\n        return \"500\";\n    }\n}\n```\n\n接着我们编写一个专门显示异常的页面：\n\n```java\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Ctitle>Title\u003C\u002Ftitle>\n\u003C\u002Fhead>\n\u003Cbody>\n  500 - 服务器出现了一个内部错误QAQ\n  \u003Cdiv th:text=\"${e}\">\u003C\u002Fdiv>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n接着修改：\n\n```java\n@RequestMapping(\"\u002Findex\")\npublic String index(){\n    System.out.println(\"我是处理！\");\n    if(true) throw new RuntimeException(\"您的氪金力度不足，无法访问！\");\n    return \"index\";\n}\n```\n\n访问后，我们发现控制台会输出异常信息，同时页面也是我们自定义的一个页面。\n\n## JSON数据格式与Axios请求\n\nJSON (JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。\n\n我们现在推崇的是前后端分离的开发模式，而不是所有的内容全部交给后端渲染再发送给浏览器，也就是说，整个Web页面的内容在一开始就编写完成了，而其中的数据由前端执行JS代码来向服务器动态获取，再到前端进行渲染（填充），这样可以大幅度减少后端的压力，并且后端只需要传输关键数据即可（在即将到来的SpringBoot阶段，我们将完全采用前后端分离的开发模式）\n\n### JSON数据格式\n\n既然要实现前后端分离，那么我们就必须约定一种更加高效的数据传输模式，来向前端页面传输后端提供的数据。因此JSON横空出世，它非常容易理解，并且与前端的兼容性极好，因此现在比较主流的数据传输方式则是通过JSON格式承载的。\n\n一个JSON格式的数据长这样，以学生对象为例：\n\n```json\n{\"name\": \"杰哥\", \"age\": 18}\n```\n\n多个学生可以以数组的形式表示：\n\n```json\n[{\"name\": \"杰哥\", \"age\": 18}, {\"name\": \"阿伟\", \"age\": 18}]\n```\n\n嵌套关系可以表示为：\n\n```json\n{\"studentList\": [{\"name\": \"杰哥\", \"age\": 18}, {\"name\": \"阿伟\", \"age\": 18}], \"count\": 2}\n```\n\n它直接包括了属性的名称和属性的值，与JavaScript的对象极为相似，它到达前端后，可以直接转换为对象，以对象的形式进行操作和内容的读取，相当于以字符串形式表示了一个JS对象，我们可以直接在控制台窗口中测试：\n\n```javascript\nlet obj = JSON.parse('{\"studentList\": [{\"name\": \"杰哥\", \"age\": 18}, {\"name\": \"阿伟\", \"age\": 18}], \"count\": 2}')\n\u002F\u002F将JSON格式字符串转换为JS对象\nobj.studentList[0].name   \u002F\u002F直接访问第一个学生的名称\n```\n\n我们也可以将JS对象转换为JSON字符串：\n\n```javascript\nJSON.stringify(obj)\n```\n\n我们后端就可以以JSON字符串的形式向前端返回数据，这样前端在拿到数据之后，就可以快速获取，非常方便。\n\n那么后端如何快速创建一个JSON格式的数据呢？我们首先需要导入以下依赖：\n\n```xml\n\u003Cdependency>\n      \u003CgroupId>com.alibaba.fastjson2\u003C\u002FgroupId>\n      \u003CartifactId>fastjson2\u003C\u002FartifactId>\n      \u003Cversion>2.0.34\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\nJSON解析框架有很多种，比较常用的是Jackson和FastJSON，这里我们使用阿里巴巴的FastJSON进行解析，这是目前号称最快的JSON解析框架，并且现在已经强势推出FastJSON 2版本。\n\n首先要介绍的是JSONObject，它和Map的使用方法一样，并且是有序的（实现了LinkedHashMap接口），比如我们向其中存放几个数据：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic String index(){\n    JSONObject object = new JSONObject();\n    object.put(\"name\", \"杰哥\");\n    object.put(\"age\", 18);\n    System.out.println(object.toJSONString());   \u002F\u002F以JSON格式输出JSONObject字符串\n    return \"index\";\n}\n```\n\n最后我们得到的结果为：\n\n```json\n{\"name\": \"杰哥\", \"age\": 18}\n```\n\n实际上JSONObject就是对JSON数据的一种对象表示。同样的还有JSONArray，它表示一个数组，用法和List一样，数组中可以嵌套其他的JSONObject或是JSONArray：\n\n```java\n@RequestMapping(value = \"\u002Findex\")\npublic String index(){\n    JSONObject object = new JSONObject();\n    object.put(\"name\", \"杰哥\");\n    object.put(\"age\", 18);\n    JSONArray array = new JSONArray();\n    array.add(object);\n    System.out.println(array.toJSONString());\n    return \"index\";\n}\n```\n\n得到的结果为：\n\n```json\n[{\"name\": \"杰哥\", \"age\": 18}]\n```\n\n当出现循环引用时，会按照以下语法来解析：\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F08\u002F14\u002FMjO4awH3X1YnlmR.png)\n\n我们可以也直接创建一个实体类，将实体类转换为JSON格式的数据：\n\n```java\n@RequestMapping(value = \"\u002Findex\", produces = \"application\u002Fjson\")\n@ResponseBody\npublic String data(){\n    Student student = new Student();\n    student.setName(\"杰哥\");\n    student.setAge(18);\n    return JSON.toJSONString(student);\n}\n```\n\n这里我们修改了`produces`的值，将返回的内容类型设定为`application\u002Fjson`，表示服务器端返回了一个JSON格式的数据（当然不设置也行，也能展示，这样是为了规范）然后我们在方法上添加一个`@ResponseBody`表示方法返回（也可以在类上添加`@RestController`表示此Controller默认返回的是字符串数据）的结果不是视图名称而是直接需要返回一个字符串作为页面数据，这样，返回给浏览器的就是我们直接返回的字符串内容。\n\n接着我们使用JSON工具类将其转换为JSON格式的字符串，打开浏览器，得到JSON格式数据。\n\nSpringMVC非常智能，我们可以直接返回一个对象类型，它会被自动转换为JSON字符串格式：\n\n```java\n@RequestMapping(value = \"\u002Fdata\", produces = \"application\u002Fjson\")\n@ResponseBody\npublic Student data(){\n    Student student = new Student();\n    student.setName(\"杰哥\");\n    student.setAge(18);\n    return student;\n}\n```\n\n注意需要在配置类中添加一下FastJSON转换器，这里需要先添加一个依赖：\n\n```xml\n\u003Cdependency>\n    \u003CgroupId>com.alibaba.fastjson2\u003C\u002FgroupId>\n    \u003CartifactId>fastjson2-extension-spring6\u003C\u002FartifactId>\n    \u003Cversion>2.0.34\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n然后编写配置：\n\n```java\n@Override\npublic void configureMessageConverters(List\u003CHttpMessageConverter\u003C?>> converters) {\n    converters.add(new FastJsonHttpMessageConverter());\n}\n```\n\n再次尝试，内容就会自动转换为JSON格式响应给客户端了。\n\n### Axios异步请求\n\n前面我们讲解了如何向浏览器发送一个JSON格式的数据，那么我们现在来看看如何向服务器请求数据。\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F08\u002F14\u002FfaYcVC6dpIOuJyA.png)\n\n这一部分，我们又要回到前端相关内容的介绍中，当然，我们依然是仅做了解，并不需要详细学习前端项目开发知识。\n\n前端为什么需要用到异步请求，这是因为我们的网页是动态的（这里的动态不是指有动画效果，而是能够实时更新内容）比如我们点击一个按钮会弹出新的内容、或是跳转到新的页面、更新页面中的数据等等，这些都需要通过JS完成异步请求来实现。\n\n> 前端异步请求指的是在前端中发送请求至服务器或其他资源，并且不阻塞用户界面或其他操作。在传统的同步请求中，当发送请求时，浏览器会等待服务器响应，期间用户无法进行其他操作。而异步请求通过将请求发送到后台，在等待响应的同时，允许用户继续进行其他操作。这种机制能够提升用户体验，并且允许页面进行实时更新。常见的前端异步请求方式包括使用XMLHttpRequest对象、Fetch API、以及使用jQuery库中的AJAX方法，以及目前最常用的Axios框架等。\n\n假设我们后端有一个需要实时刷新的数据（随时间而变化）现在需要再前端实时更新展示，这里我们以axios框架的简单使用为例子，带各位小伙伴体验如何发起异步请求并更新我们页面中的数据。\n\n首先是前端页面，直接抄作业就行：\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Ctitle>测试\u003C\u002Ftitle>\n    \u003Cscript src=\"https:\u002F\u002Funpkg.com\u002Faxios@1.1.2\u002Fdist\u002Faxios.min.js\">\u003C\u002Fscript>\n\u003C\u002Fhead>\n\u003Cbody>\n  \u003Cp>欢迎来到GayHub全球最大同性交友网站\u003C\u002Fp>\n  \u003Cp>用户名: \u003Cspan id=\"username\">\u003C\u002Fspan>\u003C\u002Fp>\n  \u003Cp>密码: \u003Cspan id=\"password\">\u003C\u002Fspan>\u003C\u002Fp>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n接着我们使用axios框架直接对后端请求JSON数据：\n\n```html\n\u003Cscript>\n    function getInfo() {\n        axios.get('\u002Fmvc\u002Ftest').then(({data}) => {\n            document.getElementById('username').innerText = data.username\n            document.getElementById('password').innerText = data.password\n        })\n    }\n\u003C\u002Fscript>\n```\n\n这样，我们就实现了从服务端获取数据并更新到页面中，前端开发者利用JS发起异步请求，可以实现各种各样的效果，而我们后端开发者只需要关心接口返回正确的数据即可，这就已经有前后端分离开发的雏形了（实际上之前，我们在JavaWeb阶段使用XHR请求也演示过，不过当时是纯粹的数据）\n\n那么我们接着来看，如何向服务端发送一个JS对象数据并进行解析，这里以简单的登录为例：\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Ctitle>测试\u003C\u002Ftitle>\n    \u003Cscript src=\"https:\u002F\u002Funpkg.com\u002Faxios@1.1.2\u002Fdist\u002Faxios.min.js\">\u003C\u002Fscript>\n\u003C\u002Fhead>\n\u003Cbody>\n  \u003Cp>欢迎来到GayHub全球最大同性交友网站\u003C\u002Fp>\n  \u003Cbutton onclick=\"login()\">立即登录\u003C\u002Fbutton>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n这里依然使用axios发送POST请求：\n\n```html\n\u003Cscript>\n    function login() {\n        axios.post('\u002Fmvc\u002Ftest', {\n            username: 'test',\n            password: '123456'\n        }, {\n            headers: {\n                'Content-Type': 'application\u002Fx-www-form-urlencoded'\n            }\n        }).then(({data}) => {\n            if(data.success) {\n                alert('登录成功')\n            } else {\n                alert('登录失败')\n            }\n        })\n    }\n\u003C\u002Fscript>\n```\n\n服务器端只需要在请求参数位置添加一个对象接收即可（和前面是一样的，因为这里也是提交的表单数据）：\n\n```java\n@ResponseBody\n@PostMapping(value = \"\u002Ftest\", produces = \"application\u002Fjson\")\npublic String hello(String username, String password){\n    boolean success = \"test\".equals(user.getUsername()) && \"123456\".equals(user.getPassword());\n    JSONObject object = new JSONObject();\n    object.put(\"success\", success);\n    return object.toString();\n}\n```\n\n我们也可以将js对象转换为JSON字符串的形式进行传输，这里需要使用ajax方法来处理：\n\n```html\n\u003Cscript>\n    function login() {\n        axios.post('\u002Fmvc\u002Ftest', {\n            username: 'test',\n            password: '123456'\n        }).then(({data}) => {\n            if(data.success) {\n                alert('登录成功')\n            } else {\n                alert('登录失败')\n            }\n        })\n    }\n\u003C\u002Fscript>\n```\n\n如果我们需要读取前端发送给我们的JSON格式数据，那么这个时候就需要添加`@RequestBody`注解：\n\n```java\n@ResponseBody\n@PostMapping(value = \"\u002Ftest\", produces = \"application\u002Fjson\")\npublic String hello(@RequestBody User user){\n    boolean success = \"test\".equals(user.getUsername()) && \"123456\".equals(user.getPassword());\n    JSONObject object = new JSONObject();\n    object.put(\"success\", success);\n    return object.toString();\n}\n```\n\n这样，我们就实现了前后端使用JSON字符串进行通信。\n\n## 实现文件上传和下载\n\n利用SpringMVC，我们可以很轻松地实现文件上传和下载，我们需要在MainInitializer中添加一个新的方法：\n\n```java\npublic class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {\n\n    ...\n\n    @Override\n    protected void customizeRegistration(ServletRegistration.Dynamic registration) {\n      \t\u002F\u002F 直接通过registration配置Multipart相关配置，必须配置临时上传路径，建议选择方便打开的\n        \u002F\u002F 同样可以设置其他属性：maxFileSize, maxRequestSize, fileSizeThreshold\n        registration.setMultipartConfig(new MultipartConfigElement(\"\u002FUsers\u002Fnagocoler\u002FDownload\"));\n    }\n}\n```\n\n接着我们直接编写Controller即可：\n\n```java\n@RequestMapping(value = \"\u002Fupload\", method = RequestMethod.POST)\n@ResponseBody\npublic String upload(@RequestParam MultipartFile file) throws IOException {\n    File fileObj = new File(\"test.png\");\n    file.transferTo(fileObj);\n    System.out.println(\"用户上传的文件已保存到：\"+fileObj.getAbsolutePath());\n    return \"文件上传成功！\";\n}\n```\n\n最后在前端添加一个文件的上传点：\n\n```html\n\u003Cdiv>\n    \u003Cform action=\"upload\" method=\"post\" enctype=\"multipart\u002Fform-data\">\n        \u003Cinput type=\"file\" name=\"file\">\n        \u003Cinput type=\"submit\">\n    \u003C\u002Fform>\n\u003C\u002Fdiv>\n```\n\n这样，点击提交之后，文件就会上传到服务器了。\n\n下载其实和我们之前的写法大致一样，直接使用HttpServletResponse，并向输出流中传输数据即可。\n\n```java\n@RequestMapping(value = \"\u002Fdownload\", method = RequestMethod.GET)\n@ResponseBody\npublic void download(HttpServletResponse response){\n    response.setContentType(\"multipart\u002Fform-data\");\n    try(OutputStream stream = response.getOutputStream();\n        InputStream inputStream = new FileInputStream(\"test.png\")){\n        IOUtils.copy(inputStream, stream);\n    }catch (IOException e){\n        e.printStackTrace();\n    }\n}\n```\n\n在前端页面中添加一个下载点：\n\n```html\n\u003Ca href=\"download\" download=\"test.png\">下载最新资源\u003C\u002Fa>\n```\n\n## 解读DispatcherServlet源码\n\n**注意：** 本部分作为选学内容！\n\n到目前为止，关于SpringMVC的相关内容就学习得差不多了，但是我们在最后还是需要深入了解一下DispatcherServlet底层是如何进行调度的，因此，我们会从源码角度进行讲解。\n\n首先我们需要找到`DispatcherServlet`的最顶层`HttpServletBean`，在这里直接继承的`HttpServlet`，那么我们首先来看一下，它在初始化方法中做了什么：\n\n```java\npublic final void init() throws ServletException {\n  \t\u002F\u002F读取配置参数，并进行配置\n    PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);\n    if (!pvs.isEmpty()) {\n        try {\n            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);\n            ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());\n            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));\n            this.initBeanWrapper(bw);\n            bw.setPropertyValues(pvs, true);\n        } catch (BeansException var4) {\n            if (this.logger.isErrorEnabled()) {\n                this.logger.error(\"Failed to set bean properties on servlet '\" + this.getServletName() + \"'\", var4);\n            }\n\n            throw var4;\n        }\n    }\n\t\t\u002F\u002F此初始化阶段由子类实现，\n    this.initServletBean();\n}\n```\n\n我们接着来看`initServletBean()`方法是如何实现的，它是在子类`FrameworkServlet`中定义的：\n\n```java\nprotected final void initServletBean() throws ServletException {\n    this.getServletContext().log(\"Initializing Spring \" + this.getClass().getSimpleName() + \" '\" + this.getServletName() + \"'\");\n    if (this.logger.isInfoEnabled()) {\n        this.logger.info(\"Initializing Servlet '\" + this.getServletName() + \"'\");\n    }\n\n    long startTime = System.currentTimeMillis();\n\n    try {\n      \t\u002F\u002F注意：我们在一开始说了SpringMVC有两个容器，一个是Web容器一个是根容器\n      \t\u002F\u002FWeb容器只负责Controller等表现层内容\n      \t\u002F\u002F根容器就是Spring容器，它负责Service、Dao等，并且它是Web容器的父容器。\n      \t\u002F\u002F初始化WebApplicationContext，这个阶段会为根容器和Web容器进行父子关系建立\n        this.webApplicationContext = this.initWebApplicationContext();\n        this.initFrameworkServlet();\n    } catch (RuntimeException | ServletException var4) {\n      \u002F\u002F...以下内容全是打印日志\n}\n```\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F06\u002F30\u002F6ZAyRrDw2QMU8Xv.png)\n\n我们来看看`initWebApplicationContext`是如何进行初始化的：\n\n```java\nprotected WebApplicationContext initWebApplicationContext() {\n  \t\u002F\u002F这里获取的是根容器，一般用于配置Service、数据源等\n    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());\n    WebApplicationContext wac = null;\n    if (this.webApplicationContext != null) {\n      \t\u002F\u002F如果webApplicationContext在之前已经存在，则直接给到wac\n        wac = this.webApplicationContext;\n        if (wac instanceof ConfigurableWebApplicationContext) {\n            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;\n            if (!cwac.isActive()) {\n                if (cwac.getParent() == null) {\n                  \t\u002F\u002F设定根容器为Web容器的父容器\n                    cwac.setParent(rootContext);\n                }\n\n                this.configureAndRefreshWebApplicationContext(cwac);\n            }\n        }\n    }\n\n    if (wac == null) {\n      \t\u002F\u002F如果webApplicationContext是空，那么就从ServletContext找一下有没有初始化上下文\n        wac = this.findWebApplicationContext();\n    }\n\n    if (wac == null) {\n      \t\u002F\u002F如果还是找不到，直接创个新的，并直接将根容器作为父容器\n        wac = this.createWebApplicationContext(rootContext);\n    }\n\n    if (!this.refreshEventReceived) {\n        synchronized(this.onRefreshMonitor) {\n          \t\u002F\u002F此方法由DispatcherServlet实现\n            this.onRefresh(wac);\n        }\n    }\n\n    if (this.publishContext) {\n        String attrName = this.getServletContextAttributeName();\n      \t\u002F\u002F把Web容器丢进ServletContext\n        this.getServletContext().setAttribute(attrName, wac);\n    }\n\n    return wac;\n}\n```\n\n我们接着来看DispatcherServlet中实现的`onRefresh()`方法：\n\n```java\n@Override\nprotected void onRefresh(ApplicationContext context) {\n    initStrategies(context);\n}\n    \nprotected void initStrategies(ApplicationContext context) {\n  \t\u002F\u002F初始化各种解析器\n    initMultipartResolver(context);\n    initLocaleResolver(context);\n    initThemeResolver(context);\n  \t\u002F\u002F在容器中查找所有的HandlerMapping，放入集合中\n  \t\u002F\u002FHandlerMapping保存了所有的请求映射信息（Controller中定义的），它可以根据请求找到处理器Handler，但并不是简单的返回处理器，而是将处理器和拦截器封装，形成一个处理器执行链（类似于之前的Filter）\n    initHandlerMappings(context);\n  \t\u002F\u002F在容器中查找所有的HandlerAdapter，它用于处理请求并返回ModelAndView对象\n  \t\u002F\u002F默认有三种实现HttpRequestHandlerAdapter，SimpleControllerHandlerAdapter和AnnotationMethodHandlerAdapter\n  \t\u002F\u002F当HandlerMapping找到处理请求的Controller之后，会选择一个合适的HandlerAdapter处理请求\n  \t\u002F\u002F比如我们之前使用的是注解方式配置Controller，现在有一个请求携带了一个参数，那么HandlerAdapter会对请求的数据进行解析，并传入方法作为实参，最后根据方法的返回值将其封装为ModelAndView对象\n    initHandlerAdapters(context);\n  \t\u002F\u002F其他的内容\n    initHandlerExceptionResolvers(context);\n    initRequestToViewNameTranslator(context);\n    initViewResolvers(context);\n    initFlashMapManager(context);\n}\n```\n\nDispatcherServlet初始化过程我们已经了解了，那么我们接着来看DispatcherServlet是如何进行调度的，首先我们的请求肯定会经过`HttpServlet`，然后其交给对应的doGet、doPost等方法进行处理，而在`FrameworkServlet`中，这些方法都被重写，并且使用`processRequest`来进行处理：\n\n```java\nprotected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n    this.processRequest(request, response);\n}\n\nprotected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n    this.processRequest(request, response);\n}\n```\n\n我们来看看`processRequest`做了什么：\n\n```java\nprotected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n  \t\u002F\u002F前期准备工作\n    long startTime = System.currentTimeMillis();\n    Throwable failureCause = null;\n    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();\n    LocaleContext localeContext = this.buildLocaleContext(request);\n    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();\n    ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);\n    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);\n    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());\n    this.initContextHolders(request, localeContext, requestAttributes);\n\n    try {\n      \t\u002F\u002F重点在这里，这里进行了Service的执行，不过是在DispatcherServlet中定义的\n        this.doService(request, response);\n    } catch (IOException | ServletException var16) {\n        \u002F\u002F...\n}\n```\n\n请各位一定要耐心，这些大型框架的底层一般都是层层套娃，因为这样写起来层次会更加清晰，那么我们来看看`DispatcherServlet`中是如何实现的：\n\n```java\nprotected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {\n   \u002F\u002F...\n    try {\n      \t\u002F\u002F重点在这里，这才是整个处理过程中最核心的部分\n        this.doDispatch(request, response);\n    } finally {\n        \u002F\u002F...\n}\n```\n\n终于找到最核心的部分了：\n\n```java\nprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {\n    HttpServletRequest processedRequest = request;\n    HandlerExecutionChain mappedHandler = null;\n    boolean multipartRequestParsed = false;\n    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);\n\n    try {\n        try {\n            ModelAndView mv = null;\n            Object dispatchException = null;\n\n            try {\n                processedRequest = this.checkMultipart(request);\n                multipartRequestParsed = processedRequest != request;\n              \t\u002F\u002F在HandlerMapping集合中寻找可以处理当前请求的HandlerMapping\n                mappedHandler = this.getHandler(processedRequest);\n                if (mappedHandler == null) {\n                    this.noHandlerFound(processedRequest, response);\n                  \t\u002F\u002F找不到HandlerMapping则无法进行处理\n                    return;\n                }\n\n              \t\u002F\u002F根据HandlerMapping提供的信息，找到可以处理的HandlerAdapter\n                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());\n                String method = request.getMethod();\n                boolean isGet = HttpMethod.GET.matches(method);\n                if (isGet || HttpMethod.HEAD.matches(method)) {\n                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());\n                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {\n                        return;\n                    }\n                }\n\n              \t\u002F\u002F执行所有拦截器的preHandle()方法\n                if (!mappedHandler.applyPreHandle(processedRequest, response)) {\n                    return;\n                }\n\n              \t\u002F\u002F使用HandlerAdapter进行处理（我们编写的请求映射方法在这个位置才真正地执行了）\n              \t\u002F\u002FHandlerAdapter会帮助我们将请求的数据进行处理，再来调用我们编写的请求映射方法\n              \t\u002F\u002F最后HandlerAdapter会将结果封装为ModelAndView返回给mv\n                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());\n                if (asyncManager.isConcurrentHandlingStarted()) {\n                    return;\n                }\n\n                this.applyDefaultViewName(processedRequest, mv);\n              \t\u002F\u002F执行所有拦截器的postHandle()方法\n                mappedHandler.applyPostHandle(processedRequest, response, mv);\n            } catch (Exception var20) {\n                dispatchException = var20;\n            } catch (Throwable var21) {\n                dispatchException = new NestedServletException(\"Handler dispatch failed\", var21);\n            }\n\n          \t\u002F\u002F最后处理结果，对视图进行渲染等，如果抛出异常会出现错误页面\n            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);\n        } catch (Exception var22) {\n            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);\n        } catch (Throwable var23) {\n            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException(\"Handler processing failed\", var23));\n        }\n\n    } finally {\n        if (asyncManager.isConcurrentHandlingStarted()) {\n            if (mappedHandler != null) {\n                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);\n            }\n        } else if (multipartRequestParsed) {\n            this.cleanupMultipart(processedRequest);\n        }\n\n    }\n}\n```\n\n所以，根据以上源码分析得出最终的流程图：\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F08\u002F14\u002FIzWB8LGjwo1DPml.png)\n\n虽然完成本章学习后，我们已经基本能够基于Spring去重新编写一个更加高级的图书管理系统了，但是登陆验证复杂的问题依然没有解决，如果我们依然按照之前的方式编写登陆验证，显然太过简单，它仅仅只是一个登陆，但是没有任何的权限划分或是加密处理，我们需要更加高级的权限校验框架来帮助我们实现登陆操作，下一章，我们会详细讲解如何使用更加高级的SpringSecurity框架来进行权限验证。",{"data":466,"status":460,"success":461},[467,472],{"id":8,"image":468,"link":469,"name":470,"type":471},"\u002Fimage\u002Fadv\u002Frainyun-2025-06.webp","https:\u002F\u002Fwww.rainyun.com\u002Fitbaima_","雨云优惠购","cloud",{"id":66,"image":473,"link":474,"name":475,"type":476},"\u002Fimage\u002Fadv\u002Fsimcard-2025-11.webp","https:\u002F\u002Fmall.itbaima.cn","号卡优惠","simcard"]