news 2026/9/16 16:30:33

JavaWeb金融借贷系统源码解析:Servlet+JDBC全流程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb金融借贷系统源码解析:Servlet+JDBC全流程实战

简介:这套基于JavaWeb实现的金融借贷系统(即P2P金融管理系统/小额贷款平台)主要面向计算机相关专业毕业设计学生与Java项目实战开发者,前台提供融资产品浏览、贷款申请、每日新闻查询,后台涵盖贷款申请审核、融资产品与周期管理、新闻发布、企业管理等常用功能,整体业务链完整。资源包共15个文件,包含项目源码、MySQL数据库脚本、项目文档(PDF/Markdown)、运行截图、软件工具说明等,压缩包大小约14.67MB,其中源码与数据库脚本可直接用于本地部署调试。目前已有2535人学习下载,适合需要快速搭建可演示金融借贷系统的人群。项目基于Servlet、JDBC、Bootstrap、jQuery、Ajax等主流技术实现,界面简洁、操作流畅,附带文档和截图便于二次开发与论文撰写,能有效支撑毕业设计答辩与项目实训需求。

1. 一个JavaWeb金融借贷系统,为什么值得把源码拆开看一遍

每年毕业设计选题目录里,“基于JavaWeb的金融借贷系统”都会反复出现。这类系统有清晰的双端闭环——前台查融资产品、申请贷款、看每日新闻,后台做贷款审核、产品维护和信息发布,一套代码覆盖了Servlet路由、JDBC操作、Session鉴权、文件上传这些JavaWeb核心知识点。相比用Spring Boot注解一键封装,Servlet + JDBC反而更直接地回答了答辩时必被追问的两个问题:URL如何被映射、数据库连接如何被复用。

项目附带完整源码(p2p.zip)、MySQL脚本iting.sql、项目文档和运行工具,前后台共九个功能模块,导入数据库、启动Tomcat就能跑,适合做毕设二次开发,也适合刚学完JavaWeb想拆开完整项目看代码结构的人。

2. 数据库设计与连接层:从iting.sql反推表结构

拿到压缩包先别急着启动,第一件事是打开iting.sql把表结构过一遍。金融借贷系统的业务复杂度不高,但表与表之间的外键、状态字段和金额字段足以体现一个毕设项目的设计水平。数据库用MySQL还是MariaDB并不关键,关键是表拆到什么粒度能让前后台代码都写得顺。

2.1 核心表结构与字段约束

融资产品这一侧,项目里拆出了产品类型、产品、贷款周期三张字典/业务表。产品类型表存放“抵押贷”“信用贷”“车贷”之类的分类,贷款周期表存“1个月、3个月、6个月、12个月”这类档位,产品表通过外键引用两张字典表。这样拆的好处是后台“融资产品管理”和“产品类型管理”两个菜单可以独立维护,增加一种新产品类型时不需要动产品表结构。

