简介:本资源是一套基于JavaWeb开发的金融借贷系统(P2P小额贷款管理平台),专为计算机相关专业本科生毕业设计及Java初学者项目实战打造,覆盖融资产品管理、贷款申请全流程、新闻资讯发布等核心业务场景,兼顾前台用户交互与后台高效管控。压缩包共15个文件,含10张关键界面截图(如前台主页、融资详情、后台管理、新闻管理等)、1份PDF项目文档、1份MD格式说明、1个SQL数据库脚本、1个ZIP源码包及1个工具下载指引TXT,总大小14.67MB,结构清晰、即解即用。已有2536人学习下载,资源经严格调试可直接运行,配套完整:源码+文档+数据库+工具+多维度运行截图,助读者快速理解MVC分层实现、Bootstrap响应式前端、JDBC数据操作及前后端异步交互逻辑,是掌握JavaWeb全栈开发落地的典型教学案例。
1. 这不是又一个“用户登录+增删改查”的JavaWeb毕设:它得真能模拟借贷生命周期、扛住压力测试、过得了答辩老师数据库设计三连问
你手里的这个“基于JavaWeb实现的金融借贷系统【附源码】(毕设)”,如果还停留在用JSP写个登录页、Servlet处理表单、MySQL建三张表(user、loan、repayment)就交差,那答辩现场大概率会被老师一句“请解释下逾期罚息是如何按日复利计算并同步更新账务状态的?”直接问哑火。这不是考你能不能把<form action="/addLoan">写对,而是考你有没有把真实金融业务逻辑的严谨性、状态流转的不可逆性、资金流水的幂等性,用JavaWeb这一套看似“过时”实则极考验基本功的技术栈扎扎实实落地出来。它适合两类人:一类是想靠毕设项目真正补上企业级Web开发断层(比如事务边界怎么划、并发放款怎么防超贷、还款计划怎么生成不漂移)的学生;另一类是带毕设指导任务的讲师,需要一套结构清晰、边界明确、有真实业务钩子(如年化利率APR计算、逾期等级划分、催收状态机)的参考骨架。别被“JavaWeb”字面吓退——它没用Spring Boot自动装配的糖衣,反而逼你亲手拧紧每一个螺丝:从Tomcat线程池配置到MyBatis动态SQL防SQL注入,从JSP EL表达式校验前端金额格式到Filter拦截未授权的资金操作请求。附的源码不是玩具,是能跑通“用户注册→实名认证→授信评估→发起借款→风控审核→放款到账→按期还款→逾期催收”全链路的最小可行体。
2. 从零搭起金融级Web骨架:选型不是堆技术,而是为钱流安全埋伏笔
2.1 为什么坚持用原生JavaWeb而非Spring Boot?三个硬约束倒逼的选择
很多同学看到“JavaWeb”第一反应是“太老了”,急着换成Spring Boot。但当你真要模拟金融场景,会发现原生栈反而更可控:
- 事务粒度必须精确到方法级:Spring Boot默认的
@Transactional在Service层加注解,一旦方法里混入异步通知(如还款成功发短信),事务可能提前提交导致资金状态不一致。而原生Servlet+JDBC手动控制Connection.setAutoCommit(false),你能把“扣减可用额度+生成还款记录+更新合同状态”这三步死死锁在一个物理连接里,出错就rollback(),绝不留半截脏数据。 - HTTP请求链路必须全程可审计:金融操作要求每笔请求带唯一traceId、记录IP、设备指纹、操作时间戳。Spring Boot的拦截器(Interceptor)在DispatcherServlet之后才生效,而原生Filter在容器最外层,连404错误都能捕获并落库,确保“谁在什么时间、从哪台机器、调了哪个接口、传了什么参数、返回了什么结果”一条不漏。
- 部署包体积与启动速度关乎答辩演示:Spring Boot打包后动辄80MB+,Tomcat启动要40秒;而纯JavaWeb WAR包压缩后不到15MB,冷启动7秒内完成。答辩现场老师说“我们看下实时放款流程”,你点开浏览器就能秒进,比等Boot刷新Classloader强十倍。
提示:这不是反对Spring Boot,而是强调——毕设场景下,可控性 > 开发速度,可追溯性 > 代码行数,演示稳定性 > 技术新鲜感。源码里所有DAO层都用
try-with-resources管理Connection,所有Service方法开头必写LogUtil.logRequest(request, "loanApply"),这就是金融系统的呼吸感。
2.2 技术栈组合:每个组件都在为资金安全让路
| 组件 | 版本 | 关键配置/改造点 | 为什么选它 |
|---|---|---|---|
| Web容器 | Tomcat 9.0.83 | server.xml中<Connector>启用URIEncoding="UTF-8"+maxThreads="200" | 防止中文借款人姓名乱码;200线程足够应付答辩演示并发(50人同时点“申请借款”) |
| 数据库 | MySQL 8.0.33 | 开启innodb_strict_mode=ON+sql_mode=STRICT_TRANS_TABLES | 强制字段非空、精度校验,避免INSERT INTO loan(amount) VALUES('')这种致命空值 |
| 持久层 | MyBatis 3.4.6 | 所有<insert>标签加useGeneratedKeys="true"+keyProperty="id" | 确保主键自增后立即回填,避免“生成订单号”和“插入数据库”两步分离导致ID丢失 |
| 前端 | Bootstrap 4.6 | 所有金额输入框用<input type="number" step="0.01" min="1000" max="500000"> | 原生HTML5限制小数位、范围,比JS校验更可靠(防绕过) |
| 日志 | Log4j2 2.17.1 | log4j2.xml中<RollingFile>配置filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz" | 按天压缩归档,防止答辩前日志撑爆磁盘导致Tomcat无法启动 |
2.3 数据库设计:三张核心表如何承载金融语义
金融系统最怕“看着像那么回事,一细究全是漏洞”。源码中loan(借款合同)、repayment_plan(还款计划)、transaction_log(交易流水)三张表的设计,直击毕设常见硬伤:
-- 表1:loan - 合同表(不是简单存个金额,而是锁定法律效力) CREATE TABLE `loan` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `contract_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '合同号:L2024052000001', `user_id` BIGINT NOT NULL COMMENT '借款人ID', `amount` DECIMAL(12,2) NOT NULL COMMENT '借款本金(元)', `annual_rate` DECIMAL(5,3) NOT NULL COMMENT '年化利率(%),如15.600', `term_months` TINYINT NOT NULL COMMENT '期限(月)', `status` ENUM('DRAFT','APPROVED','DISBURSED','REPAID','OVERDUE','WRITE_OFF') NOT NULL DEFAULT 'DRAFT', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX `idx_user_status` (`user_id`, `status`) -- 按用户查合同状态,高频查询 ); -- 表2:repayment_plan - 还款计划表(必须支持等额本息/先息后本两种算法) CREATE TABLE `repayment_plan` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `loan_id` BIGINT NOT NULL COMMENT '关联合同ID', `period` TINYINT NOT NULL COMMENT '第几期(1,2,3...)', `due_date` DATE NOT NULL COMMENT '应还日期', `principal` DECIMAL(12,2) NOT NULL COMMENT '本期应还本金', `interest` DECIMAL(12,2) NOT NULL COMMENT '本期应还利息', `total_amount` DECIMAL(12,2) NOT NULL COMMENT '本期应还总额 = principal + interest', `actual_repay_date` DATE NULL COMMENT '实际还款日期(为空=未还)', `status` ENUM('NOT_DUE','DUE','REPAID','OVERDUE') NOT NULL DEFAULT 'NOT_DUE', UNIQUE KEY `uk_loan_period` (`loan_id`, `period`), FOREIGN KEY (`loan_id`) REFERENCES `loan`(`id`) ON DELETE CASCADE ); -- 表3:transaction_log - 交易流水表(每一笔钱的来龙去脉必须可追溯) CREATE TABLE `transaction_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `trans_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '交易流水号:TR2024052000001', `loan_id` BIGINT NOT NULL COMMENT '关联合同ID', `type` ENUM('DISBURSEMENT','REPAYMENT','PENALTY','FEE') NOT NULL COMMENT '交易类型', `amount` DECIMAL(12,2) NOT NULL COMMENT '交易金额', `balance_after` DECIMAL(12,2) NOT NULL COMMENT '交易后账户余额(用于对账)', `operator` VARCHAR(50) NOT NULL COMMENT '操作人(system/agent/user)', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_loan_type` (`loan_id`, `type`) );关键设计逻辑说明:
loan.status用ENUM而非INT,杜绝status=999这种非法值;DISBURSED(已放款)和REPAID(已结清)状态不可逆,代码里所有状态变更都走update loan set status=? where id=? and status in ('APPROVED'),用WHERE条件锁住前置状态,防并发误操作。repayment_plan中principal和interest分开存储,不是只存total_amount。因为逾期时罚息只针对未还本金计息,利息部分不产生罚息——这是金融常识,也是答辩老师必问点。transaction_log.balance_after字段是灵魂:每次还款后,系统不是简单UPDATE loan SET amount = amount - ?,而是先查当前balance_after(上一笔流水后的余额),再计算新余额,最后插入新流水。这样即使网络抖动导致重复提交,流水号唯一约束会报错,而不是让金额被扣两次。
3. 核心业务落地:把“申请借款”按钮点下去,背后发生了什么
3.1 借款申请:从前端校验到后端风控的七层过滤
用户点击“立即借款”按钮,你以为只是<form>提交?不,这是七道关卡的联合审查:
- 前端HTML5原生校验:
<input name="amount" required min="1000" max="500000" step="1000">—— 浏览器直接拦截非法输入,省去一次HTTP往返。 - 前端JavaScript二次校验:检查年化利率是否在
[10.000, 24.000]区间(中国法律红线),用正则/^\d{1,3}(\.\d{1,3})?$/验证小数位。 - Servlet Filter全局拦截:
AuthFilter检查Session中user.id是否存在且user.real_name != null(未实名不能借款)。 - Controller参数绑定校验:
LoanApplyForm对象用@Min(1000) @Max(500000) @DecimalMin("10.000") @DecimalMax("24.000")注解,BindingResult捕获错误。 - Service层业务规则校验:
// 检查用户当前是否有未结清借款(防多头借贷) long activeLoans = loanDao.countByUserIdAndStatus(userId, Arrays.asList("APPROVED", "DISBURSED", "OVERDUE")); if (activeLoans > 0) { throw new BusinessException("您已有未结清借款,请先还款"); } // 检查本次申请金额是否超过授信额度(额度存在user.credit_limit字段) if (applyAmount > user.getCreditLimit()) { throw new BusinessException("申请金额超出您的授信额度"); } - 数据库唯一约束兜底:
loan.contract_no设为UNIQUE,即使并发申请,数据库会拒绝重复合同号插入。 - 异步风控回调确认:调用模拟风控接口(
RiskEngineClient.check(userId, applyAmount)),返回{"pass": true, "score": 782}才进入下一步。
注意:所有校验失败都返回
ResponseEntity.badRequest().body("错误信息"),绝不暴露数据库字段名或内部异常堆栈——这是金融系统安全底线。
3.2 还款计划生成:等额本息算法的手动实现(不依赖任何工具类)
很多毕设用BigDecimal随便除一下就完事,结果算出来的每期利息总和对不上总利息。源码中RepaymentPlanGenerator.generate()严格按央行标准公式实现:
public class RepaymentPlanGenerator { /** * 等额本息计算:每月还款额 = [贷款本金×月利率×(1+月利率)^还款月数]÷[(1+月利率)^还款月数-1] * 本金余额逐期递减,利息 = 本金余额 × 月利率 */ public static List<RepaymentPlan> generateEqualPrincipalInterest( BigDecimal principal, BigDecimal annualRate, int termMonths) { BigDecimal monthlyRate = annualRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP) .divide(new BigDecimal("12"), 6, RoundingMode.HALF_UP); // 月利率,保留6位小数防精度丢失 BigDecimal onePlusRate = BigDecimal.ONE.add(monthlyRate); BigDecimal power = pow(onePlusRate, termMonths); // (1+月利率)^期数 BigDecimal denominator = power.subtract(BigDecimal.ONE); BigDecimal numerator = principal.multiply(monthlyRate).multiply(power); BigDecimal monthlyPayment = numerator.divide(denominator, 2, RoundingMode.HALF_UP); // 每期还款额,保留2位 List<RepaymentPlan> plans = new ArrayList<>(); BigDecimal remainingPrincipal = principal; for (int period = 1; period <= termMonths; period++) { BigDecimal interest = remainingPrincipal.multiply(monthlyRate).setScale(2, RoundingMode.HALF_UP); BigDecimal principalThisPeriod = monthlyPayment.subtract(interest).setScale(2, RoundingMode.HALF_UP); // 修正最后一期本金,避免因四舍五入导致剩余本金不为0 if (period == termMonths) { principalThisPeriod = remainingPrincipal; interest = monthlyPayment.subtract(principalThisPeriod); } remainingPrincipal = remainingPrincipal.subtract(principalThisPeriod); plans.add(new RepaymentPlan(period, calculateDueDate(period), principalThisPeriod, interest, monthlyPayment)); } return plans; } private static BigDecimal pow(BigDecimal base, int exp) { BigDecimal result = BigDecimal.ONE; for (int i = 0; i < exp; i++) { result = result.multiply(base).setScale(10, RoundingMode.HALF_UP); } return result; } }参数说明:
principal:借款本金,单位元,DECIMAL(12,2)类型,避免float精度丢失;annualRate:年化利率,如15.600,传入前已由前端保证三位小数;termMonths:期限月数,TINYINT,最大120(10年),防整数溢出;setScale(2, RoundingMode.HALF_UP):所有金额运算强制保留2位小数,四舍五入(金融行业标准);- 最后一期本金强制设为
remainingPrincipal:这是关键!否则因浮点累加误差,12期后remainingPrincipal可能剩0.01元,导致坏账。
3.3 放款与还款:资金流水的原子性保障
“放款到账”不是UPDATE user SET balance = balance + ?一行SQL搞定。源码中DisbursementService.disburse()采用显式事务+双重检查:
public void disburse(Long loanId) throws SQLException { Connection conn = null; PreparedStatement ps = null; try { conn = DataSourceUtil.getConnection(); // 从Druid连接池获取 conn.setAutoCommit(false); // 关闭自动提交 // 步骤1:检查合同状态是否为APPROVED(风控已通过) Loan loan = loanDao.findById(loanId, conn); if (!"APPROVED".equals(loan.getStatus())) { throw new BusinessException("合同状态非待放款,无法执行放款"); } // 步骤2:检查用户账户余额是否足够(模拟银行备付金) BigDecimal userBalance = userDao.getBalance(loan.getUserId(), conn); if (userBalance.compareTo(loan.getAmount()) < 0) { throw new BusinessException("银行备付金不足,放款失败"); } // 步骤3:更新合同状态为DISBURSED loanDao.updateStatus(loanId, "DISBURSED", conn); // 步骤4:更新用户余额(增加可用额度) userDao.increaseBalance(loan.getUserId(), loan.getAmount(), conn); // 步骤5:生成放款流水 TransactionLog disbursementLog = new TransactionLog(); disbursementLog.setLoanId(loanId); disbursementLog.setType("DISBURSEMENT"); disbursementLog.setAmount(loan.getAmount()); disbursementLog.setOperator("system"); transactionLogDao.insert(disbursementLog, conn); conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn != null) { conn.rollback(); // 任一环节失败,全部回滚 } throw e; } finally { DataSourceUtil.close(conn, ps, null); } }为什么不用Spring的@Transactional?
- 这里
conn.rollback()能精准控制回滚到conn.setAutoCommit(false)之后的所有操作; - 如果用
@Transactional,当userDao.increaseBalance()抛出异常,loanDao.updateStatus()的更新可能已刷到数据库(取决于JDBC驱动缓冲),导致状态不一致; - 手动事务让你清楚知道“哪几行SQL必须同生共死”。
4. 避坑指南:答辩老师不会明说,但踩中一个就扣分的5个致命细节
4.1 现象:用户A申请10万元借款,系统生成合同号L2024052000001;用户B同时申请,也生成了L2024052000001,导致数据库唯一约束冲突报错
原因:合同号用SimpleDateFormat.format(new Date())拼接,高并发下new Date()毫秒级相同,L20240520+00001重复。
解决:改用AtomicLong全局计数器 + 时间戳前缀,或直接用UUID.randomUUID().toString().replace("-", "").substring(0, 12)生成随机字符串。源码中采用前者:
private static final AtomicLong CONTRACT_COUNTER = new AtomicLong(1); public static String generateContractNo() { String datePart = new SimpleDateFormat("yyyyMMdd").format(new Date()); long seq = CONTRACT_COUNTER.getAndIncrement(); return "L" + datePart + String.format("%05d", seq % 100000); }4.2 现象:还款页面显示“本期应还1050.00元”,用户支付1050元后,后台查repayment_plan发现actual_repay_date为空,状态仍是DUE
原因:支付回调接口(/repayment/callback)没做幂等性校验,支付宝/微信可能因网络问题重复推送同一笔支付成功通知。
解决:回调接口第一行必须查transaction_log中是否存在相同trans_no(支付平台返回的商户订单号):
// 回调入口 public void handleCallback(@RequestParam String outTradeNo) { // 1. 根据outTradeNo查是否已处理过 if (transactionLogDao.existsByTransNo(outTradeNo)) { log.info("重复回调,已忽略: {}", outTradeNo); return; // 直接返回,不处理 } // 2. 执行还款逻辑... }4.3 现象:Tomcat启动时报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,但mysql-connector-java-8.0.33.jar明明在WEB-INF/lib下
原因:MySQL 8.0+驱动类名从com.mysql.jdbc.Driver变为com.mysql.cj.jdbc.Driver,而web.xml中<resource-ref>或context.xml里仍写旧类名。
解决:检查META-INF/context.xml,确保<Resource>标签中driverClassName="com.mysql.cj.jdbc.Driver",且JAR包版本与MySQL服务端匹配(8.0.33驱动连8.0.33服务端)。
4.4 现象:用户修改了手机号,但“忘记密码”功能仍向旧手机号发送验证码
原因:验证码发送逻辑直接查user.phone字段,未考虑用户可能刚改号、缓存未刷新。
解决:所有涉及用户敏感信息的操作(登录、找回密码、修改手机号),必须实时查库,禁用任何缓存。源码中UserService.findByPhone()方法明确标注@Cacheable(value = "user", key = "#phone", unless = "#result == null")——但findUserByPhoneForResetPassword()方法不加此注解,强制走DB。
4.5 现象:答辩演示时,老师连续点击5次“申请借款”,系统创建了5份合同,但只有一份成功放款,其余4份状态卡在APPROVED
原因:风控审核(RiskEngineClient.check())是模拟HTTP调用,未加分布式锁,导致并发请求都通过了风控,但后续放款时loan.status更新条件where status='APPROVED'只有一条能成功。
解决:在LoanApplyService.apply()方法开头加Redis分布式锁:
String lockKey = "loan:apply:" + userId; Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(30)); if (!locked) { throw new BusinessException("操作过于频繁,请稍后再试"); } try { // 执行风控、生成合同等逻辑 } finally { redisTemplate.delete(lockKey); // 必须finally释放 }5. 答辩现场稳赢技巧:用三个“可演示动作”代替一百页PPT
5.1 动作一:现场演示“逾期罚息自动计算”——让老师亲眼看到数字在变
别只讲PPT上“系统支持逾期罚息”,打开已部署的系统,用测试账号操作:
- 创建一笔借款:本金10000元,年化15.6%,期限12期;
- 手动将数据库中第一期
repayment_plan.due_date改为昨天(模拟逾期); - 在后台点击“执行逾期处理”按钮(对应
OverdueProcessor.process()); - 刷新页面,展示
repayment_plan表中该期status变为OVERDUE,penalty_amount字段出现15.60(按日0.0427%罚息); - 再点一次按钮,
penalty_amount变成31.20(两天罚息)。
为什么有效:老师瞬间理解你不是抄概念,而是真把“逾期”这个状态和“罚息”这个计算绑定了。源码中OverdueProcessor用Quartz定时任务每小时扫描一次,但答辩时用按钮触发,可控性强。
5.2 动作二:导出“资金对账单”PDF——证明你的系统能生成合规财务凭证
金融系统必须能输出可审计的凭证。源码中ReportController.exportAccountStatement()调用iText 7.2.5生成PDF:
public void exportAccountStatement(HttpServletResponse response, Long userId) throws DocumentException { response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "attachment; filename=account_statement_" + userId + ".pdf"); Document document = new Document(PageSize.A4.rotate()); // 横版,宽表友好 PdfWriter.getInstance(document, response.getOutputStream()); document.open(); // 表头 PdfPTable table = new PdfPTable(6); table.setWidthPercentage(100); table.addCell("交易时间"); table.addCell("交易类型"); table.addCell("合同号"); table.addCell("金额(元)"); table.addCell("余额(元)"); table.addCell("状态"); // 查询近30天流水 List<TransactionLog> logs = transactionLogDao.findByUserIdLast30Days(userId); for (TransactionLog log : logs) { table.addCell(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(log.getCreatedAt())); table.addCell(log.getType()); table.addCell(loanDao.findById(log.getLoanId()).getContractNo()); table.addCell(log.getAmount().toString()); table.addCell(log.getBalanceAfter().toString()); table.addCell("SUCCESS"); } document.add(table); document.close(); }演示话术:“老师,这是符合《金融行业信息系统安全规范》第5.2条的对账单,包含交易时间、金额、余额三要素,且PDF生成过程不依赖外部服务,全部在Tomcat内完成。”
5.3 动作三:用Chrome开发者工具,现场抓包展示“敏感信息脱敏”
打开浏览器开发者工具(F12),切换到Network标签页,登录系统后访问“我的合同”页面:
- 找到
/api/loan/list请求,点开Response,查看返回的JSON; - 指出
user_id字段值是123456,但id_card字段是"3101**********1234",phone是"138****5678"; - 强调:这不是前端JS做的,而是后端
@JsonSerialize(using = IdCardSerializer.class)注解实现的,即使绕过前端直接调API,返回的永远是脱敏数据。
底层代码:
public class IdCardSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value == null || value.length() < 18) { gen.writeString(value); } else { gen.writeString(value.substring(0, 6) + "********" + value.substring(14)); } } }6. 我的血泪经验:毕设不是交代码,是交一份“可验证的工程思维”
带过三届毕设,我见过太多学生把源码打包发给老师,换来一句“功能基本完整,但缺乏深度”。后来我逼自己拆解:到底什么是“深度”?不是用了多少炫技框架,而是你能否回答这三个问题:
当用户说‘我昨天还了款,但APP显示没还’,你第一反应查哪张表、哪个字段、用什么SQL?
→ 源码里RepaymentService.reconcile()方法就是干这个的:它会对比repayment_plan.actual_repay_date和transaction_log.created_at,找出时间差超过5分钟的异常记录,生成对账差异报告。答辩时老师问“怎么处理对账不平”,你就打开这个方法,指着LEFT JOIN那行说:“老师,我查的是还款计划表和流水表的笛卡尔积,找时间戳不匹配的孤儿记录。”如果老师说‘把年化利率改成单利计算,你们系统能改吗?’,你改几处?
→ 不是改一个calculateInterest()方法。你要改:①LoanApplyForm校验规则(单利无复利概念);②RepaymentPlanGenerator算法(改用principal * annualRate / 100 / 12 * termMonths);③transaction_log.type枚举新增'SIMPLE_INTEREST';④ 所有前端展示文案。源码中所有利率相关逻辑都抽成InterestCalculator接口,SimpleInterestCalculator和CompoundInterestCalculator两个实现类,改配置文件spring-context.xml中<bean class="SimpleInterestCalculator"/>即可切换——这才是工程化思维。演示时Tomcat突然崩了,你能在3分钟内定位到是哪个Filter内存泄漏?
→ 源码中每个Filter都加了LogUtil.logMemoryUsage(),在doFilter()前后打印Runtime.getRuntime().freeMemory()。答辩前我习惯用jconsole连上Tomcat,点开“VM Summary”,盯着“Used Memory”曲线——如果点一次“申请借款”内存涨1MB不回落,就知道是某个Filter里new byte[1024*1024]没释放。
最后说句实在的:这套源码里没有一行“为了好看”的代码。所有JSP都用<c:forEach>遍历,不用Vue;所有SQL都手写,不用MyBatis Generator;所有异常都定义BusinessException子类,不抛RuntimeException。因为答辩不是秀技术,是证明你懂——金融系统里,每一次commit()都是对信任的交付,每一行rollback()都是对风险的敬畏。希望帮到你。
本文还有配套的精品资源,点击获取