JavaWeb作业管理系统源码拆解:从Druid连接池到Tomcat部署排错全攻略
简介面向JavaWeb初学者的作业管理网站完整项目资料包适配课程设计、毕业设计及Servlet、JSP阶段的实战训练。项目覆盖教师发布作业、学生提交作业、管理员统一管理等功能围绕Servlet、JSP、JDBC、MVC及常用数据库操作展开源码结构清晰适合对照学习传统JavaWeb开发流程。压缩包共53个文件以9个JSP页面、9个Java类、10个map文件为主附有jar依赖、CSS/JS前端样式、数据库配置文件等整体仅7.73MB便于快速部署与二次修改。已有97人学习使用说明资料具备一定参考价值。资料内包含完整的MVC分层代码、作业发布/提交/汇总模块、bootstrap前端页面以及基于druid连接池的数据库配置样例可帮助读者理解登录鉴权、数据表设计、JDBC封装和安全校验等关键细节。目录层级包含web目录、src源码、META-INF配置等结构规整适合按模块拆解学习。1. 拿到gdes-master压缩包后先别急着双击运行把“基于JavaWeb的作业管理网站.zip”解压后看到的gdes-master目录是一个比想象中完整的ServletJSPJDBC项目。很多人第一次跑这类JavaWeb项目完整案例习惯先点Idea的Run按钮结果迎接他的是一堆ClassNotFoundException和数据库连接超时。真正的问题往往不在代码逻辑而在druid.properties里的参数值、WEB-INF下的依赖包是否齐全以及Tomcat的访问路径是否带了上下文名。这个项目麻雀虽小五脏俱全登录、作业发布、提交作业、收集汇总、信息展示都有适合正在准备毕业设计或想弄懂JavaWeb底层交互的开发者。你不需要先学Spring只要懂Servlet生命周期、JDBC基本API和JSP内置对象就能顺着这套代码把整个请求-响应链路啃下来。我接下来的拆解会从数据层写到控制层再到前端集成和部署排错每一步都能对着源码找到对应文件。2. Druid连接池配置与JDBC数据访问层搭建2.1 从druid.properties看连接池参数打开src目录下的druid.properties你会看到一行行键值对它们决定了网站怎么连接MySQL。我摘录最典型的配置driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/gdes?useUnicodetruecharacterEncodingutf-8useSSLfalse usernameroot password123456 initialSize5 maxActive10 maxWait3000这段配置是数据访问层的地基。driverClassName指定MySQL驱动类名如果你的MySQL是8.0以上需要改成com.mysql.cj.jdbc.Driver并在URL里追加serverTimezoneAsia/Shanghai否则会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。url中的gdes是数据库库名useUnicodetrue配合characterEncodingutf-8保证中文正常读写useSSLfalse避免本机调试时出现SSL握手警告。initialSize是连接池初始的连接数应用一启动就会创建5个物理连接备用maxActive是池中最大连接数超过这个数的新请求会进入等待队列maxWait表示获取连接的最大等待时间单位毫秒超过3秒就抛异常。这些参数对作业管理系统来说已经是宽松配置因为学生提交作业的并发量通常不会超过100。如果把maxActive设置成500反而会占用MySQL的连接数上限导致数据库自身的连接不够用。这里有一个容易踩的坑Druid的DruidDataSourceFactory.createDataSource(props)要求properties里的key必须是Druid能识别的标准项如果你误写了driverClassName的别名比如driverClass就会抛出property name not found异常。另外如果项目的lib目录里没有druid的jar包即使properties写对了DruidDataSourceFactory类也加载不到。我拿到源码后第一件事就是检查WEB-INF/lib下是否有druid-1.1.x.jar和mysql-connector-java-x.jar没有就补上否则后面所有DAO方法都会在静态代码块里直接报ClassNotFoundException。2.2 封装Druid连接工具类连接池不会自己工作你需要一个工具类来从池子里获取Connection。gdes-master里通常有类似DbUtils的类核心实现如下public class DbUtils { private static DruidDataSource dataSource; static { Properties props new Properties(); try { InputStream is DbUtils.class.getClassLoader().getResourceAsStream(druid.properties); props.load(is); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }静态代码块在类第一次被加载时执行后面的线程池中的每一个DAO调用都会复用同一个DruidDataSource实例避免反复解析配置。getResourceAsStream从classpath根目录读取文件这意味着druid.properties必须和编译后的class文件在同一个根目录下。在Eclipse或Idea中运行时src目录的根就是classpath但如果部署成war包它会被复制到WEB-INF/classes下。close方法里分别判断rs、ps、conn是否为null防止对已关闭的结果集重复调用close抛出空指针。需要注意的是这个close方法关闭的Connection实际上是被Druid代理的close操作会把物理连接归还连接池而不是真正断开数据库连接。使用这个工具类时我建议把获取连接的代码放在try-with-resources里比如try (Connection conn DbUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 执行操作 }这样即使中间发生异常conn也会自动关闭。但DbUtils.close里手动关闭的代码也不冲突只是两种写法任选其一不要重复关闭否则个别版本Druid会输出重复close的警告日志。2.3 DAO层的增删改查套路Dao类负责把关系型数据转换成Java对象这是JDBC项目中代码量最大的部分。以用户表查询为例源码中的UserDao通常写成这样public User findByUsername(String username) throws SQLException { String sql SELECT id, username, password, role FROM user WHERE username?; try (Connection conn DbUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setRole(rs.getString(role)); return user; } } } return null; }这段代码体现了PreparedStatement的两大价值一是预编译SQL让数据库提前解析语法多次执行时性能更好二是setString方法会把传入值包在单引号里并转义危险字符从底层杜绝SQL注入。这里的?占位符索引从1开始如果把username放在第二个条件里就必须用setString(2, username)。getInt(id)是按下标列名获取如果你改动了SELECT的字段列表比如把id改成uid那么这里要同步修改否则会报Column id not found。一个常见的坏习惯是写SELECT *然后把rs.getString(1)这种按数字取列的方式写进业务代码一旦表结构调整这种代码就会悄悄返回错误数据。我推荐永远写显式列名并按列名从ResultSet取值即使多用几个字符但能让代码在数据库变更时更容易排查。下面这张表是我维护DAO代码时常用的参数类型对照项目中同样适用MySQL字段类型Java类型PreparedStatement方法int / bigintint / longsetInt / setLongvarchar / charStringsetStringdatetime / timestampjava.util.DatesetTimestampdatejava.sql.DatesetDatedecimal / floatBigDecimal / doublesetBigDecimal / setDoubletinyint(1)booleansetBoolean如果你要从ResultSet读取时间列推荐用getTimestamp(create_time)然后拿到一个java.sql.Timestamp对象它继承自java.util.Date可以直接给实体类的时间字段赋值。反过来写入时如果你的实体类用的是java.util.Date不能直接setDate因为java.sql.PreparedStatement没有setDate(java.util.Date)方法必须先new java.sql.Timestamp(date.getTime())再传入。这个小问题在作业提交时间、截止时间的存取上会反复出现提前处理好能省很多调试时间。3. 作业发布、提交、收集的MVC流程与前端表单处理3.1 页面与Servlet的映射关系从web目录可以看到index.jsp、login.jsp、issue.jsp、work.jsp、gather.jsp、submit.jsp、information.jsp再结合com.web包里的Servlet类就能拼出一条完整的请求链路。我习惯先用一张表把页面、Servlet和动作的对应关系列出来页面对应的Servlet类推测核心操作index.jspIndexServlet展示最近作业概览login.jspLoginServlet校验账号密码issue.jspIssueServlet教师发布作业work.jspWorkServlet查询作业列表submit.jspSubmitServlet学生提交作业gather.jspGatherServlet教师查看收集进度information.jspInfoServlet查看作业详情这张表不是从源码抄的而是我从文件名和URL-pattern反推出来的。拿到一个陌生JavaWeb项目最忌讳从头到尾顺序读代码。正确做法是先打开web.xml把 和 全部抄出来再对照JSP页面里form标签的action属性就能知道每次提交数据会进入哪个Servlet。如果项目用的是注解配置那就搜索WebServlet的urlPatterns。很多时候页面和Servlet并不是一一对应可能出现submission.jsp和submit.jsp两个名字但实际指向同一个SubmitServlet这种命名不统一是历史项目里最常见的问题只能靠web.xml来判断。3.2 用MVC思想拆解作业发布流程作业发布的Controller是IssueServlet它接收来自issue.jsp的表单数据再委托HomeworkDao完成数据库插入操作。一个典型的doPost方法如下WebServlet(/issue) public class IssueServlet extends HttpServlet { protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); if (add.equals(action)) { addHomework(request, response); } else if (update.equals(action)) { updateHomework(request, response); } } private void addHomework(HttpServletRequest request, HttpServletResponse response) throws IOException { String title request.getParameter(title); String content request.getParameter(content); String deadline request.getParameter(deadline); Homework homework new Homework(); homework.setTitle(title); homework.setContent(content); homework.setDeadline(java.sql.Date.valueOf(deadline)); HomeworkDao dao new HomeworkDao(); boolean ok dao.insert(homework); response.sendRedirect(request.getContextPath() /work?status (ok ? success : fail)); } }这里用service方法统一处理GET和POST并根据action参数分发到不同的私有方法是一种常见的轻量级MVC写法避免了同一Servlet里堆满doGet和doPost。注意在调用getParameter之前必须设置了请求编码否则之后read的参数全部是乱码。setCharacterEncoding放在service方法第一行是合理的因为整个方法内只需要设置一次。deadline从请求中拿到的字符串格式可能是2025-07-01而数据库字段类型是date因此用java.sql.Date.valueOf(String)来转换它要求严格的yyyy-MM-dd格式。如果前端用的是datetime-local控件format会变成2025-07-01T10:20这时直接valueOf会抛IllegalArgumentException需要先把T替换成空格再按时间解析。我在这个项目的实际改造中更倾向于用SimpleDateFormat指定格式然后new java.sql.Date(sdf.parse(deadlineStr).getTime())这样可以兼容多种输入格式。重定向语句response.sendRedirect(request.getContextPath() /work)是处理表单提交的标准姿势。它告诉浏览器重新发起一个GET请求到/work路径这样用户刷新列表页时不会再次触发doPost避免重复插入同一条作业。如果这里换成request.getRequestDispatcher(/work).forward(request, response)地址栏不会变化用户一旦按F5浏览器会重新提交整个表单数据库里就会出现两条一样的作业。这个区别在作业提交场景里尤其要重视我已经见过不少学生因为用了forward导致一节课提交了几十条重复记录。3.3 学生提交作业时的文件上传与文本处理submit.jsp实现的是学生把作业答案保存到系统。如果只是提交文本处理方式与发布作业类似用getParameter(answer)即可。但如果表单里有附件前端必须设置enctypemultipart/form-data此时Servlet不能再用getParameter获取普通字段必须从Part中解析。假设表单里有学号studentNo和附件attachment两个字段Servlet端可以这样写WebServlet(/submit) public class SubmitServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String studentNo request.getParameter(studentNo); // 注意multipart下这里返回null Part part request.getPart(attachment); String submittedFileName part.getSubmittedFileName(); InputStream in part.getInputStream(); String savePath getServletContext().getRealPath(/uploads) java.io.File.separator submittedFileName; java.io.FileOutputStream fos new java.io.FileOutputStream(savePath); byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { fos.write(buffer, 0, len); } fos.close(); in.close(); response.sendRedirect(request.getContextPath() /work); } }注意看在multipart/form-data请求中getParameter(studentNo)并不会自动解析普通字段除非你使用request.getParameter的替代方案比如先遍历Parts解析非文件域或者用第三方库如commons-fileupload。很多初学者在这里卡住以为代码没写对实际上是因为没有处理multipart的格式。如果项目使用Servlet 3.0的MultipartConfig注解或者web.xml中配置了multipart-configTomcat才会把请求体封装成Part对象组。另外part.getSubmittedFileName()只在Tomcat 8.5可用Tomcat 7下会返回null需要从Content-Disposition头正则解析。文件保存路径getRealPath(/uploads)返回的是部署目录下的物理路径如果目录不存在FileOutputStream会抛FileNotFoundException所以先要new File(savePath).getParentFile().mkdirs()。保存文件名建议用UUID替换原始名防止不同学生上传同名文件互相覆盖同时在数据库里保留原始文件名用于显示下载列表。4. 登录鉴权与SQL注入防护从Filter到PreparedStatement4.1 用Filter拦截未登录请求没有登录就访问作业列表这个系统就是裸奔的。gdes-master里一般会有一个AuthFilter专门负责在请求到达Servlet之前检查Session。以下代码可以直接复用到任何JavaWeb项目中WebFilter(/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页、登录接口和静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(LoginServlet) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(request, response); return; } // 校验session中是否存在用户信息 Object user request.getSession().getAttribute(user); if (user ! null) { chain.doFilter(request, response); } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }这段过滤器的逻辑可以归纳为一个两张表的判断哪些请求是公开的哪些请求必须登录。请求特征是否放行说明/login.jsp放行登录页面本身需要访问/LoginServlet放行登录接口未登录时也要能访问静态资源如.css/.js/.png放行否则页面样式和脚本加载不出来其他任意路径需要认证未登录则重定向到login.jsp这里最容易被忽略的是静态资源放行。如果不放行.css和.js那么即便你成功登录浏览器解析login.jsp时也会因为样式表被Filter重定向到登录页导致整个页面没有样式看起来像是白屏。实际排查时你会在Network面板里看到一堆302响应。对于需要区分角色的页面比如只有老师能访问issue.jsp可以在判断user不为null之后再检查user.getRole()。更规范的做法是定义角色常量比如teacher、student并根据路径前缀配置角色权限映射表。要注意Session中的user对象必须是登录成功时放入的且每次请求都要从Session重新读取不能使用静态变量保存用户信息否则多个用户之间会发生串号。4.2 登录SQL的预编译写法与密码加密在LoginServlet中最危险的代码是这样写的String sql SELECT * FROM user WHERE username username AND password password ;如果username输入admin --这条SQL就会变成WHERE usernameadmin -- AND password后面的密码判断被注释掉直接绕过登录。这类漏洞在各大漏洞平台上已经出现无数次修复方法只有一条使用PreparedStatement。我给出的标准写法是String sql SELECT * FROM user WHERE username? AND password?; try (Connection conn DbUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username.trim()); ps.setString(2, md5(password salt)); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 登录成功 } } }这里有两个细节。第一username在传入之前先trim掉首尾空格避免用户误输入空格导致查不到数据。第二密码不能明文比对正确的做法是在注册时生成一个随机盐值把md5(盐密码)存进数据库登录时用同样的盐拼接输入值计算摘要再比对。如果整个项目里密码都是明文存储我建议你增加一个统一的加密工具类。除了登录其他所有涉及用户输入的SQL、排序字段、模糊查询参数都必须使用?占位符。JPA或MyBatis的#{}本质上也是预编译但如果你在项目里用了字符串拼接的ORDER BY仍然无法防注入因为ORDER BY后面不能绑定参数这种情况下只能用白名单校验比如把允许的排序字段放在数组里进行匹配。4.3 角色权限控制的前后端配合作业管理系统通常有两种用户角色教师和学生。教师能发布作业、查看收集情况、管理所有记录学生只能查看作业列表、提交自己的作业。这种权限差异除了在按钮上做隐藏服务端必须同样校验。我在AuthFilter里会再增加一个角色-资源映射表比如角色可访问的URL模式teacher/issue, /gather, /work, /submit, /informationstudent/work, /submit, /information未登录/login, 静态资源实现方式是先根据当前URI判断用户需要哪种角色再把Session中的user.getRole()与要求比对。前端隐藏按钮只是提升用户体验真正的安全边界在服务端。另外注意像issue.jsp这样的动态页面如果用户猜到路径直接访问而Servlet接口又没有做角色校验就必须在Servlet入口也加一次session检查。我在实际审查这个项目时除了Filter还会在IssueServlet的service方法第一行加一个简单的HttpSession session request.getSession(false); if (session null || !teacher.equals(session.getAttribute(role)))这种双保险能有效防止配置漏网。5. BootstrapjQuery页面集成与Tomcat部署常见故障排除5.1 静态资源路径为什么不加载在JSP页面里使用相对路径是最容易踩的坑。比如在index.jsp中写link hrefbootstrap-4.3.1-dist/css/bootstrap.min.css当页面URL是/gdes/index.jsp时浏览器请求的资源路径是/gdes/bootstrap-4.3.1-dist/css/bootstrap.min.css这没问题。但当页面通过/work这个Servlet转发到/work.jsp时地址栏显示的是/gdes/work此时相对路径就会解析成/gdes/bootstrap-4.3.1-dist/...仍然正确吗不一定因为Servlet映射是/work没有后缀相对路径是基于最后一个/的位置。如果URL是/gdes/work/list相对路径就会变成/gdes/work/bootstrap...然后就404。解决这个问题永远只使用绝对上下文路径link relstylesheet href${pageContext.request.contextPath}/bootstrap-4.3.1-dist/css/bootstrap.min.css${pageContext.request.contextPath}会输出项目上下文名例如/gdes。这样无论请求被转发多少次前缀始终是项目根。同样Ajax请求的URL也要拼接这个前缀否则在servlet路径下发起url:/gather会访问到http://localhost:8080/gather而Tomcat默认端口8080根目录没有这个应用返回404。5.2 Tomcat启动时连接池初始化失败排查当你在Catalina日志中看到Cannot create PoolableConnectionFactory时说明数据层有硬伤。按照下面四步排查通常能在五分钟内定位问题确认MySQL服务是否启动命令行执行mysql -uroot -p -e show databases能列出库就说明服务正常。检查druid.properties中的用户名密码是否有特殊字符如果密码包含#或properties解析会截断需要用反斜杠转义或改用其他方式加载。核对JDBC URL中的数据库名gdes是否存在mysql -uroot -p -e use gdes测试。确认WEB-INF/lib目录下已经有mysql驱动jar包。如果你用的是Tomcat 10还需要把javax.servlet的依赖切换为jakarta.servlet否则Servlet类会报ClassNotFoundException。我在实际部署这个项目时最常遇到的其实是数据库版本和驱动不匹配。MySQL 8.0用MySQL 5.x的驱动会报Communications link failure。检查方式是在Tomcat的lib目录下看jar包名称5.x驱动是mysql-connector-java-5.1.x.jar8.x驱动是mysql-connector-java-8.0.x.jar或mysql-connector-j-8.x.jar后者内部类名才是com.mysql.cj.jdbc.Driver。5.3 中文参数乱码的三处设防这个项目的JSP页面如果没设置编码就会出现中文乱码。你需要检查三处设置。第一处JSP页面顶部必须写% page contentTypetext/html;charsetUTF-8 languagejava %第二处Servlet读取参数前调用request.setCharacterEncoding(UTF-8)这个调用要放在第一次getParameter之前。第三处JDBC URL中加上characterEncodingutf-8。如果这三处都做了但GET请求仍然乱码还需要修改Tomcat的server.xml。在Connector这个标签上加URIEncodingUTF-8否则Tomcat 8以前的版本默认用ISO-8859-1解析URL。由于源码里的useUnicodetrue已经指定了Unicode所以问题更可能出现在前两处。建议用浏览器开发者工具看Response Headers里的Content-Type如果显示text/html;charsetUTF-8说明JSP设置已生效。5.4 用开发者工具快速定位404和500页面打不开时不要直接猜是代码问题。按F12打开Network面板刷新页面点击报错的请求看状态码404表示路径错误看请求URL和实际Servlet映射500表示Java代码异常切换到Console或服务器日志看异常栈。比如你访问/submit返回404但web.xml里明明配置了SubmitServlet这时检查一下是不是项目上下文名漏了。Tomcat的默认部署方式是把war包解压到webapps下上下文名就是war包名。如果war包叫gdes.war访问地址必须是http://localhost:8080/gdes/如果只访问http://localhost:8080/就会404。为了减少这类问题我通常会在所有JSP的form action和重定向路径前都加上${pageContext.request.contextPath}这样即便换了一个部署名代码也能继续工作。对于500错误重点看异常栈中的Caused by部分那才是根源。最常见的是ClassNotFoundException说明jar包缺失其次是SQLException: Table gdes.homework doesnt exist说明数据库脚本没有执行或者表名不一致。检查gdes-master目录下是否有.sql文件导入到MySQL后再对比Entity类和表的字段名是否一一对应。总之把错误定位到具体文件和行号比盲目修改代码高效十倍。本文还有配套的精品资源点击获取