简介:《软件工程实习记录》是一份面向本科软件工程专业学生、尤其是需完成实习报告或毕业实习记录的学习者整理的文档型参考资料,可用于了解企业实习的组织方式、每日记录写法与软件开发全流程。压缩包内共1个doc文件,约40KB,按周记录实习过程,涵盖可行性分析、需求分析、概要设计、详细设计、编码与项目验收等环节,并穿插项目经理职责、开发岗位分工及技术栈说明。内容预览还涉及Eclipse Swing界面、HTML/CSS/JavaScript页面、Node.js与Express、MySQL增删改查、Linux与ARM开发环境等实践场景,便于读者参考记录结构、补充知识点,并在此基础上修改成自己的实习材料。已有2924人学习,适合作为实习记录撰写与软件开发流程梳理的对照样本。
1. 从一份 25 天的开发日志说起
拿到《软件工程实习记录》这份文档的人,多半不是想读故事,而是想找一份能照着改的实习日志底稿。它的价值不在文笔,而在于它是一份按天推进、真名实姓的软件工程实习记录:五周时间,从可行性分析讲到项目验收,从 Swing 桌面工具写到 Node.js + MySQL 的网页增删改查,中间还夹着 Linux 交叉编译、IIS 站点权限、Tomcat 端口占用这些只有真上手才会撞见的事。里面那句“代码实现只占整个开发过程的百分之十五”,是我见过对这个行业最直白的注解。它适合三类人:要交实习材料的在校生、刚进项目组想理清流程的新人、想拿一个完整小项目练手的开发者。下面按模块拆开,能跑的地方给命令,能改的地方给位置。
2. 骨架拆解:五周任务线怎么对上六大开发流程
2.1 五周任务线的真实分布
这份记录不是按“知识点”组织的,而是按周组织,每周一条主线,越往后越接近一个完整项目。第一周是认知周,前半段听流程、分小组,后半段直接上 Eclipse 写 Swing 面板;第二周从静态网页切到 Node.js,把 Express 和 MySQL 串起来;第三周开始碰真实环境,Linux、ARM 开发板、IIS、Tomcat 轮着来;第四周补框架理论,Struts、Spring、Hibernate 各讲一层,同时开始搭“某网上书城项目”的模型;第五周做完整的增删改查和部署,最后一天卡在端口占用上。
把它抽成一张表会更清楚,这也是我拿到任何一份项目日志时做的第一件事——先看时间轴上每个阶段交付了什么,而不是看它写了多少字。
| 周次 | 主线任务 | 涉及技术 | 当周交付物 |
|---|---|---|---|
| 第一周 | 流程认知 + 桌面工具 | Java Swing、Eclipse、HTML/CSS/JS | 文件查找与排序面板、静态教师信息页 |
| 第二周 | 前后端打通 | Node.js、Express、MySQL、JSON | 可增删改查的单页 |
| 第三周 | 真实运行环境 | 企业级 Linux 9.0、ARM 交叉工具链、IIS、Tomcat | hello 程序上板、本地站点可访问 |
| 第四周 | 框架与建模 | Struts、Spring、Hibernate、JSP | 书城项目需求模型、信息录入功能 |
| 第五周 | 功能补全与上线 | JSP、Tomcat、端口配置 | 部署完成的完整项目 |
这张表能解释一个很多人忽略的问题:为什么日志里第一周还在写 Swing,第二周突然跳到 Node.js。不是作者东一榔头西一棒子,而是公司的任务安排本身就是“先让你把界面和逻辑摸熟,再让你理解数据从哪来、到哪去”。桌面端和 Web 端在这份记录里不是两个项目,是同一件事的两个侧面——都是“输入条件、查数据、回显结果”,只是承载它的壳子换了。
提示:如果你要拿这份记录改自己的实习日志,别照抄任务顺序。先确定你所在岗位的真实主线(前端、后端还是实施),再按那条线重排周次,否则读起来会像拼凑。
2.2 六大流程在日志里的落点
文档开头列了软件开发的六个步骤:可行性分析、需求分析、概要设计、详细设计、编码、项目验收。很多人写实习记录时会把这段当“帽子”扣在开头,后面该写啥写啥,流程和正文两张皮。这份记录好在哪?它后面每一周的任务,其实都能对应回这六步里的某一步,只是作者当时没意识到。
可行性分析落在第一天的动员会上——公司介绍业务范围、说明项目类型,本质上是在判断“这个需求我们接不接、用什么技术接”。需求分析的影子在第四周最明显,一群人围在一起讨论书城系统要哪些功能,图书信息录入怎么设计字段。概要设计和详细设计被压缩了,表现出来就是“建模型”那两天:先画功能结构,再定页面和数据库表的对应关系。编码占了篇幅的大头,这是必然的,因为这是唯一能截图、能跑出结果的部分。项目验收则是第五周的部署,把本地能跑的东西搬到服务器上,让别人也能访问。
这里有个反直觉的点值得展开:日志里花最多笔墨的是编码,但作者自己在第一天就写了“代码实现只占百分之十五”。这不是自相矛盾,而是新手视角和项目视角的差别。新手看代码,因为代码是唯一他能完全掌控的部分;项目看流程,因为流程决定这个代码最终会不会被验收、会不会被返工。我在带人的时候经常说,你写的第一版代码能不能活下来,八成不取决于你写得多漂亮,而取决于需求阶段有没有人把“模糊查找”这四个字定义清楚——是匹配文件名,还是匹配全路径,还是匹配文件内容。
所以读这份记录,建议倒着读一遍:先看第五周部署时出了什么问题,再回头看第二周写代码时有没有为这些问题埋下伏笔。你会发现端口号写死、字符集没统一、路径硬编码,这些坑在写代码那天就已经挖好了,只是到部署那天才掉进去。
2.3 工具与环境清单:从 Eclipse 到 ARM 交叉工具链
把日志里出现的工具按用途归一下类,方便你复现时一次性把环境备齐。桌面端用 Eclipse,Web 端用 MyEclipse,这两者在当年是标配,现在用 IntelliJ IDEA 或 VS Code 加插件也能替代,只是 Swing 的可视化面板设计器在新 IDE 里体验差一些,常见做法还是拿 Eclipse 的 WindowBuilder 拖控件。数据库选 MySQL,辅助包用 node 侧的 mysql 驱动;Web 容器用 Tomcat,另外还碰了 IIS,因为公司网站那台机器是 Windows 环境。
第三周的 Linux 部分值得单独说,因为它是整份记录里唯一涉及嵌入式的段落。任务是写一个 hello.c,配一个 Makefile,用交叉编译工具链编出能在 ARM 开发板上跑的二进制。这条链路新手容易卡在“编出来了但跑不了”,原因通常是编译器选错——宿主机上的 gcc 编出来的是 x86 指令,开发板认不了。常见做法是显式指定交叉编译器前缀,把所有编译动作收进 Makefile,避免手敲命令时漏参数。
# 交叉编译 hello.c,产物给 ARM 开发板用 CC = arm-linux-gcc TARGET = hello OBJ = hello.o $(TARGET): $(OBJ) $(CC) -o $(TARGET) $(OBJ) $(OBJ): hello.c $(CC) -c hello.c clean: rm -f $(TARGET) $(OBJ)这段 Makefile 的逻辑很直白:CC决定用哪个编译器,改这一行就能在宿主编译和交叉编译之间切换;$(TARGET): $(OBJ)说明可执行文件依赖目标文件,make 会自动判断谁新谁旧,只重编改过的部分;clean是后悔药,产物出问题时先make clean再make,比对着报错猜半天快。参数上唯一要注意的是arm-linux-gcc这个名字跟你实际装的工具链前缀必须一致,有的环境叫arm-none-linux-gnueabi-gcc,抄之前先ls /usr/local/bin | grep gcc看一眼。
编译完用file hello验证,输出里带“ARM”字样才算对;如果显示的是“x86-64”,说明 Makefile 里的CC没生效,多半是终端里还有环境变量覆盖了它。这一步验证花十秒,能省掉后面把程序拷到板子上跑不起来、又回头怀疑板子坏了的半小时。
3. 桌面端模块:Swing 文件查找与冒泡排序的两处硬伤
3.1 com.shixi.viwe 与 com.shixi.main 的分包逻辑
日志里出现的包名是com.shixi.viwe,这个拼写是view少写了一个字母,属于典型的手快错误。我没打算替他美化,反而觉得这个细节值得留着——真实项目里的包名、变量名,多多少少带点这种痕迹,你二次开发时第一件事往往不是读逻辑,而是先把这些拼写统一掉,否则后面 grep 都搜不全。
分包思路本身是对的:视图放view包,主程序和运行逻辑放main包。这是最朴素的 MVC 雏形,虽然只有 V 和入口,没有独立的 Controller 和 Model。放在当年那个练手规模下够用,但如果要扩展,建议再拆两个包:service放查找、排序这些与界面无关的逻辑,model放文件条目这类数据结构。拆完之后视图只负责“拿输入、显示结果”,逻辑可以单独写单元测试,不用每次启动界面点按钮验证。
具体到代码组织,视图类的职责应该收敛到三件事:摆放组件、绑定事件、把结果刷到列表上。很多新手会把扫描磁盘的循环直接写在按钮的actionPerformed里,结果界面点一下卡三秒,因为 Swing 的事件分发线程被阻塞了。常见做法是把耗时操作丢到SwingWorker的doInBackground里,做完再回主线程更新组件。这一步在文件数量少的时候感觉不出来,一旦目录深一点、文件多几千个,界面假死就是必然的。
3.2 模糊查找:从输入框到结果回显的链路
这个功能的需求一句话能说完:输入框里敲文件名或者商品名的一部分,点查找,把匹配到的结果列出来。难点不在匹配,在“从哪儿找”和“怎么找得快而不卡”。
先看视图层的骨架,这段是日志里描述过的面板结构,我按常见写法补全:
// com.shixi.view.FindPanel public class FindPanel extends JPanel { private final JTextField keywordField = new JTextField(20); private final JButton searchBtn = new JButton("查找"); private final DefaultListModel<String> model = new DefaultListModel<>(); private final JList<String> resultList = new JList<>(model); private final JLabel statusLabel = new JLabel("就绪"); public FindPanel() { setLayout(new BorderLayout(8, 8)); JPanel top = new JPanel(new FlowLayout(FlowLayout.LEFT)); top.add(new JLabel("关键字:")); top.add(keywordField); top.add(searchBtn); add(top, BorderLayout.NORTH); add(new JScrollPane(resultList), BorderLayout.CENTER); add(statusLabel, BorderLayout.SOUTH); searchBtn.addActionListener(e -> startSearch()); } }keywordField是输入框,searchBtn触发查找,DefaultListModel是列表的数据源,往 model 里加元素,界面自动刷新,不用手动调repaint。BorderLayout把界面切成上中下三块,中间的列表套一层JScrollPane,结果多了能滚动。这些参数没有玄学,照抄能跑,唯一要注意的是FlowLayout的对齐方式,默认居中,改成LEFT视觉上更自然。
真正干活的是扫描逻辑:
// 递归扫描目录,返回文件名包含关键字的文件 private List<File> scan(File dir, String keyword) { List<File> hits = new ArrayList<>(); File[] files = dir.listFiles(); if (files == null) return hits; // 目录无权限或不存在,直接返回空 for (File f : files) { if (f.isDirectory()) { hits.addAll(scan(f, keyword)); // 子目录递归下去 } else if (f.getName().toLowerCase() .contains(keyword.toLowerCase())) { hits.add(f); // 模糊匹配:包含即命中 } } return hits; }listFiles()返回 null 的情况必须处理,这是新手最容易漏的一行——遇到系统目录或者被占用的目录,不判空直接遍历就是空指针。toLowerCase()两边都转,是为了让匹配不区分大小写,用户输入“report”也能查到“Report.docx”。这里的模糊匹配用的是contains,属于子串匹配,够用且好理解;如果需要通配符(比如*.txt),就得换成PathMatcher,复杂度上一个台阶。
回显部分把File转成字符串塞进 model 就行,但建议只显示文件名和大小,不显示完整路径,否则列表横向拉得老长。完整路径放在选中项的详情区或者双击打开时再用。
3.3 冒泡排序按文件名排:能跑,但要知道代价
日志里写得很清楚,排序依赖文件名,算法用冒泡排序。这个选择在教学场景下完全合理,因为冒泡是最好讲清楚的排序;但放到真实目录扫描的结果上,就得知道它的边界在哪。
// 冒泡排序:按文件名升序,带提前退出优化 public static void bubbleSort(File[] arr) { for (int i = 0; i < arr.length - 1; i++) { boolean swapped = false; for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j].getName().compareToIgnoreCase(arr[j + 1].getName()) > 0) { File tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; swapped = true; } } if (!swapped) break; // 本轮没交换,说明已有序,提前收工 } }外层循环控制趟数,内层循环做相邻比较和交换,arr.length - 1 - i是因为每趟结束最大的元素已经沉到末尾,不用再比。swapped这个标志是性价比最高的一处优化:对近乎有序的数据,第一趟跑完就退出,复杂度从 O(n²) 退到 O(n)。比较用compareToIgnoreCase,保证大小写混排时顺序稳定。
代价在哪?冒泡的时间复杂度是 O(n²),一千个文件大概百万次比较,用户感觉不出来;一万个文件就是一亿次,界面会明显卡顿。这时候正确做法不是优化冒泡,是直接换Arrays.sort(arr, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)),底层是归并或快排,几行代码解决。所以我的建议是:冒泡留着,但要在注释里写明“仅适用于小数据量”,别让后面接手的人以为这是深思熟虑的选择。
排序和查找还有个顺序问题。日志里的做法是先查再排,这是对的——先缩小结果集再排序,比全盘排序后再过滤省太多。如果反过来,每次输入关键字都要重排整个磁盘的文件列表,体验会很差。
3.4 路径写死那次翻车:改成可选目录
日志里第二天写完查找功能,第三天就发现“只能从固定的地址进行查找”,去问了老师才改好。这个坑太典型了,几乎每个第一次写文件工具的人都会踩:扫描的根目录直接写死在代码里,比如new File("D:/data"),换台机器就废。
改法不复杂,加一个目录选择按钮,把路径变成运行时可变的参数:
// 选择扫描根目录,替代硬编码路径 private File rootDir = new File(System.getProperty("user.home")); private void chooseDir() { JFileChooser chooser = new JFileChooser(rootDir); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); // 只让选目录 if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { rootDir = chooser.getSelectedFile(); statusLabel.setText("当前目录:" + rootDir.getAbsolutePath()); } }System.getProperty("user.home")取当前用户主目录作为默认值,跨平台,比写D:/稳。DIRECTORIES_ONLY限制只能选目录,避免用户选了个文件导致listFiles()返回 null。选中后立刻更新状态栏,让用户知道当前扫的是哪,这是个小细节,但能省掉“为什么搜不到我桌上的文件”这类追问。
真正值得记的不是这段代码,而是发现问题的路径:功能写完先自己点一遍,换个目录再点一遍,发现结果没变,才意识到路径写死了。这个自测习惯比任何调试技巧都管用,我到现在还保持着“每个带路径的功能至少换两个目录试”的规矩。
4. Web 端链路:静态表格怎么长成 Express + MySQL 的增删改查
4.1 第一版静态页:数组渲染与重复显示的修复
第一版页面最简单:教师信息存在数组里,用表格显示出来。这一步的目的是先把“数据长什么样、页面怎么摆”定下来,不涉及网络请求。日志里第二周第一天做的就是这个,结构大致是页面顶部三个输入框(编号、姓名、课程名),一个添加按钮,下面一张表。
问题出在添加功能上——点一次按钮,数据追加一次,但页面没刷新干净,旧数据和新数据叠在一起显示。日志里的解决方法是“添加信息前先删除原来的信息”,翻译成代码就是渲染前清空容器:
// 先清空 tbody,再整体重绘,避免重复追加 function renderTeachers(list) { const tbody = document.querySelector('#teacherTable tbody'); tbody.innerHTML = ''; // 关键:清空旧行 list.forEach(t => { const tr = document.createElement('tr'); tr.innerHTML = `<td>${t.id}</td><td>${t.name}</td><td>${t.course}</td>`; tbody.appendChild(tr); }); }innerHTML = ''是清空最直接的方式,代价是丢掉原有 DOM 节点上的事件绑定,所以如果表格行上有删除按钮,得用事件委托挂在tbody上,而不是逐行绑定。这个细节在静态数据阶段无所谓,一旦接上数据库做增删改查,就会变成“删了第一行,第二行的按钮失灵”这种诡异 bug。
顺带说一句,用模板字符串拼 HTML 在这里没问题,但如果数据来自用户输入,就得防注入——<td>${t.name}</td>里如果 name 是<script>,页面就中招了。常见做法是用textContent赋值,或者引入转义函数。这是静态页升级到真实项目时最容易漏的一环。
4.2 Express 接管请求:get 与 post 的分工
静态页只能自己跟自己玩,要连数据库就得有个服务端。日志里选了 Node.js + Express,思路是先跑通请求响应,再接数据库。这一步的学习曲线主要在“前后端怎么约定数据格式”,代码本身不长。
const express = require('express'); const app = express(); app.use(express.json()); // 解析 post 提交的 json 体 // 查询:get 带查询参数,返回列表 app.get('/api/teachers', async (req, res) => { const keyword = req.query.kw || ''; // 可选关键字,空串表示全部 const [rows] = await pool.query( 'SELECT id, name, course FROM teacher WHERE name LIKE ?', [`%${keyword}%`] ); res.json(rows); }); // 新增:post 提交完整对象 app.post('/api/teachers', async (req, res) => { const { id, name, course } = req.body; await pool.execute( 'INSERT INTO teacher(id, name, course) VALUES (?, ?, ?)', [id, name, course] ); res.json({ ok: true }); }); app.listen(3000, () => console.log('listening on 3000'));express.json()这行不能省,省了req.body就是 undefined,前端 post 过去的数据全丢。get 用req.query取 URL 上的参数,post 用req.body取请求体,这是两者的本质分工:get 适合查询,参数可见、可缓存、可收藏;post 适合写入,参数在体里,长度不受 URL 限制。
SQL 里用?占位符而不是字符串拼接,这是硬性要求,不是风格问题。拼接写法遇到名字里带单引号就会报错,更严重的是给注入留了门。pool.query和pool.execute的区别在于后者走预编译,参数类型处理更稳,写入场景优先用 execute。
4.3 接上 MySQL:辅助包、连接池与 JSON 回显
日志里写“需要安装 node 对 MySQL 数据库的辅助包,再将读出的数据用 json 工具进行格式转化”。这一步的坑集中在一处:连上了但读出来是乱码,或者读出来是 Buffer 不是字符串。
const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: '127.0.0.1', port: 3306, user: 'root', password: '******', database: 'school', charset: 'utf8mb4', // 连接层字符集,必须和表、页面一致 connectionLimit: 10 // 连接池上限,按并发量调 });mysql2/promise是带 Promise 的版本,能直接await,不用回调套回调。charset这一项是中文乱码的高发区:数据库表是 utf8mb4,连接层写 utf8,某些字符就会在传输环节被截断或替换。三个地方——建库建表、连接配置、页面 meta——字符集必须统一,缺一个都可能出问题。
connectionLimit控制连接池大小,写 10 对练手项目绰绰有余。设太大数据库端会拒绝,设太小高并发时请求排队。这个值不是拍脑袋定的,要看你数据库的max_connections和实际压测结果。
查询结果rows直接res.json(rows)就能回给前端,Express 会自动序列化。要注意的是 MySQL 里的DECIMAL、BIGINT在某些驱动版本下会返回字符串,前端做数值计算前先Number()转一下,否则会出现“1 + 1 = 11”这种哭笑不得的结果。
4.4 增删改查的按钮链路:一次点击背后发生了什么
日志里第五周的核心是把四个操作补齐,实现方式是“点击按钮激活不同的 js 函数,由 js 函数执行不同的 SQL”。这个描述略粗,实际链路是四层:按钮 → 事件处理函数 → fetch 请求 → Express 路由 → SQL。中间任何一层断了,表现都是“点了没反应”,所以排查时要从后往前看。
| 操作 | 前端触发 | 请求方式 | 服务端 SQL |
|---|---|---|---|
| 查询 | 页面加载 / 搜索框回车 | GET /api/teachers?kw= | SELECT ... WHERE name LIKE ? |
| 新增 | 点“添加”按钮 | POST /api/teachers | INSERT INTO teacher ... |
| 修改 | 点行内“编辑”后保存 | PUT /api/teachers/:id | UPDATE teacher SET ... WHERE id=? |
| 删除 | 点行内“删除” | DELETE /api/teachers/:id | DELETE FROM teacher WHERE id=? |
删除按钮的绑定方式值得单独讲。如果每渲染一行就addEventListener一次,行被删掉再重绘,旧监听器不会自动回收,次数多了内存就上去了。稳妥写法是事件委托:
// 事件委托:只在 tbody 上绑一次,动态行也能响应 document.querySelector('#teacherTable tbody') .addEventListener('click', async (e) => { if (!e.target.matches('.btn-del')) return; // 只处理删除按钮 const id = e.target.dataset.id; // 行上带>// check-records.js:校验每篇记录字段是否齐全 const fs = require('fs'); const required = ['日期', '任务', '技术点', '问题', '解决']; fs.readdirSync('./records').forEach(file => { const text = fs.readFileSync(`./records/${file}`, 'utf8'); const missing = required.filter(k => !text.includes(k)); if (missing.length) console.warn(`${file} 缺字段:${missing.join('、')}`); });readdirSync遍历目录,includes做粗粒度检查,够用。真要严格可以改成解析 Markdown 的二级标题,但练手阶段没必要。跑一遍,缺字段的文件一目了然,比人工翻快得多。
第二件是把技术点从叙述里抽出来,单独维护一张问题索引表。日志里那些坑——字符集、端口、驱动版本、目录权限——本质上是同一类问题的不同表现,抽成表之后,下次遇到“页面能开但没数据”,直接按表里的排查顺序走,不用重新想。
| 症状 | 优先怀疑 | 验证命令 / 动作 |
|---|---|---|
| 页面空白但状态 200 | 前端请求地址写错 | 浏览器 Network 面板看请求 URL |
| 中文变问号 | 字符集三处不一致 | SHOW VARIABLES、看页面 meta |
| 启动报端口占用 | 旧进程未退出 | netstat -ano 查 PID |
| 500 且日志含糊 | 驱动版本不匹配 | 核对数据库版本与 jar 版本 |
| 本地站点访问不到 | 目录权限 / 磁盘格式 | 查 NTFS、加来宾账户读权限 |
改造的时候还有个容易忽略的点:日志里所有涉及具体环境的信息,比如机器名、内网地址、账号,二次使用时都要替换掉。这份记录里已经做了模糊处理,只保留技术事实,这个尺度是对的——保留“端口被占用”这件事,删掉“哪台机器上的哪个端口”,信息量不减,风险归零。
我自己的习惯是,每份要交出去的技术记录,写完先放两天,再以“完全没参与过这个项目的人”的视角读一遍,凡是读不懂、需要猜的地方,就是需要补的地方。这份实习记录里有几处就是这样的:提到“老师修改后成功实现”,但没说改了什么;提到“配置好端口和 IP”,但没写具体配在哪。这些位置如果补上一两句,整份材料的可复用性会上一个台阶。
从那以后我每次整理项目记录,都强制走一遍“字段校验 + 问题索引”这两步,宁可多花半小时,也不让文档变成只有作者自己能看懂的流水账。希望帮到你。
本文还有配套的精品资源,点击获取