DROP TABLE IF EXISTS t_product_type; CREATE TABLE t_product_type ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, type_name VARCHAR(50) NOT NULL UNIQUE COMMENT '类型名,如信用贷', sort INT DEFAULT 0 COMMENT '排序权重', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS t_product; CREATE TABLE t_product ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(80) NOT NULL COMMENT '产品名称', type_id INT NOT NULL COMMENT '外键,产品类型', period_id INT NOT NULL COMMENT '外键,贷款周期', annual_rate DECIMAL(5,2) NOT NULL COMMENT '年化利率%', min_amount DECIMAL(10,2) DEFAULT 0, max_amount DECIMAL(10,2) DEFAULT 0, description TEXT, cover_image VARCHAR(255) COMMENT '产品配图路径', status TINYINT DEFAULT 1 COMMENT '0下架 1上架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_product_type (type_id), KEY idx_product_period (period_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_loan_apply ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL COMMENT '申请的融资产品', apply_no VARCHAR(32) UNIQUE COMMENT '业务编号,L+时间戳', username VARCHAR(30) NOT NULL COMMENT '客户姓名', phone VARCHAR(20) NOT NULL, amount DECIMAL(12,2) NOT NULL COMMENT '申请金额', period INT NOT NULL COMMENT '借款周期,单位月', purpose VARCHAR(200) COMMENT '借款用途', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1已通过 2已拒绝 3已放款', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME COMMENT '审核时间', audit_reason VARCHAR(200) COMMENT '审核意见', KEY idx_apply_product (product_id), KEY idx_apply_status (status), CONSTRAINT fk_apply_product FOREIGN KEY (product_id) REFERENCES t_product(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

上面两张建表语句里有三个细节,答辨时讲出来比单纯念表名有价值。第一,金额用DECIMAL(12,2)而不用FLOAT/DOUBLE,浮点数计算有精度误差,金融系统对金额精度敏感,DECIMAL(12,2)最大支持十亿级金额,毕设场景足够。第二,状态字段用TINYINT而非字符串,由代码层的常量或枚举维护状态含义,避免字符串拼写不一致。第三,业务单号apply_no加了UNIQUE约束,一条申请从生成起就保证编号唯一。

数据库脚本中还有新闻、企业、后台用户、公告等表,结构上没有特殊难点,基本就是上述字段风格的延伸。下面把四个业务域的常用表整理出来:

业务域核心表关键字段关联对象
融资产品t_product / t_product_type / t_loan_periodannual_rate、type_id类型表、周期表
贷款申请t_loan_applystatus、amount、product_id产品表
内容管理t_news / t_news_typetitle、content、publish_time新闻分类
后台账号t_adminusername、password、salt单个管理员
2.1.1 外键与索引的取舍

使用FOREIGN KEY在毕设里能直观展示关联关系,但真实生产系统经常移除外键,把一致性交给应用层。高频写入时外键带来额外检查开销,分库分表后数据库层外键更不好维护。本项目保留外键没问题,但你应该清楚这个权衡:例如审核贷款申请时需要读取产品利率和金额上限,如果把 product_id 外键去掉,DAO层就得自己先查产品再查申请记录,一致性逻辑也挪到应用里。

连接参数的时区设置容易被忽略,尤其MySQL 8.x下缺了会直接报异常:

jdbc.url=jdbc:mysql://localhost:3306/iting?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

2.2 连接管理:手写JDBC还是引入连接池

项目后台框架是Servlet + JDBC,没有引入MyBatis。这里实际有个选择:数据源直接用DriverManager,还是用DBCP/C3P0或Druid管连接。如果源码里就是纯DriverManager,每个DAO方法会重复类似的样板代码:

Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection(url, username, password); String sql = "SELECT id, name FROM t_product WHERE status = 1 AND type_id = ?"; ps = conn.prepareStatement(sql); ps.setInt(1, typeId); rs = ps.executeQuery(); // 组装对象并返回 } catch (Exception e) { e.printStackTrace(); } finally { if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); }

这段代码本地单用户访问没问题,但每次执行SQL都创建和销毁一个TCP连接,MySQL服务端也在持续新增和释放线程,并发上来必然扛不住。常见做法是引入DBCP或Druid,用少量配置把连接管理交给连接池:

BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); ds.setInitialSize(5); ds.setMaxActive(20); ds.setMaxIdle(10); // 每个DAO通过 ds.getConnection() 替换 DriverManager.getConnection()

改造量很小,替换DriverManager.getConnection那几行为ds.getConnection()即可,DAO对外返回值不变,但是50个并发请求下,有连接池与无连接池的响应时间差异能明显感知。initialSize是启动时预建的连接数,maxActive是最大并发连接数,这两个参数直接决定系统能扛住多少并发,写文档时值得提一句。

3. 前台查询链路:产品列表、Ajax新闻详情与贷款申请

前台主页是用户第一眼看到的东西,也是整个系统数据最密集的一块。它同时涉及列表页渲染、详情页跳转、异步接口和表单提交四类典型场景。把这条链路拆清楚,后面做后台管理就轻松很多。

3.1 Servlet路由与请求生命周期

这个项目没有用SpringMVC,请求通过@WebServlet注解映射到Servlet类。一行@WebServlet("/product/list")就把“URL到处理类”的映射关系写明白了,搜索URL就能直接定位处理逻辑,比注解路径更直观,这对代码阅读者是友好的。

从浏览器发起一次请求,按生命周期拆解:

浏览器发起 GET /product/list?type=1&page=1 -> Tomcat 接收请求 -> 匹配到 ProductListServlet.doGet() -> 设置编码、读取参数 -> 调用 ProductDao.queryProducts(type, page) -> PreparedStatement 执行 SQL 并返回 List -> request.setAttribute() 传给 JSP -> JSP 用 JSTL/EL 渲染表格或卡片 -> 服务器返回 HTML

项目一般按Servlet -> Service -> DAO -> DB分层,业务量不大但边界清晰。分层带来的直接好处是后续把DAO替换成MyBatis时,Servlet和Service基本不用动。

