又是一年毕设季,估计不少人手里正捏着这个题目:“JSP企业内部办公系统的设计与实现”。这个标题我太熟了,典型的JavaWeb课程设计/毕业设计选题,一听就知道是那套经典组合:JSP + Servlet + MySQL + Tomcat。它的好处是技术栈经典、业务场景清晰、可发挥空间大,缺点是如果你只是把别人的源码down下来改个名字交上去,答辩的时候基本一问三不知。这篇博文我想把这个项目从头到尾掰开揉碎,从环境搭建、数据库设计到核心代码实现、调试部署,再到那些源码里不会告诉你的坑,都梳理一遍。适合正在做毕设的学生、想复习JavaWeb基础的老手,以及刚入职需要快速上手传统Web项目的开发者。
1. 项目拆解:所谓“办公系统”到底在做什么
1.1 为什么JSP都“过时”了,还在用它做系统
每次一提到JSP,总有人说这是上古技术。确实,现在新项目很少用纯JSP了,前后端分离是主流。但高校的课程设计和毕设,还是大量选这种题,原因很实在:JSP涉及的知识点覆盖了JavaWeb最核心的链路,从HTTP请求生命周期、Servlet运行机制,到Session会话管理、JDBC数据库操作、过滤器拦截器,这些东西搞明白了,后面学Spring Boot、Spring MVC会顺畅很多。
再一个,企业内部办公系统这个业务场景,非常适合用来练手。它不像电商系统那样需要复杂的商品模型、支付流程,也不像社交软件那样要处理高并发消息。办公系统的本质是“多人协作 + 流程流转 + 权限管控”,业务逻辑直白,但该有的技术点一个不少:登录认证、角色权限、增删改查、文件上传、数据统计。作为教学项目和毕设,它的复杂度刚刚好,既不会让你无从下手,又不至于像“图书管理系统”那样千篇一律到答辩老师都懒得问。
所以别被“旧技术”三个字吓跑。选这个题目,本质上是在用最朴素的技术栈,把Web开发的底层逻辑串起来。这也是为什么市面上这类源码最多、资料最全,但真正能把每个环节讲清楚的文章反而稀少的直接原因——大家忙着下载源码,却没人复现一遍过程。
1.2 典型功能模块:一套能自洽的业务闭环
拿到“企业内部办公系统”这个题目,第一件事不是写代码,而是画功能导图。我见过太多人上来就建表,结果做到一半发现模块之间对不上。一个能用来答辩的办公系统,通常由这几块组成:
首先是登录与个人中心。所有功能围绕用户身份展开,登录页要支持用户名密码验证,密码存储不能是明文(至少做一次MD5加盐),登录成功后把用户信息放进Session。个人中心要能展示当前登录人的部门、职位、联系方式,还要支持修改密码和上传头像,“jsp个人信息展示页面”这个热搜词指的就是这一块。
然后是组织架构管理:部门管理和员工管理。部门表负责维护公司的树形组织,员工表关联部门,员工状态要有在职/离职区分。这里有个常见的细分方向:如果题目强调“企业”属性,可以考虑加入多级审批流;如果只是单纯“内部办公”,那就做到部门员工的基础CRUD。
核心业务模块一般选两到三个做深,不要全部铺开。常见的选择是:公告管理(发布、查看、置顶、过期自动下线)、请假审批(提交申请、直属领导审批、审批历史记录)、考勤打卡(上下班打卡、考勤统计日历)。这三个模块各有特点:公告是典型的“一对多信息发布”,请假是“状态机驱动流程流转”,考勤是“数据统计与报表展示”。三个模块做完,覆盖了Web系统的大部分核心模式。
最后是辅助功能:文件管理(上传下载,支持附件格式校验)、系统日志(记录登录、关键业务操作)、数据统计(按部门维度展示人数、请假时长等统计图表,用ECharts加一个柱状图/饼图就够亮眼)。
注意模块之间要有数据关联,不能是孤岛。员工属于部门、公告由员工发布、请假关联员工和审批人、考勤关联员工和日期,这样的数据模型才是一个整体,答辩的时候老师问“你这个部门删除后员工怎么办”“请假审批人如何确定”,你才能从容回答。
1.3 技术选型背后的取舍逻辑
JSP系统的技术选型,其实是个“标准答案”问题。JDK用1.8,这个版本稳了十年,兼容性最好;服务器用Tomcat 8.5或者9.x,对应Servlet 3.1/4.0规范,支持JSP 2.3;数据库几乎无悬念选MySQL,免费、常见、资料多,5.7和8.0都可以,要注意驱动版本匹配;IDE用IDEA,社区版完全够用。
开发模式上,我强烈建议用Maven管理项目,而不是直接在IDEA里建一个Web项目然后把jar包扔进WEB-INF/lib。原因很简单:Maven能帮你解决依赖传递问题。MySQL驱动、Druid连接池、JSTL标签库、commons-fileupload上传组件,这些在pom.xml里加几行依赖就能拉齐,后续部署到别的机器也不会因为缺jar包报ClassNotFoundException。如果你看到某份源码里没有pom.xml,只有一堆lib目录下的jar包,那多半是Eclipse时代的老项目,你得手动去核对每个jar的版本,非常痛苦。
数据库访问层,建议用Druid连接池 + JdbcTemplate或者纯JDBC封装DAO。不要一上来就引入MyBatis,不是不能用,而是这个项目的核心考察点就是JDBC和连接池,你用了MyBatis反而把关键环节藏起来了,老师一问“连接池参数怎么配的”你会露馅。前端部分,以JSP + JSTL + EL表达式为主,CSS框架可以引入Bootstrap或者Layui,快速成型且风格统一。
这套选型的目的只有一个:降低不确定因素,把精力花在业务逻辑和代码质量上,而不是折腾环境。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境准备:5分钟确认你的工具链没问题
2.1 JDK、Tomcat、IDEA的版本搭配
版本搭配是第一个坑。很多人从网上下源码,结果在自己电脑上跑不起来,一半以上是版本错配。我给出一个经过大量验证的搭配:
| 组件 | 建议版本 | 说明 |
|---|---|---|
| JDK | 1.8(8u202) | 最后免费商用版本,兼容性最好 |
| Tomcat | 8.5.x 或 9.0.x | 支持Servlet 3.1/4.0,对应JSP 2.3 |
| IDEA | 2021.x 或 2023.x Community | 社区版免费,足够用 |
| MySQL | 5.7.x | 稳定,驱动用5.1.49 |
| Maven | 3.6.x 或 3.8.x | 国内建议配阿里云镜像仓库 |
有一点要注意:JDK版本不要追新。Oracle JDK 17虽然也能跑Tomcat 9,但有同学遇到过编译级别问题、也遇到过JSP编译器的兼容性报错。做这个项目,简化是第一原则,老老实实JDK 8,凌驾于一切花活之上。
Tomcat与JDK的对应关系可以直接查官网的“Servlet Spec”对照表。Tomcat 9对应Servlet 4.0、JSP 2.3,支持JDK 8+,是当前性价比最高的组合。Tomcat 10不是不能用,但它的包名从javax.servlet变更到jakarta.servlet,网上大量JSP旧教程和源码都是javax命名空间,直接用会编译报错,新手不建议碰。
2.2 IDEA中新建JSP项目的正确姿势
用IDEA新建一个Maven项目时,很多人被困扰:Maven骨架里没有标准的webapp模板怎么办?其实正确流程是这样的:
第一步:File -> New -> Project,选择Maven,不勾选骨架(archetype),直接建一个空Maven项目。然后手动在src/main下创建java目录,在src/main下创建webapp目录,在webapp下创建WEB-INF目录。IDEA会识别这些目录并标记源代码根路径。
第二步:在pom.xml里声明打包方式为war,引入Servlet API、JSP API、JSTL、MySQL驱动、Druid连接池、commons-fileupload等依赖。注意Servlet和JSP API的scope要设置成provided,因为Tomcat本身有这些jar,重复引入会导致冲突。
第三步:配置Tomcat。在Run/Debug Configurations里新增Tomcat Server -> Local,在Server标签页指定Tomcat路径(你的本地解压目录),在Deployment标签页点击+号,选择Artifact,把webapp的war exploded部署上去。这里建议选war exploded模式,支持热部署,改完代码不用重启Tomcat,调试效率高很多。
第四步:右键运行,访问 http://localhost:8080/ 应该能看到Tomcat默认页面。如果端口被占用,改Server标签页里的port即可,我通常用8080边上备一个8081。
这套流程走完,你的“idea新建jsp项目”就搭建好了。看起来麻烦,但每一步都是必要的,特别是Artifact配置那一步,很多人漏了导致启动时找不到上下文路径,页面404。
2.3 Maven配置与本地仓库的坑
Maven配置就一句话:修改settings.xml,把中央仓库镜像换成阿里云。不然你的依赖下载速度会让人怀疑人生,项目建完半小时还在下载jar包。
具体来说,找到Maven安装目录下的conf/settings.xml(或者你自己指定的settings.xml),在mirrors节点里加:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
然后在IDEA里File -> Settings -> Build, Execution, Deployment -> Build Tools -> Maven,指定你所用的settings.xml路径。这一步不走完,后面Druid、JSTL这些依赖永远是红色波浪线。
另外一个经验:本地仓库默认在用户目录下的.m2/repository,文件多了之后容易污染。建议在settings.xml里把localRepository改成项目外的一个统一路径,比如D:\maven-repo,这样C盘空间不会被堆爆,而且重装系统后依赖还在。
3. 数据库设计与核心代码实现
3.1 表结构设计:先想清楚数据之间的关系
数据库设计是整个系统最内容丰富的部分,也是最容易踩坑的部分。办公系统的核心表我建议至少设计这六张:
- department 部门表:id, name(部门名称), parent_id(上级部门), create_time
- user 员工表:id, username, password(加盐哈希), real_name, gender, phone, email, avatar, department_id(所属部门), role(角色:管理员/普通员工/部门经理), status(在职/离职), create_time
- notice 公告表:id, title, content, publisher_id(发布人), publish_time, top_flag(置顶), expire_time(过期时间)
- leave_info 请假表:id, user_id(申请人), start_date, end_date, leave_type(事假/病假/年假), reason, status(待审批/通过/驳回), approver_id(审批人), apply_time, approve_time, approve_comment
- attendance 考勤表:id, user_id, check_date, check_in_time, check_out_time
- system_log 日志表:id, user_id, action(操作类型), target(操作对象), detail, create_time
这里有几个设计关键点需要说明:
外键约束要不要加?我的建议是逻辑外键即可,也就是不建物理外键约束,但在查询时用JOIN关联。物理外键在数据量大之后会影响插入性能,而且删除部门时需要遍历子表,很多生产系统都放弃物理外键。不过你要在答辩时解释清楚“为什么不用外键”,不能说因为懒。
角色权限怎么建模?简单方案就是在user表里加一个role字段,取值0/1/2分别代表管理员、部门经理、普通员工。更规范的做法是独立的role表 + user_role中间表,但对于这个项目,直接字段枚举更直观,也好操作。权限判断在Filter里完成,后面会讲。
请假审批的“审批人”怎么定?这是业务逻辑的核心。常见做法有两种:一是申请人所在部门的经理作为审批人,需要关联到department的manager_id字段;二是指定一个管理员专门审批。我建议用第一种,加一个manager_id到部门表,体现业务合理性。
3.2 JDBC连接池与DAO层封装
数据库操作这块,用Druid连接池可以省掉自己写连接管理的烦恼,同时你还能在监控页面里看到SQL执行情况,答辩时是一个加分点。
在src/main/resources下创建druid.properties:
properties复制driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/office?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username=root
password=你的密码
initialSize=5
minIdle=5
maxActive=20
filters=stat
然后封装一个工具类DruidUtils,里面静态代码块初始化连接池,对外提供getConnection()方法和close()方法:
java复制public class DruidUtils {
private static DruidDataSource dataSource;
static {
try {
Properties props = new Properties();
props.load(DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props);
} catch (Exception e) {
throw new RuntimeException("初始化连接池失败", e);
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
DAO层我习惯按这个模板写:BaseDAO封装通用增删改查,通过反射和JavaBean内省机制,把ResultSet映射成对象。但考虑到JSP项目的规模,我觉得直接手写映射反而更稳,每张表写一个DAO实现类,方法命名成findAll()、findById()、insert()、update()、deleteById()。手写映射的缺点是代码重复,但优点是你能清楚看到JDBC的执行链路,也方便加缓存和日志。
使用PreparedStatement加占位符,既能防止SQL注入,又不用拼字符串。连接对象一定要在finally里关闭,而且要遵循“先关闭ResultSet,再关闭Statement,最后关闭Connection”的顺序。用Druid连接池的话,connection.close()只是把连接归还给池子,不是真正关闭,但依然要养成释放资源的习惯。
3.3 登录会话与权限过滤器
登录功能几乎每个系统都有,但写得好不好差别很大。我的实现思路是:
登录接口在LoginServlet中处理。接收用户名密码,先判断验证码(如果你做了验证码的话),然后调用UserService的login方法:查询用户表,用MD5(密码加盐后)比对哈希值。比对成功,查询用户完整信息,放入Session:session.setAttribute("loginUser", user),同时写一条登录日志。返回JSON,前端通过Ajax跳转。
PasswordUtil里可以这样生成加盐哈希:
java复制public static String md5Salt(String password, String salt) {
String str = salt + password;
return DigestUtils.md5Hex(str);
}
注意加盐不是直接把盐拼在密码后面那么简单,盐值要每个用户唯一,通常用注册时间戳或者UUID,存在user表的salt字段里。这样即使两个用户密码相同,哈希结果也不同,防御彩虹表攻击。
权限控制用Filter实现。我写一个LoginFilter,在web.xml里配置过滤规则,拦截除/login、/css、/js、/images之外的路径:
java复制public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
HttpSession session = req.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null) {
// 跳转到登录页,并且带上当前请求路径,登录后可以跳回来
resp.sendRedirect(req.getContextPath() + "/login.jsp?redirect=" + req.getRequestURI());
return;
}
chain.doFilter(request, response);
}
管理员接口再额外加一个adminFilter,检查loginUser.role是否为管理员,不是就返回403页面。这样权限控制和业务代码解耦,加多级权限只增加过滤器即可。
还有一个细节:Session超时时间在web.xml里设置,默认30分钟,办公系统可以适当延长到60分钟,太久也不好,安全问题。登录页要做记住我功能的话,就用Cookie保存一个随机token,服务端对应存一份,这个各凭喜好,不是必须项。
3.4 文件上传与公告发布
公告模块必然涉及文件上传。传统的jsp:useBean加commons-fileupload组件写法虽然老,但放在这个项目里反而合理。核心代码大致是:
java复制// 检查是否multipart类型
if (ServletFileUpload.isMultipartContent(request)) {
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(new File("临时目录"));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(5 * 1024 * 1024); // 单文件5M
upload.setSizeMax(20 * 1024 * 1024); // 总大小20M
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) {
// 普通表单字段
} else {
// 文件字段,保存到 upload 目录,文件名用UUID重命名
}
}
}
这里最关键的就是文件名重命名。用户上传的可能叫“公司内部培训资料.pdf”,但如果你直接存储原名,一旦两个用户上传了同名文件就会互相覆盖;同时中文文件名在部分Tomcat版本下会乱码。正确做法是用UUID + 原文件扩展名生成新文件名,比如“a3f9c1d2-7b14-4e01-9c56-1234567890ab.pdf”,然后把原始文件名、存储路径、大小、上传人存到数据库的file表里。下载时再从库里取出真实文件名,通过Response设置Content-Disposition为附件形式返回。
公告发布页面的编辑器,我建议用百度UEditor或者wangEditor,后者更轻量。发布时把富文本内容存到数据库,展示页直接用EL表达式输出。这里有个安全隐患:富文本内容可能包含恶意脚本,要过滤掉script标签。一个简单的做法是用Jsoup的clean方法做白名单过滤。
3.5 考勤与统计报表的实现思路
考勤模块可以做得很简单,也可以做得很丰富。基础版是:员工点“上班打卡”按钮,后台获取当前时间,在同一天、同一用户下记录一条打卡数据,如果已存在则更新打卡时间(只记录每天第一次上班打卡和最后一次下班打卡)。展示页显示当月的日历视图,用JSP循环输出30天/31天的格子,已有打卡记录的日期标记出来,方便管理员查看。
统计这块用JDBC写聚合SQL:
sql复制SELECT d.name AS dept_name, COUNT(DISTINCT u.id) AS emp_count
FROM department d
LEFT JOIN user u ON u.department_id = d.id
WHERE u.status = 1
GROUP BY d.id;
查出结果后,用Gson转换成JSON数组,前端用ECharts画饼图。这种“SQL取数 + JSON传给前端 + 图表渲染”的流程,是所有报表模块的通用套路,熟练掌握之后做任何统计都不慌。
有人问:考勤数据是实时生成的,还是初始化固定数据?我的建议是写一个DataInitServlet或者在项目启动时插入一批历史数据,比如过去三个月的随机打卡记录,这样页面上有内容可看,演示效果比空页面好太多。但记住给这批数据的来源做好标记,答辩时明确说明这是演示数据。
4. 调试部署全流程实录
4.1 IDEA本地调试的断点技巧
本地调试时,我用的最多的是三种断点:行断点、条件断点、异常断点。
行断点是最常见的,在代码行左边单击打点,运行到这一行时程序暂停。但要注意断点打在调用方法的那一行时,要按F7(Step Into)才能进入方法内部;按F8则是执行完当前行跳到下一行。很多人都把这两个搞混,导致“断点没生效”的错觉。
条件断点就更有用了。比如你要排查“为什么某个用户登录后会一直跳回登录页”,就可以在LoginFilter的if判断那行,右键断点,设置条件为 loginUser == null,只有这个条件成立时才会暂停。这在循环里排查特定数据时非常实用,不用一遍遍按F9跳过。
异常断点是我的法宝。在IDEA的Run -> View Breakpoints -> Add Exception Breakpoint里,添加NullPointerException和SQLException。这样无论哪里抛异常,程序都会准确停留在异常发生的那行,不需要早早打一堆断点再一遍遍跑。排查“500错误在哪个Servlet”时,这个技巧效率提升十倍。
还有一个小技巧是JSP页面调试。JSP编译后的Servlet会生成在Tomcat的work/Catalina/localhost/项目名/org/apache/jsp目录下,报错页面显示的行号对应的其实是生成的Java源文件行号,而不是JSP文件行号。你可以在IDEA里把Tomcat的work目录加进SourcePath,这样点异常堆栈才能跳转到相应的Java代码。否则你会对着JSP第一行报错、实际第五十行的代码束手无策。
4.2 打包war与Tomcat部署
本地开发调试没问题后,重点就是打包部署。这个过程包含了传统JavaWeb项目部署的完整技能链,学会之后Spring Boot项目的部署也会顺手很多。
首先用Maven的生命周期执行clean package命令。打包前检查pom.xml的finalName标签,建议设置为项目名,比如office,这样打出的包是office.war,方便管理:
xml复制<build>
<finalName>office</finalName>
</build>
打完包之后,在target目录下会生成一个office.war。Finder里压缩软件解压半天都正常,那说明兄弟你jvm设置的内存太低了。正式环境的Tomcat部署有两种方式:
方式一:把war包扔到Tomcat的webapps目录下,启动Tomcat后它会自动解压部署。这种方式简单,但每次更新都要把包名、版本搞对,旧包要手动删除。
方式二:在Tomcat的conf/server.xml里配置一个单独的Host,把应用直接解压好放在指定目录下,docBase指向你的项目目录。这种方式适合发布到生产环境,避免每次更新war包覆盖出问题。
我用的是方式一居多,毕竟毕设项目或者中小型内部系统,部署一台小服务器,一个Tomcat就够用了。
部署之后有个高频问题:访问地址带上了项目名。比如http://localhost:8080/office/login.jsp。如果你希望直接通过http://localhost:8080/访问,可以把war包改名为ROOT.war再部署,Tomcat的根路径默认指向ROOT应用。改名后要清空Tomcat/work目录的缓存,否则偶尔会遇到旧页面。
4.3 不同环境下的数据库配置切换
开发环境、测试环境、生产环境的数据库连接信息往往不一样,简单粗暴地改druid.properties然后重新打包,非常容易出事故。我给你一个更稳妥的做法:用Profile机制实现配置切换,本质上还是Maven的resource过滤功能。
具体思路是在pom.xml里配置两个profile,一个是dev,一个是prod。每个profile下用properties标签定义dbUrl、dbUsername、dbPassword,然后再配置resources插件,允许在打包时对src/main/resources下的文件做占位符替换:
xml复制<profile>
<id>prod</id>
<properties>
<db.url>jdbc:mysql://192.168.1.10:3306/office</db.url>
<db.username>office</db.username>
<db.password>prod密码</db.password>
</properties>
</profile>
druid.properties里的URL写成:
properties复制url=${db.url}
username=${db.username}
password=${db.password}
这样用Maven打包时加-P prod参数,产出的war包就是生产环境的配置;加-P dev就是开发环境配置。从根源上杜绝“忘了改数据库URL”导致的连接失败。
对于没有精力搞这套配置的同学,我给一个最小可行方案:把druid.properties放到Tomcat的lib目录下的一个独立配置文件里,web应用的classpath读取顺序一般会把Tomcat的类路径放到前面,这样同一份war包在不同服务器上只需要替换Tomcat里的配置,不需要动应用。这个方法更灵活,尤其适合公司里因为合规要求不能把生产数据库密码写进版本库的场景。
4.4 内网穿透与演示场景的准备
毕设答辩或者给客户演示系统的时候,经常需要在另一台电脑上访问你的电脑。同一局域网下,直接输入你的局域网IP加端口就行,比如http://192.168.1.100:8080/office/login.jsp。要注意Windows防火墙要放行8080端口,否则访问超时。
跨网络演示就稍微麻烦一些,我不展开细说原理,只提醒一句:涉及在公网发布演示环境的做法,务必符合安全规范,演示结束后立即撤销暴露,避免数据泄露风险。办公系统里有员工真实姓名、手机号、考勤记录,这类敏感信息不该暴露在公网。如果学校要求远程演示,我更建议录好一段屏幕录像,或者用会议室投屏,既稳定又安全。这不是技术问题,是数据保护意识问题。
5. 常见问题与排查技巧实录
5.1 中文乱码问题全家桶
JSP项目中文乱码可以说是第一大坑,几乎每个用这个项目的同学都会遇到。我总结下来有四个层次,你可以按顺序排查一遍:
第一层:JSP页面自身编码。每个JSP文件顶部要有这个声明:
jsp复制<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
同时文件本身要用UTF-8编码保存。IDEA里在Settings -> Editor -> File Encodings里把所有编码都设为UTF-8,而且勾选Transparent native-to-ascii conversion,这样就可以用中文写properties文件,不用转成Unicode转义序列。不勾的话,druid.properties里的utf8不会出问题,但MessageBundle这类文件的中文会变成网上那种\u开头的天书。
第二层:Tomcat请求编码。Tomcat默认对POST请求用ISO-8859-1解码,对GET请求也有一堆历史包袱。最好的办法是写一个全局过滤器CharacterEncodingFilter,设置request.setCharacterEncoding("UTF-8")和response.setContentType("text/html; charset=UTF-8"),并在这个过縩器里执行chain.doFilter之前设置。这个方法对POST请求完全有效,GET请求的乱码还需要在server.xml里给Connector加URIEncoding="UTF-8":
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
第三层:数据库连接编码。druid.properties的URL里必须带characterEncoding=utf8,否则即使页面和请求都对,存进MySQL的中文也会变成问号或者乱码。MySQL 8.0还建议带上serverTimezone=Asia/Shanghai,不然时区问题会让你dates的字段错8小时。
第四层:数据库本身的字符集。建库时用UTF-8。我建议在创建数据库时就指定:
sql复制CREATE DATABASE office DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
如果你的库已经建好了,改成utf8mb4也是一条DDL的事。utf8mb4比utf8多支持表情符号,办公系统里用户姓氏生僻字也更能兜住。
乱码排查的一个通用断案法是:用Navicat直接插一条中文记录到数据库,然后写一个独立的Servlet查出来输出到页面。如果数据库里是好的、页面输出乱码,那就是配置第三层或第二层的问题;如果库里就是乱的,那就是第四层的问题;如果库里正常、页面也正常,只是用户提交时乱码,那才是第一层和第二层的过滤器没生效。
5.2 数据库连接失败的典型原因
“com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure”这行错误我见得太多了,十次有八次是下面原因:
一是MySQL服务没有启动。Window下按Win+R输入services.msc查看MySQL服务是否在运行。很多同学的电脑开机后MySQL不会自动启动,你不小心重启了一次,服务挂了,自然就连不上。
二是URL写错了。MySQL 5.7默认端口3306,如果买的是云数据库,端口可能是3306但不一定对外开放。不要想当然,用Navicat测一遍能连上,再把同样参数放到druid.properties里。
三是驱动版本问题。MySQL 8.0不能再用老旧的com.mysql.jdbc.Driver,要用com.mysql.cj.jdbc.Driver。而且8.0的URL里有时区参数。我在帮一个同学排查时,他拿了一份2018年的源码,驱动是5.1.x,数据库是8.0,每次启动Tomcat后第一次查询就报Connection失效,原因就是驱动版本太老和MySQL 8.0的认证插件不兼容。
四是连接池参数问题。Druid的maxActive设置过大,比如设成100,而MySQL默认max_connections是151,多个应用共享这个数据库时,连接数打满就会出现“Too many connections”。办公系统的并发量根本用不到100,20足够。
排查时还有一个技巧:临时把Druid过滤掉,用最原始的DriverManager写个测试类,看能不能连上。能连上就说明问题出在Druid配置上;连不上就说明问题在MySQL自身或URL。这种“二分法”排错方式,比盯着配置文件看好用得多。
5.3 JSP页面报错定位技巧
JSP页面的错误提示往往是这样的:
code复制org.apache.jasper.JasperException: Unable to compile class for JSP
很多人看到就慌了,其实JSP编译错误的大头只有几类:标签库引用路径写错、EL表达式里方法不存在、try/catch里throw了但外层没有catch。编译器提示的“An error occurred at line: [12] in the jsp file”,那个line对应的是JSP生成Java源文件的行号,不是页面实际行号。
一个高效做法是:把JSP里的Java代码块逐步迁移到Servlet或者JavaBean中,页面只保留JSTL和EL表达式。这不仅是调试方便的问题,也是代码规范的问题。JSP里塞大量业务代码,编译错误既难定位,又没法单元测试,还容易遭遇“JSP标签与Java代码混乱”的经典答辩批评。
再补充一个保存JSP前的小习惯:用IDEA的格式化快捷键Ctrl+Alt+L,把JSP的缩进理顺。这个动作在JSP里不是可有可无,因为JSP的指令和脚本元素混在HTML里,缩进乱了之后,哪个标签对应哪个scope都会看不清。特别是jsp:include和<c:import>的嵌套,混乱缩进会直接导致你忽略标签闭合问题。
5.4 源码里看不到的独家避坑心得
最后分享几个我这些年经手这类项目之后沉淀下来的实操心得,这些内容任何一份源码的README里都不会写。
关于Session与跨页面数据传递:很多人喜欢在JSP里写<%= request.getParameter("id") %>,从URL参数拿值,但这在修改功能里简直是个坑。用户点开公告列表的第一条记录,URL是notice_detail.jsp?id=1,然后他可能开了好几个标签页,最后一个标签页把id覆盖了,其他页面再提交都是拿最后一个id。建议所有跨页面传参一律通过Session或者表单隐藏字段走,而不是依赖URL参数。
关于代码版本管理:这个项目如果是多人协作做毕设,强烈建议从第一天就建Git仓库,用Gitee或者GitHub私有仓库。每完成一个模块commit一次,commit信息写清楚点,比如“feat: 完成请假审批流程”。答辩时打开提交历史给老师看,比嘴上说“这个模块是我做的”可靠得多。别等写完了再一次性提交,那样出了问题想回退某个功能都找不到节点。
关于演示数据:前面提到过插入演示数据,这个操作我再强调一遍。没有演示数据的办公系统,登录进去是空白页面,答辩时的效果会大打折扣。写一个DataInitializer,在项目启动时检测数据库是否为空,为空就插入预设的管理员账号、20个员工、3个部门、几十条公告、近三个月的考勤和请假记录。数据之间要有逻辑关系:请假单状态有通过的、有待审批的,考勤有迟到、有正常,这样老师问“你的状态流转展示一下”你立刻就有素材。
关于密码安全:管理员初始密码不要写“admin123”这种,稍微认真一点写成“Admin@2024”这样的复杂度,并且登录成功后强制要求修改初始密码。虽然只是一个课程项目,但这个习惯会伴随你进入真实生产环境。连毕设都明文存密码的项目,面试官只要看一眼就不想再问。
关于“源码”的正确使用姿势:我知道有人下载源码是为了直接交作业,但我更建议把它当作一个“参考实现”。拿到源码的第一件事不是解压导入,而是看完它的表结构和路由设计,然后关掉源码,自己动手从空项目开始写一遍。写不出来的地方再回头查它怎么实现。这个过程走完,你收获的不只是一个能运行的系统,而是一整套JavaWeb的排错直觉。这份直觉,才是面试时真正值钱的东西。
办公系统这个题目看着朴素,但它把Web开发里最核心的技术栈串了一个遍。把JSP、Servlet、JDBC、Filter、Session这些底层概念真正搞懂了,往后再学Spring Boot、MyBatis,你会发现那些框架不过是对这套底层逻辑的封装和简化。技术永远在迭代,但底层的请求-处理-响应模型、会话保持、数据持久化这几个核心思路,二十年没变过。这个项目做完,你带走的不仅是一个毕设,更是一套能迁移到任何Web框架上的思考方式。
