
简介一套基于SSM框架与HTML/CSS/JavaScript技术实现的简易记事本Java Web应用源码面向Java Web初学者与想快速上手框架整合的开发者。项目以Spring、SpringMVC和MyBatis为核心覆盖用户注册、登录验证、记事新建与编辑保存等日常功能分层清晰兼顾学习与实用。压缩包共66个文件大小仅249KB包含22个Java源文件承载Controller、Service等后台逻辑17个XML配置文件负责Spring容器与MyBatis数据映射13个HTML页面配合3个JavaScript和2个CSS文件完成界面展示与交互另附SQL脚本便于直接运行。目前已有133人学习该资源。通过阅读源码可以理解SSM框架的完整调用链路、MyBatis持久层写法以及前端与后端的数据交互还可在现有功能上扩展分类、提醒等进阶能力是一份实用的Java Web项目参考。1. 一个记事本项目背后的SSM工程全貌如果只看名字你大概会以为这是一个只够应付课程设计的玩具。但这套基于SSM框架的简易记事本源码解压后一共67个文件却把注册、登录、记事新增、编辑、删除、按用户隔离数据这种完整闭环全部跑通。22个Java文件、17个XML配置、13个HTML文档、3个JavaScript文件和2个CSS文件加上1个SQL脚本这个配比是典型的SSM教学项目结构没做前后端分离但三层职责被框架切得非常清楚。对刚学完Spring、SpringMVC、MyBatis却没写过完整工程的开发者这是很好的练手脚手架对想快速搭一个内部小工具的工程师它的登录态处理和CRUD写法也值得直接摘出来改。记事本这个切入点选得很聪明——一张用户表、一张记事表就能表达数据模型业务链却完整覆盖注册、登录、会话保持和数据持久化跑通之后你能把SSM三个框架各自做了什么从抽象概念落到具体文件上。2. SSM框架配置拆解从pom.xml到MyBatis SqlMapConfig的分析2.1 目录布局前端资源为什么和后端工程放在同一层解压upload.zip之后第一眼看到的目录和常见Maven工程不太一样login.html、register.html、main.html、1.html这些页面放在外层css、img、eventsControl.js也放在外层而后端工程整体被包在一个名为ssm - 副本的文件夹里内部才是规范的src/main目录。这个文件夹名不是规范命名是作者复制工程时IDE自动追加的不影响编译但IDEA导入后模块名也会带着“副本”字样建议在Project Structure里把Module改名为ssm避免后面打war包时中文路径出问题。我一般会把前端页面和后端工程分开看HTML和JavaScript属于Web层资源按Servlet规范应放在webapp目录下但这套源码把它们放在工程外层也能跑原因在于SpringMVC的视图解析器配置了静态资源目录。你打开13个HTML文件会发现页面之间通过相对路径引用js和css只要Tomcat的webapps下目录结构与源码层级保持一致渲染就不会出问题。理解这一点对部署很关键很多人直接把pom.xml所在目录扔进Tomcat结果找不到register.html问题就出在这。这里先把配置文件的职责总表放出来方便对照源码检查配置文件职责对应框架pom.xml依赖管理、构建插件、打包方式Mavenweb.xmlDispatcherServlet入口、编码过滤器Servlet规范applicationContext.xmlService层、DAO层、数据源、事务管理Springspring-mvc.xmlController扫描、视图解析器、注解驱动SpringMVCmybatis-config.xml全局设置、类型别名、Mapper扫描MyBatis2.2 pom.xml依赖按三层拆成三组打开pom.xml依赖不是一坨堆在一起而是按SSM三件套的职责拆开的。常见做法是分三组spring-context和spring-jdbc支撑容器与数据访问spring-webmvc支撑Web层mybatis与mybatis-spring负责持久层接入。核心依赖结构如下dependencies !-- Spring 基础容器与 JDBC 支持 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.x/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.x/version /dependency !-- SpringMVC Web 层 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.x/version /dependency !-- MyBatis 与 Spring 整合桥接 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.x/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.x/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.x/version /dependency /dependencies版本号我没有照抄源码里的具体数字你在IDE里打开pom.xml会看到项目实际锁定的版本。注意一个关键点mybatis-spring的版本要和Spring大版本匹配如果Spring是5.x而mybatis-spring拉到1.x启动时会直接抛BeanDefinitionStoreExceptionMySQL驱动驱动则要注意8.0.x的driverClassName是com.mysql.cj.jdbc.Driver这个差异会直接影响数据源初始化是否成功。2.3 数据源的写法连接参数与编码时区SSM工程里数据库配置通常不放在Mapper XML里而是集中在Spring管理的dataSource bean中。这套源码在applicationContext.xml里定义数据源用properties文件存放url、username、password。从jdbc连接池的选型来看源码用的是Apache DBCP核心片段如下context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/notepad_db?useSSLfalseamp;characterEncodingutf-8/ property nameusername valueroot/ property namepassword value123456/ /beanurl里必须保留useSSLfalse和characterEncodingutf-8两个参数。前者避免MySQL 8版本连接时的手动SSL证书配置后者保证写入数据库的汉字不出现乱码。如果本地MySQL是8.0及以上驱动类名要换成com.mysql.cj.jdbc.Driver并且最好补上serverTimezoneAsia/Shanghai否则连接阶段会直接报时区无法识别的错误。这段配置改完之后记得检查jdbc.properties里的账号密码是否和本地环境一致这是整个项目第一个容易绊倒人的地方。2.4 SqlMapConfig与Mapper扫描MyBatis全局配置一般保持精简开启下划线转驼峰、声明类型别名包、必要时打印SQL日志。这份源码里mybatis-config.xml的常见写法如下configuration settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings typeAliases package namecom.notepad.pojo/ /typeAliases mappers package namecom.notepad.dao/ /mappers /configurationmapUnderscoreToCamelCasetrue是SSM开发里最容易被漏掉的一行配置。数据库字段create_time在没有这行配置时映射进JavaBean会得到null因为属性名是createTime开启后MyBatis自动完成下划线到驼峰转换。typeAliases里的包扫描让Mapper XML的resultType可以直接写Note而不必写全限定名。mappers包扫描则一次性加载com.notepad.dao下所有Mapper接口再配合Spring的MapperScannerConfigurerDAO层就不需要任何实现类这也是SSM工程能比传统Servlet工程少写大量代码的原因。3. 前端交互实现HTML表单、CSS样式与JavaScript事件绑定3.1 login.html与register.html表单结构与提交路径两个页面的骨架几乎一致一个form表单、两个输入框、一个提交按钮。这套源码里登录注册页走的是表单直接提交记事操作走的是JavaScript拦截后异步提交两条路径分别对应传统表单交互和Ajax交互正好覆盖了Java Web开发里的两种典型做法。看注册页的简化结构form idregisterForm actionregister methodpost label forusername用户名/label input typetext idusername nameusername placeholder请输入用户名 required/ label forpassword密码/label input typepassword idpassword namepassword placeholder请输入密码 required/ button typesubmit注册/button a hreflogin.html已有账号去登录/a /formactionregister是相对路径最终拼接成contextPath/register由Controller层的PostMapping(/register)接收。这里要明确id和name的分工id是给JavaScript和CSS用的name是浏览器提交表单时自动带上的请求参数名。SpringMVC用RequestParam(username)取值时取的是input的name值很多初学者把id和name混用导致后端拿到null先记住这一点能省很多排错时间。required属性是HTML5原生校验能挡住空字符串直接提交。3.2 CSS伪类鼠标移入和焦点状态的交互反馈搜索热词里出现的“css伪类选择器”和“css 鼠标移入事件”在这套源码的css目录里都有对应实现。整个项目只有2个CSS文件却要覆盖登录页、注册页、主界面三种页面靠的是class复用加伪类。伪类的价值在于不用写一行JavaScript就能完成交互反馈.login-btn { width: 100%; padding: 10px 0; background: #4a90d9; color: #fff; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s ease; } .login-btn:hover { background: #357abd; } .login-btn:active { transform: scale(0.98); } .input-field:focus { border-color: #4a90d9; box-shadow: 0 0 4px rgba(74, 144, 217, 0.4); outline: none; }:hover伪类实现鼠标移入按钮时背景色加深:active模拟按钮按下的微缩放效果:focus给输入框加描边提示。这套写法的性能开销几乎为零浏览器原生处理不需要JavaScript事件监听。transition属性给背景色变化加了一个0.2秒的过渡颜色不是瞬跳而是渐变视觉效果比硬切自然得多。如果需要更进一步的点击反馈还可以叠加transform属性来触发重绘。3.3 eventsControl.js把DOM事件和后端接口串起来项目里只有3个JavaScript文件eventsControl.js是核心主界面main.html上的新增、删除操作都由它接管。它的工作可以概括为监听提交动作、组装请求参数、用XMLHttpRequest发给后端Controller、拿到JSON后刷新页面。以下是新增记事对应的代码片段function saveNote() { var title document.getElementById(noteTitle).value.trim(); var content document.getElementById(noteContent).value.trim(); if (!title) { alert(标题不能为空); return; } var params title encodeURIComponent(title) content encodeURIComponent(content); var xhr new XMLHttpRequest(); xhr.open(POST, note/add, true); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.onreadystatechange function () { if (xhr.readyState 4 xhr.status 200) { var result JSON.parse(xhr.responseText); if (result.code 200) { window.location.href main.html; } else { alert(result.message); } } }; xhr.send(params); }这段代码有三个关键点。第一encodeURIComponent对输入内容编码避免标题里出现特殊字符时破坏请求体格式。第二xhr.open的url用的相对路径note/add因为静态页面和Controller在同一个contextPath下浏览器会自动补全但如果你手动改过Application context这里就要跟着改成/你的应用名/note/add。第三后端返回的JSON字符串通过JSON.parse转成对象读code字段判断业务成功还是失败——这个code字段就是后端统一封装成Result对象里的状态码。整个事件控制不依赖jQuery页面里通过onclick属性直接调用这些函数。JavaScript文件的职责可以拆成下表文件或函数职责触发时机eventsControl.js 中的 saveNote新增并保存一条记事点击保存按钮eventsControl.js 中的 deleteNote按ID删除记事点击删除按钮eventsControl.js 中的 initBind初始化页面上所有事件绑定onload事件3.4 main.html的列表渲染与字符串拼接main.html是记事本的主工作台渲染记事列表用的是最朴素的字符串拼接后端返回JSON数组前端遍历后拼成HTML字符串塞入容器节点。这种写法在数据量不大时完全够用也方便初学者看懂数据从接口到页面的完整链路function renderNotes(notes) { var list document.getElementById(noteList); var html ; for (var i 0; i notes.length; i) { var n notes[i]; html div classnote-item h3 n.title /h3 p n.content /p span classtime n.updateTime /span button onclickdeleteNote( n.id )删除/button /div; } list.innerHTML html; }字符串拼接的隐患是XSS注入如果记事内容里包含script片段会被浏览器当作脚本执行。作为教学源码这种写法胜在直观但生产环境至少应该用textContent写入文本或者在服务端对HTML标签做转义。改造时可以把innerHTML换成createElement加appendChild同时把onclick改成事件委托在noteList容器上用addEventListener统一捕获删除点击这样列表项多了之后性能也不会明显劣化。4. 数据表设计与会话控制txt.sql与登录态的Java Web实现4.1 txt.sql两张表撑起整个业务整个项目只有1个SQL文件说明数据模型极其精简。按记事本场景最合理的结构就是用户表加记事表通过外键关联这也与后端按用户隔离数据的需求对应。设计思路如下CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_note ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(100) NOT NULL, content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_note_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;用户表里username加UNIQUE约束保证注册时账号不重复password字段长度设成100而不是32说明源码里很可能对密码做了哈希处理后存储没有用明文记事表的user_id外键是数据隔离的基础查询时强制带登录用户ID避免A用户看到B用户的记事。字符集用utf8mb4而不是utf8因为utf8mb4才能完整存下四字节的emoji字符如果生产环境遇到奇怪的乱码先排查建表字符集。4.2 MyBatis的Mapper参数绑定与结果映射在com.notepad.dao包下每个Mapper接口对应一个XML文件。查询用户自己的记事列表时SQL通常按用户ID过滤并按更新时间倒序select idselectByUserId parameterTypeint resultTypeNote SELECT id, title, content, create_time, update_time FROM t_note WHERE user_id #{userId} ORDER BY update_time DESC /select参数绑定这里强烈建议只用#{}。#{}在MyBatis编译阶段会转成JDBC的PreparedStatement占位符SQL语句先预编译再传值能有效挡住SQL注入${}则是字符串直接拼接把用户输入的内容拼进SQL输入里出现OR 11时就会构成注入漏洞。resultTypeNote能直接写简写类名依赖的是mybatis-config.xml里typeAliases的包扫描。同时别忘了create_time能自动映射到createTime属性这是mapUnderscoreToCamelCase开关在起作用。4.3 Controller层接收参数、调用Service、返回JSONController负责把前端HTTP请求翻译成Java方法调用。记事相关的Controller一般放在com.notepad.controller包下典型方法如下Controller RequestMapping(/note) public class NoteController { Autowired private NoteService noteService; PostMapping(/add) ResponseBody public Result addNote(RequestParam(title) String title, RequestParam(content) String content, HttpSession session) { User user (User) session.getAttribute(LOGIN_USER); if (user null) { return Result.fail(401, 未登录); } Note note new Note(); note.setUserId(user.getId()); note.setTitle(title); note.setContent(content); noteService.addNote(note); return Result.success(); } }RequestParam(title)拿的是前端表单或Ajax请求体里title参数对应HTML里input的name属性HttpSession session直接从方法参数注入SpringMVC会自动把当前会话对象传进来。方法第一件事就是检查session里有没有LOGIN_USER没有就返回一个带code401的JSON这一步是应用数据安全的第一道闸门。Result是统一返回对象封装code、message、data三个字段前端JavaScript里读到的result.code就来自这里。Service层在这套简单场景里主要承担事务边界的职责可以先用Service加Transactional包住写入操作。4.4 登录态保持Session与拦截器登录成功后Controller把用户对象放进session浏览器靠Cookie里携带的JSESSIONID维持会话。单纯在每个方法里显式检查session也能凑合但更规范的做法是抽一个拦截器所有需要登录的请求在进入Controller前统一校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(LOGIN_USER); if (user null) { response.sendRedirect(request.getContextPath() /login.html); return false; } return true; } }这段代码在spring-mvc.xml里注册并用mvc:exclude-mappings把/login、/register这些公开接口排除掉剩下的/note/**全部收进拦截范围。拦截器的另一个作用是收敛会话逻辑以后要改成Redis保存session或接入Spring Security只需要改这个类和注册配置不用动各Controller方法。注意前端在Ajax请求里遇到sendRedirect时不会自动跳转页面而是拿到一个HTML段落所以更实践的做法是拦截器里用response.getWriter().write(JSON)返回401的JSON让前端在onreadystatechange里根据code做跳转这样前后端的会话失效处理才算闭环。5. 从启动到改造记事本项目的部署验证与二次开发技巧5.1 用IDEA把项目跑起来导入时不要直接选中ssm - 副本文件夹Open建议在IDEA的Welcome界面选择Import Project定位到pom.xml以Maven工程导入。依赖下载完成后配置TomcatRun → Edit Configurations → 新增Tomcat Server LocalDeployment页签里添加Artifact类型选war explodedApplication context填/notepad。保存后用本地MySQL先执行txt.sql建库建表再改jdbc.properties里的账号密码最后启动Tomcat。注意数据源连不上时不要急着怀疑代码。先确认MySQL服务已启动再用命令行mysql -uroot -p测试账号密码。SSM项目最常犯的错误是URL里的数据库名和实际库名不一致导致连接成功但表找不到。5.2 启动失败的高频故障点症状排查方向启动直接报ClassNotFoundMaven依赖没下载全先执行mvn clean compile看哪个包缺失页面能打开登录后500看IDEA控制台Java异常堆栈优先查数据源和Mapper XML路径连不上数据库核对jdbc.properties的url、username、password以及MySQL端口登录成功但no such user重启Tomcat或确认fastjson是否已引入样式和JS全部失效访问路径里多了contextPath前端资源改为相对路径或加base标签页面能打开但接口返回404时最佳调试路径是打开浏览器Network面板复制实际请求URL跟Controller层的RequestMapping路径比对。这个项目里最容易踩的坑就是contextPath不对齐前端相对路径note/add实际URL可能变成/notepad/note/add差一个应用名前缀就406或404。5.3 二次开发给记事本加一个分类标签介绍一个不伤筋动骨的改造路径在记事表上新增category字段走一遍完整的SSM调用链。先执行ALTER TABLE t_note ADD COLUMN category VARCHAR(20) DEFAULT 默认;然后在NoteMapper接口里加一个按分类查询的方法XML里写对应的update或select语句。第二步在Note实体类加category属性Controller的addNote方法增加RequestParam(valuecategory, requiredfalse)入参Service层把这个值正常透传给Mapper。第三步在main.html的表单区域加一个select下拉框saveNote函数里把选中的分类拼进请求参数。第四步在列表渲染函数里把分类显示出来。四个步骤覆盖SQL、MyBatis映射、Controller参数、JavaScript交互四个层级改完刚好把SSM完整调用链复习一遍。注意下拉框的value属性存分类名后端用String category接收不要用Integer以免空值和类型转换问题。改动完成后重启Tomcat打开main.html新建一条记事看数据库的category字段是否按预期写入就能验证一整条链路是否真的打通。本文还有配套的精品资源点击获取