3.2 融资产品列表查询与分页

列表页最常用的参数是“按类型筛选”和“按页翻”,对应的SQL用LIMIT实现。参数化查询是底线,字符串拼接会把系统暴露在SQL注入风险中:

@WebServlet("/product/list") public class ProductListServlet extends HttpServlet { private ProductDao productDao = new ProductDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); int typeId = parseInt(req.getParameter("type"), 0); int page = parseInt(req.getParameter("page"), 1); int pageSize = 6; List<Product> list = productDao.queryByPage(typeId, page, pageSize); int total = productDao.countByType(typeId); int totalPage = (int) Math.ceil(total / (double) pageSize); req.setAttribute("productList", list); req.setAttribute("totalPage", totalPage); req.setAttribute("currentPage", page); req.getRequestDispatcher("/WEB-INF/jsp/product_list.jsp").forward(req, resp); } private int parseInt(String v, int def) { if (v == null || v.trim().isEmpty()) return def; try { return Integer.parseInt(v); } catch (NumberFormatException e) { return def; } } }

这段代码里三个参数值得说明。pageSize=6是每页展示的产品数,改这个值直接影响首页滚动长度,金融/贷款类展示页放6到12个比较合适,取决于产品卡片高度。totalPage由总数除以每页数再向上取整得出,必须转成double再除,否则整数相除直接截断,最后一页数据会丢失。parseInt方法建议保留,前端传入page=abc时不会直接抛500,而是回落成默认值,这是一种很省事的防御性写法。

DAO层分页查询用LIMIT ?, ?,第一个参数是偏移量,第二个是查询条数:

public List<Product> queryByPage(int typeId, int page, int pageSize) { String sql = "SELECT id, name, annual_rate, min_amount, max_amount, loan_limit, cover_image " + "FROM t_product WHERE status = 1 AND (? = 0 OR type_id = ?) " + "ORDER BY create_time DESC LIMIT ?, ?"; int offset = (page - 1) * pageSize; try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, typeId); ps.setInt(2, typeId); ps.setInt(3, offset); ps.setInt(4, pageSize); try (ResultSet rs = ps.executeQuery()) { List<Product> list = new ArrayList<>(); while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setAnnualRate(rs.getBigDecimal("annual_rate")); // 其余字段按同样方式填充 list.add(p); } return list; } } catch (SQLException e) { throw new RuntimeException("查询产品列表失败", e); } }

这里用(? = 0 OR type_id = ?)避免写两条SQL分支:前端不传类型时 typeId=0,第一个条件成立,第二个条件虽然存在但不影响结果;传了具体类型时第一个条件为false,第二个生效。JDK 7之后的 try-with-resources 语法能自动关闭连接和语句,不需要手写finally块,代码短很多。DAO层只返回List<Product>,不在Servlet里拼接SQL,后续替换存储层或加缓存都更容易。

3.3 新闻列表与详情的Ajax交互

新闻模块的前台交互有两种做法:超链接直接跳转JSP,或者进入页面后通过Ajax异步加载内容。这个项目采用Ajax,页面不刷新,体验接近现在的单页应用。相应的Servlet端返回JSON而不是返回HTML片段:

@WebServlet("/news/detail") public class NewsDetailServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("application/json;charset=UTF-8"); int id = Integer.parseInt(req.getParameter("id")); News news = newsDao.findById(id); JSONObject result = new JSONObject(); if (news != null) { result.put("code", 200); result.put("data", new JSONObject() .put("title", news.getTitle()) .put("content", news.getContent()) .put("publishTime", news.getPublishTime() + "")); } else { result.put("code", 404); result.put("msg", "新闻不存在"); } resp.getWriter().write(result.toJSONString()); } }
$.ajax({ url: '/news/detail', type: 'GET', data: { id: newsId }, dataType: 'json', beforeSend: function () { $('#newsContent').html('<div class="loading">加载中...</div>'); }, success: function (res) { if (res.code === 200) { $('#newsTitle').text(res.data.title); $('#newsContent').html(res.data.content); } else { alert(res.msg || '加载失败'); } }, error: function () { $('#newsContent').html('<span class="text-danger">网络异常,稍后重试</span>'); } });

上面Ajax代码的dataType: 'json'意味着后端必须返回合法JSON,Servlet某处异常导致输出非JSON内容,前端会进入error回调。实际开发建议所有异步接口统一返回壳{code, msg, data},错误时也返回{code: 500, msg: "..."},前端只判断code,比用HTTP状态码分发分支更一致。后端拼JSON时注意news.getPublishTime()返回java.util.Date,直接用Fastjson序列化往往是一串时间戳,这里手动转字符串可保证和前端展示格式一致。

