[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"\u002Fresource\u002Fdocument\u002Flist?undefined":3,"\u002Fresource\u002Fdocument\u002Fquery\u002Fycpagby2v7j4p728?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":181,"content":464,"id":194,"indexOrder":108,"name":195},"# JavaWeb后端\n\n经过前面的学习，现在终于可以正式进入到后端的学习当中，不过，我们还是需要再系统地讲解一下HTTP通信基础知识，它是我们学习JavaWeb的基础知识，我们之前已经学习过TCP通信，而HTTP实际上是基于TCP协议之上的应用层协议，因此理解它并不难理解。\n\n打好基础是关键！为什么要去花费时间来讲解计算机网络基础，我们学习一门技术，如果仅仅是知道如何使用却不知道其原理，那么就成了彻头彻尾的“码农”，只知道搬运代码实现功能，却不知道这行代码的执行流程，在遇到一些问题的时候就不知道如何解决，无论是知识层面还是应用层面都得不到提升。\n\n无论怎么样，我们都要明确，我们学习JavaWeb的最终目的是为了搭建一个网站，并且让用户能访问我们的网站并在我们的网站上做一些事情。\n\n## 计算机网络基础\n\n在计算机网络（谢希仁 第七版 第264页）中，是这样描述万维网的：\n\n>  万维网（World Wide Web）并非是某种特殊的计算机网络，万维网是一个大规模的联机式信息储藏所，英文简称`Web`，万维网用**\u003Cu>链接\u003C\u002Fu>**的方法，能够非常方便地从互联网上的一个站点访问另一个站点，从而主动地按需求获取丰富的信息。\n\n这句话说的非常官方，但是也蕴藏着许多的信息，首先它指明，我们的互联网上存在许许多多的服务器，而我们通过访问这些服务器就能快速获取服务器为我们提供的信息（比如打开百度就能展示搜索、打开小破站能刷视频、打开微博能查看实时热点）而这些服务器就是由不同的公司在运营。\n\n其次，我们通过浏览器，只需要输入对应的网址或是点击页面中的一个链接，就能够快速地跳转到另一个页面，从而按我们的意愿来访问服务器。\n\n而书中是这样描述万维网的工作方式：\n\n> 万维网以客户服务器的方式工作，浏览器就是安装在用户主机上的万维网客户程序，万维网文档所驻留的主机则运行服务器程序，因此这台主机也称为万维网服务器。**客户程序向服务器程序发出请求，服务器程序向客户程序送回客户所要的万维网文档**，在一个客户程序主窗口上显示出的万维网文档称为页面。\n\n上面提到的客户程序其实就是我们电脑上安装的浏览器，而服务端就是我们即将要去学习的Web服务器，也就是说，我们要明白如何搭建一个Web服务器并向用户发送我们提供的Web页面，在浏览器中显示的，一般就是HTML文档被解析后的样子。\n\n那么，我们的服务器可能不止一个页面，可能会有很多个页面，那么客户端如何知道该去访问哪个服务器的哪个页面呢？这个时候就需要用到`URL`统一资源定位符。互联网上所有的资源，都有一个唯一确定的URL，比如`http:\u002F\u002Fwww.baidu.com`\n\nURL的格式为：\n\n> \u003C协议>:\u002F\u002F\u003C主机>:\u003C端口>\u002F\u003C路径>\n>\n> 协议是指采用什么协议来访问服务器，不同的协议决定了服务器返回信息的格式，我们一般使用HTTP协议。\n>\n> 主机可以是一个域名，也可以是一个IP地址（实际上域名最后会被解析为IP地址进行访问）\n>\n> 端口是当前服务器上Web应用程序开启的端口，我们前面学习TCP通信的时候已经介绍过了，HTTP协议默认使用80端口，因此有时候可以省略。\n>\n> 路径就是我们希望去访问此服务器上的某个文件，不同的路径代表访问不同的资源。\n\n我们接着来了解一下什么是HTTP协议：\n\n> HTTP是面向事务的应用层协议，它是万维网上能够可靠交换文件的重要基础。HTTP不仅传送完成超文本跳转所需的必须信息，而且也传送任何可从互联网上得到的信息，如文本、超文本、声音和图像。\n\n实际上我们之前访问百度、访问自己的网站，所有的传输都是以HTTP作为协议进行的。\n\n我们来看看HTTP的传输原理：\n\n> HTTP使用了面向连接的TCP作为运输层协议，保证了数据的可靠传输。HTTP不必考虑数据在传输过程中被丢弃后又怎样被重传。但是HTTP协议本身是无连接的。也就是说，HTTP虽然使用了TCP连接，但是通信的双方在交换HTTP报文之前不需要先建立HTTP连接。1997年以前使用的是HTTP\u002F1.0协议，之后就是HTTP\u002F1.1协议了。\n\n那么既然HTTP是基于TCP进行通信的，我们首先来回顾一下TCP的通信原理：\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FTsOduK6njA47EM9.jpg)\n\nTCP协议实际上是经历了三次握手再进行通信，也就是说保证整个通信是稳定的，才可以进行数据交换，并且在连接已经建立的过程中，双方随时可以互相发送数据，直到有一方主动关闭连接，这时在进行四次挥手，完成整个TCP通信。\n\n而HTTP和TCP并不是一个层次的通信协议，TCP是传输层协议，而HTTP是应用层协议，因此，实际上HTTP的内容会作为TCP协议的报文被封装，并继续向下一层进行传递，而传输到客户端时，会依次进行解包，还原为最开始的HTTP数据。\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FspBTAKanGg456ry.jpg)\n\nHTTP使用TCP协议是为了使得数据传输更加可靠，既然它是依靠TCP协议进行数据传输，那么为什么说它本身是无连接的呢？我们来看一下HTTP的传输过程：\n\n> 用户在点击鼠标链接某个万维网文档时，HTTP协议首先要和服务器建立TCP连接。这需要使用三报文握手。当建立TCP连接的三报文握手的前两部分完成后（即经过了一个RTT时间后），万维网客户就把HTTP请求报文作为建立TCP连接的三报文握手中的第三个报文的数据，发送给万维网服务器。服务器收到HTTP请求报文后，就把所请求的文档作为响应报文返回给客户。\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FJzRBt1CovVsh9Ne.jpg)\n\n因此，我们的浏览器请求一个页面，需要两倍的往返时间。\n\n最后，我们再来了解一下HTTP的报文结构：\n\n![image-20230306164008613](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002Fkv8ctdzprnM3ise.png)\n\n由客户端向服务端发送是报文称为请求报文，而服务端返回给客户端的称为响应报文，实际上，整个报文全部是以文本形式发送的，通过使用空格和换行来完成分段。\n\n现在，我们已经了解了HTTP协议的全部基础知识，那么什么是Web服务器呢，实际上，它就是一个软件，但是它已经封装了所有的HTTP协议层面的操作，我们无需关心如何使用HTTP协议通信，而是直接基于服务器软件进行开发，我们只需要关心我们的页面数据如何展示、前后端如何交互即可。\n\n## 认识Tomcat服务器\n\n[![Tomcat Home](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FIfnu5kFWxAOPd6E.png)](http:\u002F\u002Ftomcat.apache.org\u002F)\n\nTomcat（汤姆猫）就是一个典型的Web应用服务器软件，通过运行Tomcat服务器，我们就可以快速部署我们的Web项目，并交由Tomcat进行管理，我们只需要直接通过浏览器访问我们的项目即可。\n\n那么首先，我们需要进行一个简单的环境搭建，我们需要在Tomcat官网下载最新的Tomcat服务端程序：https:\u002F\u002Ftomcat.apache.org\u002Fdownload-10.cgi（下载速度可能有点慢）\n\n- 下载：64-bit Windows zip\n\n下载完成后，解压，并放入桌面，接下来需要配置一下环境变量，打开`高级系统设置`，打开`环境变量`，添加一个新的系统变量，变量名称为`JRE_HOME`，填写JDK的安装目录+\u002Fjre，比如Zulujdk默认就是：C:\\Program Files\\Zulu\\zulu-8\\jre\n\n设置完成后，我们进入tomcat文件夹bin目录下，并在当前位置打开CMD窗口，将startup.sh拖入窗口按回车运行，如果环境变量配置有误，会提示，若没问题，服务器则正常启动。\n\n如果出现乱码，说明编码格式配置有问题，我们修改一下服务器的配置文件，打开`conf`文件夹，找到`logging.properties`文件，这就是日志的配置文件（我们在前面已经给大家讲解过了）将ConsoleHandler的默认编码格式修改为GBK编码格式：\n\n```properties\njava.util.logging.ConsoleHandler.encoding = GBK\n```\n\n现在重新启动服务器，就可以正常显示中文了。\n\n服务器启动成功之后，不要关闭，我们打开浏览器，在浏览器中访问：http:\u002F\u002Flocalhost:8080\u002F，Tomcat服务器默认是使用8080端口（可以在配置文件中修改），访问成功说明我们的Tomcat环境已经部署成功了。\n\n整个Tomcat目录下，我们已经认识了bin目录（所有可执行文件，包括启动和关闭服务器的脚本）以及conf目录（服务器配置文件目录），那么我们接着来看其他的文件夹：\n\n* lib目录：Tomcat服务端运行的一些依赖，不用关心。\n* logs目录：所有的日志信息都在这里。\n* temp目录：存放运行时产生的一些临时文件，不用关心。\n* work目录：工作目录，Tomcat会将jsp文件转换为java文件（我们后面会讲到，这里暂时不提及）\n* webapp目录：所有的Web项目都在这里，每个文件夹都是一个Web应用程序：\n\n我们发现，官方已经给我们预设了一些项目了，访问后默认使用的项目为ROOT项目，也就是我们默认打开的网站。\n\n我们也可以访问example项目，只需要在后面填写路径即可：http:\u002F\u002Flocalhost:8080\u002Fexamples\u002F，或是docs项目（这个是Tomcat的一些文档）http:\u002F\u002Flocalhost:8080\u002Fdocs\u002F\n\nTomcat还自带管理页面，我们打开：http:\u002F\u002Flocalhost:8080\u002Fmanager，提示需要用户名和密码，由于不知道是什么，我们先点击取消，页面中出现如下内容：\n\n> You are not authorized to view this page. If you have not changed any configuration files, please examine the file `conf\u002Ftomcat-users.xml` in your installation. That file must contain the credentials to let you use this webapp.\n>\n> For example, to add the `manager-gui` role to a user named `tomcat` with a password of `s3cret`, add the following to the config file listed above.\n>\n> ```\n> \u003Crole rolename=\"manager-gui\"\u002F>\n> \u003Cuser username=\"tomcat\" password=\"s3cret\" roles=\"manager-gui\"\u002F>\n> ```\n>\n> Note that for Tomcat 7 onwards, the roles required to use the manager application were changed from the single `manager` role to the following four roles. You will need to assign the role(s) required for the functionality you wish to access.\n>\n> - `manager-gui` - allows access to the HTML GUI and the status pages\n> - `manager-script` - allows access to the text interface and the status pages\n> - `manager-jmx` - allows access to the JMX proxy and the status pages\n> - `manager-status` - allows access to the status pages only\n>\n> The HTML interface is protected against CSRF but the text and JMX interfaces are not. To maintain the CSRF protection:\n>\n> - Users with the `manager-gui` role should not be granted either the `manager-script` or `manager-jmx` roles.\n> - If the text or jmx interfaces are accessed through a browser (e.g. for testing since these interfaces are intended for tools not humans) then the browser must be closed afterwards to terminate the session.\n>\n> For more information - please see the [Manager App How-To](http:\u002F\u002Flocalhost:8080\u002Fdocs\u002Fmanager-howto.html).\n\n现在我们按照上面的提示，去配置文件中进行修改：\n\n```xml\n  \u003Crole rolename=\"manager-gui\"\u002F>\n  \u003Cuser username=\"admin\" password=\"admin\" roles=\"manager-gui\"\u002F>\n```\n\n现在再次打开管理页面，已经可以成功使用此用户进行登陆了。登录后，展示给我们的是一个图形化界面，我们可以快速预览当前服务器的一些信息，包括已经在运行的Web应用程序，甚至还可以查看当前的Web应用程序有没有出现内存泄露。\n\n同样的，还有一个虚拟主机管理页面，用于一台主机搭建多个Web站点，一般情况下使用不到，这里就不做演示了。\n\n我们可以将我们自己的项目也放到webapp文件夹中，这样就可以直接访问到了，我们在webapp目录下新建test文件夹，将我们之前编写的前端代码全部放入其中（包括html文件、js、css、icon等），重启服务器。\n\n我们可以直接通过 http:\u002F\u002Flocalhost:8080\u002Ftest\u002F 来进行访问。\n\n***\n\n## 使用Maven创建Web项目\n\n虽然我们已经可以在Tomcat上部署我们的前端页面了，但是依然只是一个静态页面（每次访问都是同样的样子），那么如何向服务器请求一个动态的页面呢（比如显示我们访问当前页面的时间）这时就需要我们编写一个Web应用程序来实现了，我们需要在用户向服务器发起页面请求时，进行一些处理，再将结果发送给用户的浏览器。\n\n**注意：** 这里需要使用终极版IDEA，如果你的还是社区版，就很难受了。\n\n我们打开IDEA，新建一个项目，选择Java Enterprise（社区版没有此选项！）项目名称随便，项目模板选择Web应用程序，然后我们需要配置Web应用程序服务器，将我们的Tomcat服务器集成到IDEA中。配置很简单，首先点击新建，然后设置Tomcat主目录即可，配置完成后，点击下一步即可，依赖项使用默认即可，然后点击完成，之后IDEA会自动帮助我们创建Maven项目。\n\n创建完成后，直接点击右上角即可运行此项目了，但是我们发现，有一个Servlet页面不生效。\n\n需要注意的是，Tomcat10以上的版本比较新，Servlet API包名发生了一些变化，因此我们需要修改一下依赖：\n\n```xml\n\u003Cdependency>\n    \u003CgroupId>jakarta.servlet\u003C\u002FgroupId>\n    \u003CartifactId>jakarta.servlet-api\u003C\u002FartifactId>\n    \u003Cversion>5.0.0\u003C\u002Fversion>\n    \u003Cscope>provided\u003C\u002Fscope>\n\u003C\u002Fdependency>\n```\n\n注意包名全部从javax改为jakarta，我们需要手动修改一下。\n\n感兴趣的可以了解一下为什么名称被修改了：\n\n> Eclipse基金会在2019年对 Java EE 标准的每个规范进行了重命名，阐明了每个规范在Jakarta EE平台未来的角色。\n>\n> 新的名称Jakarta EE是Java EE的第二次重命名。2006年5月，“J2EE”一词被弃用，并选择了Java EE这个名称。在YouTube还只是一家独立的公司的时候，数字2就就从名字中消失了，而且当时冥王星仍然被认为是一颗行星。同样，作为Java SE 5（2004）的一部分，数字2也从J2SE中删除了，那时谷歌还没有上市。\n>\n> **因为不能再使用javax名称空间，Jakarta EE提供了非常明显的分界线。**\n>\n> - Jakarta 9（2019及以后）使用jakarta命名空间。\n> - Java EE 5（2005）到Java EE 8（2017）使用javax命名空间。\n> - Java EE 4使用javax命名空间。\n\n我们可以将项目直接打包为war包（默认），打包好之后，放入webapp文件夹，就可以直接运行我们通过Java编写的Web应用程序了，访问路径为文件的名称。\n\n## Servlet\n\n前面我们已经完成了基本的环境搭建，那么现在我们就可以开始来了解我们的第一个重要类——Servlet。\n\n它是Java EE的一个标准，大部分的Web服务器都支持此标准，包括Tomcat，就像之前的JDBC一样，由官方定义了一系列接口，而具体实现由我们来编写，最后交给Web服务器（如Tomcat）来运行我们编写的Servlet。\n\n那么，它能做什么呢？我们可以通过实现Servlet来进行动态网页响应，使用Servlet，不再是直接由Tomcat服务器发送我们编写好的静态网页内容（HTML文件），而是由我们通过Java代码进行动态拼接的结果，它能够很好地实现动态网页的返回。\n\n当然，Servlet并不是专用于HTTP协议通信，也可以用于其他的通信，但是一般都是用于HTTP。\n\n### 创建Servlet\n\n那么如何创建一个Servlet呢，非常简单，我们只需要实现`Servlet`类即可，并添加注解`@WebServlet`来进行注册。\n\n```java\n@WebServlet(\"\u002Ftest\")\npublic class TestServlet implements Servlet {\n\t\t...实现接口方法\n}\n```\n\n我们现在就可以去访问一下我们的页面：http:\u002F\u002Flocalhost:8080\u002Ftest\u002Ftest\n\n我们发现，直接访问此页面是没有任何内容的，这是因为我们还没有为该请求方法编写实现，这里先不做讲解，后面我们会对浏览器的请求处理做详细的介绍。\n\n除了直接编写一个类，我们也可以在`web.xml`中进行注册，现将类上`@WebServlet`的注解去掉：\n\n```xml\n\u003Cservlet>\n    \u003Cservlet-name>test\u003C\u002Fservlet-name>\n    \u003Cservlet-class>com.example.webtest.TestServlet\u003C\u002Fservlet-class>\n\u003C\u002Fservlet>\n\u003Cservlet-mapping>\n    \u003Cservlet-name>test\u003C\u002Fservlet-name>\n    \u003Curl-pattern>\u002Ftest\u003C\u002Furl-pattern>\n\u003C\u002Fservlet-mapping>\n```\n\n这样的方式也能注册Servlet，但是显然直接使用注解更加方便，因此之后我们一律使用注解进行开发。只有比较新的版本才支持此注解，老的版本是不支持的哦。\n\n实际上，Tomcat服务器会为我们提供一些默认的Servlet，也就是说在服务器启动后，即使我们什么都不编写，Tomcat也自带了几个默认的Servlet，他们编写在conf目录下的web.xml中：\n\n```xml\n\u003C!-- The mapping for the default servlet -->\n    \u003Cservlet-mapping>\n        \u003Cservlet-name>default\u003C\u002Fservlet-name>\n        \u003Curl-pattern>\u002F\u003C\u002Furl-pattern>\n    \u003C\u002Fservlet-mapping>\n\n    \u003C!-- The mappings for the JSP servlet -->\n    \u003Cservlet-mapping>\n        \u003Cservlet-name>jsp\u003C\u002Fservlet-name>\n        \u003Curl-pattern>*.jsp\u003C\u002Furl-pattern>\n        \u003Curl-pattern>*.jspx\u003C\u002Furl-pattern>\n    \u003C\u002Fservlet-mapping>\n\n```\n\n我们发现，默认的Servlet实际上可以帮助我们去访问一些静态资源，这也是为什么我们启动Tomcat服务器之后，能够直接访问webapp目录下的静态页面。\n\n我们可以将之前编写的页面放入到webapp目录下，来测试一下是否能直接访问。\n\n### 探究Servlet的生命周期\n\n我们已经了解了如何注册一个Servlet，那么我们接着来看看，一个Servlet是如何运行的。\n\n首先我们需要了解，Servlet中的方法各自是在什么时候被调用的，我们先编写一个打印语句来看看：\n\n```java\npublic class TestServlet implements Servlet {\n\n    public TestServlet(){\n        System.out.println(\"我是构造方法！\");\n    }\n\n    @Override\n    public void init(ServletConfig servletConfig) throws ServletException {\n        System.out.println(\"我是init\");\n    }\n\n    @Override\n    public ServletConfig getServletConfig() {\n        System.out.println(\"我是getServletConfig\");\n        return null;\n    }\n\n    @Override\n    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {\n        System.out.println(\"我是service\");\n    }\n\n    @Override\n    public String getServletInfo() {\n        System.out.println(\"我是getServletInfo\");\n        return null;\n    }\n\n    @Override\n    public void destroy() {\n        System.out.println(\"我是destroy\");\n    }\n}\n```\n\n我们首先启动一次服务器，然后访问我们定义的页面，然后再关闭服务器，得到如下的顺序：\n\n> 我是构造方法！\n> 我是init\n> 我是service\n> 我是service（出现两次是因为浏览器请求了2次，是因为有一次是请求favicon.ico，浏览器通病）\n>\n> 我是destroy\n\n我们可以多次尝试去访问此页面，但是init和构造方法只会执行一次，而每次访问都会执行的是`service`方法，因此，一个Servlet的生命周期为：\n\n- 首先执行构造方法完成 Servlet 初始化\n- Servlet 初始化后调用 **init ()** 方法。\n- Servlet 调用 **service()** 方法来处理客户端的请求。\n- Servlet 销毁前调用 **destroy()** 方法。\n- 最后，Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。\n\n现在我们发现，实际上在Web应用程序运行时，每当浏览器向服务器发起一个请求时，都会创建一个线程执行一次`service`方法，来让我们处理用户的请求，并将结果响应给用户。\n\n我们发现`service`方法中，还有两个参数，`ServletRequest`和`ServletResponse`，实际上，用户发起的HTTP请求，就被Tomcat服务器封装为了一个`ServletRequest`对象，我们得到是其实是Tomcat服务器帮助我们创建的一个实现类，HTTP请求报文中的所有内容，都可以从`ServletRequest`对象中获取，同理，`ServletResponse`就是我们需要返回给浏览器的HTTP响应报文实体类封装。\n\n那么我们来看看`ServletRequest`中有哪些内容，我们可以获取请求的一些信息：\n\n```java\n@Override\npublic void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {\n    \u002F\u002F首先将其转换为HttpServletRequest（继承自ServletRequest，一般是此接口实现）\n        HttpServletRequest request = (HttpServletRequest) servletRequest;\n        \n        System.out.println(request.getProtocol());  \u002F\u002F获取协议版本\n        System.out.println(request.getRemoteAddr());  \u002F\u002F获取访问者的IP地址\n  \t\t  System.out.println(request.getMethod());   \u002F\u002F获取请求方法\n        \u002F\u002F获取头部信息\n        Enumeration\u003CString> enumeration = request.getHeaderNames();\n        while (enumeration.hasMoreElements()){\n            String name = enumeration.nextElement();\n            System.out.println(name + \": \" + request.getHeader(name));\n        }\n}\n```\n\n我们发现，整个HTTP请求报文中的所有内容，都可以通过`HttpServletRequest`对象来获取，当然，它的作用肯定不仅仅是获取头部信息，我们还可以使用它来完成更多操作，后面会一一讲解。\n\n那么我们再来看看`ServletResponse`，这个是服务端的响应内容，我们可以在这里填写我们想要发送给浏览器显示的内容：\n\n```java\n\u002F\u002F转换为HttpServletResponse（同上）\nHttpServletResponse response = (HttpServletResponse) servletResponse;\n\u002F\u002F设定内容类型以及编码格式（普通HTML文本使用text\u002Fhtml，之后会讲解文件传输）\nresponse.setHeader(\"Content-type\", \"text\u002Fhtml;charset=UTF-8\");\n\u002F\u002F获取Writer直接写入内容\nresponse.getWriter().write(\"我是响应内容！\");\n\u002F\u002F所有内容写入完成之后，再发送给浏览器\n```\n\n现在我们在浏览器中打开此页面，就能够收到服务器发来的响应内容了。其中，响应头部分，是由Tomcat帮助我们生成的一个默认响应头。\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FOpTzXU5b8VjkSiB.jpg)\n\n因此，实际上整个流程就已经很清晰明了了。\n\n### 解读和使用HttpServlet\n\n前面我们已经学习了如何创建、注册和使用Servlet，那么我们继续来深入学习Servlet接口的一些实现类。\n\n首先`Servlet`有一个直接实现抽象类`GenericServlet`，那么我们来看看此类做了什么事情。\n\n我们发现，这个类完善了配置文件读取和Servlet信息相关的的操作，但是依然没有去实现service方法，因此此类仅仅是用于完善一个Servlet的基本操作，那么我们接着来看`HttpServlet`，它是遵循HTTP协议的一种Servlet，继承自`GenericServlet`，它根据HTTP协议的规则，完善了service方法。\n\n在阅读了HttpServlet源码之后，我们发现，其实我们只需要继承HttpServlet来编写我们的Servlet就可以了，并且它已经帮助我们提前实现了一些操作，这样就会给我们省去很多的时间。\n\n```java\n@Log\n@WebServlet(\"\u002Ftest\")\npublic class TestServlet extends HttpServlet {\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        resp.setContentType(\"text\u002Fhtml;charset=UTF-8\");\n        resp.getWriter().write(\"\u003Ch1>恭喜你解锁了全新玩法\u003C\u002Fh1>\");\n    }\n}\n```\n\n现在，我们只需要重写对应的请求方式，就可以快速完成Servlet的编写。\n\n### @WebServlet注解详解\n\n我们接着来看WebServlet注解，我们前面已经得知，可以直接使用此注解来快速注册一个Servlet，那么我们来想细看看此注解还有什么其他的玩法。\n\n首先name属性就是Servlet名称，而urlPatterns和value实际上是同样功能，就是代表当前Servlet的访问路径，它不仅仅可以是一个固定值，还可以进行通配符匹配：\n\n```java\n@WebServlet(\"\u002Ftest\u002F*\")\n```\n\n上面的路径表示，所有匹配`\u002Ftest\u002F随便什么`的路径名称，都可以访问此Servlet，我们可以在浏览器中尝试一下。\n\n也可以进行某个扩展名称的匹配：\n\n```java\n@WebServlet(\"*.js\")\n```\n\n这样的话，获取任何以js结尾的文件，都会由我们自己定义的Servlet处理。\n\n那么如果我们的路径为`\u002F`呢？\n\n```java\n@WebServlet(\"\u002F\")\n```\n\n此路径和Tomcat默认为我们提供的Servlet冲突，会直接替换掉默认的，而使用我们的，此路径的意思为，如果没有找到匹配当前访问路径的Servlet，那么久会使用此Servlet进行处理。\n\n我们还可以为一个Servlet配置多个访问路径：\n\n```java\n@WebServlet({\"\u002Ftest1\", \"\u002Ftest2\"})\n```\n\n我们接着来看loadOnStartup属性，此属性决定了是否在Tomcat启动时就加载此Servlet，默认情况下，Servlet只有在被访问时才会加载，它的默认值为-1，表示不在启动时加载，我们可以将其修改为大于等于0的数，来开启启动时加载。并且数字的大小决定了此Servlet的启动优先级。\n\n```java\n@Log\n@WebServlet(value = \"\u002Ftest\", loadOnStartup = 1)\npublic class TestServlet extends HttpServlet {\n\n    @Override\n    public void init() throws ServletException {\n        super.init();\n        log.info(\"我被初始化了！\");\n    }\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        resp.setContentType(\"text\u002Fhtml;charset=UTF-8\");\n        resp.getWriter().write(\"\u003Ch1>恭喜你解锁了全新玩法\u003C\u002Fh1>\");\n    }\n}\n```\n\n其他内容都是Servlet的一些基本配置，这里就不详细讲解了。\n\n### 使用POST请求完成登陆\n\n我们前面已经了解了如何使用Servlet来处理HTTP请求，那么现在，我们就结合前端，来实现一下登陆操作。\n\n我们需要修改一下我们的Servlet，现在我们要让其能够接收一个POST请求：\n\n```java\n@Log\n@WebServlet(\"\u002Flogin\")\npublic class LoginServlet extends HttpServlet {\n    @Override\n    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        req.getParameterMap().forEach((k, v) -> {\n            System.out.println(k + \": \" + Arrays.toString(v));\n        });\n    }\n}\n```\n\n`ParameterMap`存储了我们发送的POST请求所携带的表单数据，我们可以直接将其遍历查看，浏览器发送了什么数据。\n\n现在我们再来修改一下前端：\n\n```html\n\u003Cbody>\n    \u003Ch1>登录到系统\u003C\u002Fh1>\n    \u003Cform method=\"post\" action=\"login\">\n        \u003Chr>\n        \u003Cdiv>\n            \u003Clabel>\n                \u003Cinput type=\"text\" placeholder=\"用户名\" name=\"username\">\n            \u003C\u002Flabel>\n        \u003C\u002Fdiv>\n        \u003Cdiv>\n            \u003Clabel>\n                \u003Cinput type=\"password\" placeholder=\"密码\" name=\"password\">\n            \u003C\u002Flabel>\n        \u003C\u002Fdiv>\n        \u003Cdiv>\n            \u003Cbutton>登录\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n    \u003C\u002Fform>\n\u003C\u002Fbody>\n```\n\n通过修改form标签的属性，现在我们点击登录按钮，会自动向后台发送一个POST请求，请求地址为当前地址+\u002Flogin（注意不同路径的写法），也就是我们上面编写的Servlet路径。\n\n运行服务器，测试后发现，在点击按钮后，确实向服务器发起了一个POST请求，并且携带了表单中文本框的数据。\n\n现在，我们根据已有的基础，将其与数据库打通，我们进行一个真正的用户登录操作，首先修改一下Servlet的逻辑：\n\n```java\n@Override\nprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n    \u002F\u002F首先设置一下响应类型\n    resp.setContentType(\"text\u002Fhtml;charset=UTF-8\");\n    \u002F\u002F获取POST请求携带的表单数据\n    Map\u003CString, String[]> map = req.getParameterMap();\n    \u002F\u002F判断表单是否完整\n    if(map.containsKey(\"username\") && map.containsKey(\"password\")) {\n        String username = req.getParameter(\"username\");\n        String password = req.getParameter(\"password\");\n\n        \u002F\u002F权限校验（待完善）\n    }else {\n        resp.getWriter().write(\"错误，您的表单数据不完整！\");\n    }\n}\n```\n\n接下来我们再去编写Mybatis的依赖和配置文件，创建一个表，用于存放我们用户的账号和密码。\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\u003C!DOCTYPE configuration\n        PUBLIC \"-\u002F\u002Fmybatis.org\u002F\u002FDTD Config 3.0\u002F\u002FEN\"\n        \"http:\u002F\u002Fmybatis.org\u002Fdtd\u002Fmybatis-3-config.dtd\">\n\u003Cconfiguration>\n    \u003Cenvironments default=\"development\">\n        \u003Cenvironment id=\"development\">\n            \u003CtransactionManager type=\"JDBC\"\u002F>\n            \u003CdataSource type=\"POOLED\">\n                \u003Cproperty name=\"driver\" value=\"${驱动类（含包名）}\"\u002F>\n                \u003Cproperty name=\"url\" value=\"${数据库连接URL}\"\u002F>\n                \u003Cproperty name=\"username\" value=\"${用户名}\"\u002F>\n                \u003Cproperty name=\"password\" value=\"${密码}\"\u002F>\n            \u003C\u002FdataSource>\n        \u003C\u002Fenvironment>\n    \u003C\u002Fenvironments>\n\u003C\u002Fconfiguration>\n```\n\n```xml\n\u003Cdependency>\n    \u003CgroupId>org.mybatis\u003C\u002FgroupId>\n    \u003CartifactId>mybatis\u003C\u002FartifactId>\n    \u003Cversion>3.5.7\u003C\u002Fversion>\n\u003C\u002Fdependency>\n\u003Cdependency>\n    \u003CgroupId>mysql\u003C\u002FgroupId>\n    \u003CartifactId>mysql-connector-java\u003C\u002FartifactId>\n    \u003Cversion>8.0.27\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n配置完成后，在我们的Servlet的init方法中编写Mybatis初始化代码，因为它只需要初始化一次。\n\n```java\nSqlSessionFactory factory;\n@SneakyThrows\n@Override\npublic void init() throws ServletException {\n    factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(\"mybatis-config.xml\"));\n}\n```\n\n现在我们创建一个实体类以及Mapper来进行用户信息查询：\n\n```java\n@Data\npublic class User {\n    String username;\n    String password;\n}\n```\n\n```java\npublic interface UserMapper {\n\n    @Select(\"select * from users where username = #{username} and password = #{password}\")\n    User getUser(@Param(\"username\") String username, @Param(\"password\") String password);\n}\n```\n\n```xml\n\u003Cmappers>\n    \u003Cmapper class=\"com.example.dao.UserMapper\"\u002F>\n\u003C\u002Fmappers>\n```\n\n好了，现在完事具备，只欠东风了，我们来完善一下登陆验证逻辑：\n\n```java\n\u002F\u002F登陆校验（待完善）\ntry (SqlSession sqlSession = factory.openSession(true)){\n    UserMapper mapper = sqlSession.getMapper(UserMapper.class);\n    User user = mapper.getUser(username, password);\n    \u002F\u002F判断用户是否登陆成功，若查询到信息则表示存在此用户\n    if(user != null){\n        resp.getWriter().write(\"登陆成功！\");\n    }else {\n        resp.getWriter().write(\"登陆失败，请验证您的用户名或密码！\");\n    }\n}\n```\n\n现在再去浏览器上进行测试吧！\n\n注册界面其实是同理的，这里就不多做讲解了。\n\n### 上传和下载文件\n\n首先我们来看看比较简单的下载文件，首先将我们的icon.png放入到resource文件夹中，接着我们编写一个Servlet用于处理文件下载：\n\n```java\n@WebServlet(\"\u002Ffile\")\npublic class FileServlet extends HttpServlet {\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n      resp.setContentType(\"image\u002Fpng\");  \n      OutputStream outputStream = resp.getOutputStream();\n      InputStream inputStream = Resources.getResourceAsStream(\"icon.png\");\n\n    }\n}\n```\n\n为了更加快速地编写IO代码，我们可以引入一个工具库：\n\n```xml\n\u003Cdependency>\n    \u003CgroupId>commons-io\u003C\u002FgroupId>\n    \u003CartifactId>commons-io\u003C\u002FartifactId>\n    \u003Cversion>2.6\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n使用此类库可以快速完成IO操作：\n\n```java\nresp.setContentType(\"image\u002Fpng\");\nOutputStream outputStream = resp.getOutputStream();\nInputStream inputStream = Resources.getResourceAsStream(\"icon.png\");\n\u002F\u002F直接使用copy方法完成转换\nIOUtils.copy(inputStream, outputStream);\n```\n\n现在我们在前端页面添加一个链接，用于下载此文件：\n\n```html\n\u003Chr>\n\u003Ca href=\"file\" download=\"icon.png\">点我下载高清资源\u003C\u002Fa>\n```\n\n下载文件搞定，那么如何上传一个文件呢？\n\n首先我们编写前端部分：\n\n```html\n\u003Cform method=\"post\" action=\"file\" enctype=\"multipart\u002Fform-data\">\n    \u003Cdiv>\n        \u003Cinput type=\"file\" name=\"test-file\">\n    \u003C\u002Fdiv>\n    \u003Cdiv>\n        \u003Cbutton>上传文件\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Fform>\n```\n\n注意必须添加`enctype=\"multipart\u002Fform-data\"`，来表示此表单用于文件传输。\n\n现在我们来修改一下Servlet代码：\n\n```java\n@MultipartConfig\n@WebServlet(\"\u002Ffile\")\npublic class FileServlet extends HttpServlet {\n\n    @Override\n    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        try(FileOutputStream stream = new FileOutputStream(\"\u002FUsers\u002Fnagocoler\u002FDocuments\u002FIdeaProjects\u002FWebTest\u002Ftest.png\")){\n            Part part = req.getPart(\"test-file\");\n            IOUtils.copy(part.getInputStream(), stream);\n            resp.setContentType(\"text\u002Fhtml;charset=UTF-8\");\n            resp.getWriter().write(\"文件上传成功！\");\n        }\n    }\n}\n```\n\n注意，必须添加`@MultipartConfig`注解来表示此Servlet用于处理文件上传请求。\n\n现在我们再运行服务器，并将我们刚才下载的文件又上传给服务端。\n\n### 使用XHR请求数据\n\n现在我们希望，网页中的部分内容，可以动态显示，比如网页上有一个时间，旁边有一个按钮，点击按钮就可以刷新当前时间。\n\n这个时候就需要我们在网页展示时向后端发起请求了，并根据后端响应的结果，动态地更新页面中的内容，要实现此功能，就需要用到JavaScript来帮助我们，首先在js中编写我们的XHR请求，并在请求中完成动态更新：\n\n```js\nfunction updateTime() {\n    let xhr = new XMLHttpRequest();\n    xhr.onreadystatechange = function() {\n        if (xhr.readyState === 4 && xhr.status === 200) {\n            document.getElementById(\"time\").innerText = xhr.responseText\n        }\n    };\n    xhr.open('GET', 'time', true);\n    xhr.send();\n}\n```\n\n接着修改一下前端页面，添加一个时间显示区域：\n\n```html\n\u003Chr>\n\u003Cdiv id=\"time\">\u003C\u002Fdiv>\n\u003Cbr>\n\u003Cbutton onclick=\"updateTime()\">更新数据\u003C\u002Fbutton>\n\u003Cscript>\n    updateTime()\n\u003C\u002Fscript>\n```\n\n最后创建一个Servlet用于处理时间更新请求：\n\n```java\n@WebServlet(\"\u002Ftime\")\npublic class TimeServlet extends HttpServlet {\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        SimpleDateFormat dateFormat = new SimpleDateFormat(\"yyyy年MM月dd日 HH:mm:ss\");\n        String date = dateFormat.format(new Date());\n        resp.setContentType(\"text\u002Fhtml;charset=UTF-8\");\n        resp.getWriter().write(date);\n    }\n}\n```\n\n现在点击按钮就可以更新了。\n\nGET请求也能传递参数，这里做一下演示。\n\n### 重定向与请求转发\n\n当我们希望用户登录完成之后，直接跳转到网站的首页，那么这个时候，我们就可以使用重定向来完成。当浏览器收到一个重定向的响应时，会按照重定向响应给出的地址，再次向此地址发出请求。\n\n实现重定向很简单，只需要调用一个方法即可，我们修改一下登陆成功后执行的代码：\n\n```java\nresp.sendRedirect(\"time\");\n```\n\n调用后，响应的状态码会被设置为302，并且响应头中添加了一个Location属性，此属性表示，需要重定向到哪一个网址。\n\n现在，如果我们成功登陆，那么服务器会发送给我们一个重定向响应，这时，我们的浏览器会去重新请求另一个网址。这样，我们在登陆成功之后，就可以直接帮助用户跳转到用户首页了。\n\n那么我们接着来看请求转发，请求转发其实是一种服务器内部的跳转机制，我们知道，重定向会使得浏览器去重新请求一个页面，而请求转发则是服务器内部进行跳转，它的目的是，直接将本次请求转发给其他Servlet进行处理，并由其他Servlet来返回结果，因此它是在进行内部的转发。\n\n```java\nreq.getRequestDispatcher(\"\u002Ftime\").forward(req, resp);\n```\n\n现在，在登陆成功的时候，我们将请求转发给处理时间的Servlet，注意这里的路径规则和之前的不同，我们需要填写Servlet上指明的路径，并且请求转发只能转发到此应用程序内部的Servlet，不能转发给其他站点或是其他Web应用程序。\n\n现在再次进行登陆操作，我们发现，返回结果为一个405页面，证明了，我们的请求现在是被另一个Servlet进行处理，并且请求的信息全部被转交给另一个Servlet，由于此Servlet不支持POST请求，因此返回405状态码。\n\n那么也就是说，该请求包括请求参数也一起被传递了，那么我们可以尝试获取以下POST请求的参数。\n\n现在我们给此Servlet添加POST请求处理，直接转交给Get请求处理：\n\n```java\n@Override\nprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n    this.doGet(req, resp);\n}\n```\n\n再次访问，成功得到结果，但是我们发现，浏览器只发起了一次请求，并没有再次请求新的URL，也就是说，这一次请求直接返回了请求转发后的处理结果。\n\n那么，请求转发有什么好处呢？它可以携带数据！\n\n```java\nreq.setAttribute(\"test\", \"我是请求转发前的数据\");\nreq.getRequestDispatcher(\"\u002Ftime\").forward(req, resp);\n```\n\n```java\nSystem.out.println(req.getAttribute(\"test\"));\n```\n\n通过`setAttribute`方法来给当前请求添加一个附加数据，在请求转发后，我们可以直接获取到该数据。\n\n重定向属于2次请求，因此无法使用这种方式来传递数据，那么，如何在重定向之间传递数据呢？我们可以使用即将要介绍的ServletContext对象。\n\n最后总结，两者的区别为：\n\n* 请求转发是一次请求，重定向是两次请求\n* 请求转发地址栏不会发生改变， 重定向地址栏会发生改变\n* 请求转发可以共享请求参数 ，重定向之后，就获取不了共享参数了\n* 请求转发只能转发给内部的Servlet\n\n### 了解ServletContext对象\n\nServletContext全局唯一，它是属于整个Web应用程序的，我们可以通过`getServletContext()`来获取到此对象。\n\n此对象也能设置附加值：\n\n```java\nServletContext context = getServletContext();\ncontext.setAttribute(\"test\", \"我是重定向之前的数据\");\nresp.sendRedirect(\"time\");\n```\n\n```java\nSystem.out.println(getServletContext().getAttribute(\"test\"));\n```\n\n因为无论在哪里，无论什么时间，获取到的ServletContext始终是同一个对象，因此我们可以随时随地获取我们添加的属性。\n\n它不仅仅可以用来进行数据传递，还可以做一些其他的事情，比如请求转发：\n\n```java\ncontext.getRequestDispatcher(\"\u002Ftime\").forward(req, resp);\n```\n\n它还可以获取根目录下的资源文件（注意是webapp根目录下的，不是resource中的资源）\n\n### 初始化参数\n\n初始化参数类似于初始化配置需要的一些值，比如我们的数据库连接相关信息，就可以通过初始化参数来给予Servlet，或是一些其他的配置项，也可以使用初始化参数来实现。\n\n我们可以给一个Servlet添加一些初始化参数：\n\n```java\n@WebServlet(value = \"\u002Flogin\", initParams = {\n        @WebInitParam(name = \"test\", value = \"我是一个默认的初始化参数\")\n})\n```\n\n它也是以键值对形式保存的，我们可以直接通过Servlet的`getInitParameter`方法获取：\n\n```java\nSystem.out.println(getInitParameter(\"test\"));\n```\n\n但是，这里的初始化参数仅仅是针对于此Servlet，我们也可以定义全局初始化参数，只需要在web.xml编写即可：\n\n```xml\n\u003Ccontext-param>\n    \u003Cparam-name>lbwnb\u003C\u002Fparam-name>\n    \u003Cparam-value>我是全局初始化参数\u003C\u002Fparam-value>\n\u003C\u002Fcontext-param>\n```\n\n我们需要使用ServletContext来读取全局初始化参数：\n\n```java\nServletContext context = getServletContext();\nSystem.out.println(context.getInitParameter(\"lbwnb\"));\n```\n\n有关ServletContext其他的内容，我们需要完成后面内容的学习，才能理解。\n\n***\n\n## Cookie\n\n什么是Cookie？不是曲奇，它可以在浏览器中保存一些信息，并且在下次请求时，请求头中会携带这些信息。\n\n我们可以编写一个测试用例来看看：\n\n```java\nCookie cookie = new Cookie(\"test\", \"yyds\");\nresp.addCookie(cookie);\nresp.sendRedirect(\"time\");\n```\n\n```java\nfor (Cookie cookie : req.getCookies()) {\n    System.out.println(cookie.getName() + \": \" + cookie.getValue());\n}\n```\n\n我们可以观察一下，在`HttpServletResponse`中添加Cookie之后，浏览器的响应头中会包含一个`Set-Cookie`属性，同时，在重定向之后，我们的请求头中，会携带此Cookie作为一个属性，同时，我们可以直接通过`HttpServletRequest`来快速获取有哪些Cookie信息。\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002F3JcLpr9GYMnbBHw.jpg)\n\n还有这么神奇的事情吗？那么我们来看看，一个Cookie包含哪些信息：\n\n* name   -   Cookie的名称，Cookie一旦创建，名称便不可更改\n* value  -   Cookie的值，如果值为Unicode字符，需要为字符编码。如果为二进制数据，则需要使用BASE64编码\n* maxAge  -  Cookie失效的时间，单位秒。如果为正数，则该Cookie在maxAge秒后失效。如果为负数，该Cookie为临时Cookie，关闭浏览器即失效，浏览器也不会以任何形式保存该Cookie。如果为0，表示删除该Cookie。默认为-1。\n* secure  -  该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS，SSL等，在网络上传输数据之前先将数据加密。默认为false。\n* path  -  Cookie的使用路径。如果设置为“\u002FsessionWeb\u002F”，则只有contextPath为“\u002FsessionWeb”的程序可以访问该Cookie。如果设置为“\u002F”，则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“\u002F”。\n* domain  -  可以访问该Cookie的域名。如果设置为“.google.com”，则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。\n* comment  -  该Cookie的用处说明，浏览器显示Cookie信息的时候显示该说明。\n* version  -  Cookie使用的版本号。0表示遵循Netscape的Cookie规范，1表示遵循W3C的RFC 2109规范\n\n我们发现，最关键的其实是`name`、`value`、`maxAge`、`domain`属性。\n\n那么我们来尝试修改一下maxAge来看看失效时间：\n\n```java\ncookie.setMaxAge(20);\n```\n\n设定为20秒，我们可以直接看到，响应头为我们设定了20秒的过期时间。20秒内访问都会携带此Cookie，而超过20秒，Cookie消失。\n\n既然了解了Cookie的作用，我们就可以通过使用Cookie来实现记住我功能，我们可以将用户名和密码全部保存在Cookie中，如果访问我们的首页时携带了这些Cookie，那么我们就可以直接为用户进行登陆，如果登陆成功则直接跳转到首页，如果登陆失败，则清理浏览器中的Cookie。\n\n那么首先，我们先在前端页面的表单中添加一个勾选框：\n\n```html\n\u003Cdiv>\n    \u003Clabel>\n        \u003Cinput type=\"checkbox\" placeholder=\"记住我\" name=\"remember-me\">\n        记住我\n    \u003C\u002Flabel>\n\u003C\u002Fdiv>\n```\n\n接着，我们在登陆成功时进行判断，如果用户勾选了记住我，那么就讲Cookie存储到本地：\n\n```java\nif(map.containsKey(\"remember-me\")){   \u002F\u002F若勾选了勾选框，那么会此表单信息\n    Cookie cookie_username = new Cookie(\"username\", username);\n    cookie_username.setMaxAge(30);\n    Cookie cookie_password = new Cookie(\"password\", password);\n    cookie_password.setMaxAge(30);\n    resp.addCookie(cookie_username);\n    resp.addCookie(cookie_password);\n}\n```\n\n然后，我们修改一下默认的请求地址，现在一律通过`http:\u002F\u002Flocalhost:8080\u002Fyyds\u002Flogin`进行登陆，那么我们需要添加GET请求的相关处理：\n\n```java\n@Override\nprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n    Cookie[] cookies = req.getCookies();\n    if(cookies != null){\n        String username = null;\n        String password = null;\n        for (Cookie cookie : cookies) {\n            if(cookie.getName().equals(\"username\")) username = cookie.getValue();\n            if(cookie.getName().equals(\"password\")) password = cookie.getValue();\n        }\n        if(username != null && password != null){\n            \u002F\u002F登陆校验\n            try (SqlSession sqlSession = factory.openSession(true)){\n                UserMapper mapper = sqlSession.getMapper(UserMapper.class);\n                User user = mapper.getUser(username, password);\n                if(user != null){\n                    resp.sendRedirect(\"time\");\n                    return;   \u002F\u002F直接返回\n                }\n            }\n        }\n    }\n    req.getRequestDispatcher(\"\u002F\").forward(req, resp);   \u002F\u002F正常情况还是转发给默认的Servlet帮我们返回静态页面\n}\n```\n\n现在，30秒内都不需要登陆，访问登陆页面后，会直接跳转到time页面。\n\n现在已经离我们理想的页面越来越接近了，但是仍然有一个问题，就是我们的首页，无论是否登陆，所有人都可以访问，那么，如何才可以实现只有登陆之后才能访问呢？这就需要用到Session了。\n\n***\n\n## Session\n\n由于HTTP是无连接的，那么如何能够辨别当前的请求是来自哪个用户发起的呢？Session就是用来处理这种问题的，每个用户的会话都会有一个自己的Session对象，来自同一个浏览器的所有请求，就属于同一个会话。\n\n但是HTTP协议是无连接的呀，那Session是如何做到辨别是否来自同一个浏览器呢？Session实际上是基于Cookie实现的，前面我们了解了Cookie，我们知道，服务端可以将Cookie保存到浏览器，当浏览器下次访问时，就会附带这些Cookie信息。\n\nSession也利用了这一点，它会给浏览器设定一个叫做`JSESSIONID`的Cookie，值是一个随机的排列组合，而此Cookie就对应了你属于哪一个对话，只要我们的浏览器携带此Cookie访问服务器，服务器就会通过Cookie的值进行辨别，得到对应的Session对象，因此，这样就可以追踪到底是哪一个浏览器在访问服务器。\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FwCYHNg39tFcK76M.gif)\n\n那么现在，我们在用户登录成功之后，将用户对象添加到Session中，只要是此用户发起的请求，我们都可以从`HttpSession`中读取到存储在会话中的数据：\n\n```java\nHttpSession session = req.getSession();\nsession.setAttribute(\"user\", user);\n```\n\n同时，如果用户没有登录就去访问首页，那么我们将发送一个重定向请求，告诉用户，需要先进行登录才可以访问：\n\n```java\nHttpSession session = req.getSession();\nUser user = (User) session.getAttribute(\"user\");\nif(user == null) {\n    resp.sendRedirect(\"login\");\n    return;\n}\n```\n\n在访问的过程中，注意观察Cookie变化。\n\nSession并不是永远都存在的，它有着自己的过期时间，默认时间为30分钟，若超过此时间，Session将丢失，我们可以在配置文件中修改过期时间：\n\n```xml\n\u003Csession-config>\n    \u003Csession-timeout>1\u003C\u002Fsession-timeout>\n\u003C\u002Fsession-config>\n```\n\n我们也可以在代码中使用`invalidate`方法来使Session立即失效：\n\n```java\nsession.invalidate();\n```\n\n现在，通过Session，我们就可以更好地控制用户对于资源的访问，只有完成登陆的用户才有资格访问首页。\n\n## Filter\n\n有了Session之后，我们就可以很好地控制用户的登陆验证了，只有授权的用户，才可以访问一些页面，但是我们需要一个一个去进行配置，还是太过复杂，能否一次性地过滤掉没有登录验证的用户呢？\n\n过滤器相当于在所有访问前加了一堵墙，来自浏览器的所有访问请求都会首先经过过滤器，只有过滤器允许通过的请求，才可以顺利地到达对应的Servlet，而过滤器不允许的通过的请求，我们可以自由地进行控制是否进行重定向或是请求转发。并且过滤器可以添加很多个，就相当于添加了很多堵墙，我们的请求只有穿过层层阻碍，才能与Servlet相拥，像极了爱情。\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002Fmd9X75EToshnH8I.jpg)\n\n添加一个过滤器非常简单，只需要实现Filter接口，并添加`@WebFilter`注解即可：\n\n```java\n@WebFilter(\"\u002F*\")   \u002F\u002F路径的匹配规则和Servlet一致，这里表示匹配所有请求\npublic class TestFilter implements Filter {\n    @Override\n    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n        \n    }\n}\n```\n\n这样我们就成功地添加了一个过滤器，那么添加一句打印语句看看，是否所有的请求都会经过此过滤器：\n\n```java\nHttpServletRequest request = (HttpServletRequest) servletRequest;\nSystem.out.println(request.getRequestURL());\n```\n\n我们发现，现在我们发起的所有请求，一律需要经过此过滤器，并且所有的请求都没有任何的响应内容。\n\n那么如何让请求可以顺利地到达对应的Servlet，也就是说怎么让这个请求顺利通过呢？我们只需要在最后添加一句：\n\n```java\nfilterChain.doFilter(servletRequest, servletResponse);\n```\n\n那么这行代码是什么意思呢？\n\n由于我们整个应用程序可能存在多个过滤器，那么这行代码的意思实际上是将此请求继续传递给下一个过滤器，当没有下一个过滤器时，才会到达对应的Servlet进行处理，我们可以再来创建一个过滤器看看效果：\n\n```java\n@WebFilter(\"\u002F*\")\npublic class TestFilter2 implements Filter {\n    @Override\n    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n        System.out.println(\"我是2号过滤器\");\n        filterChain.doFilter(servletRequest, servletResponse);\n    }\n}\n```\n\n由于过滤器的过滤顺序是按照类名的自然排序进行的，因此我们将第一个过滤器命名进行调整。\n\n我们发现，在经过第一个过滤器之后，会继续前往第二个过滤器，只有两个过滤器全部经过之后，才会到达我们的Servlet中。\n\n![点击查看源网页](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FLaDmPMWEtAB1HVF.jpg)\n\n实际上，当`doFilter`方法调用时，就会一直向下直到Servlet，在Servlet处理完成之后，又依次返回到最前面的Filter，类似于递归的结构，我们添加几个输出语句来判断一下：\n\n```java\n@Override\npublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n    System.out.println(\"我是2号过滤器\");\n    filterChain.doFilter(servletRequest, servletResponse);\n    System.out.println(\"我是2号过滤器，处理后\");\n}\n```\n\n```java\n@Override\npublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n    System.out.println(\"我是1号过滤器\");\n    filterChain.doFilter(servletRequest, servletResponse);\n    System.out.println(\"我是1号过滤器，处理后\");\n}\n```\n\n最后验证我们的结论。\n\n同Servlet一样，Filter也有对应的HttpFilter专用类，它针对HTTP请求进行了专门处理，因此我们可以直接使用HttpFilter来编写：\n\n```java\npublic abstract class HttpFilter extends GenericFilter {\n    private static final long serialVersionUID = 7478463438252262094L;\n\n    public HttpFilter() {\n    }\n\n    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {\n        if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {\n            this.doFilter((HttpServletRequest)req, (HttpServletResponse)res, chain);\n        } else {\n            throw new ServletException(\"non-HTTP request or response\");\n        }\n    }\n\n    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {\n        chain.doFilter(req, res);\n    }\n}\n```\n\n那么现在，我们就可以给我们的应用程序添加一个过滤器，用户在未登录情况下，只允许静态资源和登陆页面请求通过，登陆之后畅行无阻：\n\n```java\n@WebFilter(\"\u002F*\")\npublic class MainFilter extends HttpFilter {\n    @Override\n    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {\n        String url = req.getRequestURL().toString();\n        \u002F\u002F判断是否为静态资源\n        if(!url.endsWith(\".js\") && !url.endsWith(\".css\") && !url.endsWith(\".png\")){\n            HttpSession session = req.getSession();\n            User user = (User) session.getAttribute(\"user\");\n            \u002F\u002F判断是否未登陆\n            if(user == null && !url.endsWith(\"login\")){\n                res.sendRedirect(\"login\");\n                return;\n            }\n        }\n        \u002F\u002F交给过滤链处理\n        chain.doFilter(req, res);\n    }\n}\n```\n\n现在，我们的页面已经基本完善为我们想要的样子了。\n\n当然，可能跟着教程编写的项目比较乱，大家可以自己花费一点时间来重新编写一个Web应用程序，加深对之前讲解知识的理解。我们也会在之后安排一个编程实战进行深化练习。\n\n***\n\n## Listener\n\n监听器并不是我们学习的重点内容，那么什么是监听器呢？\n\n如果我们希望，在应用程序加载的时候，或是Session创建的时候，亦或是在Request对象创建的时候进行一些操作，那么这个时候，我们就可以使用监听器来实现。\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002Fxl4hzgOaSCdXHcu.png)\n\n默认为我们提供了很多类型的监听器，我们这里就演示一下监听Session的创建即可：\n\n```java\n@WebListener\npublic class TestListener implements HttpSessionListener {\n    @Override\n    public void sessionCreated(HttpSessionEvent se) {\n        System.out.println(\"有一个Session被创建了\");\n    }\n}\n```\n\n有关监听器相关内容，了解即可。\n\n***\n\n## 了解JSP页面与加载规则\n\n前面我们已经完成了整个Web应用程序生命周期中所有内容的学习，我们已经完全了解，如何编写一个Web应用程序，并放在Tomcat上部署运行，以及如何控制浏览器发来的请求，通过Session+Filter实现用户登陆验证，通过Cookie实现自动登陆等操作。到目前为止，我们已经具备编写一个完整Web网站的能力。\n\n在之前的教程中，我们的前端静态页面并没有与后端相结合，我们前端页面所需的数据全部需要单独向后端发起请求获取，并动态进行内容填充，这是一种典型的前后端分离写法，前端只负责要数据和显示数据，后端只负责处理数据和提供数据，这也是现在更流行的一种写法，让前端开发者和后端开发者各尽其责，更加专一，这才是我们所希望的开发模式。\n\nJSP并不是我们需要重点学习的内容，因为它已经过时了，使用JSP会导致前后端严重耦合，因此这里只做了解即可。\n\nJSP其实就是一种模板引擎，那么何谓模板引擎呢？顾名思义，它就是一个模板，而模板需要我们填入数据，才可以变成一个页面，也就是说，我们可以直接在前端页面中直接填写数据，填写后生成一个最终的HTML页面返回给前端。\n\n首先我们来创建一个新的项目，项目创建成功后，删除Java目录下的内容，只留下默认创建的jsp文件，我们发现，在webapp目录中，存在一个`index.jsp`文件，现在我们直接运行项目，会直接访问这个JSP页面。\n\n```jsp\n\u003C%@ page contentType=\"text\u002Fhtml; charset=UTF-8\" pageEncoding=\"UTF-8\" %>\n\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\n    \u003Ctitle>JSP - Hello World\u003C\u002Ftitle>\n\u003C\u002Fhead>\n\u003Cbody>\n\u003Ch1>\u003C%= \"Hello World!\" %>\n\u003C\u002Fh1>\n\u003Cbr\u002F>\n\u003Ca href=\"hello-servlet\">Hello Servlet\u003C\u002Fa>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n但是我们并没有编写对应的Servlet来解析啊，那么为什么这个JSP页面会被加载呢？\n\n实际上，我们一开始提到的两个Tomcat默认的Servlet中，一个是用于请求静态资源，还有一个就是用于处理jsp的：\n\n```xml\n\u003C!-- The mappings for the JSP servlet -->\n    \u003Cservlet-mapping>\n        \u003Cservlet-name>jsp\u003C\u002Fservlet-name>\n        \u003Curl-pattern>*.jsp\u003C\u002Furl-pattern>\n        \u003Curl-pattern>*.jspx\u003C\u002Furl-pattern>\n    \u003C\u002Fservlet-mapping>\n```\n\n那么，JSP和普通HTML页面有什么区别呢，我们发现它的语法和普通HTML页面几乎一致，我们可以直接在JSP中编写Java代码，并在页面加载的时候执行，我们随便找个地方插入：\n\n```jsp\n\u003C%\n    System.out.println(\"JSP页面被加载\");\n%>\n```\n\n我们发现，请求一次页面，页面就会加载一次，并执行我们填写的Java代码。也就是说，我们可以直接在此页面中执行Java代码来填充我们的数据，这样我们的页面就变成了一个动态页面，使用`\u003C%=  %>`来填写一个值：\n\n```jsp\n\u003Ch1>\u003C%= new Date() %>\u003C\u002Fh1>\n```\n\n现在访问我们的网站，每次都会创建一个新的Date对象，因此每次访问获取的时间都不一样，我们的网站已经算是一个动态的网站的了。\n\n虽然这样在一定程度上上为我们提供了便利，但是这样的写法相当于整个页面既要编写前端代码，也要编写后端代码，随着项目的扩大，整个页面会显得难以阅读，并且现在都是前后端开发人员职责非常明确的，如果要编写JSP页面，那就必须要招一个既会前端也会后端的程序员，这样显然会导致不必要的开销。\n\n那么我们来研究一下，为什么JSP页面能够在加载的时候执行Java代码呢？\n\n首先我们将此项目打包，并在Tomcat服务端中运行，生成了一个文件夹并且可以正常访问。\n\n我们现在看到`work`目录，我们发现这个里面多了一个`index_jsp.java`和`index_jsp.class`，那么这些东西是干嘛的呢，我们来反编译一下就啥都知道了：\n\n```java\npublic final class index_jsp extends org.apache.jasper.runtime.HttpJspBase  \u002F\u002F继承自HttpServlet\n    implements org.apache.jasper.runtime.JspSourceDependent,\n                 org.apache.jasper.runtime.JspSourceImports {\n\n ...\n\n  public void _jspService(final jakarta.servlet.http.HttpServletRequest request, final jakarta.servlet.http.HttpServletResponse response)\n      throws java.io.IOException, jakarta.servlet.ServletException {\n\n    if (!jakarta.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {\n      final java.lang.String _jspx_method = request.getMethod();\n      if (\"OPTIONS\".equals(_jspx_method)) {\n        response.setHeader(\"Allow\",\"GET, HEAD, POST, OPTIONS\");\n        return;\n      }\n      if (!\"GET\".equals(_jspx_method) && !\"POST\".equals(_jspx_method) && !\"HEAD\".equals(_jspx_method)) {\n        response.setHeader(\"Allow\",\"GET, HEAD, POST, OPTIONS\");\n        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, \"JSP 只允许 GET、POST 或 HEAD。Jasper 还允许 OPTIONS\");\n        return;\n      }\n    }\n\n    final jakarta.servlet.jsp.PageContext pageContext;\n    jakarta.servlet.http.HttpSession session = null;\n    final jakarta.servlet.ServletContext application;\n    final jakarta.servlet.ServletConfig config;\n    jakarta.servlet.jsp.JspWriter out = null;\n    final java.lang.Object page = this;\n    jakarta.servlet.jsp.JspWriter _jspx_out = null;\n    jakarta.servlet.jsp.PageContext _jspx_page_context = null;\n\n\n    try {\n      response.setContentType(\"text\u002Fhtml; charset=UTF-8\");\n      pageContext = _jspxFactory.getPageContext(this, request, response,\n             null, true, 8192, true);\n      _jspx_page_context = pageContext;\n      application = pageContext.getServletContext();\n      config = pageContext.getServletConfig();\n      session = pageContext.getSession();\n      out = pageContext.getOut();\n      _jspx_out = out;\n\n      out.write(\"\\n\");\n      out.write(\"\\n\");\n      out.write(\"\u003C!DOCTYPE html>\\n\");\n      out.write(\"\u003Chtml>\\n\");\n      out.write(\"\u003Chead>\\n\");\n      out.write(\"    \u003Ctitle>JSP - Hello World\u003C\u002Ftitle>\\n\");\n      out.write(\"\u003C\u002Fhead>\\n\");\n      out.write(\"\u003Cbody>\\n\");\n      out.write(\"\u003Ch1>\");\n      out.print( new Date() );\n      out.write(\"\u003C\u002Fh1>\\n\");\n\n    System.out.println(\"JSP页面被加载\");\n\n      out.write(\"\\n\");\n      out.write(\"\u003Cbr\u002F>\\n\");\n      out.write(\"\u003Ca href=\\\"hello-servlet\\\">Hello Servlet\u003C\u002Fa>\\n\");\n      out.write(\"\u003C\u002Fbody>\\n\");\n      out.write(\"\u003C\u002Fhtml>\");\n    } catch (java.lang.Throwable t) {\n      if (!(t instanceof jakarta.servlet.jsp.SkipPageException)){\n        out = _jspx_out;\n        if (out != null && out.getBufferSize() != 0)\n          try {\n            if (response.isCommitted()) {\n              out.flush();\n            } else {\n              out.clearBuffer();\n            }\n          } catch (java.io.IOException e) {}\n        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);\n        else throw new ServletException(t);\n      }\n    } finally {\n      _jspxFactory.releasePageContext(_jspx_page_context);\n    }\n  }\n}\n```\n\n我们发现，它是继承自`HttpJspBase`类，我们可以反编译一下jasper.jar（它在tomcat的lib目录中）来看看:\n\n```java\npackage org.apache.jasper.runtime;\n\nimport jakarta.servlet.ServletConfig;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServlet;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport jakarta.servlet.jsp.HttpJspPage;\nimport java.io.IOException;\nimport org.apache.jasper.compiler.Localizer;\n\npublic abstract class HttpJspBase extends HttpServlet implements HttpJspPage {\n    private static final long serialVersionUID = 1L;\n\n    protected HttpJspBase() {\n    }\n\n    public final void init(ServletConfig config) throws ServletException {\n        super.init(config);\n        this.jspInit();\n        this._jspInit();\n    }\n\n    public String getServletInfo() {\n        return Localizer.getMessage(\"jsp.engine.info\", new Object[]{\"3.0\"});\n    }\n\n    public final void destroy() {\n        this.jspDestroy();\n        this._jspDestroy();\n    }\n\n    public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n        this._jspService(request, response);\n    }\n\n    public void jspInit() {\n    }\n\n    public void _jspInit() {\n    }\n\n    public void jspDestroy() {\n    }\n\n    protected void _jspDestroy() {\n    }\n\n    public abstract void _jspService(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException;\n}\n```\n\n实际上，Tomcat在加载JSP页面时，会将其动态转换为一个java类并编译为class进行加载，而生成的Java类，正是一个Servlet的子类，而页面的内容全部被编译为输出字符串，这便是JSP的加载原理，因此，JSP本质上依然是一个Servlet！\n\n![image-20230306164106712](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FUGJBqvOTDeX5SuM.png)\n\n如果同学们感兴趣的话，可以查阅一下其他相关的教程，本教程不再讲解此技术。\n\n***\n\n## 使用Thymeleaf模板引擎\n\n虽然JSP为我们带来了便捷，但是其缺点也是显而易见的，那么有没有一种既能实现模板，又能兼顾前后端分离的模板引擎呢？\n\n**Thymeleaf**（百里香叶）是一个适用于Web和独立环境的现代化服务器端Java模板引擎，官方文档：https:\u002F\u002Fwww.thymeleaf.org\u002Fdocumentation.html。\n\n那么它和JSP相比，好在哪里呢，我们来看官网给出的例子：\n\n```html\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth th:text=\"#{msgs.headers.name}\">Name\u003C\u002Fth>\n      \u003Cth th:text=\"#{msgs.headers.price}\">Price\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr th:each=\"prod: ${allProducts}\">\n      \u003Ctd th:text=\"${prod.name}\">Oranges\u003C\u002Ftd>\n      \u003Ctd th:text=\"${#numbers.formatDecimal(prod.price, 1, 2)}\">0.99\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n```\n\n我们可以在前端页面中填写占位符，而这些占位符的实际值则由后端进行提供，这样，我们就不用再像JSP那样前后端都写在一起了。\n\n那么我们来创建一个例子感受一下，首先还是新建一个项目，注意，在创建时，勾选Thymeleaf依赖。\n\n首先编写一个前端页面，名称为`test.html`，注意，是放在resource目录下，在html标签内部添加`xmlns:th=\"http:\u002F\u002Fwww.thymeleaf.org\"`引入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\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cdiv th:text=\"${title}\">\u003C\u002Fdiv>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n接着我们编写一个Servlet作为默认页面：\n\n```java\n@WebServlet(\"\u002Findex\")\npublic class HelloServlet extends HttpServlet {\n\n    TemplateEngine engine;\n    @Override\n    public void init() throws ServletException {\n        engine = new TemplateEngine();\n        ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();\n        engine.setTemplateResolver(r);\n    }\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        Context context = new Context();\n        context.setVariable(\"title\", \"我是标题\");\n        engine.process(\"test.html\", context, resp.getWriter());\n    }\n}\n```\n\n我们发现，浏览器得到的页面，就是已经经过模板引擎解析好的页面，而我们的代码依然是后端处理数据，前端展示数据，因此使用Thymeleaf就能够使得当前Web应用程序的前后端划分更加清晰。\n\n虽然Thymeleaf在一定程度上分离了前后端，但是其依然是在后台渲染HTML页面并发送给前端，并不是真正意义上的前后端分离。\n\n### Thymeleaf语法基础\n\n那么，如何使用Thymeleaf呢？\n\n首先我们看看后端部分，我们需要通过`TemplateEngine`对象来将模板文件渲染为最终的HTML页面：\n\n```java\nTemplateEngine engine;\n@Override\npublic void init() throws ServletException {\n    engine = new TemplateEngine();\n  \t\u002F\u002F设定模板解析器决定了从哪里获取模板文件，这里直接使用ClassLoaderTemplateResolver表示加载内部资源文件\n    ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();\n    engine.setTemplateResolver(r);\n}\n```\n\n由于此对象只需要创建一次，之后就可以一直使用了。接着我们来看如何使用模板引擎进行解析：\n\n```java\n@Override\nprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n    \u002F\u002F创建上下文，上下文中包含了所有需要替换到模板中的内容\n    Context context = new Context();\n    context.setVariable(\"title\", \"\u003Ch1>我是标题\u003C\u002Fh1>\");\n    \u002F\u002F通过此方法就可以直接解析模板并返回响应\n    engine.process(\"test.html\", context, resp.getWriter());\n}\n```\n\n操作非常简单，只需要简单几步配置就可以实现模板的解析。接下来我们就可以在前端页面中通过上下文提供的内容，来将Java代码中的数据解析到前端页面。\n\n接着我们来了解Thymeleaf如何为普通的标签添加内容，比如我们示例中编写的：\n\n```html\n\u003Cdiv th:text=\"${title}\">\u003C\u002Fdiv>\n```\n\n我们使用了`th:text`来为当前标签指定内部文本，注意任何内容都会变成普通文本，即使传入了一个HTML代码，如果我希望向内部添加一个HTML文本呢？我们可以使用`th:utext`属性：\n\n```html\n\u003Cdiv th:utext=\"${title}\">\u003C\u002Fdiv>\n```\n\n并且，传入的title属性，不仅仅只是一个字符串的值，而是一个字符串的引用，我们可以直接通过此引用调用相关的方法：\n\n```html\n\u003Cdiv th:text=\"${title.toLowerCase()}\">\u003C\u002Fdiv>\n```\n\n这样看来，Thymeleaf既能保持JSP为我们带来的便捷，也能兼顾前后端代码的界限划分。\n\n除了替换文本，它还支持替换一个元素的任意属性，我们发现，`th:`能够拼接几乎所有的属性，一旦使用`th:属性名称`，那么属性的值就可以通过后端提供了，比如我们现在想替换一个图片的链接：\n\n```java\n@Override\nprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n    Context context = new Context();\n    context.setVariable(\"url\", \"http:\u002F\u002Fn.sinaimg.cn\u002Fsinakd20121\u002F600\u002Fw1920h1080\u002F20210727\u002Fa700-adf8480ff24057e04527bdfea789e788.jpg\");\n  \tcontext.setVariable(\"alt\", \"图片就是加载不出来啊\");\n    engine.process(\"test.html\", context, resp.getWriter());\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>Title\u003C\u002Ftitle>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cimg width=\"700\" th:src=\"${url}\" th:alt=\"${alt}\">\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n现在访问我们的页面，就可以看到替换后的结果了。\n\nThymeleaf还可以进行一些算术运算，几乎Java中的运算它都可以支持：\n\n```html\n\u003Cdiv th:text=\"${value % 2}\">\u003C\u002Fdiv>\n```\n\n同样的，它还支持三元运算：\n\n```html\n\u003Cdiv th:text=\"${value % 2 == 0 ? 'yyds' : 'lbwnb'}\">\u003C\u002Fdiv>\n```\n\n多个属性也可以通过`+`进行拼接，就像Java中的字符串拼接一样，这里要注意一下，字符串不能直接写，要添加单引号：\n\n```html\n\u003Cdiv th:text=\"${name}+' 我是文本 '+${value}\">\u003C\u002Fdiv>\n```\n\n### Thymeleaf流程控制语法\n\n除了一些基本的操作，我们还可以使用Thymeleaf来处理流程控制语句，当然，不是直接编写Java代码的形式，而是添加一个属性即可。\n\n首先我们来看if判断语句，如果if条件满足，则此标签留下，若if条件不满足，则此标签自动被移除：\n\n```java\n@Override\nprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n    Context context = new Context();\n    context.setVariable(\"eval\", true);\n    engine.process(\"test.html\", context, resp.getWriter());\n}\n```\n\n```html\n\u003Cdiv th:if=\"${eval}\">我是判断条件标签\u003C\u002Fdiv>\n```\n\n`th:if`会根据其中传入的值或是条件表达式的结果进行判断，只有满足的情况下，才会显示此标签，具体的判断规则如下：\n\n- 如果值不是空的：\n  - 如果值是布尔值并且为`true`。\n  - 如果值是一个数字，并且是非零\n  - 如果值是一个字符，并且是非零\n  - 如果值是一个字符串，而不是“错误”、“关闭”或“否”\n  - 如果值不是布尔值、数字、字符或字符串。\n- 如果值为空，th:if将计算为false\n\n`th:if`还有一个相反的属性`th:unless`，效果完全相反，这里就不演示了。\n\n我们接着来看多分支条件判断，我们可以使用`th:switch`属性来实现：\n\n```html\n\u003Cdiv th:switch=\"${eval}\">\n    \u003Cdiv th:case=\"1\">我是1\u003C\u002Fdiv>\n    \u003Cdiv th:case=\"2\">我是2\u003C\u002Fdiv>\n    \u003Cdiv th:case=\"3\">我是3\u003C\u002Fdiv>\n\u003C\u002Fdiv>\n```\n\n只不过没有default属性，但是我们可以使用`th:case=\"*\"`来代替：\n\n```html\n\u003Cdiv th:case=\"*\">我是Default\u003C\u002Fdiv>\n```\n\n最后我们再来看看，它如何实现遍历，假如我们有一个存放书籍信息的List需要显示，那么如何快速生成一个列表呢？我们可以使用`th:each`来进行遍历操作：\n\n```java\n@Override\nprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n    Context context = new Context();\n    context.setVariable(\"list\", Arrays.asList(\"伞兵一号的故事\", \"倒一杯卡布奇诺\", \"玩游戏要啸着玩\", \"十七张牌前的电脑屏幕\"));\n    engine.process(\"test.html\", context, resp.getWriter());\n}\n```\n\n```html\n\u003Cul>\n    \u003Cli th:each=\"title : ${list}\" th:text=\"'《'+${title}+'》'\">\u003C\u002Fli>\n\u003C\u002Ful>\n```\n\n`th:each`中需要填写 \"单个元素名称 : ${列表}\"，这样，所有的列表项都可以使用遍历的单个元素，只要使用了`th:each`，都会被循环添加。因此最后生成的结果为：\n\n```html\n\u003Cul>\n        \u003Cli>《伞兵一号的故事》\u003C\u002Fli>\n        \u003Cli>《倒一杯卡布奇诺》\u003C\u002Fli>\n        \u003Cli>《玩游戏要啸着玩》\u003C\u002Fli>\n        \u003Cli>《十七张牌前的电脑屏幕》\u003C\u002Fli>\n    \u003C\u002Ful>\n```\n\n我们还可以获取当前循环的迭代状态，只需要在最后添加`iterStat`即可，从中可以获取很多信息，比如当前的顺序：\n\n```html\n\u003Cul>\n    \u003Cli th:each=\"title, iterStat : ${list}\" th:text=\"${iterStat.index}+'.《'+${title}+'》'\">\u003C\u002Fli>\n\u003C\u002Ful>\n```\n\n状态变量在`th:each`属性中定义，并包含以下数据：\n\n- 当前*迭代索引*，以0开头。这是`index`属性。\n- 当前*迭代索引*，以1开头。这是`count`属性。\n- 迭代变量中的元素总量。这是`size`属性。\n- 每个迭代的*迭代变量*。这是`current`属性。\n- 当前迭代是偶数还是奇数。这些是`even\u002Fodd`布尔属性。\n- 当前迭代是否是第一个迭代。这是`first`布尔属性。\n- 当前迭代是否是最后一个迭代。这是`last`布尔属性。\n\n通过了解了流程控制语法，现在我们就可以很轻松地使用Thymeleaf来快速替换页面中的内容了。\n\n### Thymeleaf模板布局\n\n在某些网页中，我们会发现，整个网站的页面，除了中间部分的内容会随着我们的页面跳转而变化外，有些部分是一直保持一个状态的，比如打开小破站，我们翻动评论或是切换视频分P的时候，变化的仅仅是对应区域的内容，实际上，其他地方的内容会无论内部页面如何跳转，都不会改变。\n\nThymeleaf就可以轻松实现这样的操作，我们只需要将不会改变的地方设定为模板布局，并在不同的页面中插入这些模板布局，就无需每个页面都去编写同样的内容了。现在我们来创建两个页面：\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\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cdiv class=\"head\">\n        \u003Cdiv>\n            \u003Ch1>我是标题内容，每个页面都有\u003C\u002Fh1>\n        \u003C\u002Fdiv>\n        \u003Chr>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"body\">\n        \u003Cul>\n            \u003Cli th:each=\"title, iterStat : ${list}\" th:text=\"${iterStat.index}+'.《'+${title}+'》'\">\u003C\u002Fli>\n        \u003C\u002Ful>\n    \u003C\u002Fdiv>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\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>Title\u003C\u002Ftitle>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cdiv class=\"head\">\n        \u003Cdiv>\n            \u003Ch1>我是标题内容，每个页面都有\u003C\u002Fh1>\n        \u003C\u002Fdiv>\n        \u003Chr>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"body\">\n        \u003Cdiv>这个页面的样子是这样的\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n接着将模板引擎写成工具类的形式：\n\n```java\npublic class ThymeleafUtil {\n\n    private static final TemplateEngine engine;\n    static  {\n        engine = new TemplateEngine();\n        ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();\n        engine.setTemplateResolver(r);\n    }\n\n    public static TemplateEngine getEngine() {\n        return engine;\n    }\n}\n```\n\n```java\n@WebServlet(\"\u002Findex2\")\npublic class HelloServlet2 extends HttpServlet {\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        Context context = new Context();\n        ThymeleafUtil.getEngine().process(\"test2.html\", context, resp.getWriter());\n    }\n}\n```\n\n现在就有两个Servlet分别对应两个页面了，但是这两个页面实际上是存在重复内容的，我们要做的就是将这些重复内容提取出来。\n\n我们单独编写一个`head.html`来存放重复部分：\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml xmlns:th=\"http:\u002F\u002Fwww.thymeleaf.org\" lang=\"en\">\n\u003Cbody>\n    \u003Cdiv class=\"head\" th:fragment=\"head-title\">\n        \u003Cdiv>\n            \u003Ch1>我是标题内容，每个页面都有\u003C\u002Fh1>\n        \u003C\u002Fdiv>\n        \u003Chr>\n    \u003C\u002Fdiv>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n现在，我们就可以直接将页面中的内容快速替换：\n\n```html\n\u003Cdiv th:include=\"head.html::head-title\">\u003C\u002Fdiv>\n\u003Cdiv class=\"body\">\n    \u003Cul>\n        \u003Cli th:each=\"title, iterStat : ${list}\" th:text=\"${iterStat.index}+'.《'+${title}+'》'\">\u003C\u002Fli>\n    \u003C\u002Ful>\n\u003C\u002Fdiv>\n```\n\n我们可以使用`th:insert`和`th:replace`和`th:include`这三种方法来进行页面内容替换，那么`th:insert`和`th:replace`（和`th:include`，自3.0年以来不推荐）有什么区别？\n\n- `th:insert`最简单：它只会插入指定的片段作为标签的主体。\n- `th:replace`实际上将标签直接*替换*为指定的片段。\n- `th:include`和`th:insert`相似，但它没有插入片段，而是只插入此片段*的内容*。\n\n你以为这样就完了吗？它还支持参数传递，比如我们现在希望插入二级标题，并且由我们的子页面决定：\n\n```html\n\u003Cdiv class=\"head\" th:fragment=\"head-title\">\n    \u003Cdiv>\n        \u003Ch1>我是标题内容，每个页面都有\u003C\u002Fh1>\n        \u003Ch2>我是二级标题\u003C\u002Fh2>\n    \u003C\u002Fdiv>\n    \u003Chr>\n\u003C\u002Fdiv>\n```\n\n稍加修改，就像JS那样添加一个参数名称：\n\n```html\n\u003Cdiv class=\"head\" th:fragment=\"head-title(sub)\">\n    \u003Cdiv>\n        \u003Ch1>我是标题内容，每个页面都有\u003C\u002Fh1>\n        \u003Ch2 th:text=\"${sub}\">\u003C\u002Fh2>\n    \u003C\u002Fdiv>\n    \u003Chr>\n\u003C\u002Fdiv>\n```\n\n现在直接在替换位置添加一个参数即可：\n\n```html\n\u003Cdiv th:include=\"head.html::head-title('这个是第1个页面的二级标题')\">\u003C\u002Fdiv>\n\u003Cdiv class=\"body\">\n    \u003Cul>\n        \u003Cli th:each=\"title, iterStat : ${list}\" th:text=\"${iterStat.index}+'.《'+${title}+'》'\">\u003C\u002Fli>\n    \u003C\u002Ful>\n\u003C\u002Fdiv>\n```\n\n这样，不同的页面还有着各自的二级标题。\n\n***\n\n## 探讨Tomcat类加载机制\n\n有关JavaWeb的内容，我们就聊到这里，在最后，我们还是来看一下Tomcat到底是如何加载和运行我们的Web应用程序的。\n\nTomcat服务器既然要同时运行多个Web应用程序，那么就必须要实现不同应用程序之间的隔离，也就是说，Tomcat需要分别去加载不同应用程序的类以及依赖，还必须保证应用程序之间的类无法相互访问，而传统的类加载机制无法做到这一点，同时每个应用程序都有自己的依赖，如果两个应用程序使用了同一个版本的同一个依赖，那么还有必要去重新加载吗，带着诸多问题，Tomcat服务器编写了一套自己的类加载机制。\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002F8JcnV2jUuiNZTXk.png)\n\n首先我们要知道，Tomcat本身也是一个Java程序，它要做的是去动态加载我们编写的Web应用程序中的类，而要解决以上提到的一些问题，就出现了几个新的类加载器，我们来看看各个加载器的不同之处：\n\n- Common ClassLoader：Tomcat最基本的类加载器，加载路径中的class可以被Tomcat容器本身以及各个Web应用程序访问。\n- Catalina ClassLoader：Tomcat容器私有的类加载器，加载路径中的class对于Web应用程序不可见。\n- Shared ClassLoader：各个Web应用程序共享的类加载器，加载路径中的class对于所有Web应用程序可见，但是对于Tomcat容器不可见。\n- Webapp ClassLoader：各个Web应用程序私有的类加载器，加载路径中的class只对当前Web应用程序可见，每个Web应用程序都有一个自己的类加载器，此加载器可能存在多个实例。\n- JasperLoader：JSP类加载器，每个JSP文件都有一个自己的类加载器，也就是说，此加载器可能会存在多个实例。\n\n通过这样进行划分，就很好地解决了我们上面所提到的问题，但是我们发现，这样的类加载机制，破坏了JDK的`双亲委派机制`（在JavaSE阶段讲解过），比如Webapp ClassLoader，它只加载自己的class文件，它没有将类交给父类加载器进行加载，也就是说，我们可以随意创建和JDK同包同名的类，岂不是就出问题了？\n\n难道Tomcat的开发团队没有考虑到这个问题吗？\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002F4hTUmkSdJHsQcYb.jpg)\n\n实际上，WebAppClassLoader的加载机制是这样的：WebAppClassLoader 加载类的时候，绕开了 AppClassLoader，直接先使用 ExtClassLoader 来加载类。这样的话，如果定义了同包同名的类，就不会被加载，而如果是自己定义 的类，由于该类并不是JDK内部或是扩展类，所有不会被加载，而是再次回到WebAppClassLoader进行加载，如果还失败，再使用AppClassloader进行加载。\n\n***\n\n## 实战：编写图书管理系统\n\n图书管理系统需要再次迎来升级，现在，我们可以直接访问网站来操作图书，这里我们给大家提供一个前端模板直接编写，省去编写前端的时间。\n\n本次实战使用到的框架：Servlet+Mybatis+Thymeleaf\n\n注意在编写的时候，为了使得整体的代码简洁高效，我们严格遵守三层架构模式：\n\n![img](https:\u002F\u002Fs2.loli.net\u002F2023\u002F03\u002F06\u002FGuxJ2UWFgkITAlS.png)\n\n就是说，表示层只做UI，包括接受请求和相应，给模板添加上下文，以及进行页面的解析，最后响应给浏览器；业务逻辑层才是用于进行数据处理的地方，表示层需要向逻辑层索要数据，才能将数据添加到模板的上下文中；数据访问层一般就是连接数据库，包括增删改查等基本的数据库操作，业务逻辑层如果需要从数据库取数据，就需要向数据访问层请求数据。\n\n当然，贯穿三大层次的当属实体类了，我们还需要创建对应的实体类进行数据的封装，以便于在三层架构中进行数据传递。\n\n接下来，明确我们要实现的功能，也就是项目需求：\n\n* 图书管理员的登陆和退出（只有登陆之后才能进入管理页面）\n* 图书的列表浏览（包括书籍是否被借出的状态也要进行显示）以及图书的添加和删除\n* 学生的列表浏览\n* 查看所有的借阅列表，添加借阅信息\n\n***\n\n## 结束语\n\n首先祝贺各位顺利完成了JavaWeb相关知识的学习。\n\n本教程创作的动力离不开各位观众姥爷们的支持，我们也会在后面为大家录制更多的Java技术栈教程，如果您喜欢本系列视频的话，直接用三连狠狠的砸向UP主吧！\n\n虽然我们现在已经学会了如何去编写一个网站，但是实际上，这样的开发模式已经过时（不过拿去当毕设当期末设计直接无敌好吧），我们还需要继续深入了解更加现代化的开发模式，这样我们才有机会参与到企业的项目开发当中。\n\n希望在后续的视频中，还能看到各位的身影，完结撒花！",{"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"]