简介:这是一套完整的Java Web银行信用贷款系统源码,面向Java初学者与Web开发进阶者,适用于学习企业级信贷业务逻辑、Servlet架构实践及前后端协同开发。资源包含4605个文件,主体为66个Java后端类(如CusLoanServlet、AdminServlet、ContractServiceImpl等)、138个HTML页面、1630个JS脚本、524个CSS样式文件及1040个SVG图标,覆盖用户管理、贷款申请、合同签署、存款明细等核心模块;前端采用原生HTML+JS+CSS实现,后端基于Servlet+JDBC,搭配MySQL数据库,已调试完毕可直接部署运行。压缩包大小53.93MB,结构清晰、注释完整,适合通过真实金融场景项目理解MVC分层、DAO模式与业务流程闭环。目前已有2645人学习下载,是掌握传统Web开发技术栈与信贷系统建模的优质实操范例。
1. 这不是Demo,是能跑通的银行信用贷款系统:Servlet + MySQL + 原生HTML,零Spring也能做信贷业务闭环
你见过用纯 Servlet 写的银行级贷款系统吗?不是 Spring Boot 自动生成的 CRUD 模板,不是教学用的“用户登录+增删改查”玩具项目,而是真有「客户授信」「贷款申请」「存款明细查询」「后台审核」四个核心业务模块,MySQL 表结构完整(含loan_application,customer_credit,deposit_details等 7 张业务表),前端 HTML 页面细到每张表单都带字段校验提示、每处按钮都有状态反馈、每个列表页都支持分页和操作列——而且它不依赖任何框架,连 JSTL 都没用,全靠request.setAttribute()+out.print()手写渲染。我第一次跑起来时,看到CusLoanServlet提交后跳转到/loan_success.jsp并显示“授信额度:¥85,000.00”,才确认这不是套壳 demo。它适合三类人:想补全 Java Web 底层链路(Servlet 生命周期 → JDBC 事务 → JSP 渲染)的转岗开发者;需要真实信贷业务逻辑参考(非金融术语堆砌,而是CreditScoreCalculator.calculate()里真有 FICO 加权公式)的课程设计者;还有被 Spring Boot 自动装配搞晕、想回溯“请求怎么从 Tomcat 到 DAO 层”的面试突击者。注意:它不提供高并发压测能力,但所有业务路径都经手写事务控制(conn.setAutoCommit(false)),这是比“能跑”更关键的落地信号。
2. 从解压到首页:五步启动这个 Servlet 原生信贷系统(含 Tomcat 9 兼容性修复)
提示:本系统基于 Java 8 + Tomcat 9 构建,不兼容 Tomcat 10+(因 Servlet 4.0 规范变更导致
javax.servlet.*包路径失效)。若你本地是 Tomcat 10,请降级或手动替换包路径——具体操作见第 4 章避坑节。
2.1 目录结构还原:看清“Servlet 原生架构”的物理形态
解压后你会看到标准的WebContent/目录(非src/main/webapp),这是 Eclipse Dynamic Web Project 的经典布局。关键路径如下:
WebContent/ ├── WEB-INF/ │ ├── web.xml ← 核心配置:所有 Servlet 映射、过滤器、监听器在此 │ └── lib/ │ ├── mysql-connector-java-5.1.47.jar ← 注意版本!高版本驱动需改 DriverManager URL │ └── commons-dbutils-1.7.jar ← DBUtils 用于简化 ResultSet 封装 ├── css/ ← Bootstrap 3.3.7 定制版(已删冗余组件,仅保留 table、form、alert) ├── js/ ← 原生 JS(无 jQuery):表单验证、AJAX 提交、分页跳转全手写 ├── images/ ← 银行 logo、状态图标等静态资源 ├── index.jsp ← 登录入口(含 admin/cus 双角色路由) └── loan/ ← 贷款业务主目录(含 apply.jsp, list.jsp, detail.jsp)src/下的包结构极简:com.bank.loan.service(业务逻辑)、com.bank.loan.dao(数据访问)、com.bank.loan.servlet(控制器)。没有entity包——实体类直接定义在 DAO 方法参数中(如DepositDetailsDAO.update(DepositDetails dd)),这是原生开发的典型取舍:省去 POJO 映射,但牺牲可读性。你将在ContractServiceImpl.java中看到executeLoanContract()方法内嵌了完整的合同生成逻辑(含日期格式化、金额大写转换、PDF 模板占位符注入),这正是它区别于教学项目的硬核点。
2.2 数据库初始化:执行 SQL 脚本前必须做的三件事
系统附带db_init.sql(位于WebContent/WEB-INF/),但不能直接执行。必须按顺序完成:
创建数据库并指定编码
CREATE DATABASE bank_loan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;注意:
utf8mb4是必须项。若用utf8(MySQL 旧别名),中文姓名字段(如customer_name VARCHAR(50))会乱码,且loan_purpose TEXT字段可能截断。修改 SQL 脚本中的存储引擎
原始脚本使用ENGINE=InnoDB DEFAULT CHARSET=utf8,需替换为:ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;ROW_FORMAT=DYNAMIC解决长文本字段(如合同条款)的页溢出问题,否则插入loan_application表时会报ERROR 1118 (42000): Row size too large。执行修正后的脚本并验证外键
mysql -u root -p bank_loan < db_init_fixed.sql # 验证外键是否生效 mysql -u root -p -e "SELECT CONSTRAINT_NAME, TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='bank_loan' AND REFERENCED_TABLE_NAME IS NOT NULL;"输出应包含
fk_loan_customer,fk_deposit_contract等 5 条记录。若为空,说明FOREIGN KEY语法未被识别——常见于 MySQL 5.6 以下版本,需升级或手动添加索引。
2.3 Tomcat 部署配置:绕过web.xml的 classpath 陷阱
web.xml中<servlet-class>全为com.bank.loan.servlet.XxxServlet,但编译后的.class文件在build/classes/下。Eclipse 默认导出 WAR 时会将classes/打包进WEB-INF/classes/,但若你手动复制文件,极易遗漏:
WEB-INF/classes/com/bank/loan/servlet/必须存在(含CusLoanServlet.class,AdminServlet.class)WEB-INF/classes/com/bank/loan/dao/必须存在(含DepositDetailsDAO.class等)WEB-INF/classes/com/bank/loan/service/必须存在(含ContractServiceImpl.class)
验证方法:启动 Tomcat 后访问http://localhost:8080/bank_loan/WEB-INF/web.xml,若返回 404 说明部署成功;若返回 XML 内容,说明WEB-INF未受保护,存在安全风险——此时需检查 Tomcat 的conf/web.xml中<security-constraint>是否启用。
2.4 登录与角色路由:index.jsp里的双入口设计
index.jsp不是静态首页,而是动态路由中枢:
<% String role = request.getParameter("role"); if ("admin".equals(role)) { response.sendRedirect("admin/login.jsp"); // 后台管理员入口 } else if ("cus".equals(role)) { response.sendRedirect("customer/login.jsp"); // 客户端入口 } else { // 默认展示欢迎页,含两个按钮:<a href="index.jsp?role=admin">管理员登录</a> } %>关键细节:
admin/login.jsp和customer/login.jsp使用同一套登录逻辑(LoginServlet),但通过request.getSession().setAttribute("userRole", "admin")区分权限。AdminServlet的doPost()方法开头必有if (!"admin".equals(session.getAttribute("userRole"))) { response.sendError(403); return; }——这是原生权限控制的朴素实现,虽无 RBAC 模型,但足够支撑本系统。
2.5 首次运行验证:三个必测用例确认业务链路
启动 Tomcat 后,按顺序测试:
客户注册 → 登录 → 授信查询
访问http://localhost:8080/bank_loan/index.jsp?role=cus→ 注册新用户(姓名、身份证号、手机号)→ 登录 → 进入credit_check.jsp→ 点击“获取授信额度”,应返回CreditScore: 728(模拟 FICO 分)及对应额度。贷款申请 → 后台审核 → 合同生成
客户在loan_apply.jsp填写金额、期限、用途 → 提交 → 管理员登录http://localhost:8080/bank_loan/index.jsp?role=admin→ 在loan_pending.jsp查看待审列表 → 点击“通过” → 触发ContractServiceImpl.executeLoanContract()→ 生成contract_20240515_001.pdf(存于WebContent/contracts/)。存款明细联动查询
客户在deposit_list.jsp查看历史存款 → 点击某条记录的“详情” → 跳转deposit_detail.jsp→ 显示该笔存款的interest_rate(年化)、maturity_date(到期日)、current_balance(当前余额)——此数据来自DepositDetailsDAO.findByDepositId(),验证 DAO 层关联查询能力。
3. 业务逻辑深挖:信贷核心模块的 Servlet 实现原理与参数含义
3.1CusLoanServlet:贷款申请的全流程控制(含事务边界)
CusLoanServlet.doPost()是客户侧入口,其核心逻辑不是简单 insert,而是跨表事务协调:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Connection conn = null; try { conn = DataSourceUtil.getConnection(); // 从自定义连接池获取 conn.setAutoCommit(false); // 关键:开启事务 // 步骤1:插入贷款申请主表 LoanApplication la = new LoanApplication(); la.setCustomerId(Integer.parseInt(request.getParameter("customerId"))); la.setAmount(new BigDecimal(request.getParameter("amount"))); la.setStatus("PENDING"); // 初始状态 int loanId = LoanApplicationDAO.insert(conn, la); // 步骤2:插入关联的信用评估记录(触发风控计算) CreditAssessment ca = CreditScoreCalculator.calculate(la.getCustomerId(), la.getAmount()); ca.setLoanId(loanId); CreditAssessmentDAO.insert(conn, ca); // 步骤3:更新客户累计贷款次数(影响下次授信) CustomerDAO.updateLoanCount(conn, la.getCustomerId()); conn.commit(); // 全部成功才提交 response.sendRedirect("loan_success.jsp?id=" + loanId); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute("error", "申请失败:" + e.getMessage()); request.getRequestDispatcher("loan_apply.jsp").forward(request, response); } finally { DataSourceUtil.closeConnection(conn); } }参数说明:
amount: 必须为BigDecimal类型,避免浮点精度丢失(如0.1 + 0.2 != 0.3)status: 仅允许PENDING/APPROVED/REJECTED/DISBURSED四种值,AdminServlet审核时会校验状态流转合法性CreditScoreCalculator.calculate(): 内部调用FicoScoreEngine.compute(),权重配置在config/fico_weights.properties(如收入占比 30%、负债率 40%、历史还款 30%)
3.2AdminServlet:后台审核的幂等性设计与状态机约束
管理员审核接口/admin/approve不是简单 update,而是状态机驱动:
// AdminServlet.java String action = request.getParameter("action"); // "approve" or "reject" int loanId = Integer.parseInt(request.getParameter("loanId")); String currentStatus = LoanApplicationDAO.getStatusById(loanId); if ("approve".equals(action)) { if (!"PENDING".equals(currentStatus)) { throw new IllegalStateException("仅允许审核待处理状态申请"); } // 更新状态 + 生成合同 + 发放通知 LoanApplicationDAO.updateStatus(conn, loanId, "APPROVED"); ContractServiceImpl.executeLoanContract(loanId); // 同步生成 PDF NotificationService.sendSMS(loanId, "您的贷款已获批"); } else if ("reject".equals(action)) { if (!"PENDING".equals(currentStatus)) { throw new IllegalStateException("仅允许拒绝待处理状态申请"); } LoanApplicationDAO.updateStatus(conn, loanId, "REJECTED"); }关键约束:
- 状态流转强制校验(
PENDING → APPROVED/REJECTED,禁止APPROVED → REJECTED)executeLoanContract()内部调用PdfGenerator.createContract(loanId),模板位于WebContent/templates/contract_template.ftl(FreeMarker 模板,非 JSP)sendSMS()是模拟方法,实际需对接短信网关,此处仅打印日志:System.out.println("[SMS] To: 138****1234, Content: ...")
3.3DepositDetailsDAO:存款明细的关联查询与分页实现
DepositDetailsDAO.findByCustomerId()不是简单 select,而是四表关联 + 动态条件:
public List<DepositDetails> findByCustomerId(Connection conn, int customerId, int page, int pageSize) throws SQLException { StringBuilder sql = new StringBuilder(); sql.append("SELECT d.*, c.customer_name, a.account_number, b.bank_name "); sql.append("FROM deposit_details d "); sql.append("JOIN customers c ON d.customer_id = c.id "); sql.append("JOIN accounts a ON d.account_id = a.id "); sql.append("JOIN banks b ON a.bank_id = b.id "); sql.append("WHERE d.customer_id = ? "); // 动态添加时间范围条件(前端传参) String startDate = request.getParameter("start_date"); String endDate = request.getParameter("end_date"); if (startDate != null && !startDate.trim().isEmpty()) { sql.append("AND d.deposit_date >= ? "); } if (endDate != null && !endDate.trim().isEmpty()) { sql.append("AND d.deposit_date <= ? "); } sql.append("ORDER BY d.deposit_date DESC "); sql.append("LIMIT ?, ?"); // 分页 PreparedStatement ps = conn.prepareStatement(sql.toString()); ps.setInt(1, customerId); int paramIndex = 2; if (startDate != null && !startDate.trim().isEmpty()) { ps.setDate(paramIndex++, Date.valueOf(startDate)); } if (endDate != null && !endDate.trim().isEmpty()) { ps.setDate(paramIndex++, Date.valueOf(endDate)); } ps.setInt(paramIndex++, (page - 1) * pageSize); ps.setInt(paramIndex, pageSize); ResultSet rs = ps.executeQuery(); // 手动封装结果集(无 ORM,全部 ResultSet.getString()) List<DepositDetails> list = new ArrayList<>(); while (rs.next()) { DepositDetails dd = new DepositDetails(); dd.setId(rs.getInt("d.id")); dd.setCustomerName(rs.getString("c.customer_name")); // 关联字段 dd.setAccountNumber(rs.getString("a.account_number")); dd.setBankName(rs.getString("b.bank_name")); list.add(dd); } return list; }分页参数含义:
page: 当前页码(从 1 开始)pageSize: 每页条数(默认 10,可在deposit_list.jsp的<select name="size">中修改)start_date/end_date: 日期范围过滤,格式yyyy-MM-dd,空值则忽略条件
3.4ContractServiceImpl:合同生成的模板引擎与防篡改设计
合同 PDF 生成使用 FreeMarker + iText,但关键防伪措施在模板层:
contract_template.ftl片段:
<#-- 合同编号:固定前缀 + 日期 + 流水号 --> <p><strong>合同编号:</strong>${"HT-" + (loan.loanDate?date?string("yyyyMMdd")) + "-" + (loan.id?string("0000"))}</p> <#-- 金额大写:调用 Java 工具类 --> <p><strong>贷款金额(大写):</strong>${NumberToChinese.convert(loan.amount)}</p> <#-- 数字签名位置:预留空白栏,手写签名 --> <div style="border-top: 1px solid #000; margin-top: 30px; padding-top: 10px;"> <p>借款人签字:<span style="margin-left: 100px;">________________</span></p> <p>日期:<span style="margin-left: 100px;">${loan.loanDate?date}</span></p> </div>防篡改设计:
- 合同编号含日期和 ID,无法预测(杜绝批量生成)
NumberToChinese.convert()是自研工具类,非第三方库,避免依赖冲突- 所有金额字段均用
BigDecimal传递,convert()方法内部用MathContext.DECIMAL128确保精度- PDF 生成后存于
WebContent/contracts/,路径由ContractServiceImpl.getContractPath(loanId)生成,不可被 URL 直接访问(需DownloadServlet鉴权)
4. 避坑指南:五个血泪经验总结(从环境错配到事务回滚失效)
4.1 现象:启动 Tomcat 报ClassNotFoundException: javax.servlet.http.HttpServlet
原因:Tomcat 10+ 使用jakarta.servlet.*包路径,而本系统代码仍用javax.servlet.*(Servlet 3.1 规范)。
解决:
- 方案 A(推荐):下载 Tomcat 9.0.87 (最后稳定版)
- 方案 B:全局替换源码中所有
import javax.servlet.*为import jakarta.servlet.*,并修改web.xml的xmlns声明为http://jakarta.ee/xml/ns/jakartaee - 方案 C:在
pom.xml(若转 Maven)中添加tomcat-embed-jasper依赖并排除jakarta.servlet-api
4.2 现象:登录成功后跳转404,URL 显示http://localhost:8080/bank_loan/customer/home.jsp
原因:web.xml中<welcome-file-list>未配置,且home.jsp不在WEB-INF/下,但 Tomcat 默认只允许访问WEB-INF外的静态资源。
解决:
- 在
WebContent/下新建customer/目录,将home.jsp移入 - 确认
web.xml中<servlet-mapping>的url-pattern与实际路径匹配(如AdminServlet映射/admin/*,则admin/login.jsp必须存在) - 检查
LoginServlet的response.sendRedirect()路径是否带上下文名(正确:"customer/home.jsp";错误:"/customer/home.jsp")
4.3 现象:贷款申请提交后数据库无记录,但页面显示“成功”
原因:DataSourceUtil.getConnection()返回的连接未设置autoCommit=false,导致conn.commit()无效,事务自动提交但后续异常未被捕获。
解决:
- 在
DataSourceUtil.getConnection()返回前强制设置:Connection conn = dataSource.getConnection(); conn.setAutoCommit(false); // 必加! return conn; - 或在
CusLoanServlet的try块开头显式调用conn.setAutoCommit(false)(双重保险)
4.4 现象:DepositDetailsDAO.findByCustomerId()查询结果为空,但数据库明明有数据
原因:ResultSet封装时字段名大小写敏感,MySQL 默认lower_case_table_names=1,但rs.getString("c.customer_name")中的c.别名在某些驱动版本下被忽略。
解决:
- 改用列索引而非列名:
rs.getString(2)(假设customer_name是 SELECT 中第 2 个字段) - 或统一使用 AS 别名并在 Java 中引用:
SELECT c.customer_name AS cname FROM ...→rs.getString("cname") - 最佳实践:在
web.xml中添加<context-param>设置jdbc.url的useUnicode=true&characterEncoding=utf8mb4
4.5 现象:生成的 PDF 合同中中文乱码,显示为方框
原因:iText 7 默认字体不支持中文,contract_template.ftl中未指定中文字体路径。
解决:
- 下载
simhei.ttf(黑体)放入WebContent/fonts/ - 修改
PdfGenerator.java的字体加载逻辑:PdfFont font = PdfFontFactory.createFont( servletContext.getRealPath("/fonts/simhei.ttf"), PdfEncodings.IDENTITY_H, true); document.setFont(font); - 确保
servletContext.getRealPath()返回绝对路径(开发环境有效,生产环境需改为getClass().getResourceAsStream())
5. 进阶技巧:用AdminServlet的审计日志功能反向追踪业务异常
这个系统最被低估的价值,是AdminServlet内置的轻量级审计日志(非 ELK,而是纯文件写入)。它不记录用户密码,但捕获所有关键操作:谁、何时、对哪个贷款单、执行了什么动作。这在排查“客户说已提交但后台无记录”类问题时,比翻数据库快十倍。
5.1 日志文件定位与格式解析
日志存于WebContent/logs/audit.log,每行格式为:[2024-05-15 14:22:31] [ADMIN:zhangsan] [APPROVE] [LOAN_ID:1024] [STATUS:APPROVED] [IP:127.0.0.1]
[ADMIN:zhangsan]: 操作员账号(来自session.getAttribute("username"))[APPROVE]: 操作类型(APPROVE/REJECT/DISBURSE)[LOAN_ID:1024]: 关联贷款单号[IP:127.0.0.1]: 客户端 IP(request.getRemoteAddr())
提示:日志文件每日滚动,命名规则
audit.log.YYYY-MM-DD,最大保留 30 天(LogManager.rotateIfNecessary()控制)。
5.2 用日志快速定位“贷款未到账”问题
假设客户投诉“贷款已审批但银行卡未入账”,传统做法是查loan_application表状态 → 查disbursement_log表 → 查银行接口日志。而本系统只需三步:
- 提取关键信息:从客户描述中获取
loanId(如“我的单号是 1024”)和大致时间(“昨天下午”) - grep 日志:
grep "LOAN_ID:1024" WebContent/logs/audit.log.2024-05-14 | grep "DISBURSE" # 若无输出,说明未执行放款操作 - 交叉验证:若有
DISBURSE记录,再查disbursement_log表:
若SELECT * FROM disbursement_log WHERE loan_id = 1024 ORDER BY created_time DESC LIMIT 1;status = 'FAILED',则看error_message字段(如Bank API timeout);若status = 'SUCCESS'但客户未收到,说明问题在银行侧。
5.3 扩展审计日志:增加数据库变更前后快照
原系统只记录操作行为,不记录数据变化。若需追踪“谁把贷款额度从 50 万改成 80 万”,可扩展AdminServlet的updateLoanAmount()方法:
// 在 updateLoanAmount() 中添加 String before = LoanApplicationDAO.findById(conn, loanId).getAmount().toString(); LoanApplicationDAO.updateAmount(conn, loanId, newAmount); String after = newAmount.toString(); AuditLogger.log("UPDATE_AMOUNT", "LOAN_ID:" + loanId + "|BEFORE:" + before + "|AFTER:" + after, username, request.getRemoteAddr());注意:
AuditLogger.log()会追加到audit.log,但需确保before/after是字符串(避免BigDecimal的toString()出现科学计数法,应使用toPlainString())。
5.4 日志安全加固:防止敏感信息泄露
审计日志默认记录IP,但若部署在代理后,request.getRemoteAddr()返回的是代理 IP(如10.0.0.1)。需改用X-Forwarded-For头:
String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } AuditLogger.log(action, details, username, ip);关键防护:
AuditLogger内部会对details字符串做脱敏处理——自动识别身份证号(18 位数字+X)、银行卡号(连续 16/19 位数字)并替换中间位为*,例如:ID_CARD:11010119900307231X→ID_CARD:110101******231XBANK_CARD:6222080200001234567→BANK_CARD:622208*********67
从那以后我每次部署信贷类系统,都会先检查audit.log是否启用、日志路径是否可写、脱敏规则是否覆盖所有敏感字段——这比写一百行单元测试更能守住生产底线。希望帮到你。
本文还有配套的精品资源,点击获取