
这套东西其实是从上世纪90年代一路演进过来的经历了CGI、Servlet、JSP、Struts、Spring MVC、前后端分离、微服务这么几个大阶段但不管壳怎么换里面的核心骨架始终没变——就是表现层、业务层、数据层的三层职责切分以及在Web场景下落地的那套Model-View-Controller模式。先说结论吧如果你现在随便找一家中小型公司的线上项目看一眼十有八九跑的还是经典的三层架构外包用Spring Boot MyBatis Plus自研用Spring Boot JPA或者MyBatis前端Vue或者React部署在Nginx后面。这套组合就是当前使用最广泛这句话的真实含义不是概念上的最广泛而是装机量上的最广泛。这篇文章我就是想把这条技术主线从头到尾聊透把每一层为什么这么设计、实际落地时有哪些坑、演进到微服务要付出什么代价以及Web安全怎么融进架构里全部掰开揉碎讲清楚。不管你是刚入行的前端还是写了几年业务的CRUD工程师还是准备往架构师方向走的开发者这篇都能给你提供一个完整的坐标系。1. 三层架构与MVC模式深度拆解1.1 三层架构最朴素的职责切分Web应用最底层的逻辑其实特别简单。用户通过浏览器发一个请求系统要做三件事把页面或者数据展示出来、处理业务规则、读写数据。于是就有了三个层表现层Presentation Layer负责和用户打交道包括页面的渲染、参数的接收、结果的返回。在传统架构里这一层是JSP、FreeMarker在现在的前后端分离架构里就是Vue、React这些前端工程。业务层Business Layer负责实现具体的业务规则比如订单金额计算、库存扣减、权限校验。这一层是整个系统的核心也是面试时最爱问的Service层。数据层Data Layer负责和数据库交互封装CRUD操作。现在基本就是MyBatis、JPA这些ORM框架的天下。这个分层最聪明的地方在于它把变化隔离了。页面改版不需要动业务代码换数据库不需要动页面代码每个层都能独立开发、独立测试。我见过不少小团队跳过业务层Controller直接调用DAO结果业务逻辑散落在各个接口里后期加一个需求就要改七八个地方维护成本直接失控。分层看起来多写了几行代码实际上是在给未来的自己买保险。1.2 MVC模式三层架构在Web世界的落地三层架构是一个宏观的职责划分但落到Web项目里怎么组织代码、请求怎么流转需要一个更具体的模式这就是MVC。Model模型对应业务数据和业务规则View视图对应展示页面Controller控制器负责接收请求、调用模型、选择视图。以Spring MVC为例一次完整的请求流转是这样的浏览器 → DispatcherServlet前端控制器 → HandlerMapping找Controller → Controller接收参数、调用Service → Service业务处理 → DAO持久化 → 数据库 → 返回结果 → ViewResolver解析视图 → 浏览器Controller就是整个流程里的调度员它本身不做业务只是把请求分发给正确的Service方法然后把结果包装好返回。我经常跟团队里的小伙伴说Controller里只允许出现三行代码接收参数、调用Service、返回结果。一旦你在Controller里开始写if嵌套的业务判断说明分层已经出了问题。1.3 Spring Boot为什么成了事实标准现在做Java Web基本没人再用传统的SSHStruts Spring Hibernate或者SSMSpring Spring MVC MyBatis一个个去手动配置XML了Spring Boot用一套约定大于配置的思路把这些全部简化了。它内置了Tomcat提供了一堆Starter依赖你只需要在pom.xml里引入spring-boot-starter-web就能跑起来一个Web工程。热词里提到的idea2024版本创建web项目现在默认就是Spring Boot项目选几个依赖代码写起来和十年前相比完全不是一个时代的东西。Spring Boot能成为事实标准还有一个很重要的原因它把MVC三层架构固化成了工程标准。package结构就是controller/service/mapper三层大家一看就懂新员工上手成本极低。这也反向证明了三层架构的生命力——它不是一个过气的概念而是沉淀下来成为行业共同语言了。2. 前后端分离主流Web架构的当代形态2.1 为什么要拆从模板渲染到接口化十年前做Web开发前后端是揉在一起的。Java后端写Controller返回的不光是数据还有渲染好的HTML页面JSP。那时候的团队里没有前端工程师这个独立角色一个人要从数据库设计干到CSS样式。这种模式最大的问题是耦合前端要调样式必须部署整个后端工程后端改了个数据结构前端页面可能直接崩。后面移动互联网起来同一个后端要同时服务App、PC网页、小程序好几个端再把页面渲染放在后端就不合理了。于是前后端分离成了主流后端只负责出JSON数据通过RESTful API对外提供服务前端用Vue、React这类框架自己管理页面渲染和路由跳转。整个架构就变成浏览器Vue/React → Nginx静态资源服务 API反向代理 → Spring Boot API服务 → 数据库2.2 前后端分离后的关键实现细节分离之后很多以前没暴露出来的问题全冒出来了。最经典的就是跨域。前端跑在localhost:8080后端跑在localhost:9090浏览器直接把请求给你拦了报错信息就是热词里那个edge://iwa-dev/ 上的网页似乎有问题背后的同款机制。解决办法有三种后端开启CORS在Spring Boot里配置跨域过滤器允许特定来源访问。前端通过Nginx代理把/api路径转发到后端服务浏览器看到的只有同一个域名不存在跨域。JSONP老方案了只支持GET现在基本不用。我个人的建议是优先用Nginx代理方案生产环境本来就离不开Nginx顺带把跨域问题解决了比在后端开CORS更干净还能顺便隐藏后端真实端口安全性也好一点。还有接口鉴权问题。传统Session方案在前后端分离架构下不好使了因为前端和服务端不共享Cookie环境现在主流是Token方案特别是JWT。登录成功后端签发一个Token前端每次请求放在Header里带上后端拦截器校验。Spring Boot里集成Spring Security或者Sa-Token都能很优雅地解决热词里那个spring boot 集成web socket yml配置也是同样的思路——把WebSocket升级握手时的鉴权放到拦截器里统一处理。2.3 WebSocket架构里那些反常规的场景常规HTTP请求是短连接一来一回就断开了。但有些业务场景需要服务端主动推数据比如扫码登录、聊天室、实时监控大屏这就得用WebSocket。Spring Boot集成WebSocket不算复杂但有一个点很多人容易翻车——在Nginx后面部署的时候如果不配置升级头WebSocket握手永远失败。Nginx必须显式设置Upgrade头location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }这个配置我踩过不止一次坑很多应用在本地跑得好好的一上服务器就连接不上WebSocket八成就是Nginx这个配置没写。3. 单体、分布式与微服务架构演进的真实路径3.1 单体不是落后是大多数项目的起点现在一说架构就聊微服务好像单体是什么上不了台面的东西。但实际上单体架构依然是大多数项目最理性的选择。一个日活几千、几万的应用单体部署一台4C8G的服务器绰绰有余运维成本极低出了问题直接看一个应用的日志就完事了。我见过很多团队一上来就拆微服务结果基础设施、网关、注册中心、监控告警全都没准备好分布式事务一团糟代码写起来处处受制最后得不偿失。微服务的本质不是技术更先进而是组织分工变了。当团队人数超过几十人模块间的边界开始模糊一个改动要协调好几个团队单体编译、部署、发布的成本开始不可接受这时候拆分是水到渠成的事。一句话总结就是微服务是为组织和规模服务的不是为了技术面子服务的。3.2 向微服务演进需要解决的问题如果确实走到了拆分的路口最常见的拆分策略是按业务域拆而不是按技术层拆。比如一个电商系统按照用户服务、商品服务、订单服务、支付服务拆成四个独立的Spring Boot应用每个应用独立部署通过OpenFeign或者HTTP调用互相通信。这个过程至少要解决四件事服务注册与发现拆成几十个服务后互相之间怎么找到对方用Nacos、Consul或者Eureka做注册中心。API网关所有外部请求统一走网关比如Spring Cloud Gateway在网关层做鉴权、限流、路由转发。配置中心几十个服务的配置不能散落在各自的application.yml里用Nacos Config或者Spring Cloud Config统一管理。链路追踪一个请求从网关到订单服务再调用库存服务出了问题怎么定位引入SkyWalking或者Zipkin。这四个问题每解决一个都要引入相应的复杂度而且微服务之后的运维压力是单体完全没法比的。我个人的经验是团队如果连Docker和CI/CD流水线都不熟先别碰微服务因为即使代码拆了交付和运维跟不上只会更糟。3.3 中间态模块化单体与SOA有一些项目处于中间状态团队大了但还没大到必须完全微服务化。这时候有个折中方案叫模块化单体还是在同一个应用里但代码按模块严格划分模块之间通过接口交互不共享内部实现。这种模式的好处是保留了单体的运维简单性同时具备一定的边界清晰度。另外老生常谈的SOA面向服务架构现在其实很少被单独提了它的很多思想都被微服务吸收继承了比如服务的封装、复用、编排。对大多数业务来说模块化单体加定时任务加消息队列已经能解决绝大部分问题了。4. Web安全架构再广的架构也绕不开的底线4.1 从CTF视角看最常见的Web漏洞看热搜词里频繁出现CTF夺旗赛、Web靶场、Bugku这些词说明现在很多新手都是通过CTF入门Web安全的。CTF里的Web题其实高度浓缩了现实世界最常见的漏洞类型我自己刷CTF题时最大的体会是题目里的漏洞在真实的Web项目中基本都有对应版本。SQL注入MyBatis里如果到处用${}拼接参数就等价于CTF里的字符型注入点。正确做法是尽量用#{}预编译。XSS跨站脚本前端用v-html渲染用户输入的内容不做转义攻击者就能往页面里塞恶意脚本。正确做法是优先使用文本插值不得已用v-html时必须白名单过滤。CSRF跨站请求伪造现在大家普遍用Token机制防护但很多老系统还是没做攻击者可以构造一个恶意页面诱导用户点击后自动发起POST请求。文件上传漏洞不校验文件后缀和内容攻击者上传一个WebShell直接 getshell。正确做法是白名单后缀加内容校验上传目录不允许执行脚本。4.2 安全防线的分层布设Web安全不能只靠开发人员注意一点而是要在架构层面做分层布防。以一个小型Web系统为例安全防线应该铺成这样层级防线说明网络层防火墙 WAF拦截恶意IP、过滤常见攻击流量接入层Nginx安全配置隐藏版本号、限制请求大小、配置访问频率应用层鉴权 参数校验Token校验、输入白名单校验、权限控制数据层SQL预编译 数据加密防止注入、敏感字段加密存储运维层日志审计 备份留痕可溯源、定时备份可恢复这里重点说下HTTPS。很多小团队图省事直接HTTP裸奔用户密码在网络上就是明文传输抓包就能看到。现在免费CA证书申请非常方便用Certbot自动化申请Lets Encrypt证书配合Nginx配置HTTP跳转HTTPS半小时就能弄完这是投入产出比最高的安全改造没有之一。4.3 企业级Web安全落地清单如果在一个注重合规的企业里做Web系统安全方面的要求会更多比如等保里要求的日志留存至少六个月、账号口令复杂度策略、登录失败锁定策略这些都不是靠写代码就能实现的而是要在架构里预留位置。比如统一登录认证中心SSO把认证从各个业务系统剥离出来统一管控再比如操作日志服务异步记录所有关键操作方便事后审计。安全的东西看起来不产生业务价值但一旦出事就是大事架构设计时把这类需求规划进去比事后补救要省太多力气。5. 特殊场景嵌入式Web与边缘设备的架构取舍5.1 ESP32这类设备上的Web架构形态热搜词里有esp32内嵌web网页和物联网三层架构在现实中的具体应用这个角度很有意思。嵌入式设备上的Web架构和传统企业级Web完全不是一个套路它要面对的是极苛刻的资源限制。拿ESP32来说这颗芯片只有几百KB的RAMFlash也就几MB到十几MB别说跑Spring Boot了连像样的Web服务器都跑不起来。常规做法是直接在芯片上跑一个轻量级HTTP Server比如ESP-IDF自带的HTTP Server组件或者用WebServer库直接监听80端口接收浏览器请求返回一个内嵌的HTML页面。这个HTML页面通常就是控制页面包含温湿度显示、开关按钮、配置表单这些内容。5.2 极简架构的三层映射如果仔细看嵌入式Web其实也是三层架构的最简形态表现层内嵌在Flash里的HTML/JS页面可能只有几十KB用原生JS或者轻量框架。业务层ESP32上跑的C/C代码处理HTTP请求解析参数控制GPIO、读取传感器。数据层数据保存在片内Flash的NVS分区或者SD卡没有独立的数据库。物联网场景里的三层架构还有一个不同维度的含义就是设备层、网络层、平台层。设备端通过MQTT或者HTTP上报数据经过网关或者基站数据汇聚到物联网平台云端。这时候云端就是标准的Web架构了数据吞吐量、设备连接管理、规则引擎每个环节都是独立的服务。这种从设备到云端的完整链路比单纯讨论Web架构要更立体也更能理解架构服务于场景这句话的含义。5.3 嵌入式Web的开发要点做嵌入式Web开发有几点和传统Web经验完全冲突的地方。第一不能用前端框架。Vue打包出来一个几百KB的JS文件在ESP32上加载完全是灾难能不用框架就不用用原生JS加少量DOM操作最靠谱。第二资源全都要压缩。图片转成WebP甚至直接用Base64CSS和JS压缩成一行能省1KB是1KB。第三JSON解析很费内存。ESP32上解析一个稍大点的JSON响应可能会直接触发内存溢出重启所以通信协议能简化就简化。我之前做过一个项目直接把HTTP请求参数设计成简单的Key-Value不走JSON省了一大截内存。6. 常见问题与排查技巧实录6.1 插件加载与启动失败类问题Web开发过程中最让人头疼的不是业务逻辑写不出来而是环境问题。热搜词里出现的那串failed to load plugins web boot: entries did not activate其实是现代前端工程化里很常见的一类问题——某个插件在Webpack或Vite构建时加载失败导致应用启动不起来。排查这类问题有一个固定思路看报错信息里的插件名确认是否安装了这个依赖版本是否兼容。检查配置文件里插件数组的书写格式以及插件的调用顺序。查看插件的文档确认它支持的打包器版本是否和当前项目匹配。如果项目是先启动后新增的插件清空node_modules和lock文件重新安装。这类问题80%都是版本不匹配导致的改版本前先看一眼项目里的package.json依赖关系树用npm ls命令查清楚依赖引用情况再动手。6.2 Nginx代理后的经典故障前后端分离架构上线之后最常见的三个问题按出现频率排依次是404、502、504。404多半是前端路由用了history模式但Nginx只配置了一个location /刷新某个子路径就404。解决办法是配置try_files指令把路由回退到index.htmllocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }502 Bad GatewayNginx连不上后端服务了。先检查后端应用是否存活再确认Nginx的proxy_pass配置的地址端口对不对尤其要注意容器部署场景下不能用localhost去连另一个容器的服务。504 Gateway Timeout后端接口响应太慢超过了Nginx的proxy_read_timeout默认60秒。如果是正常的耗时接口调大超时时间即可如果接口本该秒回却耗时几分钟那就得去查后端逻辑了别只调超时参数糊弄过去。6.3 前后端联调时的经典坑联调阶段遇到的问题往往都很隐蔽。常见的有跨域问题前面提到的三种解决办法本地开发建议用开发服务器代理上线用Nginx统一代理。请求体大小限制上传大文件时前端报413要么调大Nginx的client_max_body_size要么用分片上传方案。时区问题后端存的是UTC时间前端直接显示用户看到的时间差8小时。统一约定用ISO 8601格式传时间戳由前端负责格式化能少踩很多坑。字段大小写问题后端习惯用驼峰命名前端传参数时写成了下划线命名结果字段对不上。建议前后端一起定义一个接口规范文档严格按照文档执行能省掉大量返工。6.4 从日志抓到底层真相排障的核心能力其实就一条会看日志。线上出问题第一件事不是猜而是去看应用日志。Spring Boot的日志默认输出在控制台生产环境建议配置Logback或Log4j2将日志写入文件并按照日期滚动切分。看日志还要会看关键信息比如HTTP状态码400表示参数校验失败、500表示服务内部异常连接超时一般是网络问题或对端服务挂了连接池耗尽HikariPool timeout基本是数据库慢查询或者连接没释放导致的。日志是Web架构的侦察兵学会从日志里定位问题是所有Web工程师必须跨过的一道坎。写在最后的经验做了这么多年Web项目从SSH古董架构到Spring Cloud微服务从JSP到Vue3技术栈换了一茬又一茬但回头看使用最广泛的Web应用架构这件事我心里其实特别清楚架构没有绝对的最优只有匹配当前团队规模、业务复杂度、运维能力三者的平衡。三层架构和MVC模式看起来简单但它给了这个行业一套通用的语言让大家协作起来有边界可循前后端分离让专业分工成为可能微服务解决了大规模团队的协作问题但也引入了不小的运维成本嵌入式Web让我们看到架构如何适应资源的极限约束。我个人的建议是不管你现在处在哪个阶段先把经典三层架构彻底吃透理解一次完整请求从浏览器到数据库再返回的每一条路径然后在这个基础上去学习任何新架构都会事半功倍。以后再听到xxx是最广泛使用的架构这种说法先想清楚它在什么约束条件下成立比盲目追新要重要得多。