ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java实现记账本项目:从数据库设计到部署避坑全指南

2026/10/7 4:30:34 拓冰建站 浏览量
Java实现记账本项目:从数据库设计到部署避坑全指南 简介基于Java实现的小小记账本项目是一套完整的个人财务管理应用源码面向Java初学者及需要GUI与数据库集成实践的开发者。项目采用J2SE技术栈利用Swing构建交互界面SQLite负责数据存储实现了收支记录、分类管理与统计展示等核心功能适合作为课程设计或入门练手项目。资源包含173个文件压缩包约29.45MB主要文件类型有java源码、class编译文件、jar依赖库以及png图标、xml配置和db数据库文件便于直接运行与二次开发。目前已有744人浏览学习项目注释较完整可通过源码学习Swing组件与数据库操作的结合、用户输入验证及异常处理方式。此外资源还展示了将项目打包成可执行jar文件的方法体现Java跨平台特性核心DAO类、面板类与工具类的组织方式也能帮助读者掌握分层设计与事件监听等实用技能。1. 基于java实现的小小记账本项目看起来简单为什么很多新手照样翻车打开招聘网站的Java岗位要求十条里有八条写着“有项目经验”。可对刚学完Java基础的人来说第一个能完整跑起来的项目选什么最合适我的答案一直是记账本。它不像电商那样涉及分布式、消息队列业务规则简单到一天能捋完但它的技术覆盖面恰好卡在“会写CRUD”和“能独立做小工具”的分界线上。用Java实现一个记账本既能验证你对面向对象编程的掌握又能练习JDBC、MyBatis或Spring Boot等真实开发环境里的常用库函数。本文就拿这样一个小项目从数据库设计讲到避坑再讲到怎么把它变成面试作品或参加比如蓝桥杯一类比赛的练手项目。2. 先把地基打牢技术选型与数据模型设计的3个决策点2.1 用Spring Boot还是纯Servlet两条路线的取舍我见过不少人用Servlet JSP写记账本理由是“这样更能理解底层原理”。这话对了一半。如果你准备面试纯Servlet确实能让面试官确认你懂HTTP请求的生命周期、Filter和Session但从投入产出比看Spring Boot MyBatis Plus才是java工程师日常工作的主流配置而且热词里经常出现“spring boot mybatis 的 java 开源多商户跨境商城源码下载”这类需求说明市场对这套组合的认可度很高。我的建议是分情况。如果是课程设计选纯Servlet写容易讲清楚原理如果是自己练手或准备面试直接上Spring Boot。但无论哪条路线架子都别搭太复杂。记账本的核心就三件事记录一笔收支、查历史账单、按分类或月份统计。不要一上来就引入Redis、MQ那是把项目做歪了。如果你选Spring Boot版本用2.x稳定线即可3.x也不是不行但Spring Boot 3要求JDK 17不少人的机器上还跑着JDK 8环境这关就卡住了。我一般用Spring Boot 2.7 JDK 8的组合兼容性最好排查问题的资料也最全。2.2 数据模型收支分类与金额字段的选型先想清楚记账本的表结构。最基本的是一张账单表但我不建议只有一张表而是加一张分类表。想象一下你记了一笔“早餐 - 15元”如果直接用字符串存分类名等你想统计“这个月早餐花了多少”时SQL里就得写死字符串匹配。加个分类表把“早午晚餐”“交通”“购物”“工资”等收入支出类型统一管理统计和扩展都轻松。类别表结构大概是这样CREATE TABLE t_category ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, type TINYINT NOT NULL COMMENT 类型1-支出2-收入, name VARCHAR(50) NOT NULL COMMENT 分类名称, user_id BIGINT NOT NULL COMMENT 所属用户 ) COMMENT 分类表;注意这里有个小细节分类要冗余一个user_id。虽然demo阶段可能只有一个用户但加上这个字段后整套代码天然支持多用户后面接登录逻辑时不用改表。账单表CREATE TABLE t_bill ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, category_id BIGINT NOT NULL COMMENT 分类ID, amount DECIMAL(10,2) NOT NULL COMMENT 金额, remark VARCHAR(255) COMMENT 备注, user_id BIGINT NOT NULL COMMENT 所属用户, bill_date DATE NOT NULL COMMENT 记账日期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) COMMENT 账单表;金额字段用DECIMAL(10,2)这一点必须养成习惯。Java里float和double在二进制下无法精确表示小数哪怕你只是在做记账本长期累计也会出现0.999999这种诡异结果。项目里的金额、余额、统计汇总全部用BigDecimal对应数据库的DECIMAL。2.3 实体的定义与MyBatis Plus的约定Java实体类直接用MyBatis Plus风格写。表名和字段都带下划线实体类用驼峰MyBatis Plus默认开启了下划线转驼峰映射省去一堆TableField注解。一个常见做法是定义BaseEntity把主键、创建时间、更新时间抽出来Data public class BaseEntity { TableId(type IdType.AUTO) private Long id; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }对应的Bill实体Data EqualsAndHashCode(callSuper true) TableName(t_bill) public class Bill extends BaseEntity { private Long categoryId; private BigDecimal amount; private String remark; private Long userId; private LocalDate billDate; }这里createTime和updateTime用自动填充需要实现MyBatis Plus的MetaObjectHandler。如果你没配这个类insert时会发现时间字段永远是null。这是新手常踩的坑后面避坑章还会细说。MyBatis Plus的Mapper层比传统MyBatis省事很多基础CRUD不需要手写XMLpublic interface BillMapper extends BaseMapperBill { }到此一个可运行的最小数据模型就算立住了。不要急着写业务代码先拿一个测试类验证Mapper能不能正常insert、select确认环境没问题再往前走。3. 核心功能实现记账、分页查询与统计聚合3.1 记账流程从Controller到Mapper的一条完整链路记账本最核心的功能是“记一笔”。Controller接收前端传来的分类ID、金额、备注、记账日期组装成Bill对象后调Service落库。这里不要直接在Controller里写业务逻辑哪怕再简单也走Service层这是java项目里最基本的约定。RestController RequestMapping(/api/bill) public class BillController { Resource private BillService billService; PostMapping(/add) public ResultString add(RequestBody BillAddDTO dto) { billService.addBill(dto); return Result.success(记账成功); } }DTO和实体分离的意义在于DTO只需要接收前端必填字段而实体的userId、createTime等字段不应该由前端指定。Service层做校验、补齐字段Service public class BillServiceImpl implements BillService { Resource private BillMapper billMapper; Resource private CategoryMapper categoryMapper; Override public void addBill(BillAddDTO dto) { // 校验分类是否存在 Category category categoryMapper.selectById(dto.getCategoryId()); if (category null) { throw new BizException(分类不存在); } Bill bill new Bill(); bill.setCategoryId(dto.getCategoryId()); bill.setAmount(dto.getAmount()); bill.setRemark(dto.getRemark()); bill.setBillDate(dto.getBillDate()); bill.setUserId(1L); // 当前登录用户 billMapper.insert(bill); } }这段逻辑的关键点在于Service层做业务校验、实体字段填充、以及最终落库。Controller保持轻量Mapper只负责SQL。3.2 分页查询MyBatis Plus分页插件与排序账单会越记越多一次性查全量列表会让页面越来越慢。MyBatis Plus的分页插件是PaginationInnerInterceptor需要先配一个配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个配置你用Page对象查询会发现SQL里根本没有LIMIT照样全表捞数据。配置完成后分页查询这样写Override public PageResultBillVO pageQuery(Integer pageNum, Integer pageSize, Long categoryId, LocalDate startDate, LocalDate endDate) { LambdaQueryWrapperBill wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Bill::getCategoryId, categoryId) .ge(startDate ! null, Bill::getBillDate, startDate) .le(endDate ! null, Bill::getBillDate, endDate) .orderByDesc(Bill::getBillDate) .orderByDesc(Bill::getId); PageBill page billMapper.selectPage(new Page(pageNum, pageSize), wrapper); // 把实体转成VO拼接分类名称等展示字段 return PageResult.of(page); }很多新人会踩两个坑一是条件构造用字符串硬拼虽然能跑但容易SQL注入二是排序不稳定同一天有多笔账单时如果不加id作为第二排序翻页时数据顺序可能会乱。把实体转成VO这一步容易被忽略。实体里的categoryId只是数字前端展示需要分类名称所以查询后要批量查出分类名再组装。不要逐条查分类那样N1条SQL会把接口拖慢。3.3 分组统计与汇总从Java代码到SQL的取舍记账本最常见的统计需求就是“这个月花了多少钱、花在哪些分类”常见写法是查询全部账单在Java内存里分组汇总。这个做法在小数据量时没问题但账单上万条之后内存和响应速度都不好看。直接交给SQL聚合更合理。Override public ListCategoryStatVO sumByCategory(Long userId, LocalDate monthStart, LocalDate monthEnd) { // 直接使用Mapper层自定义SQL return billMapper.sumByCategory(userId, monthStart, monthEnd); }对应的Mapper方法不需要写XML用注解就行。如果是复杂SQL我建议写XML更清晰。这里给个常见做法select idsumByCategory resultTypecom.example.vo.CategoryStatVO SELECT c.name AS categoryName, SUM(b.amount) AS totalAmount, COUNT(*) AS billCount FROM t_bill b INNER JOIN t_category c ON b.category_id c.id WHERE b.user_id #{userId} AND b.bill_date BETWEEN #{startDate} AND #{endDate} GROUP BY c.id, c.name ORDER BY totalAmount DESC /select执行统计时碰到的第一个坑是时间范围。前端传“2024-06-01 到 2024-06-30”SQL里的BETWEEN在日期类型上没问题但如果bill_date字段含时分秒就要注意右边界匹配问题。这里将bill_date设置成date类型BETWEEN可以直接用如果不是建议endDate加一天再查用b.bill_date gt; #{startDate} AND b.bill_date lt; #{endDate}这种左闭右开的写法避免丢数据。4. 把项目跑起来前端页面、环境变量配置与数据库切换4.1 用Thymeleaf Bootstrap搭一个最小页面很多Java开发只写后端接口前端丢给同事或直接不做。但一个记账本至少要有个能录入、能查看的页面否则面试时不好演示。用Thymeleaf模板不搞前后端分离是最快的路线。页面主要就两个记账表单页和账单列表页。记账页拿到分类列表渲染下拉框提交时用jQuery的Ajax调接口$(#addForm).on(submit, function(e) { e.preventDefault(); $.ajax({ url: /api/bill/add, method: POST, contentType: application/json, data: JSON.stringify({ categoryId: $(#categoryId).val(), amount: $(#amount).val(), remark: $(#remark).val(), billDate: $(#billDate).val() }), success: function(res) { if (res.code 200) { alert(记账成功); location.reload(); } else { alert(res.msg); } } }); });Thymeleaf的好处是Controller返回字符串时能直接渲染模板不需要跨域配置也不用启动前端工程。注意别把接口路径写错Controller前缀加的是/api页面请求路径也要带上。4.2 金额校验、日期选择与前端拦截金额输入框是个玄学重灾区。用户什么都能给你填进来负数、小数点后三位、字符串。前端要做校验后端更不能裸奔。前端用简单的JS校验var amount $(#amount).val(); if (!/^[0-9](\.[0-9]{1,2})?$/.test(amount)) { alert(金额格式不正确最多两位小数); return; }后端同样要校验因为绕过前端直接调接口这种事太常见了。用Spring的Validation注解最省事。DTO里这样标public class BillAddDTO { NotNull(message 分类不能为空) private Long categoryId; NotNull(message 金额不能为空) DecimalMin(value 0.01, message 金额必须大于0) Digits(integer 8, fraction 2, message 金额最多两位小数) private BigDecimal amount; NotBlank(message 备注不能为空) Size(max 100, message 备注不能超过100字) private String remark; NotNull(message 记账日期不能为空) JsonFormat(pattern yyyy-MM-dd) private LocalDate billDate; }金额类型在Java里对应BigDecimal序列化时要注意如果前端传来的是字符串Jackson通常能自动转。日期字段一般用LocalDate如果前端传的是2024-06-01符合ISO标准可以顺利转成LocalDate。注意如果Controller的入参没加Valid注解这些校验规则不会生效。日期选择器我一般直接改input标签的type为date浏览器原生就能弹日历。这个在移动端可能有点不一样但做demo够用了。4.3 java环境变量配置与H2切换MySQL的方法许多人卡在这个项目的第一个问题不是代码而是java环境变量配置。千言万语汇成三步安装JDK设置JAVA_HOME指向JDK目录把%JAVA_HOME%\bin加入PATH。难住很多人的是装了多个JDK版本比如同时有JDK 8和17命令行里java -version显示的版本和自己预期的不一样。把Path里靠前的java.exe路径去掉或者直接删掉系统变量里其他版本的JAVA_HOME就能消除这个混乱。数据库方面为了零配置跑通application.yml里先配H2内存数据库spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:bookdb;DB_CLOSE_DELAY-1 username: sa password: h2: console: enabled: trueH2的好处是下载依赖就能跑不需要装MySQL非常适合起步。但发展到需要长期保存、积累几百条真实账单时H2内存库就扛不住了。这时平滑切到MySQL主要是三处改动spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/bookdb?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码切换后最大的问题通常是时区。不写serverTimezone参数MySQL 8的驱动会拿不到正确的连接。而装了MySQL 5.x的人可能碰到驱动名不一样的问题MySQL 5要用com.mysql.jdbc.DriverMySQL 8用com.mysql.cj.jdbc.Driver。先确认本机版本再填多数人卡在这个小地方半天。建表SQL用之前设计的t_category和t_bill建一遍再跑一次插入验证配置无误。5. 避坑指南记账本项目最常见的5个坑与排查办法5.1 时间字段填充失败insert后create_time为null现象调用billMapper.insert()插入成功但数据库里create_time字段是空的。原因多数情况下是忘了配置MetaObjectHandler或者配置了但TableField里的fill属性没写对。MyBatis Plus只会在fill值为INSERT或INSERT_UPDATE时触发自动填充。解决写一个组件实现MetaObjectHandler接口Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }注意如果实体类里字段名和数据库字段名不一样比如Java里叫createTime数据库叫create_timeMyBatis Plus能自动映射但strictInsertFill的第一个参数必须写实体字段名createTime不是数据库列名。这个细节很容易出错建议核对一遍。5.2 BigDecimal直接接收前端的字符串导致精度异常现象前端传15.10后端拿到的金额变成了15.1网页上显示“15.1”跟原始输入不一致。或者更糟传0.10.2保存后查询变成0.30000000000000004。原因虽然BigDecimal能精确表示小数但如果用构造函数传double例如new BigDecimal(0.1)得到的其实是二进制近似值用new BigDecimal(0.1)才是精确的。JSON反序列化时Jackson通常会用字符串构造所以问题主要出在手动new BigDecimal的场景。解决所有从外部传入的金额统一走字符串。如果从DTO直接绑定到BigDecimal字段Jackson会自己处理成字符串构造。需要做运算时调用add、subtract这些方法不要用号。5.3 MySQL时区问题配置了serverTimezone依然报错现象应用启动时报Could not create connection to database server错误信息里出现Public Key Retrieval is not allowed或时区相关字样。原因MySQL 8驱动默认要求建立安全连接而本机连接串里没指定allowPublicKeyRetrieval或者服务器时区未设置。解决常见的做法是把连接URL补全jdbc:mysql://localhost:3306/bookdb?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue还有一个容易忽略的点本地MySQL如果装在Windows上系统时区本身如果是UTC也会出现查询结果和实际时间差8小时。把serverTimezone显式写成Asia/Shanghai是治本的办法。5.4 MyBatis Plus分页失效SQL里没有LIMIT现象上一章写了分页插件的Configuration配置类但项目启动后分页查询查出了所有数据控制台打印SQL发现没有LIMIT关键字。原因分页插件没被加载。常见的是Spring Boot 3 MyBatis Plus 3.4以下版本组合不兼容或者MybatisPlusConfig类没被扫描到。另一个新手常见的错误是建了配置类但没加Configuration注解方法上的Bean也不会生效。解决检查启动类所在包与配置类所在包的关系确保配置类在启动类扫描范围内。再确认MyBatis Plus的版本与Spring Boot版本兼容。3.5.3以上的MyBatis Plus对Spring Boot 2.x支持稳定Spring Boot 3建议使用3.5.3及以上版本。5.5 分类表SQL主键策略导致插入失败现象t_category插入数据报主键冲突或者插入成功后ID不是自增的。原因MyBatis Plus默认的主键生成策略是ASSIGN_ID跟数据库自增主键不搭。尤其当你有代码里手动设置ID的习惯时会互相干扰。解决实体上直接在TableId注解明确指定自增策略TableId(type IdType.AUTO) private Long id;这样无论在MySQL还是H2中都能正确获取自增主键。如果不指定MyBatis Plus会用雪花算法生成ID数据库主键类型如果设置为BIGINT AUTO_INCREMENT倒是也能插入但ID变成一堆数字调试时很不舒服。5.6 忘记写Valid导致后端校验全失效现象明明在BillAddDTO上加了NotNull、DecimalMin注解但传空值时后端没有报错照样插入了数据库里。原因Controller方法入参需要加Valid注解校验注解才会生效。Spring Boot不会自动启用校验。解决把Controller方法的入参改成RequestBody Valid BillAddDTO dto。常见的写法是PostMapping(/add) public ResultString add(RequestBody Valid BillAddDTO dto) { billService.addBill(dto); return Result.success(记账成功); }之后如果校验还是没生效检查pom.xml里是否引入了spring-boot-starter-validation依赖。Spring Boot 2.7以上这个依赖不再由web starter自动带要显式添加。6. 把记账本升级成拿得出手的作品导出CSV与部署检查清单基础的增删改查做完以后记账本充其量是个练习册。想让它进入“作品”的行列至少要再补两件事一是导出功能二是打包部署。导出CSV是最快见效的因为记账数据本质上是表格用户想把账单导出到Excel里二次处理是高频需求。导出接口用HttpServletResponse写文件流记得设置响应头GetMapping(/export) public void export(HttpServletResponse response, Long userId, LocalDate startDate, LocalDate endDate) throws IOException { ListBillVO list billService.queryList(userId, startDate, endDate); response.setContentType(text/csv;charsetutf-8); response.setCharacterEncoding(UTF-8); String fileName URLEncoder.encode(账单导出.csv, UTF-8); response.setHeader(Content-Disposition, attachment;filename fileName); // 写表头和每行数据 StringBuilder sb new StringBuilder(); sb.append(分类,金额,备注,日期\n); for (BillVO vo : list) { sb.append(vo.getCategoryName()).append(,) .append(vo.getAmount()).append(,) .append(vo.getRemark()).append(,) .append(vo.getBillDate()).append(\n); } response.getWriter().write(sb.toString()); response.getWriter().flush(); }这个实现里藏着三个容易翻车的点一是文件名中文乱码需要URLEncoder编码二是CSV里的金额如果直接拼接BigDecimalExcel打开时会变成科学计数法可以先把金额转成字符串再用三是备注里如果本身有逗号CSV会被破坏需要给备注字段加引号转义。常见做法是把字段值用双引号包起来内部的双引号再转义成两个双引号。关于项目的安全加固可以顺手做一个针对防爬虫的Controller层防护——比如简单判断User-Agent或者加一个请求频率限制的拦截器这个作为加分项写进README里能让面试官眼前一亮。再来说部署。项目打成jar包命令行直接java -jar bookkeeping.jar启动前提是确保环境变量配置正确。之前提到的java环境变量配置如果没搞好这里就会启动失败。Linux服务器上部署我一般打成tar包把jar、启动脚本、配置文件放在一起tar -czf bookkeeping.tar.gz bookkeeping.jar start.sh application.ymlstart.sh里先检查JAVA_HOME是否存在再启动Java进程#!/bin/bash export JAVA_HOME/usr/local/jdk1.8 export PATH$JAVA_HOME/bin:$PATH nohup java -Xms256m -Xmx512m -jar bookkeeping.jar --spring.config.locationapplication.yml nohup.out 21 启动后先看端口是否监听再访问接口。如果服务没起来多半是配置文件的数据库连接串写错了登录服务器排错的命令基本是先看进程、再看磁盘和内存、最后看日志。这恰恰是Java程序员的基本功远比代码本身更值钱。我自己的教训是一直在本地跑H2内存库从不连MySQL到了真机上才发现连接串和时区问题一大堆折腾到半夜。后来养成了一个习惯真机部署前一定先本机连MySQL跑一遍全流程。这个习惯帮我避开了很多次翻车。希望帮到你。本文还有配套的精品资源点击获取