3.4 申请贷款的参数组织与后端校验

申请贷款表单一般包含姓名、手机号、申请金额、借款周期、借款用途,以及选定的产品ID。最容易出现的问题是只有前端校验,绕过页面直接POST就为空数据入库。后端至少做三件事:非空校验、手机号格式校验、金额范围校验。

表单字段校验规则后端处理
姓名必填,长度2-20空白字符直接拦截
手机号正则^1\\d{10}$不匹配返回提示
申请金额大于0且不超过产品上限BigDecimal compareTo
借款周期必须存在于周期表外键关联验证
产品ID必须存在且状态为上架不存在返回提示
public Result apply(String username, String phone, BigDecimal amount, Integer period, Integer productId) { if (StringUtils.isBlank(username)) return Result.fail("姓名不能为空"); if (StringUtils.isBlank(phone) || !phone.matches("^1\\d{10}$")) return Result.fail("手机号格式不正确"); if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) return Result.fail("申请金额不合法"); Product product = productDao.findById(productId); if (product == null || product.getStatus() != 1) { return Result.fail("产品不存在或已下架"); } if (amount.compareTo(product.getMaxAmount()) > 0) { return Result.fail("超过产品最高可借金额"); } String applyNo = "L" + System.currentTimeMillis(); loanApplyDao.insert(applyNo, username, phone, amount, period, productId); return Result.success("申请提交成功,等待后台审核"); }

注意BigDecimal的比较必须用compareTo而不是equalsequals会同时比较精度,new BigDecimal("100.0").equals(100)返回false,这是金额校验最常见的坑。手机号校验用一个正则就够,不需要引入额外依赖。防重复提交可以在前端把提交按钮置灰,后端再对同一手机号短期内的提交次数做限制,毕设阶段做到这个程度足够。

4. 后台管理:登录鉴权、贷款审核与产品维护

后台是整个系统真正“干活”的地方,也是答辩时老师问得最深的部分。后台的复杂点不在代码难度,而在状态管理和权限控制。一个贷款申请从提交到放款要经历多次状态变更,产品上下线还要考虑联动。

4.1 登录与Session会话保持

后台登录常见方案是“用户名 + 密码”提交到AdminLoginServlet,验证通过后写Session,页面放WEB-INF下避免直接被URL访问。密码存储不能是明文,毕设源码里即使只有MD5,文档里也要写明“生产环境务必加盐哈希”。

@WebServlet("/admin/login") public class AdminLoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); Admin admin = adminDao.findByUsername(username); if (admin == null) { req.setAttribute("error", "用户不存在"); req.getRequestDispatcher("/admin/login.jsp").forward(req, resp); return; } String inputPwd = MD5Util.md5(password + admin.getSalt()); if (!inputPwd.equals(admin.getPassword())) { req.setAttribute("error", "密码错误"); req.getRequestDispatcher("/admin/login.jsp").forward(req, resp); return; } req.getSession().setAttribute("admin", admin); resp.sendRedirect(req.getContextPath() + "/admin/index"); } protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.getSession().removeAttribute("admin"); resp.sendRedirect(req.getContextPath() + "/admin/login.jsp"); } }

MD5Util.md5(password + admin.getSalt())是加盐哈希的简化版:密码明文拼接随机salt再做MD5,即使两个用户密码相同,存储的哈希值也不同。MD5本身已不适合做密码存储,毕设文档里写“生产环境换为BCrypt/Argon2”反而说明你知道边界。

这中间还有个关键点:forwardsendRedirect的区别。forward是服务端内部转发,URL地址不变,适合登录失败后把错误信息带回登录页;sendRedirect是浏览器重新发起请求,URL改变,适合登录成功后跳转新页面,避免刷新时重复提交表单。

