每年到了3月份和10月份,我后台私信里来问得最多的就是两类人:一类是大四要交毕设论文的,另一类是培训完要找工作准备项目经验的。问的话题出奇一致——"能不能带我做套Java项目?"其实我特别理解大家的焦虑,刚学完Java基础,一上来就要做一个"XXX管理系统",根本不知道从哪下手。
今天我想跟你聊一个经典的不能再经典的选题:基于Java的个人通讯录系统。别觉得它土,你别看它小,五脏俱全,用好它,能撑起一篇漂亮的毕业论文,也能沉淀出Java Web最落地的基础功底。既适合做课程设计,也适合做毕业论文选题,更重要的是,你不需要多高深的技术,就能把一个完整项目讲得清清楚楚。
这篇文章不是带你从0敲代码,而是把我当时做这个项目的全套思路、技术选型、数据库设计、论文撰写经验,还有答辩时导师最爱问的几个坑,一次性给你讲透。你可以把它当做一个"论文+实战"的完整参考基线。
1. 毕设选题的"心机":为什么说个人通讯录系统是Java Web的最佳练手项目
1.1 从课程设计到毕业设计,通讯录系统能装下多少东西
很多同学觉得"通讯录"太简单了,不就是个增删改查吗?这个想法大错特错。
我见过太多人选了"电商系统""社交平台"这种大而全的题目,结果论文写得像说明书的目录,代码堆出来几千行跑都跑不通。选题的第一原则是"小切口、深挖掘、能闭环"。通讯录系统表面看起来功能有限,但它能装下的东西一点都不少:
- 从用户角度来看:注册、登录、个人信息修改、联系人分组、联系人增删改查、分组管理、关键字搜索、导出Excel。这些功能足够覆盖一个典型Web应用的完整链路。
- 从技术角度来看:前后端交互、数据库CRUD、参数校验、异常处理、事务管理、文件导出。再往上加,还能引出JWT认证、Redis缓存、MQ消息通知等技术点。
- 从论文角度来看:你可以写"基于Spring Boot的通讯录管理系统的设计与实现",也可以写"基于SSM框架的个人通讯录系统研究与设计",甚至可以写"个人通讯录系统的设计与实现——基于Vue前后端分离架构"。题目怎么包装都不会空。
最关键的一点是,这个系统的业务逻辑非常清晰,几乎不存在需求歧义。导师一眼就能看懂你要做什么,不会在开题答辩时反复追问"你的系统到底解决了什么痛点"。
1.2 论文写作的骨架:如何把一个小系统写出大格局
我帮你把论文的整体结构大致梳理了一下,这套骨架对于绝大多数"XX系统设计与实现"的毕设都通用,但我的建议是不要直接用"第一章绪论"这种烂大街的写法。
我当时用的是**"引言-需求分析-系统设计-系统实现-系统测试-总结"**六个部分,但在每部分里做了差异化处理:
| 章节 | 核心内容 | 写作关注点 |
|---|---|---|
| 摘要与关键词 | 一句话说清背景,一句话说清功能和设计,一句话说清技术路线 | 三句话结构最稳妥,关键词控制在4-6个 |
| 绪论 | 通讯录管理背景与现状、研究意义、国内外研究现状 | 重点写"数字化通讯录取代纸质通讯录"这个必然性,拉高格局 |
| 需求分析 | 可行性分析、功能性需求(登录、分组、联系人增删改查等)、非功能性需求(性能、安全、扩展性) | 功能性需求必须有用例表,非功能性需求必须有具体指标 |
| 系统设计 | 总体架构(B/S三层架构)、功能模块设计、数据库设计、接口设计 | 必须配架构图、ER图、表结构说明表,这是论文的核心章节 |
| 系统实现 | 环境配置、核心代码逻辑(登录、增删改查、分组、导出)、界面展示 | 代码不能全贴,只贴核心方法,比如登录校验、分页查询、导出逻辑 |
| 系统测试 | 测试环境、功能测试用例表、性能测试结论 | 用例表要写清楚预期结果与实际结果,不要只写"通过" |
这个框架的好处在于,它既符合学校毕业论文的格式规范,又能把你的真实工作全面展示出来。很多同学论文被批"内容空洞",本质上是前面需求分析写得跟说明书一样,后面系统实现又全是代码,中间缺了"为什么这么设计"的逻辑链条。
2. 技术选型的底层逻辑:Spring Boot与MyBatis Plus在这个项目里的真实价值
2.1 如果十年前做这个系统,我会怎么选技术
先别急着上Spring Boot,我来说说技术选型背后的逻辑。
在讲技术栈之前,我要先强调一点:论文里的技术选型,不是越新越好,而是越合适越好。如果你的论文题目是"基于Java个人通讯录系统",那么你选的任何框架都要能回答"为什么选它"这个问题。
如果把时间倒推十年,那时候做个人通讯录系统,主流方案就是JSP + Servlet + JDBC,连MyBatis都还没有普及。当年做这个项目,你得手动写一大堆Servlet类,每个请求写一个doGet和doPost,数据库操作用PreparedStatement手动拼参数,页面跳转全靠转发和重定向。那又怎样?照样能完成功能,而且你能把Java Web的底层原理吃得非常透。
现在做这个项目,我推荐你考虑以下技术栈,这也是目前大多数Java后端岗位的主流标配:
- 开发框架:Spring Boot 2.7 + MyBatis Plus 3.5
- 前端页面:Thymeleaf模板引擎(不用前后端分离,减少不必要的工作量)
- 数据库:MySQL 5.7或8.0
- 安全认证:Spring Security 或 JWT 任选一(建议JWT,论文里好写)
- 构建工具:Maven
- 其他:Lombok、Hutool、Apache POI(导出Excel用)
用Spring Boot的核心原因,我写论文的时候是这样总结的:Spring Boot 通过自动配置和约定优于配置的原则,极大简化了Spring应用的初始搭建和开发过程,使开发者可以快速构建独立运行的Spring应用程序。配合MyBatis Plus的通用Mapper和条件构造器,我们无需手写大量重复的CRUD SQL语句,可以把更多精力放在业务逻辑和系统设计上。
当然,如果你想让导师眼前一亮,可以在技术上这么做升级:
- 在联系人查询接口上加Redis缓存,论文里写"针对高频读取操作引入Redis集中缓存,降低数据库查询压力"
- 在登录模块使用JWT(JSON Web Token)替代传统的Session,论文里写"实现了无状态认证机制,提升系统的横向扩展性"
- 在架构上用Vue + Spring Boot前后端分离模式,论文里写"前后端通过RESTful API进行数据交互,提高系统的解耦性和可维护性"
2.2 依赖注入、ORM与面向对象,通讯录系统是如何体现Java核心思想的
看完上面的内容,你可能会想:Java核心不是面向对象吗?这个通讯录系统怎么体现面向对象的"三大特性"?
这个问题,其实是我毕业答辩时遇到的第一个问题。
我当时是这么回答的,现在也把这套逻辑分享给你:
- 封装:系统设计了User实体、Contact实体、ContactGroup实体,每个实体内部隐藏了字段的访问细节,外部只能通过getter/setter方法访问,数据校验和默认值设置在实体类内部完成,这就是封装思想。比如Contact类的phone字段,我在setter方法里加入了正则校验,不符合规则的手机号直接抛出参数异常。
- 继承:使用MyBatis Plus框架时,所有实体类都继承自Model接口(或者说具备对应的实体父类接口约束),按照框架规范实现了一系列通用方法。另一方面,在Service层,我们提取了BaseService类,公共的增删改查方法放在基类中,每个具体的Service子类只需继承并扩展自己的业务方法。
- 多态:接口定义和实现分离,ContactService接口定义了addContact、updateContact等抽象方法,ContactServiceImpl类实现这些方法时给出了数据库操作的具体实现。Controller层面向接口编程,不依赖具体实现类,未来如果想替换成其他数据库实现,只需要重写实现类,而不用修改Controller代码。
还有依赖注入(DI),我认为这是Spring框架对Java面向对象编程的重要增强。传统开发中,Service层需要自己new一个Mapper实例去操作数据库,耦合度很高。在通讯录系统中,我们通过@Autowired注解把ContactMapper注入到ContactService中,通过构造函数注入方式把ContactService注入到ContactController中,这样各层之间的依赖关系完全由Spring容器接管,代码的可测试性和可维护性显著提升。
你们要注意,这些技术点在论文中不能只出现名词解释,一定要结合系统的实际代码具体阐述。比如"依赖注入"这个概念,你在论文里可以这样写:系统采用Spring的IoC容器管理各业务组件的生命周期,例如在ContactServiceImpl中通过构造器注入ContactMapper接口,使Service层与数据访问层进行解耦,便于单元测试时直接Mock数据访问层。
3. 高内聚低耦合:个人通讯录系统的模块划分与数据库设计
3.1 五个功能模块的边界划定与用例分析
很多同学设计系统的时候,喜欢把功能列得特别多,什么"系统管理""用户管理""日志管理"全堆上。这里我要提醒你:功能越多,论文越好写?恰恰相反,功能越多,你的逻辑越稀碎。
个人通讯录系统的核心价值是"管理联系人"。围绕这个核心,我建议划分五个功能模块,不多不少:
| 模块名称 | 具体功能 | 核心用例 |
|---|---|---|
| 用户管理模块 | 注册、登录、退出登录、密码修改 | 用户注册成功后自动创建默认分组,用户登录时进行账号密码校验 |
| 分组管理模块 | 查看分组、新增分组、修改分组名、删除分组 | 删除分组时该分组下的联系人同步删除或迁移至默认分组 |
| 联系人管理模块 | 联系人列表分页、新增联系人、修改联系人、删除联系人 | 联系人列表按创建时间倒序,删除联系人需二次确认 |
| 搜索模块 | 按姓名/手机号模糊搜索、按分组筛选联系人 | 搜索时自动忽略首尾空格,支持电话和手机号两种格式 |
| 数据导出模块 | 导出当前用户的全部联系人到Excel文件 | 导出文件命名格式为"通讯录_YYYYMMDD.xlsx" |
模块划分遵循了一个重要原则:单一职责原则。以联系人管理为例,它只管联系人本身的增删改查,不掺和分组管理的事。联系人要挂到某个分组下面,那就通过分组的ID作为外键关联。而分组管理模块只管分组的生命周期,分组表被删除时,由数据库层的外键约束或者Service层的事务逻辑来处理级联效果。
在论文的需求分析阶段,建议给每个模块配一个用例表,用例表格式大致如下:
- 用例名称:新增联系人
- 用例编号:UC-02-01
- 参与者:已认证用户
- 前置条件:用户已登录且至少存在一个联系人分组
- 基本流程:用户点击"新增联系人"按钮,输入姓名、手机号、分组ID、备注信息,点击保存。系统校验姓名和手机号非空,校验手机号格式合法,查询分组ID是否存在,插入数据库记录。
- 异常流程:分组ID不存在时提示"分组信息异常,请刷新后重试";手机号格式不合法时提示"请输入正确的手机号"。
- 后置条件:提示"添加成功"并刷新联系人列表。
这种写法会让导师觉得你对业务的理解非常清晰,也是论文拿高分的重要加分项。
3.2 从需求到数据表:用户表、联系人表、分组表的设计实战
数据库设计是我认为这个项目中最能体现"功底"的部分。三个核心表的关系其实很简单,但越简单越容易忽略约束和索引。
正常的建表SQL我写出来给你看一下:
-- 用户表 CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', username VARCHAR(64) NOT NULL UNIQUE COMMENT '用户名/登录账号', password VARCHAR(128) NOT NULL COMMENT '密码(SHA256加密)', nickname VARCHAR(64) DEFAULT NULL COMMENT '用户昵称', email VARCHAR(128) DEFAULT NULL COMMENT '邮箱', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记,0未删除,1已删除' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表'; -- 分组表 CREATE TABLE contact_group ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '分组ID', user_id BIGINT NOT NULL COMMENT '所属用户ID', group_name VARCHAR(64) NOT NULL COMMENT '分组名称', sort_order INT NOT NULL DEFAULT 0 COMMENT '排序序号,越小越靠前', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', INDEX idx_user_id (user_id), CONSTRAINT fk_group_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='联系人分组表'; -- 联系人表 CREATE TABLE contact ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '联系人ID', user_id BIGINT NOT NULL COMMENT '所属用户ID', group_id BIGINT NOT NULL COMMENT '所属分组ID', name VARCHAR(64) NOT NULL COMMENT '联系人姓名', phone VARCHAR(32) DEFAULT NULL COMMENT '手机号', email VARCHAR(128) DEFAULT NULL COMMENT '邮箱', address VARCHAR(255) DEFAULT NULL COMMENT '通讯地址', company VARCHAR(128) DEFAULT NULL COMMENT '公司', remark VARCHAR(512) DEFAULT NULL COMMENT '备注', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', INDEX idx_user_id (user_id), INDEX idx_group_id (group_id), CONSTRAINT fk_contact_user FOREIGN KEY (user_id) REFERENCES sys_user(id), CONSTRAINT fk_contact_group FOREIGN KEY (group_id) REFERENCES contact_group(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='联系人表';3.3 为什么我不建议在联系人表里加"多个手机号"字段
这里要重点辟个谣。很多同学设计通讯录时喜欢把手机号设计成varchar(500),觉得"以后一个联系人可以有多个手机号,用逗号隔开不就行了?"
这种设计我强烈反对,原因主要有两点:
第一,它违反了数据库的第一范式。一个字段存储多个值,在后续做搜索时会让SQL变得非常低效。比如你要查"手机号包含1388888的联系人",如果手机号字段是"1380001,1388888,1389999"这种格式,LIKE查询会匹配出所有拥有任意一个包含该片段手机号的联系人,甚至可能出现误报。
第二,它会把复杂度转嫁到Java代码里。为了把逗号分隔的字符串拆成List,你得写一堆split方法,还要考虑去空格、空字符串过滤等琐碎逻辑。如果未来要统计"一个联系人有几个手机号",更是无从下手。
如果你真的要支持一个联系人有多个手机号,正确做法是建立一个中间表:
CREATE TABLE contact_phone ( id BIGINT AUTO_INCREMENT PRIMARY KEY, contact_id BIGINT NOT NULL, phone_type VARCHAR(16) NOT NULL DEFAULT 'mobile' COMMENT '号码类型,mobile为手机号,home为座机', phone_number VARCHAR(32) NOT NULL, INDEX idx_contact_id (contact_id), CONSTRAINT fk_contact_phone FOREIGN KEY (contact_id) REFERENCES contact(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;但是,对于个人通讯录系统这种规模的毕设,我建议不要引入过多中间表。一个联系人一个手机号,就够你完成核心功能了。如果你非要在论文里体现一下"扩展性",完全可以写"考虑到未来可能存在的多号码需求,设计了联系人与号码的扩展表,但目前版本采用一对一方式简化操作"。一句话带过,反而让导师觉得你有全局思考能力。
另外,关于逻辑删除字段(deleted),我发现很多同学不知道它是干什么用的。MyBatis Plus中有个@TableLogic注解,配置了逻辑删除之后,所有的查询都会自动加上WHERE deleted = 0,删除操作也会自动变成UPDATE deleted = 1。这样做的好处是数据不会真正丢失,误删还可以恢复。在通讯录这种对数据准确性要求较高的场景里,逻辑删除是非常合理的设计。论文的系统实现部分,你可以专门拿出一个表格来说明逻辑删除与物理删除的对比:
| 维度 | 物理删除 | 逻辑删除 |
|---|---|---|
| 数据恢复 | 无法恢复 | 可修改标记位恢复 |
| 查询性能 | 数据减少查询更快 | 数据量增长需要索引配合 |
| 系统设计复杂度 | 简单 | 需要所有SQL查询配合过滤条件 |
| 适用场景 | 临时数据、日志类数据 | 核心业务数据(联系人、用户等) |
4. 手把手实战:从空目录到跑通"增删改查"
4.1 环境准备与初始化Spring Boot工程
这部分如果你已经熟练了可以直接跳过,但如果你是第一次做,下面每一步都值得仔细看。
反正我们做的是Java个人通讯录系统,那开发环境就要准备好。我最常用的组合是:JDK 1.8(实际项目用JDK 8最稳定)+ Maven 3.6.3 + IDEA 2021+ + MySQL 8.0。
打开IDEA,新建Spring Boot项目。这里要提醒你一下:不要用Spring Initializr默认生成的包名结构,自己改成com.xxx.contact这种清晰的结构。包名结构建议按照Controller/Service/Mapper/Entity四层来划分:
com.contact ├── controller ├── service │ └── impl ├── mapper ├── entity ├── config ├── common │ ├── result │ └── exception └── util创建项目时选择的依赖有:Spring Web、Thymeleaf、MySQL Driver、Lombok。后面通过Maven手动加入MyBatis Plus和Apache POI。
pom.xml里的核心依赖这样配置:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.5</version> </dependency>这里有一个生产环境才懂的细节:MyBatis Plus启动器里面自带了MyBatis和Spring Boot的关联依赖,不需要你再单独引入mybatis-spring-boot-starter。我当年就吃过这个亏,引了两份依赖,项目启动时不断报DataSource初始化失败,排查了好几个小时才发现是包冲突。
application.yml配置文件的核心内容如下:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/contact_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意map-underscore-to-camel-case这一项,数据库里create_time这种下划线命名要自动映射到Java实体类的createTime驼峰属性,必须开启这个配置。MyBatis Plus默认是开启的,但有些老项目里往往会手动关闭,导致字段值全部为null,很经典的坑。
4.2 MyBatis Plus自动建表与代码生成器:省时省力的正确姿势
你们知道用MyBatis Plus根据Java实体类直接生成建表SQL语句吗?这个方向很多人没搞清楚,其实MyBatis Plus官方提供了一个MybatisPlusGenerator,但它的主要作用是生成Entity、Mapper、Service代码,而不是帮你建表。
如果真要根据实体类生成建表SQL,我的做法是写一个单元测试,在项目启动时利用MyBatis Plus的DbService或者自定义JDBC工具来做。但我在这里更建议你手动去整理SQL脚本,原因很简单:
- 实体类上你使用的是
@TableName、@TableId、@TableField注解,它本身并不携带字段的长度、非空约束、默认值信息。 - 手动建表脚本让你对数据库结构有绝对掌控力,尤其是加索引、外键约束等细节,自动生成很难保证。
- 论文中需要展示表结构设计文档,手动写的SQL可以直接贴上去。
如果你的思路是"用逆向工程生成代码",那我推荐的做法是:先手动写好SQL脚本,然后在IDEA中连接数据库,右键点击表,使用MyBatis Plus的代码生成插件(比如EasyCode)生成Entity、Mapper、Service、Controller代码。这样生成出来的代码是干干净净的,不需要大改。
这里有个细节:Entity类里的字段要使用包装类型Long、Integer,不要用基本类型long、int。原因很直接:数据库字段值为NULL时,基本类型会直接爆NullPointerException,包装类型可以正常接收NULL。在联系人表中,remark字段一定有很多NULL,你要用String来接收,道理是一样的。
4.3 核心业务的编码实现与常见报错排查
核心功能里的登录模块,我推荐用最简单的Session方式做。如果你想进阶,可以引入JWT,但那样你还需要写一个拦截器或者过滤器,对刚做毕设的同学来说稍复杂。Session方式只需要一个简单的LoginInterceptor:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session != null && session.getAttribute("userId") != null) { return true; } response.sendRedirect("/login"); return false; } }然后在Spring配置类里注册这个拦截器,并配置放行路径,比如/login、/register、/static/**、/css/**、/js/**。放行static路径特别重要,否则你的CSS样式表会被拦截器拦住,页面丑得没法看,很多同学第一次都会踩这个坑。
联系人列表的分页查询,我会用MyBatis Plus的Page对象:
public Page<Contact> getContactPage(Long userId, Long groupId, String keyword, int pageNum, int pageSize) { Page<Contact> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Contact> wrapper = Wrappers.<Contact>lambdaQuery(); wrapper.eq(Contact::getUserId, userId) .eq(groupId != null, Contact::getGroupId, groupId) .and(StringUtils.hasText(keyword), w -> w .like(Contact::getName, keyword) .or() .like(Contact::getPhone, keyword)); wrapper.orderByDesc(Contact::getCreateTime); return contactMapper.selectPage(page, wrapper); }上面这段代码里的eq(groupId != null, Contact::getGroupId, groupId)是MyBatis Plus条件构造器的典型用法,第二个参数为false时自动忽略该条件。在论文里描述这个设计时,可以写"通过条件构造器实现了查询条件的动态拼装,避免了XML中繁琐的if判断",这是很讨巧的描述。
实际开发中,我遇到的最常见的报错是:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):这通常是因为Mapper接口在启动类扫描路径之外,或者Mapper XML的namespace写错了。检查启动类上有没有@MapperScan注解。java.sql.SQLSyntaxErrorException:因为你写的SQL里用了MySQL的保留字当字段名,比如order、desc。所以我建表时干脆把分组字段命名为group_name而不是group。NullPointerException:如果你在Service层直接使用实体类的getter方法,并且数据库里允许该字段为空,就会触发。解决方案就是在service层加判空。
4.4 导出Excel通讯录功能的POI实现思路与避坑
导出功能是最能体现"实用价值"的一个功能。很多同学做过增删改查就算了,其实加一个导出Excel功能,不仅让导师觉得你有工程化思维,也是答辩时能现场演示的亮点功能。
Apache POI的操作流程大致是:
- 创建一个
XSSFWorkbook对象 - 创建Sheet并设置列宽
- 创建表头行,在单元格中写入"姓名""手机号""邮箱""分组"等字段
- 查询当前用户的所有联系人数据,循环遍历写入每一行
- 设置响应头
Content-Disposition,告诉浏览器以附件形式下载
核心代码大致是这样的:
public void exportContacts(Long userId, HttpServletResponse response) throws IOException { List<Contact> contactList = contactMapper.selectList( Wrappers.<Contact>lambdaQuery().eq(Contact::getUserId, userId) ); // 查询分组ID和分组名称的映射 Map<Long, String> groupMap = getGroupNameMap(userId); XSSFWorkbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("通讯录"); sheet.setColumnWidth(0, 12 * 256); sheet.setColumnWidth(1, 14 * 256); sheet.setColumnWidth(2, 22 * 256); sheet.setColumnWidth(3, 20 * 256); String[] headers = {"姓名", "手机号", "邮箱", "分组"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } int rowIndex = 1; for (Contact c : contactList) { Row row = sheet.createRow(rowIndex++); row.createCell(0).setCellValue(c.getName()); row.createCell(1).setCellValue(c.getPhone() == null ? "" : c.getPhone()); row.createCell(2).setCellValue(c.getEmail() == null ? "" : c.getEmail()); row.createCell(3).setCellValue(groupMap.getOrDefault(c.getGroupId(), "未分组")); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); String fileName = URLEncoder.encode("通讯录_" + System.currentTimeMillis(), "UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + ".xlsx\""); workbook.write(response.getOutputStream()); workbook.close(); }这里有三个容易踩的坑,提前帮你避开:
- 很多人直接
workbook.close()后,再调用response.getOutputStream().close(),其实不需要手动关OutputStream,只要关掉workbook就够了,而且workbook必须放在finally块或者用try-with-resources,否则文件流没写完整,导出Excel文件是损坏的。 - 如果联系人数据特别多,导出时
new XSSFWorkbook()会占用大量内存。不过对通讯录这种数据量来说,根本不用担心这个问题,论文里可以写"考虑到系统数据量规模较小,采用XSSFWorkbook即可满足需求,该方案适合单次导出小于10万条数据的场景"。 - 文件名中的中文必须做
URLEncoder.encode处理,否则在IE和部分浏览器中会变成下划线或者乱码。
5. 让论文看起来像"系统设计与实现"而不是"代码堆砌"
5.1 需求分析章节怎么写才能让导师点头
论文最容易翻车的就是需求分析。很多同学从百度抄一段"随着信息化时代的发展",再抄一段"传统的纸质通讯录已经无法满足需求",然后就开始列功能。这种写法在导师那里过不了关,因为缺少"你自己的理解"。
我建议你从以下两个维度重新构思需求分析:
第一,用户角色描述。你的系统只有一类用户,那就是"个人用户"。别小看这个角色,你可以把用户故事写出来:作为个人用户,我希望能够对联系人分类管理,这样可以快速找到特定人群;作为个人用户,我希望能够导出通讯录,这样在换手机时可以直接备份。
第二,需求分析要分层。把需求拆成三个层次:
- 必备需求(Must Have):用户注册登录、联系人增删改查、分组管理。
- 期望需求(Should Have):关键字搜索、数据分页展示、登录状态保持。
- 扩展需求(Could Have):数据导入导出、密码加密、记住我。
这个"需求分层"的理念来源于Kano模型,你可以不写这么深,但按照这个思路去组织需求章节,会让导师觉得你具备系统性的需求分析能力。
5.2 画图与表:用例图、ER图、流程图相关注意事项
论文中必要的图示包括:
- 系统架构图:展示浏览器、Controller层、Service层、Mapper层、MySQL数据库之间的调用关系。画法用Visio或者Draw.io都行,注意层次清晰即可。
- 用例图:以"参与者"为中心,画出登录、注册、联系人管理、分组管理、导出等用例。如果画得有些乱,宁可少画也不要画错。
- ER图(实体-联系图):标出sys_user、contact_group、contact三个实体,以及它们之间的关系(1:N)。
- 核心业务流程图:比如登录流程图、新增联系人流程图。流程图的要点是分支条件要画清楚,比如"校验手机号格式不合法 -> 提示错误"这条路径一定要画出来。
在画这些图的时候,你要注意学校对论文格式的要求。我当时把图导成PDF再插入Word中,确保在任何一台电脑上打开都不会出现乱版。
5.3 论文查重与降AIGC的实战策略
这部分是大家最爱问的。我要说一句大实话:论文查重是查文字相似度,而AIGC检测是查机器语言的痕迹。你不能指望通过某一种方式彻底避免。
查重降重的核心原则是"去掉口号、保留数据"。举个例子,如果你写到"随着互联网的飞速发展,人们的生活发生了翻天覆地的变化",这种句子十个人里有八个人会写,查重必中。但如果你改成"截至2024年6月,我国网民规模已达10.9亿,个人数据的电子化管理需求持续上升",这种数据化的表述很难被标红。
AIGC检测的降重思路则不同。机器生成的语言通常结构过于工整、逻辑连接词偏多。我的经验是:
- 把文中的"首先、其次、最后"这类词全部删掉,改用更自然的过渡句
- 多使用第一人称的描述:"我在实际开发过程中遇到一个问题""经过反复调试后我发现"
- 加入具体的感知细节,比如"控制台输出了一行红色异常信息,定位源码才发现是数据库密码包含特殊字符导致"之类的内容
但这不代表要你造假或者乱写,而是要你把自己真实做的事情描述得更具体、更个人化。我一直跟学弟学妹们说,你不能论文写完了都不知道自己代码里用了@TableLogic是干嘛的。老老实实用心做了,自然有内容可写。
6. 调试时最折磨人的几个问题与复盘清单
这部分内容是我额外加进来的。因为我发现大家在开发过程中,栽跟头的往往不是复杂的业务逻辑,而是那些在环境配置、基础API使用上的小问题。我把高频问题整理成一张复盘清单,方便你实际动手时快速定位:
| 问题现象 | 根本原因 | 排查方法 |
|---|---|---|
项目启动报错,提示Invalid value type for attribute 'factoryBeanObjectType' | Spring Boot与MyBatis Plus版本冲突 | 检查Spring Boot版本是否过旧,更换为2.x系列并升级MyBatis Plus |
| 页面能打开但CSS样式全部失效 | 拦截器没有放行静态资源 | 在WebMvcConfigurer中放行/static/**和/css/** |
| 登录后访问任何页面又跳回登录页 | Session失效或未正确读取 | 检查浏览器开发者工具中Cookie是否正常;检查LoginInterceptor中是否使用request.getSession(false)导致误判 |
| 联系人分页数据正常,但总数不对 | 分页插件未正确配置 | 检查MyBatis Plus的PaginationInnerInterceptor是否注册为Bean |
| 导出Excel时中文文件名乱码 | 未对文件名进行编码 | 使用URLEncoder.encode()处理文件名的中文部分 |
| 数据库连接成功但中文乱码 | JDBC URL未设置characterEncoding=utf8 | 在url末尾追加useUnicode=true&characterEncoding=utf8 |
光看清单你可能没有体感,我挑两个我当年卡得最久的问题展开说说。
第一个是MyBatis Plus乐观锁插件失效。我当时在联系人实体中加了@Version注解,用乐观锁控制并发修改。结果测试时发现无论怎么并发修改,数据都没有被正确锁住。排查到最后,发现是我把乐观锁插件配在了MyBatis的配置类里,但因为配置类没有加@Configuration注解,插件的Bean根本没被Spring扫描到。那种控制台没有任何报错、但功能就是不符合预期的bug,真的是最折磨人的。
第二个是POI导出时内存溢出。我一开始用的是HSSFWorkbook,这个对象是专门处理.xls格式的,每次操作都会先把整个Excel加载进内存。当我测试导出2万条数据时,JVM直接OOM了。后来改成XSSFWorkbook配合SXSSFWorkbook,问题才解决。我们做通讯录系统一般用不到SXSSFWorkbook,但如果你论文里的导出模块想展示优化思路,这一点是很值得写的。
这些没有标准教程告诉你的坑,恰恰是你在论文"系统实现"章节里最能展现真实开发经验的部分。答辩的时候,导师问"你在开发中遇到的最大挑战是什么",你就可以把这些排查过程讲出来,这比背八股文有说服力得多。
最后聊几句我的心里话
关于这个项目,我能给的最核心的建议就是:别把通讯录系统当成一个"玩具项目"看。它的功能边界清晰,技术栈通用,扩展空间也足够大。聪明的人会把基础版本加登录、加分组、加导出,再往上能加Redis缓存、加JWT、加WebSocket在线通讯、加Vue前后端分离。
实际带过不少人做完这个项目后,我的体会是:这个项目虽然代码量不大,但它可以串起Java面试基础里的几乎所有问题。从集合到泛型,从异常到反射,从MySQL索引到Spring事务,从JSP/Servlet到Spring Boot自动配置。你只要跟着敲一遍,哪怕是抄一遍,把每一行代码都弄明白,毕业答辩和初级Java岗位的技术面都不虚。
最后分享一个小技巧:写论文的"系统实现"章节时,不要按"登录功能实现了什么""联系人管理实现了什么"这样逐个列,而是按"核心流程的实现"来组织。比如"系统采用拦截器实现登录认证机制""系统基于MyBatis Plus条件构造器实现动态条件查询""系统通过Apache POI实现联系人数据导出功能"。这样写出来的论文,条理会清楚得多,也更能体现你的系统设计能力。
如果你正准备开题或者已经卡在开发中途,希望这篇东西能给你省下几个通宵。有问题可以在评论区交流,我看到都会回复。