很多人学 JavaEE 都有一种共同体验:某个教学系列看得热血沸腾,弹幕里全是"老师牛逼",收藏夹里躺了一堆视频,可真要自己动手搭一个能跑通的前后端项目,却突然发现连从哪下手都不知道。我当年也是这么过来的,所以这篇博文不打算跟你重复课本目录,而是把 JavaEE 这条学习线按我实际踩过的路重新捋一遍——学什么、为什么学、怎么练、哪些地方最容易翻车,一次性讲透。
如果你正处在"看过视频但写不出项目"的阶段,或者准备从 Servlet 一路学到 Spring Boot,这篇文章应该能帮你省掉不少试错的时间。
1. 先别急着写代码:搞懂JavaEE到底在解决什么问题
1.1 从"动态网页"到"企业级应用",JavaEE扛起了什么
很多人对 JavaEE 的第一印象是"一堆规范"、"XML 配置文件"、"古老的东西",然后转头去学 Spring Boot 了。这个认知不能说全错,但问题在于:你跳过了 JavaEE,后面遇到 Spring Boot 的自动配置、内嵌容器、约定大于配置这些概念时,根本不知道它在帮你省什么事,出了问题也一脸懵。
我更喜欢把 JavaEE 理解为一套"约法三章"。它不具体实现某个功能,而是规定了一个 Java 程序要想在服务器环境里对外提供服务,应该遵循什么样的接口、什么样的组件模型、什么样的目录结构。比如 Servlet 规范说"你要处理 HTTP 请求,就实现一个接口,然后把它扔进容器里,容器会在合适的时机调用你",JSP 规范说"你可以在 HTML 里嵌 Java 代码,但我帮你翻译成 Servlet",EJB 规范说"你要做分布式事务,就按我的规则来"。
企业级应用真正需要的,无非是这么几件事:
- 处理并发的 HTTP 请求,并且不能因为某个请求失败就拖垮整个进程;
- 把业务逻辑和底层数据库访问解耦,方便改其中一个不影响另一个;
- 统一管理对象之间的依赖关系,而不是每个类都自己 new 一堆依赖;
- 事务、安全、日志这类横切关注点,不能散落在每个业务方法里重复写。
JavaEE 就是针对这些需求给出的一套成熟方案。你不用从零发明轮子,只需要遵守它的规则,容器会替你做掉大量底层脏活。更直白地说,JavaEE 给了你一套"如何组织一个大型 Java 服务端程序"的思维框架,Spring Boot 只是把这个框架的装配过程自动化了。
1.2 为什么说Servlet和容器才是JavaEE的地基
在 JavaEE 整个体系里,最核心、最绕不开的就是 Servlet。你后面学的所有框架,SpringMVC、Struts2,本质都是在一个 Servlet 之上套了一层壳。弄懂了 Servlet 的生命周期和请求处理模型,你就能看穿框架的"魔法"。
Servlet 说白了就是一个 Java 类,但它必须遵守 Servlet 规范里定义的接口。容器(Tomcat 就是最常见的 Servlet 容器)负责创建它、调用它、销毁它。一次完整的 HTTP 请求走下来大致是这个流程:
- 用户在浏览器输入 URL,发出请求;
- Tomcat 监听端口收到请求,解析出 HTTP 报文,封装成
HttpServletRequest和HttpServletResponse对象; - Tomcat 根据请求的 URL 路径,匹配
web.xml(或者注解)里注册的映射关系,找到对应的 Servlet; - 如果这个 Servlet 还没创建,Tomcat 会实例化它,然后调用
init()方法让 Servlet 做初始化; - Tomcat 调用
service()方法,把请求和响应对象传进去; - Servlet 内部根据请求方法(GET/POST),通常会走到
doGet()或doPost()里执行业务逻辑; - 响应对象里的输出流把结果写回给 Tomcat,Tomcat 把响应报文返回给浏览器;
- 容器关闭时,调用
destroy()让 Servlet 做资源清理。
这个生命周期里有个细节面试和实战都爱问:Servlet 是单实例多线程的。Tomcat 默认对同一个 Servlet 只创建一个实例,多个请求并发过来时,多个线程共享这个实例。所以你在 Servlet 里写成员变量一定要小心,如果成员变量是可变状态,并发修改就会出现数据混乱。这也是为什么后来 SpringMVC 里的 Controller 默认是单例的,但要求你不往里面存可变状态——根子就在 Servlet 的模型上。
1.3 常见误解:学完JSP就等于会JavaEE了吗
很多零基础教程为了降低门槛,会先教 JSP,让你在 HTML 里用<% %>写 Java 代码,好像很快就能跑出动态页面。这个路径作为入门体验是好的,但如果把 JSP 当成 JavaEE 的全部,那方向就歪了。真实的开发中,JSP 已经越来越边缘化,前后端分离是大势所趋,后端返 JSON,前端负责渲染,JSP 的定位非常尴尬。
但我想说的是,JSP 的原理依然值得学。因为 JSP 本质上就是"被包装过的 Servlet"——容器会把 JSP 文件翻译成 Java 源文件,再编译成 Servlet 类。你写的 JSP 页面,最终跑起来时还是 Servlet 的生命周期。理解这一层,你就知道为什么 JSP 里的内置对象(request、response、session、application)能直接用,因为它们就是容器在翻译时注入的 Servlet 方法局部变量。
所以正确的心态是:把 JSP 当做一个理解"Servlet 如何渲染视图"的载体,而不是把它当做一个要继续深耕的技术。学完 JSP 之后,你反而应该主动抛弃它,转向 JSON 交互的模式,这样你学 SpringMVC、学前后端分离时会非常顺。
2. 从Servlet到Spring Boot的必经之路:核心组件逐个拆
2.1 Servlet生命周期里的关键细节:重定向与转发不能混
在真正动手写项目之前,Servlet 里有几个极其高频的基础点值得单独掰开讲,因为它们直接决定了你后面写框架代码时的直觉对不对。
第一个是重定向和请求转发的区别。重定向是服务器返回一个 302 状态码和 Location 头,浏览器看到后再发起一个新的请求,地址栏会变;转发是服务器内部"暗箱操作",request.getRequestDispatcher("xxx").forward(request, response)直接把控制权交到另一个资源手里,浏览器从头到尾只发了一次请求,地址栏不变。很多初学者在写注册成功后跳转登录页时搞混这两个概念,结果刷新页面就重复提交表单,或者地址栏路径和实际页面不匹配。记住一个原则:需要在浏览器地址栏体现"本次处理完毕,请重新访问一个新地址"时用重定向,比如注册成功跳登录;需要在后端把请求转给另一个组件继续处理,不想让浏览器感知时用转发,比如经过过滤器后转给 Servlet。
第二个是乱码问题的根源。HTTP 请求从浏览器到服务器,经过了三层解码:URL 编码、请求体编码、响应体编码。Tomcat 默认的 URI 编码是 ISO-8859-1,如果浏览器用 UTF-8 提交了带中文的参数,后端不设置request.setCharacterEncoding("UTF-8")就解析,拿到的就是乱码。同理,返回页面时不在响应头里指定Content-Type里的 charset,浏览器就按自己默认的编码去解码,又是一种乱码。这两个坑几乎每个做 JavaWeb 的人都踩过,但理解了编码链路,排查起来就非常有条理。
2.2 JSP的九大内置对象:别背表,理解来源
JSP 里被反复强调的九大内置对象,很多教材让你背,但我想给你一个更底层、更好记的理解方式。这九个对象分别是:
- 请求相关的:request、response
- 会话相关的:session(绑定在当前用户的会话上)
- 全局相关的:application(整个 Web 应用共享一个,相当于全局变量池)
- 页面相关的:pageContext(当前页面的作用域,还封装了其他八个对象的引用)
- 输出相关的:out(向页面输出内容)
- Servlet 相关的:page(当前 JSP 对应的 Servlet 实例)、config(当前 Servlet 的配置)
- 异常相关的:exception(只在错误页面里存在)
我的记忆方法是:按照"一次请求、一个会话、整个应用"三个生命周期去划分作用域。request 只在当前请求里存活,一次重定向后之前的 request 就没了;session 在用户会话期间存活,关掉浏览器再开着不一定立刻失效,得等超时;application 从应用启动到关闭都活着,人人都能读写。理解了作用域之后,你就自然知道"用户登录信息该放 session,临时提示信息该放 request,全站统计数字可以放 application"。不需要死记表格,遇到具体场景就能选对。
2.3 Spring的IOC和AOP:为什么这一步是分水岭
学完 Servlet 和 JSP,你能写出来的程序,代码里到处是new。注册业务要用new UserService(),用户服务里又要new UserDao(),数据库连接池又要new DruidDataSource()。这种写法的问题在于:每创建一个类,你都必须手工把它所有依赖也创建出来,一旦某个依赖的构造方法变了,所有 new 过它的地方全要改。这就是耦合。
Spring 的 IOC(控制反转)解决的就是这个问题。你告诉 Spring 容器"UserService 依赖 UserDao,UserDao 依赖 DataSource,DataSource 的 URL 是 xxx",Spring 容器就自动帮你把所有对象创建好,并在需要的时候注入进去。你写代码时只需要声明@Autowired UserDao userDao;,不需要自己 new。用大白话说,以前是你主动去"找"依赖,现在是容器"喂"给你依赖,这个反转就是 IOC 名字的由来。
AOP(面向切面编程)则是另一个维度的解耦。业务方法里最常见的冗余代码就是"开始事务、提交事务、回滚事务、记日志、做权限校验"。这些逻辑跟业务本身无关,却散落在每一个方法里。AOP 的做法是:你写一个切面类,声明"哪些方法执行前要做什么、执行后要做什么",Spring 会在运行时把这些逻辑织入到目标方法周围。你写业务代码时就只关心纯业务,其他横切关注点全部交给切面。
我见过很多人学到 Spring 这一步就开始浮了,觉得"哦 IOC 就是不用 new 了,AOP 就是代理",然后急着去学 Spring Boot。但实际写代码时,IOC 容器里 bean 的作用域、懒加载、循环依赖、自动装配失败的原因,这些全是坑。这个阶段宁可多花时间手写 XML 配置,也别直接用注解一把梭,因为注解只是简化了表达,并没有改变底层机制。把底层机制弄明白,后面注解才会变成你的工具,而不是玄学。
2.4 MyBatis与事务:数据库这一层怎么接进来
JavaEE 分层架构里,数据访问层一直是重点。JDBC 你能写,但每写一次就要处理连接、预编译、结果集映射、关闭资源,重复代码太多。MyBatis 出现以后,它的核心价值有两个:
一个是把 SQL 和 Java 代码从物理上分开。SQL 写在 Mapper XML 文件里,Java 接口只声明方法签名,MyBatis 通过动态代理帮你把接口方法映射到对应的 SQL 语句,参数自动绑定,结果集自动映射成实体对象。这种做法在复杂查询场景下非常实用,因为 SQL 可以精细控制,想怎么优化就怎么优化。
另一个是先写 SQL 再决定 Java 代码的顺序,这更符合业务开发的直觉。你用 MyBatis 写一个新功能时,先在设计器里把 SQL 跑通,再把 SQL 粘进 Mapper XML,最后在 Service 里调用接口方法,开发效率提升非常明显。
再说事务。这是 JavaEE 里最容易"看得懂但学不会"的概念。事务的四个特性 ACID 谁都能背,但真正落地时你需要理解:Spring 的声明式事务是基于 AOP 实现的,它通过代理对象在方法执行前开启事务、正常结束时提交、抛异常时回滚。这里有个著名的坑:如果你在同一个类里用this调用另一个被@Transactional标记的方法,事务是不生效的。因为 Spring 的 AOP 代理只对"外部调用"有效,this调用直接走的是原始对象,代理根本来不及拦截。要解决要么通过注入自己的代理对象来调用,要么把事务方法放到另一个 bean 里。这种细节在书上看不到,但在实际项目里就是最常见的"事务没生效"事故现场。
3. 跟着实战把骨架搭起来:一套能跑的JavaEE最小系统
3.1 开发环境与依赖:避免版本地狱的固定搭配
前面讲了这么多理论,现在进入动手环节。很多人卡在第一步环境配置上,所以我先给出一套我用了很久、踩过坑之后沉淀下来的固定搭配,照着选版本不会错:
| 组件 | 推荐选择 | 说明 |
|---|---|---|
| JDK | 8 或 11 | 如果跟着老教程走,JDK 8 兼容性最好;新项目可以用 11 |
| 构建工具 | Maven 3.6+ | 统一依赖管理,不要用 IDE 内置的旧版本 |
| Servlet 容器 | Tomcat 8.5 或 9.0 | 对应 Servlet 3.1 / 4.0 规范,够用且稳定 |
| 数据库 | MySQL 5.7 或 8.0 | 注意 8.0 驱动类名和时区参数有变化 |
| 框架版本 | Spring 5.x + MyBatis 3.5.x | 这个组合资料最多,出问题容易搜到方案 |
这里有个很多新手不知道的对应关系:Tomcat 版本和 Servlet API 版本是绑定的。你在pom.xml里引入javax.servlet-api时如果选错版本,比如 Tomcat 9 是 Servlet 4.0,你却用了 Servlet 3.1 的 API,运行时会出现NoClassDefFoundError或者方法找不到的诡异报错。固定搭配的好处就是:出问题时,你能确定不是环境兼容性搞的鬼。
Maven 依赖坐标是另一个容易踩坑的地方。一个典型的 JavaEE 项目pom.xml至少要包含这几类依赖:
- servlet-api(
provided作用域,因为容器自带,不能打包进去); - spring-webmvc、spring-jdbc、spring-tx;
- mybatis、mybatis-spring;
- 数据库驱动;
- Jackson 用于 JSON 序列化。
我见过太多人把servlet-api用默认的compile作用域引入,然后把依赖一起打包进 war,结果 Tomcat 加载时类冲突,报ClassCastException。凡是容器已经提供的 API,一律用provided,这是从底层原理推出来的结论,不是死记的配置。
3.2 手写一个从请求到响应的完整链路(含代码)
我建议你的第一个 JavaEE 小项目不要用太多框架,就按照 Servlet + JSP + JDBC 手撸一个"用户管理系统",把完整的链路跑通。这里我给你一个最精简但五脏俱全的骨架。
先写实体类:
public class User { private Integer id; private String username; private String password; // getter/setter 省略 }再写一个最原始的 JDBC 工具类,用ThreadLocal保证每个线程拿同一个连接:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/demo?useSSL=false&characterEncoding=UTF-8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; private static ThreadLocal<Connection> tl = new ThreadLocal<>(); static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { Connection conn = tl.get(); if (conn == null) { conn = DriverManager.getConnection(URL, USER, PASSWORD); tl.set(conn); } return conn; } }然后是 UserDao,只负责 SQL 和取数据:
public class UserDao { public User findByUsername(String username) throws SQLException { Connection conn = DBUtil.getConnection(); String sql = "SELECT * FROM user WHERE username = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setPassword(rs.getString("password")); return u; } } } return null; } }接下来是 UserService,处理业务逻辑(比如校验密码):
public class UserService { private UserDao userDao = new UserDao(); public boolean login(String username, String password) throws SQLException { User u = userDao.findByUsername(username); return u != null && u.getPassword().equals(password); } }最后是 LoginServlet:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); try { boolean ok = userService.login(username, password); if (ok) { req.getSession().setAttribute("loginUser", username); resp.sendRedirect(req.getContextPath() + "/welcome"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } catch (SQLException e) { throw new ServletException("登录失败", e); } } }这个流程看起来很朴素,但它把前面讲的全部串起来了:请求到达 Servlet,Servlet 调用 Service,Service 调用 Dao,Dao 用 JDBC 操作数据库,然后根据结果做重定向或请求转发。你在这个小项目里亲手写过一遍req.getParameter、session.setAttribute、sendRedirect之后,后面用 SpringMVC 时,你会清楚地知道框架帮你省掉了哪些代码、这些代码原本长什么样子。
3.3 把Servlet替换成Spring MVC的演进过程
手撸完纯 Servlet 版本,下一步就是理解 SpringMVC 是怎么把上面这套流程"框架化"的。这一步是关键转折,很多人的理解在这里断档。
先看一个典型的 SpringMVC Controller:
@Controller public class LoginController { @Autowired private UserService userService; @PostMapping("/login") public String login(String username, String password, HttpSession session, Model model) { boolean ok = userService.login(username, password); if (ok) { session.setAttribute("loginUser", username); return "redirect:/welcome"; } model.addAttribute("errorMsg", "用户名或密码错误"); return "login"; } }对比之前的 LoginServlet,你会发现框架做的事就是"替你写好了重复代码":
@PostMapping("/login")替代了@WebServlet+doPost里的方法判断;- 方法参数直接写
String username,框架帮你从请求参数里取出来并做类型转换; HttpSession session和Model model直接作为参数注入,不用你自己从 request 里拿;return "redirect:/login"替代了resp.sendRedirect();return "login"替代了req.getRequestDispatcher("/login.jsp").forward()。
但你要知道,这些"便捷"背后,SpringMVC 的完整处理链路是:前端控制器DispatcherServlet收到请求,通过HandlerMapping找到对应的方法处理器,再由HandlerAdapter反射调用你写的 Controller 方法,返回的逻辑视图名经ViewResolver解析成真正的 JSP 路径,最后渲染视图。这个链路里任何一个环节的配置错了——比如组件扫描没扫到 Controller、视图解析器前缀后缀配错、@EnableWebMvc没加——你的请求都会在各种奇怪的报错里打转。
我给你的学习建议是:不要直接学 Spring Boot 的自动配置,先手动用 XML 或 Java Config 搭一个 SpringMVC 项目,哪怕只跑通一个最简单的页面。你只有亲手写过DispatcherServlet的映射配置、ViewResolver的 bean,才知道自动配置在背后做了什么,将来遇到"访问静态资源 404"、"请求进不来 Controller"这类问题,你才有排查方向。
4. 学JavaEE最容易翻车的四个坑,我用示例逐一说清楚
4.1 坑一:Tomcat版本与Servlet API不匹配导致404
第一个坑我当年栽得很惨。项目升级时我把 Tomcat 换成了新版本,代码没动,结果所有 Servlet 访问全 404。后来排查发现是pom.xml里javax.servlet-api的版本和 Tomcat 内置的 Servlet 版本不一致,导致编译用的 API 和运行时容器提供的 API 不是同一个版本,某些注解和接口方法的行为对不上。
排查思路应该是这样的:先看 Tomcat 版本对应的 Servlet 规范版本,再检查pom.xml里依赖的版本号是否匹配。比如 Tomcat 9.0 对应 Servlet 4.0,就应该引入javax.servlet-api:4.0.1,作用域用provided。如果确实因为某种原因需要高版本 API,那就得升级整个容器,而不是硬用一个不兼容的依赖。
这类问题最常见的现象组合就是:编译不报错,但启动时 Tomcat 报NoClassDefFoundError,或者运行时访问路径全部 404。只要你能想到"编译期和运行期的类不是同一套"这个方向,排查速度会快很多。
4.2 坑二:配置文件路径错误与组件扫描失效
Spring 项目里 XML 配置的路径写错,是所有新手出镜率最高的问题。classpath:springmvc-servlet.xml和classpath*:springmvc-servlet.xml的区别,就足以让人困惑一阵:单星号只从当前 classpath 根路径找,双星号会扫描所有 jar 里的 classpath 资源。如果你的配置文件在某个子模块 jar 里,单星号就找不到,Spring 启动时报找不到配置文件,或者更隐晦地直接创建一个空的容器,导致所有@Autowired注入全部失败。
组件扫描失效是另一个变体。你启动了 Spring 容器,但 bean 一个都没创建,Controller 或者 Service 全部是 null,代码一调用就空指针。这时候要检查三件事:
@ComponentScan的基础包是否覆盖了你放组件类的包路径;- 扫描到的类是否真的加了
@Component、@Service、@Repository或@Controller注解; - 如果你是 XML 配置扫描,
context:component-scan base-package的包名是否写错了一个字母。
这种问题看起来很低级,但越低级越容易在改包名、移动模块时冒出来。所以我现在的习惯是:配置完成后第一件事就是写一个测试,打印context.getBeanDefinitionCount(),快速确认容器里到底有多少个 bean。这比等部署之后跑起来再猜要快得多。
4.3 坑三:事务注解看似失效,实际是代理机制在作怪
事务失效这个问题,我在 2.3 和 2.4 已经提到过本质原因:Spring 声明式事务通过 AOP 代理实现,只有外部调用才会经过代理。很多人在 Service 类内部一个方法调用另一个@Transactional方法,发现异常时数据没回滚,整个项目的事务全废了,但代码看起来完全正常。
这类问题还有两个常见变体:
@Transactional只标注在private方法上。Spring 的代理(无论是 JDK 动态代理还是 CGLIB)都不能拦截 private 方法,注解自然不生效;- 异常被 catch 住吞掉了。事务回滚的条件是"异常从代理方法向外抛出",你如果 catch 了异常还返回了一个正常值,事务就认为方法执行成功,提交了。
排查事务问题时,我的检查顺序是:方法是不是 public → 调用方是不是 external 调用 → 异常有没有被吞 → 事务管理器和你用的数据源是不是同一个 → 是不是在同一个线程里执行的。这五步排查完,90% 的事务失效都能找到原因。剩下的 10%,通常就是事务管理器和数据源不是同一个连接对象,比如你配置了多个数据源,但事务管理器只指向了其中一个,那另一个数据源的操作自然不在事务范围内。
4.4 坑四:字符编码乱码的排查路径
乱码问题的排查路径前面提过,我再把它系统化一下。乱码出现的"位置"决定了排查的方向:
- 如果页面显示乱码,先看响应头里的
Content-Type是否带了charset=UTF-8。用 SpringMVC 时可以配置StringHttpMessageConverter的默认编码; - 如果请求参数里的中文乱码,先看
request.setCharacterEncoding("UTF-8")是否在读取参数之前调用。注意,这个方法只对请求体里的 POST 参数有效,URL 里的中文需要改 Tomcat 的URIEncoding="UTF-8"配置; - 如果数据库里存的中文乱码,检查 JDBC URL 是否加了
characterEncoding=UTF-8,以及 MySQL 表本身的字符集是不是utf8mb4(不是utf8,utf8在 MySQL 里存不了完整的四字节 emoji 和某些生僻字); - 如果以上都没问题还乱码,检查是不是多个 Filter 或拦截器里有人把编码又覆盖回去了。
乱码问题看着琐碎,但如果你建立起"数据从浏览器到数据库,经过了哪些编码和解码环节"这个全局视角,每一个环节查一遍,基本不会漏。怕的是网上随便搜一个方案就粘贴,粘完还乱,又换一个方案试,最后全试过还是乱,心态爆炸。先确认乱码发生在哪一环节,再动手改,这是最有效的思路。
5. 学完JavaEE之后的路怎么走:我给几个实在建议
5.1 把微服务思维提前装进脑子里
很多人的学习路径是:Servlet + JSP → Spring + SpringMVC + MyBatis → Spring Boot → 微服务。前面一切顺利,等到学微服务时突然觉得前面学的东西没用。我觉得这是个误会。JavaEE 层面的核心能力——比如事务边界、组件拆分、AOP、依赖管理——在微服务架构中一个都没消失,只不过表达形式变了。
你在单体应用里管理一个 Spring 容器,到了微服务架构里,你管理的是多个独立部署的 Spring Boot 应用;你在单体里用声明式事务保证一致性,到了微服务里要面对分布式事务的复杂问题;你在单体里用 AOP 做权限校验,到了微服务里要面对网关统一鉴权、服务间鉴权的问题。本质还是那几件事,只是规模和边界换了。
所以学 JavaEE 时不要有"这是过时技术"的心态。我反而建议你多想一想"这套机制在分布式环境下会遇到什么挑战"——比如你把一个 Service 方法拆成两个服务调用,原来的一次本地事务现在怎么保证?想透这类问题,你到时候学微服务会是降维打击。
5.2 一个能写进简历的实战项目到底该怎么做
最后聊一个很多人在实际找工作阶段最头疼的问题。如果你只跟着做视频里的项目,比如各种仿电商、仿论坛,简历上很难有区分度。我自己的经验是:做完参考项目之后,一定要做一次"二次改造",选一个参考项目里没有深入做的模块,自己把它做得更深,这样项目才能变成你的。
举个例子,如果你做了用户管理功能,可以往这几个方向加深:
- 给密码加盐哈希存储,而不是明文入库;
- 用拦截器或过滤器做登录状态的统一校验,并梳理清楚哪些接口放行、哪些必须登录;
- 给用户表加角色字段,实现简单的 RBAC 权限控制;
- 记录用户操作日志,用 AOP 切面实现,而不是在每个方法里手动写日志代码。
这些方向每一个都不难,但它们会把"跟着视频做项目"变成"自己有思考、有设计、有落地的项目"——面试官一眼就能看出来区别。
我的建议是别急着啃高深理论,先把上面这套从 Servlet 到 SpringMVC 的链路亲手搭完,把每个报错的报错信息原文读一遍,再跑通一个自己的完整项目。你会发现后面不管是学 Spring Boot 还是微服务,心里都非常有底,因为你知道每一层在干什么、出了问题该去哪里排查。JavaEE 不是你想象的那么"老",它只是换了种方式活在了今天所有 Java 服务端技术里。