Java实战项目拆解:秒杀系统、Shiro认证与Servlet迁移
简介这份Java实战练习合集面向需要积累项目经验的Java学习者尤其适合正在学习Servlet、JSP、Spring Boot、Shiro等技术的开发人员用来对照练习、查漏补缺。压缩包共606个文件以java源码为主体同时包含class编译文件、xml配置、jar依赖、jsp页面、html静态页以及少量sql脚本等文件类型较丰富整体约34.16MB便于按项目存放与检索。目前已有62人学习/下载。合集内整理了Swagger2接口文档集成、Spring Boot与Shiro的秒杀系统、Tomcat与Servlet 3.0配置、ServletAjax异步交互、JSP聊天室、eStore图书馆管理系统、验证码生成器、简单网络爬虫等多个实战项目每个项目都能看到完整代码和大致实现思路。读者可通过这些代码掌握REST API文档自动生成、用户认证与授权、高并发秒杀核心逻辑、前后端异步通信、图书借阅流程等知识也可将其中的代码片段或项目骨架用于自己的课程设计和二次开发。1. 从一个 Java 实战压缩包开始读代码比收藏代码更重要拿到这份 java项目实战练习.zip解压后第一眼看到的是 SeckillController.class、SaveOrderServlet.class、GatherImp.class 这些编译产物再往下翻还有 ServletAjax、JspChat、eStore、checkcode、IMOOCSpider 等目录。它不是一个整齐划一的 Spring Boot 工程而是一段时间跨度很大的 Java 练习代码合集。怕的是你解压后只看一眼 README 就收藏真正应该做的是顺着这些文件把请求链路读出来。这套代码适合准备 java 面试的人也适合想补一补 Tomcat/Servlet 老项目维护经验的一线开发者。2. 从 Seckill 到 Shiro秒杀系统的认证链路与并发扣减2.1 先通过类名还原模块边界压缩包里同时出现 Controller、Service、Imp 和 Servlet说明项目并不是单一的 Spring Boot 新手工程而可能是从传统 Servlet 项目逐步改造成 Spring Boot 时的中间状态。我收到这类代码时不会先去翻 README而是先把关键类列成一张表区分每个类的职责。类名推断职责SeckillController接收秒杀请求解析参数并调用 ServiceSeckillServiceImpl实现秒杀逻辑包括库存校验、扣减、订单生成DBStoreImp数据存储实现可能是 JDBC 或 MyBatis 的封装GatherImp商品活动信息聚合或采集ConfigurationImp框架配置入口常见的是 Shiro 或 Swagger 配置SaveOrderServlet通过 HttpServlet 方式保存订单兼容老接口把SeckillController和SaveOrderServlet放在一起看就能明白这个项目的请求入口不止一套。Controller 由 Spring 管理Servlet 由 Tomcat 容器管理二者共存在同一个 web 应用中时过滤器和拦截器的执行顺序需要额外留意。这也是我判断这个项目“值得拆”的原因比起完美工程它更能暴露出框架边界。2.2 Shiro 集成认证过滤链的写法与顺序Shiro 在这类练习项目里的主要作用是控制秒杀接口的登录权限。常见做法是新建一个继承AuthorizingRealm的类处理认证和授权然后通过ShiroFilterFactoryBean配置路径过滤规则。// 配置 Shiro 过滤链控制秒杀接口的访问权限 Bean public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factory new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); factory.setLoginUrl(/login); factory.setUnauthorizedUrl(/403); MapString, String filterChain new LinkedHashMap(); filterChain.put(/static/**, anon); filterChain.put(/swagger-ui/**, anon); filterChain.put(/api/seckill/**, authc); filterChain.put(/**, user); factory.setFilterChainDefinitionMap(filterChain); return factory; }anon允许匿名访问authc表示必须登录user则额外放行“记住我”状态。代码里必须用LinkedHashMap因为 Shiro 按照声明顺序匹配 URL如果先写了/**后面的/api/seckill/**就永远不会生效。swagger-ui放行是为了调试接口时不被登录页挡住这也是 Swagger2 与 Shiro 集成的常见配置点。2.3 库存扣减不要在 Java 里做先查后改SeckillServiceImpl是秒杀逻辑的核心。最容易写错的是“先查库存再判断再更新”// 反例并发时会超卖 Goods goods goodsMapper.findById(goodsId); if (goods.getStock() 0) { goods.setStock(goods.getStock() - 1); goodsMapper.update(goods); }这段代码的问题在于检查与更新不是原子操作。两个线程同时读到库存为 1都会判断stock 0最后执行两次扣减库存变成 -1。常见做法是把扣减逻辑收进一条条件更新 SQLUPDATE seckill_goods SET stock stock - 1 WHERE goods_id ? AND stock 0;UPDATE会触发数据库行锁stock 0条件让数据库自己完成校验。执行后如果返回影响行数为 1说明扣减成功返回 0 则说明库存不足。这个方案在练习项目里已经完全够用再往上加 Redis 预减库存和消息队列已经是电商团队的事。2.4 调试时最容易碰到的三个问题用 Shiro 保护秒杀接口时常见故障有固定规律。现象可能原因处理方式请求 /api/seckill 一直 302Shiro 过滤链顺序错误把具体路径放在/**之前页面能看到接口却 404WebServlet没被扫描Spring Boot 启动类加ServletComponentScanSwagger 页面打不开swagger 路径被/**拦截在过滤链中显式放行/swagger-ui/**、/v2/api-docs除了 Shiro 配置还要注意代码包里同时存在 Controller 和 Servlet 时WebServlet的路径不能与 Spring MVC 冲突。比如SaveOrderServlet注册为/order/save而SeckillController里也有一个/order/save方法启动时会直接报路径冲突。遇到这种问题我一般先查项目里有没有web.xml或者WebServlet注解再决定保留哪一套入口。3. Servlet 3.0、Ajax 与 JspChat老 Web 交互方式里的关键细节3.1 Servlet 3.0 让 web.xml 不再是必选项TomcatServlet3.0这个项目名称看起来很初级但它传递了一个重要信息Servlet 3.0 之后开发者可以完全用注解替代web.xml中的 Servlet 声明。对JspChat这类聊天室项目来说新增一个接口只需要写一个类加上WebServlet即可。JSP 在运行时会被容器编译成 ServletTomcat 负责管理 Servlet 实例和请求线程理解这一点就不会奇怪为什么 JSP 页面可以直接使用request、response内置对象。3.2 Servlet Ajax 返回 JSON注意编码ServletAjax项目展示的是前端用 Ajax 请求后端的典型写法。后端返回 JSON 时最容易踩的坑是中文乱码。原因往往是没有在HttpServletResponse上同时设置ContentType和字符集。// 处理聊天消息查询的 Ajax 接口 WebServlet(/api/messages) public class MessageServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String room req.getParameter(room); if (room null) { room default; } resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write( {\room\:\ room \,\messages\:[]} ); } }setContentType(application/json;charsetUTF-8)必须连在一起写如果只写application/jsonTomcat 默认编码可能不是 UTF-8中文就会变成乱码。getParameter(room)读取的是 Ajax GET 请求中的 query 参数如果前端改成 POST 并发送 FormData就需要重写doPost方法。这个实现没有做任何输入过滤room参数如果被拼进 HTML 页面就存在 XSS 风险真实项目需要转义。3.3 聊天室的消息刷新轮询与长连接怎么选JSP 聊天室最常见的实现方案是短轮询。页面里放一个setInterval每 2 秒请求一次新消息。这个方案简单但服务端压力会随着用户数量上升。方案连接方式实时性服务端压力适合场景短轮询每次新建 HTTP 请求取决于轮询间隔高练习项目、低并发房间长轮询请求挂起直到有新数据较好中小规模通知WebSocket单条 TCP 长连接最好低线上即时聊天练习项目使用短轮询没有任何问题。要注意的是不要在 JSP 的% %脚本里写while (true)死循环去检测新消息那会一直占用 Tomcat 的工作线程最终导致服务器无响应。正确做法是把轮询放在浏览器端的 JavaScript 里服务端只提供一次性查询接口。3.4 Servlet 多线程共享数据要小心MessageServlet如果声明一个成员变量private ListString messages new ArrayList()多个用户同时写入时会出现数据覆盖甚至ConcurrentModificationException。Servlet 容器默认以单实例多线程方式运行所以成员变量就是所有请求共享的全局变量。常见做法是把消息列表放到ConcurrentHashMapString, ListString中按照room隔离或者直接写入数据库。这个点既是老项目容易出问题的地方也是 java 面试题里常考的线程安全场景。3.5 从旧 Servlet 迁移到 Spring Boot 的最小改动如果要把JspChat或ServletAjax改造成 Spring Boot 风格可以对照下面的替换关系。旧写法Spring Boot 写法WebServletweb.xmlRestControllerGetMappingHttpServletRequest/HttpServletResponseRequestParam 直接返回对象JSP 页面渲染Thymeleaf 或前后端分离web.xml中的过滤器声明FilterRegistrationBean注意一个容易踩的坑WebServlet注册的 Servlet 在 Spring Boot 应用中不会自动生效除非启动类上加了ServletComponentScan。否则/api/messages会直接返回 404。这个坑我遇到过不止一次尤其是那些从独立 Tomcat 部署切换到 Spring Boot 内嵌 Tomcat 的老项目最容易漏掉这一步。4. checkcode、IMOOCSpider 与 eStore验证码、爬虫和图书管理的实践边界4.1 验证码生成器的核心是画图而不是字符串checkcode项目生成验证码图片的思路比较统一用BufferedImage创建一张空白图片再用Graphics2D把字符和干扰线画上去最后把图片写到响应输出流。// 生成四位随机验证码图片并输出到响应流 int width 100, height 40; BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g image.createGraphics(); String chars ABCDEFGHJKLMNPQRSTUVWXYZ23456789; StringBuilder code new StringBuilder(); Random random new Random(); for (int i 0; i 4; i) { char ch chars.charAt(random.nextInt(chars.length())); code.append(ch); g.setColor(new Color( random.nextInt(150) 50, random.nextInt(150) 50, random.nextInt(150) 50)); g.drawString(String.valueOf(ch), 15 i * 22, 26); } for (int i 0; i 8; i) { g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height)); } g.dispose(); ImageIO.write(image, JPEG, response.getOutputStream());这段代码的重点在字符集合去掉了I、O、0、1避免用户混淆。生成后的验证码字符串需要存到session中校验时用equalsIgnoreCase。干扰线数量不宜太多否则人工识别成功率会大幅下降。真实系统还会加入字符旋转和背景噪点但核心逻辑仍然是Graphics2D绘图。4.2 简单爬虫 IMOOCSpider拿到 HTML 只是第一步IMOOCSpider是一个用 Java 抓取网页内容的网络爬虫练习。不用引入重型爬虫框架HttpURLConnection就能完成基本请求。// 抓取列表页 HTML输出包含课程名称的行 URL url new URL(https://example.com/course/list); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(GET); conn.setRequestProperty(User-Agent, Mozilla/5.0); conn.setConnectTimeout(3000); conn.setReadTimeout(3000); try (BufferedReader reader new BufferedReader( new InputStreamReader(conn.getInputStream(), UTF-8))) { String line; while ((line reader.readLine()) ! null) { if (line.contains(course-name)) { System.out.println(line.trim()); } } }setRequestProperty(User-Agent, ...)用来模拟浏览器身份很多服务端会拒绝裸 Java 请求。setConnectTimeout控制建立连接的超时setReadTimeout控制读取数据的超时爬虫必须设置这两个值否则网络异常时线程会卡住。这个示例只是把包含course-name的行打印出来真要保存数据需要进一步解析 HTML 或使用 Jsoup 这类库。爬虫的边界在于遵守站点robots.txt和合理控制请求频率练习项目用静态页面即可。4.3 eStore 图书馆系统的核心借阅状态eStore图书馆系统覆盖图书管理、用户管理和借阅管理。借阅记录表是整个系统的关键直接影响查询和统计逻辑。CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, return_time DATETIME, status TINYINT NOT NULL DEFAULT 0, INDEX idx_user (user_id), INDEX idx_book (book_id) );status字段我这里用0表示借出1表示已还2表示逾期。但逾期状态不建议在每次还书时人工更新而是查询时根据return_time NOW()动态判断。user_id和book_id上建索引否则借阅记录增加后按用户查询和按图书查询都会退化成全表扫描。这个表设计足以支撑一个图书馆管理系统的核心功能。4.4 压缩包内文本文件的价值项目里附带.gitattributes、README.md、README_ZH.md、_config.yml、资源内容.txt、标签.txt等文件。这类文件说明代码包是从一个 Git 仓库导出的完整快照而不是刻意整理的课程目录。阅读优先级应该是先看README_ZH.md里面通常写了项目结构、依赖和启动方式.gitattributes处理换行符和文件差异和 Java 业务逻辑无关可以直接跳过。5. 从解压到验证为秒杀接口加上并发探测5.1 快速验证 Shiro 是否真正生效使用 Shiro 后直接访问受保护接口不带登录 Cookie 时应该被重定向到登录页。curl -I http://localhost:8080/api/seckill/1如果响应头中出现Location: http://localhost:8080/login说明authc拦截规则已经生效。如果返回200 OK需要检查ShiroFilterFactoryBean中的路径规则是否写在了/**之前以及是否有全局异常处理器把未登录异常吞成了成功响应。5.2 用并发请求验证库存扣减秒杀系统最常见的评判标准是最终库存不为负。可以用一个短小的 shell 循环模拟并发请求。for i in {1..20}; do curl -s -o /dev/null -w %{http_code}\n \ -H Cookie: JSESSIONID$SESSION \ http://localhost:8080/api/seckill/1 done wait$SESSION是登录后拿到的会话 ID-o /dev/null丢弃响应体-w %{http_code}只输出 HTTP 状态码。让 20 个请求并行执行wait等待全部结束。跑完之后查数据库如果stock字段出现负数说明SeckillServiceImpl仍然用的是先查后改而不是条件 UPDATE。如果库存正好从 20 变为 0就可以把这条执行记录作为并发验证的辅助证据。5.3 每次改动都保留一条执行记录在测试环境跑完并发脚本后把开始时间、初始库存、成功请求数和最终库存写入同一个日志文件。之后修改了SeckillServiceImpl再跑一遍对比日志中的库存变化。这样比口头描述“我懂秒杀”更有说服力也能在回答 java 面试八股文时直接引用具体数字。日志里留下的并发数和库存数才是这份项目练习代码真正转成项目经验的地方。本文还有配套的精品资源点击获取