后台还需一层拦截逻辑。登录成功后访问各管理页面,每个Servlet都判断Session有无admin会很啰嗦,用Filter统一拦截/admin/*更干净:

@WebFilter("/admin/*") public class AdminAuthFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpSession session = req.getSession(false); if (session != null && session.getAttribute("admin") != null) { chain.doFilter(request, response); } else { HttpServletResponse resp = (HttpServletResponse) response; resp.sendRedirect(req.getContextPath() + "/admin/login.jsp"); } } }

Filter做登录鉴权是整块方案最省代码的方式,以后新增后台管理Servlet无需重复写Session判断。唯一要留意:/admin/*会同时拦截登录接口本身,所以要么在Filter里放行登录URL,要么按上面这种“Session为空就跳到登录页”的方式处理,不会形成重定向死循环。

组件职责注意点
AdminLoginServlet账号密码校验、写入Session失败用forward带回错误信息
AdminLogoutServlet移除Session并重定向到登录页同时清掉自动登录Cookie
AdminAuthFilter拦截 /admin/* 未登录请求放行登录URL,避免重定向循环

4.2 贷款审核的状态机流转

贷款申请表t_loan_applystatus字段取值是0待审核、1已通过、2已拒绝、3已放款。四态对应的后台操作分别是:查询待审列表、通过、拒绝、确认放款。状态机最简单的实现方式是定义一个常量类:

public final class LoanStatus { public static final int PENDING = 0; public static final int APPROVED = 1; public static final int REJECTED = 2; public static final int LOANED = 3; public static String desc(int status) { switch (status) { case PENDING: return "待审核"; case APPROVED: return "已通过"; case REJECTED: return "已拒绝"; case LOANED: return "已放款"; default: return "未知"; } } }

审核操作的SQL要带条件status = 0,目的只有一个:防止两人同时打开后台对同一条申请做重复审核。第一次审核把status改成1,第二条SQL更新时WHERE id=? AND status=0影响行数为0,自然失败,已通过的申请不会被再刷新一遍。这是乐观锁的极简形态,不引入版本号也能解决大部分竞态问题。

public boolean approve(Integer applyId, String auditor, String reason) { if (applyId == null) return false; String sql = "UPDATE t_loan_apply SET status = 1, auditor = ?, " + "audit_time = NOW(), audit_reason = ? " + "WHERE id = ? AND status = 0"; try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, auditor); ps.setString(2, reason); ps.setInt(3, applyId); return ps.executeUpdate() > 0; } catch (SQLException e) { throw new RuntimeException("更新审核状态失败", e); } }

拒绝操作是同一套逻辑,只是把status改成2。审核列表页面默认只查status = 0并按创建时间倒序,否则历史审核记录一直混在待审列表里,误操作概率会明显上升。

4.3 产品与新闻的后台维护:FileUpload上传

融资产品维护必然涉及图片上传,技术栈里的FileUpload是Apache Commons FileUpload组件。使用该组件两个前提:表单加enctype="multipart/form-data",后端不能用request.getParameter直接拿文本字段,必须先解析FileItem

@WebServlet("/admin/product/save") public class ProductSaveServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); Product product = new Product(); if (ServletFileUpload.isMultipartContent(req)) { DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); factory.setRepository(new File(System.getProperty("java.io.tmpdir"))); ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(2 * 1024 * 1024); upload.setSizeMax(10 * 1024 * 1024); try { List<FileItem> items = upload.parseRequest(req); for (FileItem item : items) { if (item.isFormField()) { String fieldName = item.getFieldName(); String value = item.getString("UTF-8"); switch (fieldName) { case "name": product.setName(value); break; case "annualRate": product.setAnnualRate(new BigDecimal(value)); break; case "typeId": product.setTypeId(Integer.parseInt(value)); break; } } else { String fileName = new File(item.getName()).getName(); String ext = fileName.substring(fileName.lastIndexOf(".")); String newName = System.currentTimeMillis() + ext; File dir = new File(req.getServletContext().getRealPath("/upload")); if (!dir.exists()) dir.mkdirs(); item.write(new File(dir, newName)); product.setCoverImage("upload/" + newName); } } productDao.insert(product); resp.sendRedirect(req.getContextPath() + "/admin/product/list"); } catch (Exception e) { throw new ServletException("文件上传失败", e); } } } }

这里有几个参数值得说明。factory.setSizeThreshold(1MB)是内存缓冲区阈值,文件小于1MB放内存,超过先落临时目录;upload.setFileSizeMax(2MB)是单文件上限,upload.setSizeMax(10MB)是整次请求总大小上限。这两个限制必须在服务端做,否则传一个大文件时Tomcat直接内存溢出。new File(item.getName()).getName()是为了去掉IE/Edge浏览器提交时带上的完整本地路径,防止目录穿越。图片用时间戳重命名避免重名覆盖,但也丢失了原始文件名,生产环境一般用UUID。

新闻管理的增删改查类似,只是少了文件上传。后台表格用Bootstrap样式渲染,操作按钮绑定事件后通过Ajax或表单同步提交均可;毕设里维护操作用同步表单提交反而更直观,刷新后状态完全由后端控制。

5. 本地部署排错与从毕设到生产的改造清单

5.1 本地跑通的三个关键配置

运行JavaWeb项目最常折在三个地方:MySQL驱动版本不匹配、中文乱码、端口占用。MySQL 5.x和8.x驱动类名不同,5.x用com.mysql.jdbc.Driver,8.x必须用com.mysql.cj.jdbc.Driver,且8.x驱动连接串不带serverTimezone参数会直接抛异常。Tomcat 8.5及以上版本还要在web.xml注册编码过滤器,同时连接串设置characterEncoding=utf8,否则前台中文显示乱码。

<filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>com.it.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter>

Tomcat端口被占用时,修改conf/server.xml里的8080即可,80058009一般不会冲突,只改8080足够。

症状常见原因解决动作
ClassNotFoundException: com.mysql.jdbc.Driver驱动版本不一致换成 mysql-connector-java 8.x 或改驱动类名
中文乱码请求/响应编码未设置添加编码过滤器,连接串加 characterEncoding=utf8
denied for user 'root'@'localhost'主机或密码配置错误检查MySQL用户授权,root一般只允许本机连接
port 8080 already in use端口被占用或重复启动Tomcat杀掉占用进程,或修改 server.xml 端口
文件上传解析异常超过上传大小限制调大 setFileSizeMax,或确认表单中 enctype

5.2 从毕设到生产的九个改造点

把这份源码作为生产系统起点时,改造优先级参考下表:

优先级改造点说明
P0密码存储改为BCryptMD5加盐只是过渡,生产必须防碰撞
P0SQL参数化复核DAO全部用PreparedStatement,杜绝拼接
P0增加操作日志审核、上架、下线等关键动作记录操作人
P1增加事务控制贷款状态变更与流水写入必须在同一事务
P1连接池替换用Druid或HikariCP替换DriverManager
P1统一响应结构所有接口返回{code, msg, data}而不是裸文本
P2引入MyBatis Plus替换重复的DAO样板代码
P2文件上传改OSS本地磁盘存储不利于水平扩展
P2增加Redis缓存产品列表和新闻列表缓存,降低DB压力

排查接口类问题还有一招很实用:在DAO层把SQL连同参数拼出来打印到控制台,十秒就能判断是参数传错了还是条件写错了,比对着浏览器开发者工具猜前端传值快得多。系统回归测试做完后再统一关掉这些日志输出即可。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:29:08

DEAP脑电情绪识别二分类实战:从MNE数据处理到XGBoost建模

简介&#xff1a;面向脑电情绪识别入门者与机器学习初学者的二分类算法实现&#xff0c;基于公开 DEAP 脑电数据集&#xff0c;完整覆盖快速傅里叶变换&#xff08;FFT&#xff09;特征提取、数据预处理与模型训练评估流程。代码共5个文件&#xff0c;包含4个 Python 脚本和1个…

作者头像 李华
网站建设 2026/9/16 16:28:20

STM32L496嵌入式TLS实战:内存裁剪、证书预加载与LWIP适配

简介&#xff1a;本资源是一套基于RT-Thread操作系统的STM32L496嵌入式TLS安全通信完整工程&#xff0c;面向嵌入式开发工程师、物联网安全实践者及RTOS进阶学习者&#xff0c;解决低功耗Cortex-M4平台下mbedtls集成与TLS端到端实现难题。压缩包共7134个文件&#xff0c;主体为…

作者头像 李华
网站建设 2026/9/16 16:25:54

Wasp 子目录部署指南:baseDir 与 WASP_WEB_CLIENT_URL 的正确配置

Wasp 子目录部署指南&#xff1a;baseDir 与 WASP_WEB_CLIENT_URL 的正确配置 【免费下载链接】wasp The batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex…

作者头像 李华
网站建设 2026/9/16 16:25:31

WinForms迁移Blazor实战:MWGA工具解析与应用

1. 项目背景与核心挑战最近接手了一个历史遗留的WinForms系统迁移项目&#xff0c;这个拥有7万行代码的C#桌面程序已经稳定运行了十几年。随着业务发展&#xff0c;客户强烈要求将其改造成Web应用。面对这个看似不可能的任务&#xff0c;我发现了一个名为MWGA&#xff08;Make …

作者头像 李华