ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java基础到Web进阶:请求链路、数据一致性与防爬实战指南

Java基础到Web进阶:请求链路、数据一致性与防爬实战指南 我每次带新人都会遇到同一个问题Java基础学得不错for循环、集合、多线程都能聊但一问到Java Web就只知道Spring Boot很火MVC三个字母真要动手写一个能注册、能登录、能查数据库的Web系统完全不知道从哪儿下手。这不是个例几乎所有人从JAVA进阶到JAVA WEB都会经历一段基础全会、Web全废的迷茫期。这篇内容就是写给正在卡在这道坎上的人。我会从技术栈的选型逻辑讲起拆解一个请求从浏览器出发到数据库再返回的完整路径再用真实的业务模块演示落地过程最后把我在项目里踩过的坑和面试中反复被问的高频题目一起聊透。不管你是在校生准备找实习还是转行半年想系统补Java Web亦或是工作了两三年想回头梳理知识体系这篇都能帮你把散落的知识点串成一条能跑的链路。1. 从控制台里跑通的Hello World到能上线的Web系统中间少了什么很多自学Java的人都有一种错觉我把《Java核心技术》读完了SSH连数据库也写过关了怎么公司给我的第一个需求还是做不出来原因很简单——Java基础和Java Web之间隔着一整套你没见过的运行环境、协议约定和协作方式。1.1 你写的代码到底跑在谁的容器里纯Java阶段你写的main()方法就是程序的全部JVM加载它执行完退出。但Web系统不是这样你的代码没有main()它跑在一个叫Servlet容器的进程里。Tomcat就是最常见的容器它负责监听HTTP端口、接收网络请求、把请求数据包装成Java对象、再调用你写的代码。这个过程决定了你写Web程序的第一条转变你不再控制程序的生命周期而是响应容器交给你的事件。很多人一开始不理解什么叫容器调用你举个比较直白的例子你在餐厅后厨做菜不是你决定几点开始做、做几桌而是前厅Tomcat来了客人HTTP请求喊你一声你才开始干活。这就是Servlet规范里最重要的思想——你的类要实现Servlet接口由容器实例化和调用。1.2 技术栈选型为什么现在不用学SSH了我早年入行的时候学的是SSHStruts Spring Hibernate。现在你去问任何一个在职Java工程师都会建议你从Spring Boot学起因为Spring Boot把Web开发里大量的样板配置都自动完成了。我做过一个对比可以给你一个直观感受技术栈要自己写的配置代码跑起来一个Hello World所需步骤适合阶段传统SSHstruts.xml、spring.xml、hibernate.cfg.xml等6-8个配置文件手动搭目录结构配置web.xml部署到Tomcat老项目维护学习原理Spring MVC XML配置spring-mvc.xml、web.xml还要配视图解析器、注解驱动配置繁琐但能感知MVC运转细节理解原理时推荐Spring Boot基本零配置application.yml一个文件搞定启动一个类main方法自动拉起内嵌Tomcat日常开发主力但我要提醒你Spring Boot的零配置并不意味着你不需要知道底层。恰恰相反Spring Boot把你包得越严实你越要在进阶的时候主动拆开它。我见过太多人用着RestController很顺手问一句你的接口在Servlet规范里处在哪个位置就愣住了。Java Web的进阶恰恰体现在对这些封装层的穿透能力上。1.3 给初学者的一条进阶路线结合我带人的经验一条比较顺的学习路径是这样的先用Servlet JSP写一个最简单的登录页面手动配置web.xml把Tomcat跑起来理解请求和响应到底走了一遍什么流程。再用Spring MVC重构一次体会框架帮你省掉了哪些Servlet样板代码。然后进入Spring Boot把项目改成自动配置方式感受内嵌容器、自动装配、起步依赖是怎么回事。最后引入MyBatis/MyBatis-Plus和数据库把数据链路补齐。有余力再去看Spring Boot的自动配置源码、Starter机制、Spring Security或Sa-Token这类权限框架。千万别一上来就Spring Boot 全套微服务那样你连这个接口为什么不用web.xml就能被访问都解释不了面试的时候一问就穿。2. 环境与工程化先补课再谈框架既然要进阶Java Web第一件事不是打开IDE写代码而是把开发环境整理利索。这里要注意网上关于环境配置的教程鱼龙混杂很多都写得不够细我结合自己跳过、填过的坑把关键点说透。2.1 JDK多版本切换的正确姿势现在的项目环境很乱老项目要用JDK 8新项目可能已经上了JDK 17甚至21。同一个开发机上装多个JDK是常态。很多教程会让你反复改环境变量JAVA_HOME但改完了经常出现命令行里java -version和IDE里看到的版本对不上原因十有八九是Path路径里还残留着别的Java路径。我的做法是这样# Windows下在系统变量单独建一个JAVA_HOME指针 # 例如JAVA_HOME D:\dev\jdk17 # Path里只放%JAVA_HOME%\bin # 切换版本时只改JAVA_HOME的值所有依赖JAVA_HOME的工具都会同步Linux/Mac下我习惯用update-alternatives管理或者用sdkman# 安装sdkman后一行命令切换 sdk list java sdk install java 17.0.10-tem sdk default java 17.0.10-tem还有一个容易踩的坑很多老项目用的是JDK 8编译但IDE默认用新JDK运行最后报出UnsupportedClassVersionError。排查方法很简单看看当前进程用的JAVA_HOME指向哪、IDE里Project Structure的Project SDK是哪一版、Maven的jdk配置是哪个三处必须逐一核对。2.2 Maven到底帮你管了什么Java Web项目里依赖管理几乎都交给Maven。Maven的核心概念不是下载jar包这么简单它定义了一套标准目录结构和生命周期。一个标准的Maven Web项目结构长这样my-web-app/ ├── pom.xml └── src ├── main │ ├── java # Java源码 │ ├── resources # 配置文件application.yml、mapper.xml等 │ └── webapp # 前端静态资源传统War包方式 │ └── WEB-INF │ └── web.xml └── test └── java # 单元测试代码pom.xml里的每个依赖由groupId、artifactId、version三个坐标唯一定位。进阶的时候你会发现很多冲突的根源都出在依赖传递上——你只引入了AA又引入了BB还有个老版本结果和你的C依赖撞在一起。Maven的依赖仲裁规则是就近原则距离最近的声明覆盖更远的声明但这条规则有时候会选出错误版本所以需要# 查看依赖树排查冲突 mvn dependency:tree # 排除某个不需要的传递依赖 mvn dependency:analyze2.3 application.yml里的隐藏逻辑Spring Boot里的application.yml大家都写过但里面很多配置项的意义很少有人说清楚。我挑一个最实用的来讲数据源配置。spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这一段里有三个最容易出问题的点useSSLfalse本地开发连MySQL不需要走SSL证书不写这一项部分MySQL 8版本会在启动时打印大量SSL告警甚至报错。serverTimezoneAsia/ShanghaiMySQL 8默认时区和中国差8小时不指定时区日期时间字段可能整体偏移。driver-class-nameMySQL 8必须用com.mysql.cj.jdbc.Driver老版本的com.mysql.jdbc.Driver会提示已废弃。这些细节很琐碎但它们决定了你从照着教程抄代码到自己能从零搭出一个能跑的工程的转变。3. 请求的完整旅程Servlet容器、Spring MVC和MyBatis各自干了什么我面试别人的时候最常问的一个题目是你在浏览器里输入一个网址敲下回车到你看到页面这中间发生了什么这个问题能筛掉一大半人。因为Java Web所有知识点都藏在这条链路上。3.1 第一站Tomcat接收HTTP请求浏览器发来的请求首先到达Tomcat。Tomcat作为一个Servlet容器内部有一套线程池模型每个连接由一个线程处理线程从连接中解析出HTTP请求头、请求体封装成一个HttpServletRequest对象然后根据URL和web.xml里的映射关系或者注解上的路径找到对应处理这个请求的Servlet。Servlet接口的核心生命周期只有三个方法public interface Servlet { void init(ServletConfig config); void service(ServletRequest req, ServletResponse res); void destroy(); }init()在容器启动或第一次请求时执行只执行一次适合做初始化service()处理每一次请求destroy()在容器关闭或应用卸载时执行。理解了这三个方法你就理解了为什么说bean默认是单例的——因为Servlet容器天然就是单实例多线程模型同时处理多个请求的是一个Servlet实例的不同线程。3.2 Spring MVC把请求分发到你的方法上Spring MVC是在Servlet之上做的一层封装。它的核心是DispatcherServlet这个Servlet本身也是Servlet但它把请求转发给一堆组件处理。流程大致是DispatcherServlet收到请求问HandlerMapping这个URL对应哪个Controller里的哪个方法找到之后由HandlerAdapter去调用那个方法把请求参数绑定到方法的入参上。Controller方法返回一个对象配合ResponseBody或一个视图名。DispatcherServlet再让HandlerAdapter把返回值交给ResponseBodyResultHandler等组件最终通过HttpServletResponse写回浏览器。这一步是很多人进阶的第一个分水岭从会写Controller到知道Controller为什么能这么写。举个例子为什么你的Controller方法可以写RequestBody User user前端传过来的JSON就能自动变成User对象这是因为Spring里有HttpMessageConverter在做反序列化Jackson就是最常见的实现。同理返回值也能被序列化成JSON是因为MappingJackson2HttpMessageConverter写了回去。3.3 MyBatisJava代码和SQL之间的桥请求到了Service层下一步是操作数据库。MyBatis做的最核心的事情是两件把Mapper接口映射为一个动态代理对象以及把SQL语句占位符和Java参数一一绑定。用MyBatis-Plus的时候大家最常用的是它根据实体类自动生成SQL的能力public class User { private Long id; private String name; private Integer age; }只要在Mapper里继承BaseMapper 四条最基础的CRUD方法就已经存在了。你调用selectById(1L)MyBatis-Plus会动态拼接一条SELECT id, name, age FROM user WHERE id ?这个能力靠的是反射它读取实体类的字段名和表结构注解。但这里有几个需要留意的点实体类的默认驼峰命名如createTime要转换成数据库的下划线字段create_time必须在配置里开启mapUnderscoreToCamelCase。如果你手写XML里的resultMap字段映射必须写全否则查出Null值都不知道错在哪。实体类里加了TableField(exist false)的字段是临时字段或联表字段不会参与SQL生成不加这个注解会导致MyBatis-Plus把你代码里新增的额外属性误当成数据库列SQL一执行就报未知列错误。3.4 完整链路里最容易忽略的一个环节这条链路里还有个非常重要的角色连接池。每一次数据库操作都要建立TCP连接、做MySQL认证这是很昂贵的操作。连接池的作用是提前创建一批连接复用HikariCP是Spring Boot 2.x之后默认的连接池性能相当好。但它的有些参数需要根据业务调整。spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000maximum-pool-size不是越大越好。MySQL默认的max_connections是151你一台应用配100个连接部署三台实例单库就扛不住了。我一般在压测时通过慢日志和监控看活跃连接数再反推合理值。这里没有标准答案只有符合当前场景的答案。4. 一个真实业务模块的落地登录、鉴权、数据权限、防爬你从框架层面理解了链路接下来要面对的是真实的业务需求。我从实际项目里挑几个高频场景把实现思路和代码骨架都写出来。4.1 登录功能从Session到Token的演进初学者做登录第一反应是Session用户登录成功把用户信息放进session下次请求带着Cookie来服务端一查session就知道你是谁。这个做法在单体项目、单机部署下没问题但一旦应用做了负载均衡用户请求可能被分配到另一台机器上session就丢了。这就是分布式会话问题。现在的通行做法是Token方案。逻辑很清晰用户登录成功服务端签发一个Token常用JWT或者UUID串把用户ID存到Redis并设置过期时间。前端把Token存在本地存储中每次请求在请求头带上Authorization: Bearer token。服务端写一个拦截器或过滤器对请求头做校验Token存在、Redis中能查到、未过期就放行。代码骨架大致是这样public class LoginInterceptor implements HandlerInterceptor { private final StringRedisTemplate stringRedisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); // 判断逻辑 String userId stringRedisTemplate.opsForValue().get(login:token: token); if (StringUtils.isBlank(userId)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } request.setAttribute(userId, userId); return true; } }然后注册到Spring MVC的拦截器注册表里并排除掉登录接口本身registry.addInterceptor(loginInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login);4.2 数据库数据一致性跨表更新不是小事热搜词里有一条java怎么保证数据一致性这是面试和实战都绕不开的话题。最简单一致性问题发生在单库事务里比如订单支付成功后既要改订单状态又要扣库存。如果两步操作只做了一半系统崩了数据就脏了。解法很简单加Transactional注解让它走同一个数据库事务。但事务失效的场景非常多我列几个常见的方法在同一个类内部被另一个方法调用事务注解不生效。原因是Spring事务基于AOP动态代理内部调用走的是this调用不是代理转发。注解加在非public方法上。Spring默认只拦截public方法加了非public上等于没加。异常被try-catch吃掉了事务感知不到异常无法回滚。异常类型是Error而不是RuntimeException默认情况下事务对检查型异常不触发回滚需要rollbackFor Exception.class。再说分布式场景。比如订单服务和库存服务不在同一个库这就不能靠本地事务了要引入分布式事务方案。现在行业里的主流思路是BASE理论和最终一致性用消息队列RocketMQ、RabbitMQ做异步对账或者用Seata这类分布式事务中间件做补偿。我的经验是能不用强一致性就不用大部分业务场景下最终一致性就够了。4.3 Controller层防爬虫的基础思路热搜词里有一条java controller层如何防护防止爬虫这确实是一个真实会遇到的工程问题。最基础的做法是接口限流我常用的是在拦截器里做一个简单的基于Redis的滑动窗口限流public class RateLimitInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String ip request.getRemoteAddr(); String key rate:limit: ip; Long count stringRedisTemplate.opsForValue().increment(key); if (count ! null count 1) { stringRedisTemplate.expire(key, Duration.ofSeconds(10)); } if (count ! null count 30) { // 10秒内最多30次 response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); return false; } return true; } }但限流只是低门槛防护真正要防的还是特征识别和验证码。进阶一些的做法包括校验User-Agent和Referer拦截没有浏览器特征或来源可疑的请求。前端在请求头加签名参数如时间戳 随机数 加密后的校验值服务端验签防止接口被直接调用。对登录接口、短信接口做更强的风控连续失败次数多了直接封IP或要求滑块验证。写防爬代码之前一定要想清楚你的业务是to C还是to BC端用户走App或H5爬虫成本低收益高防护强度可以做得重一些B端接口有API密钥体系重点在鉴权粒度上。4.4 数据权限不同商户看到不同数据热搜词里有多商户跨境商城和行级权限java这指向的是一个非常典型的场景一个商城系统里不同商户登录后只能看到自己的商品和订单。实现方案分两层。第一层是简单隔离每个表加merchant_id字段所有查询SQL的条件里强制带上当前登录的商户ID。这个做法直接有效但坏处是每个查询方法都得自己写条件容易漏。第二层是注解式数据权限。思路是写一个AOP切面或者MyBatis拦截器在SQL执行前动态改写SQL自动追加商户维度的where条件。MyBatis有专门的Interceptor机制可以拦截Executor的query方法改写MappedStatement里的BoundSql。这类实现网上有大量现成方案你可以搜mybatis 数据权限插件参考但我建议你先把基础的SQL改写原理搞懂不要把别人封装好的工具类直接搬进生产环境而不看源码。我更推荐的做法是先从简单方案入手在Service层通过封装BaseService处理通用查询条件强制要求业务方法传入商户ID。等业务复杂度上来之后再考虑引入数据权限框架。软件工程先跑通再优化数据权限这种容易踩坑的模块尤其如此。5. 进阶路上我踩过的坑以及面试官最爱问的那几道题最后一个部分聊聊进阶路上那些看起来是环境问题实际是知识盲区的坑。这些内容我平时写业务代码时也不会特意整理但每次排查都要花不少精力。5.1 Java启动失败的通用排查路径热搜词里有java启动失败怎么解决和java启动失败怎么办我把它拆成一个标准的排查清单按顺序检查能覆盖八成情况# 1. 看日志最后20行报错信息永远比猜测准确 tail -50 logs/xxx.log # 2. 端口是否被占用Spring Boot内嵌Tomcat默认8080 netstat -ano | findstr :8080 # 3. 依赖能否解析Maven本地仓库是否缺包 mvn clean package -DskipTests # 4. 配置是否生效数据库连不上还是Redis连不上 # activation.yml里的地址有没有写错 # 5. 内存是否够用OutOfMemoryError时调整启动参数 java -Xms512m -Xmx1024m -jar app.jar这里面我想单独说一个现象很多人在Windows上遇到端口被占用是因为Hyper-V或Docker随机占用了端口尤其是启动虚拟机场景。遇到这个我一般先把ENDPOINT列出来看清楚PID再决定是改应用端口还是结束占用进程不要上来就kill。5.2 编码乱码全链路排查才治本乱码问题是Java Web里最容易被忽视的坑因为它的表现太迷惑了本地好好的部署到服务器上就乱码有时候是控制台乱码有时候是数据库乱码。根本原因是编码链路每一环都要一致Java内部用UTF-16但你从文件读进来、往数据库写出去、在响应里输出每一步都可能被转成不同编码。我的排查顺序是数据库连接串里有没有characterEncodingutf8。数据库表本身和字段的字符集是不是utf8mb4。接收请求时有没有配置CharacterEncodingFilterSpring Boot里可以设置hiddenHttpMethodFilter这类编码过滤器。编译期编码和文件编码是否一致pom.xml里加上project.build.sourceEncodingUTF-8/project.build.sourceEncoding这几个地方只要有一处是默认的ISO-8859-1中文大概率乱码。排查的时候建议把挖到的每一处编码都统一到UTF-8而不是只改某一个点。5.3 面试高频题解析不只是背答案Java Web方向的面试题翻来覆去就是那些但很多答案背下来没用因为面试官会追着细节问。我挑几个最有代表性的说一下说下Spring MVC的处理流程。别只说前端控制器分发要能说出HandlerMapping、HandlerAdapter、ViewResolver、HttpMessageConverter分别在什么时机干活的细节。MyBatis的#{}和${}有什么区别。简单回答前者预编译防SQL注入后者直接拼接还不够要能说明#{}走的是PreparedStatement的占位符机制并且解释为什么${}可以被用来动态指定表名和排序字段但使用它时必须人工校验白名单。Spring Bean的生命周期。这个问题的核心是让面试官相信你理解Spring容器管理对象的完整过程实例化、属性赋值、初始化、销毁以及BeanPostProcessor在这个过程中的扩展点。接口幂等性怎么设计。实践中可以给出唯一订单号 数据库唯一索引 状态机流转的组合方案面试官听到这层基本就能确认你有实操经验。5.4 最后一个隐藏技能会看源码比会炫技值钱每次带进阶期的同事我都会建议他们花一个周末做一件事把spring-boot-autoconfigure里web相关的几个自动配置类挨个打开看一眼。你不用全部看懂只要找到DispatcherServletAutoConfiguration看里面的ConditionalOnMissingBean生效条件是什么你就能理解为什么你在项目里自定义了一个DispatcherServletSpring Boot就不再给你自动装配一个默认的了。这个习惯比多看十篇框架使用教程都值钱。因为Java Web进阶的本质就是从会用框架转向懂框架为什么这么设计。你能解释自动配置的条件装配机制能说出MyBatis-Plus生成SQL的反射原理能独立排查一次乱码和启动失败你就已经脱离了只会写CRUD的阶段。后面的路比如微服务、容器化、云原生都是在这个基础上长出来的枝杈根扎稳了往上爬只是时间问题。
返回列表