你有没有过这种经历:接手一个“看起来简单”的JavaWeb课程设计题目,真上手才发现从数据库表设计到Servlet跳转,再到JSP页面取值,每一步都是坑。最近正好做完一个“共享租车信息管理系统”,用的就是java+jsp+servlet+jdbc+mysql这套经典组合,很多同学问我要源码和思路,我干脆把这套从零搭建的过程完整拆开讲一遍。这篇文章适合正在做JavaWeb课程设计、毕业设计,或者想搞懂传统JSP项目内部原理的人。不需要Spring,不需要Maven全家桶,把最朴素的Servlet+JDBC玩明白,项目一样能跑得顺,背后的原理你还能讲得比用框架的人更清楚。
1. 系统整体设计与技术选型
1.1 为什么还选JSP+Servlet这套经典组合
很多同学一上来就问为什么不用Spring Boot,毕竟现在企业里都是SpringBoot+MyBatis的天下了。但你要想明白一件事:课程设计考察的是你对JavaWeb底层原理的掌握程度,而不是你调用框架的能力。用JSP+Servlet+JDBC写一套管理系统,你能亲手摸到一次HTTP请求从浏览器到Servlet再到数据库的完整链路,这是框架封装掉的东西,恰恰是面试最常问的部分。
另外,共享租车这个业务本身不复杂。用户注册登录、浏览车辆、下单租车、归还结算、后台管理车辆和订单,这么几个核心模块用Servlet完全扛得住。技术选型的核心逻辑是:业务复杂度决定了你不需要重型框架,反而轻量方案开发更快、部署更简单、出问题也好排查。
这套项目在Tomcat上跑起来非常轻,不用配Maven私服,不用管一堆依赖冲突。把WAR包丢进webapps目录,启动Tomcat,完事。我做的时候甚至直接用Eclipse或IDEA自带的Tomcat插件考试,省去了很多环境折腾的时间。
1.2 共享租车业务的表结构设计
表结构才是一个管理系统的灵魂。代码烂一点能跑,表设计差了整个项目直接报废。我做这套租车系统时把核心表分为四张:用户表、车辆表、订单表、管理员表,简洁但覆盖了所有业务。
用户表(t_user)字段我这么定义:id自增主键、username唯一索引、password(建议MD5加密存储)、real_name、phone、id_card(身份证号,租车行业实名认证必须)、driver_license_no(驾照编号)、create_time。这里要特别说明的是,username加唯一索引能防重,另外id_card和driver_license_no是租车业务特有的字段,正常用户注册时别强制填,但在下单前必须补齐,这个逻辑后边会讲。
车辆表(t_car)字段包括:id、plate_no(车牌号,也设唯一索引)、brand(品牌)、model(车型)、seat_num(座位数)、transmission(手动/自动)、fuel_type(汽油/柴油/电动)、status(0未出租/1已出租)、price_per_day(日租金)、image_url(车辆图片地址)、create_time。
订单表(t_order)是整个系统业务流的核心:id、order_no(订单编号,可以用时间戳+随机数生成)、user_id(外键关联用户表)、car_id(外键关联车辆表)、rent_start_time、rent_end_time、total_price、status(订单状态用int标识:0待支付、1已支付/租用中、2已归还待结算、3已取消、4已完成)、create_time。
管理员表(t_admin)就是简单的id、username、password、real_name,后台登录用。
这里不得不提一个实战中容易翻车的点:MySQL表名和字段名别用中文也别用MySQL保留字,比如order是保留字,status在MySQL 8.0也是保留字,建议字段名加前缀或者统一命名成order_status这类,不然写SQL时各种报错。
1.3 三层分层与项目目录规划
JSP+Servlet项目虽然没有Spring帮你管理分层,但你在编码时心里必须有一条清晰的线:JSP只负责展示、Servlet只负责接收请求和转发、JDBC层只负责数据库操作。我习惯把项目按这个目录结构来组织:
src/ ├── com.rental.dao // 数据访问层,对接数据库 ├── com.rental.entity // 实体类,映射四张表 ├── com.rental.service // 业务逻辑层,处理判断 ├── com.rental.servlet // Servlet控制层,接收请求 ├── com.rental.util // 工具类,比如DBUtil、MD5Util └── com.rental.filter // 过滤器,登录权限控制 web/ ├── admin/ // 后台管理页面 ├── user/ // 前台用户页面 ├── common/ // 公共页面(登录、注册等) ├── static/ // css、js、图片 └── index.jsp这样的划分虽然比Spring多写几个类的代码,但改起来非常舒服。拿查询车辆列表举例:JSP发出请求到CarListServlet,Servlet调CarService的list方法,Service里内聚了status=0这类查询逻辑,CarDAO负责拼SQL和结果集映射。每一层各管各的事,出了问题一根线就能定位。
我还建议写一个统一的DBUtil工具类,把Connection、Statement、ResultSet的关闭写成close()方法,避免几十个DAO类里出现重复的连库和关库代码。这一点在后续小节会详细展开。
2. 核心基础设施:数据库连接与JDBC封装
2.1 数据库连接池的选择与配置
JDBC最原始的方式就是每次请求都拿DriverManager.getConnection去MySQL建连接。但这在开发时还好,一旦系统同时涌进几十个请求,数据库连接反复创建和销毁,很容易把MySQL搞崩。课程设计虽然不会遇到高并发,但用连接池是专业性的体现,面试时提这个能加分不少。
我用的是DBCP连接池。配置方式也很简单,在WEB-INF下放一个dbcp.properties:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/rental_car?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=yourpassword initialSize=10 maxTotal=20 maxIdle=10 minIdle=5 maxWaitMillis=3000每次从连接池拿连接,用完还回连接池,效率比裸建连接高一个量级。注意URL里serverTimezone=Asia/Shanghai是MySQL 8.x必须加的,不加会报时区错误;useSSL=false表示关闭SSL握手,本地开发别开,SSL握手浪费时间和性能。
还有一点细节:连接池参数initialSize和maxTotal分别代表初始连接数和最大连接数,课程设计场景10个初始、20个最大足够了。你要是用IDEA做项目,去Maven仓库下载的commons-dbcp2包要和commons-pool2版本匹配,这两兄弟不匹配时会直接启动报异常。
2.2 JDBC通用DAO层的封装技巧
最让我心疼的就是看到有人把JDBC代码写得又臭又长,一个查询用户列表的方法里三行建连接、五行写SQL、八行处理结果集,最后还要catch三个异常,一个类的代码量80%都是在处理连接和资源关闭。
我建议封装一个BaseDAO,把重复逻辑全收走,子类只要传SQL和参数就行。核心方法就两个:query和update。query方法里用PreparedStatement,Java 7之后可以直接用try-with-resources处理资源关闭,代码干净很多。
public class BaseDAO { protected <T> List<T> query(String sql, Object[] params, RowMapper<T> mapper) throws SQLException { List<T> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (params != null) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapper.mapRow(rs)); } } } return list; } protected int update(String sql, Object[] params) throws SQLException { try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (params != null) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } } return ps.executeUpdate(); } } }RowMapper是个函数式接口,子类里传入Lambda表达式就行:
public List<Car> listAvailableCars() throws SQLException { String sql = "SELECT * FROM t_car WHERE status = 0"; return query(sql, null, rs -> { Car car = new Car(); car.setId(rs.getInt("id")); car.setPlateNo(rs.getString("plate_no")); // 其他字段映射... return car; }); }这样做的好处特别实际:新增一张表要写DAO时,你只需要关心SQL语句和字段映射逻辑,连接管理、结果集遍历、异常处理这些纯体力活全部复用BaseDAO。我写完整套系统五个DAO类只花了不到两小时,主要时间全花在了SQL和业务逻辑上。
一定要记住:JDBC操作里参数必须用PreparedStatement的?占位符,不能String拼SQL。第一防SQL注入,第二带引号的字符串值不会被MySQL解析错。我在给其他同学review代码时,看到有人把用户输入的搜索关键词直接拼进SQL,这是安全意识薄弱的表现,课程设计也得有职业操守。
2.3 实体类与业务对象映射细节
实体类的字段命名要和数据库字段对应,这是JavaBean规范的基本要求。但有两个映射细节我要重点提醒。
第一个是日期类型。MySQL的DATETIME字段对应Java侧的java.sql.Timestamp或java.util.Date都可以,但JSP页面用EL表达式直接输出Date对象时会显示成一串英文格式,很难看。我通常在实体类里多写一个字符串格式化字段或者直接加一个getFormatCreateTime()方法,返回new SimpleDateFormat("yyyy-MM-dd HH:mm").format(createTime)。
第二个是枚举状态的映射。数据库里status用int存储,但实体类里如果把status声明为int,JSP页面判断逻辑会很绕。我建议实体类中声明为int,同时提供一个状态下标的常量配置类:
public class OrderStatus { public static final int UNPAID = 0; public static final int PAID = 1; public static final int RETURNED = 2; public static final int CANCELLED = 3; public static final int COMPLETED = 4; }这样在Java逻辑里判断订单状态时,直接if (order.getStatus() == OrderStatus.PAID),代码可读性瞬间提高一个档次。团队成员之间协作时,看到OrderStatus类就知道订单状态有哪些取值,不用翻数据库脚本。
实体类建议都用封装类型而非基本类型:用Integer不用int,用Double不用double。原因很简单,数据库里某条记录的字段可能是NULL,查询结果映射到基本类型时会NPE,封装类型为null反而能如实反映数据状态,后边业务判断还能用== null做条件。
3. 从请求到响应:Servlet层设计与实现
3.1 Servlet请求分发与路由设计
Servlet层最大的坑就是“一个功能一个Servlet”,结果项目里出现二十几个Servlet类,类名乱成一片。我推荐两种设计思路,都很适合课设规模:第一种是一个模块一个Servlet,第二种是纯路由分发加统一Servlet。
我用的是第一种思路:车辆模块全走CarServlet,订单模块全走OrderServlet,用户模块全走UserServlet。每个Servlet的doGet和doPost里,通过request.getParameter("action")来区分具体功能。
@WebServlet("/order") public class OrderServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("list".equals(action)) { myOrders(req, resp); } else if ("detail".equals(action)) { orderDetail(req, resp); } else if ("pay".equals(action)) { payOrder(req, resp); } else if ("cancel".equals(action)) { cancelOrder(req, resp); } else if ("create".equals(action)) { createOrder(req, resp); } } }URL长这样:/order?action=create&carId=12。好处很明显:第一管理Servlet数量,一个业务域一个入口;第二新增功能只需要在Servlet里加个action分支和一个方法,不用新建类;第三维护时找代码直接定位到对应Servlet就行。
这里强烈建议用@WebServlet("/order")注解,省得再去web.xml里配servlet-mapping。用注解标注URL路径之后,系统启动时Tomcat自动扫描注册。但要注意一点:如果你在类上写@WebServlet注解,同时又把这个类配置进了web.xml,启动时会报“重复映射”错误。
还有个细节:doGet和doPost不要各自写各自的逻辑,最好在doGet里加一行doPost(req, resp)或者反过来。因为前端页面有时候用GET请求跳转(比如a标签),有时候用POST表单提交,操作同一个业务功能时,两个方法都指向同一段逻辑才不会出问题。
3.2 车辆管理功能的Servlet实现细节
车辆管理是管理员后台的核心,包含车辆列表查询、新增车辆、编辑车辆、删除车辆、上下架等操作。这部分我拆解一下新增车辆和列表分页的完整逻辑。
新增车辆的Servlet处理流程是这样:管理员在admin/car_add.jsp页面上填写车辆信息,表单提交到/car?action=add。Servlet先接收车牌号、品牌、车型、日租金等参数,做基本的非空校验,然后调用CarService的addCar方法。在Service里要做的业务判断包括:车牌号是否已存在(查一次数据库)、日租金是否是正数、座位数是否在合理范围内(2-9座)。
车牌号查重用的是一个CarDAO.getByPlateNo(String plateNo)方法,返回是Car对象还是null来判断。这里有个常被忽略的坑:用户从表单提交的车牌号可能带空格,或者车牌号里的英文字母大小写不一致。我一般在Service里先统一处理成大写无空格再入库,比如StringUtils.trim(plateNo).toUpperCase(),这类脏数据清洗逻辑放在Service层最合适。
车辆列表分页的实现也是必考知识点。页面通过/car?action=list&pageNum=1请求,Servlet解析pageNum,默认第1页,每页显示8条(这个值可以做成常量)。Service层要返回两个东西:当前页车辆列表、总记录数。然后计算总页数totalPages = (totalCount + pageSize - 1) / pageSize,把列表、当前第几页、总页数存进request域,转发到list页面。
分页的SQL核心就是LIMIT ?, ?:
String sql = "SELECT * FROM t_car ORDER BY id DESC LIMIT ?, ?"; int offset = (pageNum - 1) * pageSize;第一页是LIMIT 0, 8,第二页LIMIT 8, 8,第三页LIMIT 16, 8。首页、上一页、下一页、末页四个按钮对应的URL逻辑很好写,注意JSP页面上翻页时把当前查询条件(品牌、状态)也拼进URL,不然翻一页条件就丢了。
3.3 会话管理与登录权限控制
登录权限这块我见过太多课设项目是空的——所有页面都能随便访问,后台也裸奔。但凡要求严格一点的老师,看到这个直接降档打分。
我的方案是用Filter来做统一登录验证。写一个LoginFilter,在web.xml里配置过滤路径,需要保护的路径(比如/admin/*和/order/*)都走过滤器。逻辑很简单:从session里取当前用户信息,如果取不到就跳转登录页,取到了就放行。
@WebFilter(urlPatterns = {"/admin/*", "/order/*", "/user/*"}) public class LoginFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(false); Object user = session != null ? session.getAttribute("loginUser") : null; if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }这里容易踩的坑有两个。第一个是req.getSession()和req.getSession(false)的区别:getSession()如果session不存在会强制创建一个,getSession(false)只在已存在时返回,避免每次未登录请求都新建无意义的session。第二个是admin路径下要区分管理员角色,普通用户登录了也不能进后台,所以在Filter里还得判断登录对象是不是管理员:把登录用户存进session时,我习惯存一个包含角色信息的User对象,字段role=1表示管理员、role=0表示普通用户,Filter里判断user.getRole()==1才放行。
登录成功后别忘记做一件事:更新session里的用户信息。比如用户下单后产生了订单数据,有些姓名和手机号是从session里的User对象取的,如果你不更新,用户改完资料后界面上还是旧信息,要重新登录才生效。
4. 前端与后端交互:JSP页面开发实战
4.1 JSP页面布局与JSTL/EL表达式实践
JSP页面最大的毛病就是Java代码和HTML混写,一个页面里三四段<% %>,看起来像山东煎饼夹意大利面——完全不搭。现代JSP开发讲究“页面中尽量不要出现Scriptlet”,所有Java操作逻辑都放到Servlet里完成,JSP只通过JSTL标签和EL表达式取值和循环遍历。
EL表达式取值:${car.brand}在后台其实就是找car.getBrand()方法,所以实体类的getter方法必须写对。JSTL循环遍历车辆列表时,c:forEach的格式要记熟:
<c:forEach items="${carList}" var="car"> <tr> <td>${car.plateNo}</td> <td>${car.brand}</td> <td>${car.pricePerDay}元/天</td> <td> <c:if test="${car.status == 0}">可租</c:if> <c:if test="${car.status == 1}">已租</c:if> </td> </tr> </c:forEach>要在HTML页面里跑JSTL,必须在JSP顶部引入标签库:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>忘写这一行,然后页面上一堆JSPG0048E之类的报错或者直接显示标签代码,是JSP新手最常踩的坑。JSTL标准库的jar包,Tomcat不自带,需要你自己把jstl-1.2.jar放进WEB-INF/lib目录。
我特别推荐一个习惯:JSP页面用纯HTML静态页面进行视觉设计,最后再把动态数据用EL表达式替换进去。这样做的好处是前端效果在浏览器里先用假数据跑通,再改JSP,页面出问题的时候你至少能确定不是HTML本身写错了。
4.2 车辆列表页的分页与搜索实现
车辆列表页是做这个系统时花时间最多的页面。它要满足三个需求:默认展示所有可租车辆、支持按品牌筛选、支持分页。这三个需求组合起来,页面上查询条件和当前页码的传递就必须处理好。
我的做法是这样设计URL参数:/car?action=list&brand=现代&pageNum=2。list page页面上每次翻页,都要把当前选择的条件带上,不然筛完品牌翻页后条件就丢了。搜索表单提交时,带的是brand和pageNum两个参数;排序方式可以加一个sort=price_asc参数,对应SQL里ORDER BY price_per_day ASC。
写分页SQL时要注意LIMIT的count参数类型是整型,preparedStatement里传字符串参数也能隐式转换,但最好搞干净。分页条底部显示"共页 第页",我一般用pageNum、totalPages两个字段,在JSP里通过c:choose控制"首页/上一页/下一页/末页"四个按钮的显示状态——第一页隐藏上一页,最后一页隐藏下一页。
筛选条件动态拼SQL是另一个常考的点。我用了一个取巧但正规的方式:先定义基础SQLSELECT * FROM t_car WHERE status = 0,有品牌条件就追加AND brand = ?,有价格区间就追加AND price_per_day BETWEEN ? AND ?。参数全部放进List里,最后转成数组传给BaseDAO。这样的代码虽然不如MyBatis动态SQL优雅,但逻辑在这套体系里已经算很清晰了。
4.3 表单提交与数据回显细节
表单提交和数据回显,是判断一个JavaWeb项目开发经验是否老练的分界线。合格的项目能在一张表单页面上做到三件事:表单校验、错误信息显示、提交失败后原数据回显。
举个例子:管理员新增车辆时,如果某个字段填错了(比如车牌号重复),后端Service抛出异常后,会forward回car_add.jsp。这时候又要做数据回显又要在页面上显示错误提示。我的做法是在Servlet里把用户提交的参数重新setAttribute传回request域,再从Request域里读取回显到表单的value属性:
req.setAttribute("car", car); // car是未入库前从表单收集到的实体 req.setAttribute("errorMsg", "车牌号已存在,请检查"); req.getRequestDispatcher("/admin/car_add.jsp").forward(req, resp);JSP表单里的value写${car.plateNo},如果car为null显示空。${errorMsg}用c:if判断非空时显示一个红色的错误提示div。这种模式在所有add和edit页面都可以复用。
注意一个关键分界线:回显时用request.setAttribute转发到JSP,而登录成功后跳转页面用response.sendRedirect。前者的浏览器URL不会变,适合传递数据;后者会发起新请求,适合完成操作后的页面转向。如果你在用forward的地方用了redirect,数据传不过去;在应该用redirect的地方用了forward,用户刷新页面时会反复提交表单。这两个概念搞不清楚,做JavaWeb项目寸步难行。
5. 完整业务流程与关键功能卡片
5.1 租车业务的核心流程闭环
共享租车系统的核心业务流程,我用一条线串起来是这样的:注册登录 → 浏览车辆 → 选择车辆下单 → 支付 → 到店取车 → 驾驶使用 → 归还车辆 → 结算押金和费用 → 完成订单。课程设计能不能拿高分,就看这条闭环是否完整、状态流转是否正确。
订单状态的变化是这条闭环的中枢。我的订单状态字段有五个值:0待支付、1已支付(租用中)、2待结算(已归还但费用未确认)、3已取消、4已完成。每次状态变更都必须满足前置状态条件,不能乱跳。比如用户点取消订单,前提是订单处于待支付状态;车辆归还后,管理员确认费用,订单才能从1变为2,再确认结算变成4。
这些状态判断写在哪一层学生经常搞混。我强调一下:状态流转的条件判断要写在Service层,不能在Servlet里写。举个例子,用户支付订单时有个常见漏洞:如果用户反复点击支付按钮,订单状态已经是“已支付”了,再点一次就多扣一次钱。Service层的支付方法里要先查订单当前状态,只有status==0时才执行更新SQL并把车辆状态改为已租,这样并发或者连点也只会成功一次。
5.2 订单创建与支付环节简化处理
订单创建是用户下单时调用的核心逻辑。流程里有两个容易忽略的点:第一,创建订单时必须校验车辆仍处于可租状态。这个查询和插入订单之间没有事务,在高并发时可能会超卖,但课设里通过在Service层加一条SELECT ... FOR UPDATE锁或先update再验证受影响行数就能规避。我用的方案是update车辆状态时先设置条件status=0:
int rows = carDAO.updateStatusToRented(carId); // SQL: UPDATE t_car SET status=1 WHERE id=? AND status=0 if (rows == 1) { // 说明车辆状态成功从0改为1,车辆确实可租 orderDAO.insertOrder(order); } else { // 说明车辆已经被租,提示用户 throw new BusinessException("该车辆已被预订"); }这句UPDATE ... WHERE status=0是原子操作,能防止两个用户同时租同一辆车。这是我在实际项目中用到过、在课设里又能讲清楚的并发控制小技巧,值得写在注释里。
支付环节我在项目里是用一个模拟支付的页面,点击“确认支付”后直接把订单状态改为已支付。没有对接真实的支付SDK,因为那是另一个复杂度。但我在支付成功后会顺手完善两件事:写一条支付流水到t_payment表,同时生成一个6位数的取车码存到订单记录里。取车码在业务里对应线下验车环节,有了这个细节,整个系统做演示时就显得很完整。
5.3 车辆归还与费用结算逻辑
归还车辆是反向的业务操作。用户在归还页面填入取车时间、归还时间,系统根据日租金和超时费用计算总价。这里我设计了两档计费:基础费用最小单位是天,超出部分按小时计价,超过6小时按一天计费。
long rentDays = (returnTime - getCarTime) / (1000 * 60 * 60 * 24); long extraHours = (returnTime - getCarTime) % (1000 * 60 * 60 * 24) / (1000 * 60 * 60); long totalDays = rentDays + (extraHours >= 6 ? 1 : 0); double totalPrice = totalDays * car.getPricePerDay();这里的日期计算是Java时间处理的基本功,用Date的getTime相减再取整,就能算出天数。演示的时候你可以故意选择一个还车晚了几小时的时间,验证费用增加逻辑对不对。千万别在Service里直接写死一个天数,演示和代码评审时这几行动态计算是很大的加分项。
结算完成之后,把订单状态从2改为4,同时把车辆状态恢复为0可租。这个“归还车辆后自动释放车辆状态”的逻辑必须保证执行,不然就会出现一辆已经归还的车还在列表里显示“已租”的情况。我提个醒:这个业务查询只要涉及到修改两张表,就应该放在同一个事务里。BaseDAO里如果顺序执行两个DAO,中间出了异常会导致一边成功一边失败。正确做法是手动开启事务:Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); try { ...; conn.commit(); } catch { conn.rollback(); }。
6. 常见问题排查与作业技巧
6.1 连接MySQL时的乱码与SSL问题
这套技术栈最经典的报错我帮你全部罗列出来。第一个是MySQL连接URL里没带SSL参数导致的SSL connection error,解决方式是URL后面加useSSL=false;第二个是中文字符乱码,可能的原因有三个维度,任何一个没配置都会乱码:JSP页面编码、请求参数编码、数据库连接编码。JSP页面顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>,Servlet里对POST请求加上request.setCharacterEncoding("UTF-8"),数据库连接URL里加useUnicode=true&characterEncoding=utf8。三层都对了,中文才不会变问号。
还有GET请求的中文参数乱码问题,这是因为Tomcat 8默认GET编码还是ISO-8859-1,可以通过在Tomcat的server.xml的Connector上加URIEncoding="UTF-8"解决。这个坑不遇到一次你根本不知道在哪调,现在记住了就少走很多弯路。
时区问题报错原文是The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,中文环境下MySQL 8的默认时区会造成JDBC驱动识别不了,URL上加serverTimezone=Asia/Shanghai即可。这三个问题互相独立,但经常三个一起爆,把它们叠在同一行URL都配上就能安安稳稳跑数据库。
6.2 传统JSP项目打包与部署问题
课程设计最后一步通常是打包部署。很多学生是用IDEA/Eclipse里的Tomcat直接启动,可一旦换了机器,整个项目就起不来了。我更建议学会打WAR包部署到独立Tomcat,这也是企业里老项目的常规操作。
在IDEA里打包WAR的艺术在于:Project Structure → Artifacts → Web Application: Archive → 勾选Build on make,然后Build菜单里点击Build Artifact。生成的WAR文件在out/artifacts目录下,把这个WAR文件拷到Tomcat的webapps目录,启动Tomcat会自动解压部署。如果提示端口占用,去conf/server.xml改掉8080端口就行。
还有一类同学在部署到服务器时发现页面可以打开但数据全是空的,排查思路是先连一下MySQL看服务是否在跑,然后检查数据库名、用户名、密码是否和dbcp.properties一致,最后看防火墙是否放行了3306端口。按这个顺序排查,90%的部署问题都能解决。MySQL允许远程连接还需要在MySQL里执行GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'password',默认root只允许localhost访问。
6.3 这类JavaWeb课程设计项目的加分技巧
做课设除了功能跑通,还要想着怎么在展示和答辩时把亮点打出来。我从自己的体会出发分享几个加分项。
第一,做一个好看的数据统计页。管理员首页放几个统计卡片:车辆总数、可租车辆数、今日订单数、本月营收总额。SQL用SELECT COUNT(*)和SUM(total_price)即可,再配合GROUP BY month做一个简单的订单趋势图。技术含量不高,但展示效果极其惊艳,哪怕只是几行数据,老师一看就知道你做了完整思考。
第二,传感器级的前端校验一定要做。比如订单日期范围校验:returnTime必须晚于getCarTime,否则提示错误。这个校验既要在前端用JavaScript判断,也要在后端Service再判断一次,前后端双保险体现的是工程素养。
第三,把源码注释写清楚。在关键方法前用/** ... */说明业务逻辑,在复杂SQL旁用-- 查询可租车辆这样的注释。代码写给别人看的能力,面试官也会看重。
不建议为了追求华丽而引入Redis、消息队列,那超出了课题范围,反而让老师觉得你重点跑偏。把一个CRUD系统做到结构清晰、功能闭合、演示顺畅,就已经是课设里的优秀作品了。
最后再分享一个小技巧:做这种管理系统时,把数据库初始化脚本schema.sql和测试数据data.sql单独放在项目根目录的sql文件夹下。这样无论开发还是答辩演示,都能一键重建数据库状态,不会出现演示到一半数据被我改乱了、页面一片空白的尴尬。这套JSP+Servlet+JDBC的底子打牢了,以后学Spring MVC、Spring Boot时你会发现思路是完全相通的,只不过框架把Servlet和JDBC的样板代码替你写好了。真正有沉淀的人,是能用朴素技术做出完整管理系统、又能把每一步原理讲明白的人。