资讯详情

资讯详情

smbms超市订单管理系统:Servlet+JSP+JDBC分层与分页实战拆解

很多人第一次打开 smbms 这个项目时心里想的都是不就是个增删改查吗结果从登录页点进去之后分页查不出来、中文全是问号、删除供应商直接报错、改了 JSP 路径整站 404一天下来只跑通了一个登录。smbms 全称超市订单管理系统是 Java Web 阶段最典型的综合练手项目它把用户、角色、供应商、账单、地址这几块业务串成一条完整的后台管理链路。它真正值钱的地方不是功能多而是它把 Servlet、JSP、JDBC、Filter、Session 这些底层零件全部裸露在你面前没有任何框架帮你遮丑。这篇文章不带你从头敲一遍代码而是把这个项目从业务边界、数据模型、分层职责、分页机制、过滤器链到实际运行故障一层层拆开讲透适合刚学完 Servlet 想找个完整项目落地的同学也适合已经跑通过但讲不清楚自己到底写了什么的人。1. 先厘清 smbms 的业务边界它是账期单据系统不是电商商城名字里带订单两个字最容易让人误会成淘宝那种下单支付流程。实际上 smbms 里根本没有购物车、没有库存扣减、没有支付回调它管的是超市采购侧的单据台账。一个采购员在系统里录入一条账单写明从哪个供应商进了什么货、单价多少、数量多少、总价多少、是否已付款财务和主管通过列表页按供应商、按付款状态筛选核对。整个系统的定位更接近内部台账管理工具而不是面向消费者的交易平台。这个认知非常关键因为它直接决定了后面所有的设计取舍。既然是内部台账用户量就那么几十个不需要考虑高并发数据一致性要求高但实时性要求低不需要消息队列页面是给内部人员用的不需要响应式设计一张 1024 宽度的表格能看清就行。想明白这一点你就不会去纠结为什么它不用 Redis 缓存为什么它没有分布式锁也不会在二次开发时盲目往上堆中间件。1.1 从四个角色的操作路径反推功能模块把系统里可能出现的角色列出来功能模块自然就浮出水面了。系统管理员要能管用户、管角色、重置密码采购员要能录入和修改账单、维护供应商信息财务要能按付款状态筛选账单、核对金额主管只需要看汇总列表。这四类人的动作映射到代码里就是公共模块登录、注销、改密、用户管理模块、供应商管理模块、账单管理模块部分版本还会带上角色管理和地址管理。顺着这条线去看目录结构你会发现项目划分得非常整齐pojo放实体dao放数据访问接口和实现service放业务接口和实现servlet放控制层filter放过滤器util放工具类jsp放视图。每个包对应一层职责一个模块从页面到数据库走一遍路径是完全固定的。这种整齐感是 smbms 作为教学项目最大的优点也是它作为工程项目的缺点——真实业务里模块边界不会这么干净但作为理解分层的入口它足够清晰。1.2 订单管理在这套系统里到底指什么严格说smbms 里叫账单管理更准确。核心表是账单表一条记录代表一次进货行为字段包括账单编码、商品名称、商品描述、商品单位、商品数量、总金额、是否付款、供应商 ID、创建人、创建时间。列表页默认展示全部账单支持按商品名称模糊查询、按供应商下拉筛选、按付款状态筛选再加上分页。这里有个细节值得注意账单和供应商之间是通过providerId关联的而不是把供应商名称直接冗余存进账单表。这个设计的好处是供应商改名后账单自动跟着变坏处是每次查账单列表都要关联查询供应商表拿到名称。项目里常见的做法是在账单的实体类里额外加一个providerName字段SQL 用LEFT JOIN把供应商名称带出来然后再手动映射进去。这个实体类字段多于表字段的小技巧是很多人第一次接触关联查询时想不通的地方后面讲 DAO 分层的时候还会再展开。2. 技术栈的年代感Servlet 加 JSP 这套组合为什么还值得过一遍smbms 用的技术栈非常朴素Servlet 3.0/4.0 做控制层JSP 加 JSTL 加 EL 表达式做视图JDBC 直连 MySQLDruid 或者原生连接池管理连接Filter 做编码和登录拦截Session 保存登录态。没有 Spring没有 MyBatis没有前后端分离。这套东西放到今天确实显得老旧但它有个无法替代的价值每一个请求从进来到出去中间经过了哪些类、哪些方法、哪些参数传递你可以完整地跟下来不依赖任何黑盒。用框架的时候很多人对请求参数怎么变成对象事务在哪一层开启异常怎么被统一处理这些问题是没有实感的因为框架都替你做了。而 smbms 逼着你亲手写request.getParameter()、亲手setAutoCommit(false)、亲手在catch里rollback。这种笨拙的体验恰恰是理解框架的前提。我带过不少人凡是能把这个项目的数据流讲清楚的人后面学 Spring MVC 基本都是两三天的事。2.1 一个查询用户列表的请求要经过多少手拿最典型的用户列表查询举例一次点击背后的完整链路是这样的浏览器发起GET /jsp/user/userlist.jsp?queryname张queryUserRole2pageIndex1请求先过编码过滤器把字符集统一成 UTF-8再过登录过滤器检查 Session 里有没有 user 对象没有就直接重定向到登录页。放行之后进入UserServletServlet 从request.getParameter()里取出查询条件判断当前是不是查询动作是的话把参数塞进request域再调用UserService的getUserList()。Service 层基本是个透传把参数原样交给UserDao的getUserList()顺便算一下总记录数。DAO 层拿到连接拼接带WHERE条件的 SQL用PreparedStatement绑定参数执行查询把ResultSet逐行映射成User对象塞进List返回给 Service再返回给 Servlet。Servlet 把列表和分页对象放进request域forward到 JSPJSP 用 JSTL 的c:forEach渲染表格。整个过程下来参数在四个层之间转了一圈每层的职责边界清清楚楚。2.2 和现代框架版本对比缺的到底是什么能力维度smbms 原始实现Spring Boot 版本请求映射web.xml或WebServlet手写 URLRequestMapping注解自动映射参数绑定request.getParameter()逐个取方法参数自动绑定支持对象接参数据访问手写 JDBC 加反射映射MyBatis/JPA 自动映射事务管理手动setAutoCommit加rollbackTransactional声明式视图渲染JSP 加 JSTL服务端渲染模板引擎或前后端分离依赖管理手动往WEB-INF/lib拷 jarMaven/Gradle 坐标引入这张表不是让你去贬低 smbms而是让你看清楚每一个缺背后框架到底帮你省了什么事。手动取参数省掉的是类型转换和空值判断手动映射省掉的是ResultSet逐列读取的样板代码手动事务省掉的是try-catch-finally里的回滚逻辑。你只有亲手写过这些样板才知道框架的价值在哪里也才知道框架出问题时该往哪里查。3. 五张核心表撑起的引用关系决定了你哪里能改哪里不能动smbms 的数据库设计得非常克制核心就五张表用户表、角色表、供应商表、账单表、地址表。有些扩展版本会额外加一张订单表或者权限表但主干不变。这五张表之间的引用关系构成了整个系统的骨架理解了这个骨架你才知道为什么有些删除操作会失败为什么有些查询必须用关联。用户表存的是系统操作人字段有主键、用户编码、用户名称、密码、性别、生日、电话、地址、角色 ID、创建人、创建时间、修改人、修改时间。角色表存的是角色编码和角色名称用户表里的角色 ID 指向它。供应商表存的是供应商编码、名称、描述、联系人、电话、地址、传真。账单表存的是账单编码、商品名称、商品描述、单位、数量、总价、付款状态、供应商 ID、创建人。地址表存的是某个用户的收货地址列表通过用户 ID 关联。3.1 表结构逐张拆解与字段语义用户表有两个字段容易被新人忽略createdBy和creationDate。这两个字段在很多人的实现里是空的因为添加用户时随手忘了赋值。但在真实台账场景里这条数据是谁在什么时候录的是审计的基本要求。正确的做法是在 Service 层拿到当前登录用户把它的 ID 和当前时间填进去再交给 DAO 插入。这个小动作体现的是对业务语义的尊重而不是单纯把字段填满。账单表的isPayment字段是个整型标志位1 表示已付款2 表示未付款不是布尔类型。用整型而不是布尔是为了后面可能扩展成部分付款已退款等状态。这种用小整数代替布尔的习惯在很多老系统里都能见到看起来不优雅但扩展性确实好一些。你在做二次开发时如果想加部分付款状态只需要约定一个值 3不用改表结构。-- 账单列表关联供应商名称的核心查询 SELECT b.id, b.billCode, b.productName, b.productDesc, b.productUnit, b.productCount, b.totalPrice, b.isPayment, b.providerId, p.proName AS providerName, b.creationDate FROM smbms_bill b LEFT JOIN smbms_provider p ON b.providerId p.id WHERE b.productName LIKE ? AND b.providerId ? AND b.isPayment ? ORDER BY b.creationDate DESC LIMIT ?, ?;注意这里用的是LEFT JOIN而不是INNER JOIN。原因是历史数据里可能存在供应商已被删除但账单还留着的脏数据用内连接会把这些账单直接过滤掉列表总数和实际条数对不上分页就会出现最后一页少了几条的诡异现象。用左连接至少能保证账单主体显示出来供应商名称那一列空着用户能看出哪里有问题。3.2 外键约束与删除策略的现实取舍理论上账单表应该对供应商表建外键删除供应商时数据库会直接拦住。但很多 smbms 版本的建表脚本里并没有真的加外键约束只是逻辑上关联。这就埋下了一个坑删除一个已经被账单引用的供应商数据库不会报错但账单列表里那一批记录的供应商名称就变成空了。我自己的处理方式是分两层数据库层面加上外键约束让非法删除在最早的环节就被挡住应用层面在删除前先查一次引用数量如果大于零就返回一个友好的提示而不是把数据库的原始异常直接抛到页面上。这两层缺一不可只有数据库约束没有友好提示用户看到一堆英文异常会懵只有应用层检查没有数据库约束并发情况下还是可能插进脏数据。-- 删除前的引用检查比直接 delete 稳妥得多 SELECT COUNT(1) FROM smbms_bill WHERE providerId ?;这个查询返回大于零时Service 层直接返回失败标识Servlet 把提示信息放进request域带回列表页。看起来多写了几行代码但省掉了后面无数次的数据怎么对不上的排查时间。4. 分层落地DAO、Service、Servlet 各自该守哪段边界smbms 的分层是最标准的四层结构但很多人在实现的时候会把三层揉成一团Servlet 里直接写 SQL或者 Service 里塞满了request和response。判断分层有没有做对有个很简单的标准把视图层整个换掉Service 和 DAO 能不能一行不改继续用。如果不能说明业务逻辑漏到控制层去了。DAO 层只干一件事和数据库打交道。它的方法签名应该是纯粹的入参出参比如ListUser getUserList(String userName, Integer roleId, int currentPageNo, int pageSize)不该出现任何HttpServletRequest。Service 层只干一件事组织业务规则比如添加用户前先查重名删除供应商前先查引用修改密码时校验旧密码。Servlet 层只干三件事取参数、调 Service、转发视图。4.1 BaseDao 里的反射映射与参数绑定项目里最有技术含量的一段代码就是BaseDao。它用泛型加反射把执行查询并映射成对象列表这件事抽象成一个通用方法避免每张表都写一遍while(rs.next())。核心思路是通过ClassT拿到目标类的所有字段遍历ResultSet的列名用field.setAccessible(true)打开私有字段的访问权限再把值塞进去。public T ListT executeQuery(ClassT clazz, String sql, Object... params) { ListT list new ArrayList(); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { Field[] fields clazz.getDeclaredFields(); while (rs.next()) { T obj clazz.getDeclaredConstructor().newInstance(); for (Field f : fields) { f.setAccessible(true); try { Object val rs.getObject(f.getName()); if (val ! null) f.set(obj, val); } catch (SQLException ignored) { // 结果集里没有这一列跳过即可 } } list.add(obj); } } } catch (Exception e) { throw new RuntimeException(查询失败: sql, e); } return list; }这段代码有两个地方必须注意。第一字段名必须和列名完全一致否则rs.getObject(f.getName())会抛异常。账单表那个关联查询带出来的providerName在表里根本不存在所以要么在 SQL 里用别名对齐字段名要么在实体类里手动补一个 setter 单独处理。第二内层的catch (SQLException ignored)是必要的因为实体类里可能有serialVersionUID这类非数据库字段不吞掉异常整个映射就崩了。4.2 Service 层薄一点反而是对的新手常犯的毛病是觉得 Service 层太空了显得没干活于是把参数校验、分页计算、SQL 拼接全往 Service 里塞。这恰恰是错的。Service 薄说明这个业务的规则本来就简单硬塞逻辑进来只会让职责变模糊。该在 Service 里待着的是那些跨 DAO 的协调动作比如删除用户之前要先把他的地址记录清掉这种一行 DAO 搞不定、需要组合的事情才是 Service 的用武之地。事务的边界也应该划在 Service 层。比如添加账单同时更新供应商的最近合作时间这种操作必须保证两条 SQL 要么都成功要么都回滚。写法上就是关掉自动提交执行完两条再commit中间任何一步抛异常就在catch里rollback最后finally里把连接还回去。这个模板写多了会烦但它是理解声明式事务的前提。4.3 Servlet 只做取参、调用、转发一个干净的 Servlet 长这样先拿到method参数判断当前是什么操作然后用request.getParameter()取值做最基本的非空判断调用 Service 拿到结果把结果放进request域最后forward到对应的 JSP。整个方法里不该出现任何 SQL 关键字也不该出现复杂的if-else业务判断。值得单独说一下的是forward和redirect的选择。查询类操作一律用forward因为要把数据带到页面上增删改这类会改变数据的操作用redirect跳回列表页目的是防止用户按 F5 刷新时重复提交。这个细节很多教程一带而过但它是在真实使用中必然遇到的问题表单重复提交导致的重复数据排查起来非常费劲。5. 分页这件小事是检验项目功力的试金石几乎所有人做 smbms 都会卡在分页上。不是写不出来而是写出来的分页总有那么一两个边界情况是错的最后一页数据不全、页码越界后页面空白、切换查询条件之后页码没重置。分页之所以能当试金石是因为它同时涉及 SQL 计算、参数传递、状态保持三件事任何一环想不清楚都会出问题。分页的核心对象是PageSupport五个字段当前页码、每页条数、总记录数、总页数、当前页数据列表。前两个由前端传入没传就给默认值第三个靠一条COUNT查询拿到第四个由前两个算出来第五个靠LIMIT查询拿到。逻辑一点都不复杂复杂的是边界。5.1 总页数的向上取整不能想当然总页数的计算公式是总记录数 / 每页条数向上取整。很多人直接写整除结果 21 条数据每页 10 条算出来是 2 页第 21 条永远看不到。正确写法是(totalCount pageSize - 1) / pageSize用整数运算完成向上取整避免浮点数的精度问题。int totalPageCount (totalCount pageSize - 1) / pageSize; // 总数为 0 时至少给一页避免页面显示第 1 页 / 共 0 页 if (totalPageCount 0) totalPageCount 1;还有个坑是当前页码越界。用户手动改 URL 里的pageIndex999或者删除数据后总页数变少了但页码还停在原来那页都会导致查询结果为空、页面一片空白。稳妥的做法是拿到页码之后先做一次钳制小于 1 归为 1大于总页数归为总页数。这个处理放在 Service 里最合适因为 Servlet 拿到的时候应该是已经合法的值。5.2 LIMIT 的两个参数和总条数查询必须同源LIMIT的两个参数是偏移量和条数偏移量等于(当前页码 - 1) * 每页条数。这里最常见的 bug 是页码从 0 开始还是从 1 开始没统一——前端传 1 表示第一页后端算偏移量的时候忘了减 1结果第一页直接跳过了前 10 条数据。定一个约定然后全链路遵守比反复调试有效得多。更隐蔽的问题是列表查询和总数查询的条件不一致。比如列表查询写了三个WHERE条件总数查询只写了两个那么算出来的总页数就是错的翻到后面会出现空页。解决办法是把条件拼接抽成一个方法两个查询共用同一段WHERE片段参数也共用同一个列表。现象大概率原因定位方法最后一页数据不满总页数用了整除打印 totalCount 和 totalPageCount 核对第一页少了前 N 条偏移量没减 1打印拼好的 SQL 直接丢数据库执行翻到后面是空白页总数查询条件比列表少把两条 SQL 都打出来逐字对比改查询条件后还是旧数据页码没重置为 1检查表单提交时有没有带 pageIndex6. 过滤器链编码、登录、权限这三道闸的顺序不能乱smbms 里有两个必须有的过滤器字符编码过滤器和登录拦截过滤器。前者解决中文乱码后者防止未登录用户直接敲 URL 进后台。这两个过滤器的执行顺序是有讲究的编码过滤器必须排在前面因为登录拦截器里也可能读取参数做判断如果那时编码还没设好参数就已经是乱码了。web.xml里filter-mapping的声明顺序决定了执行顺序用注解WebFilter的话顺序是不确定的所以涉及多个过滤器有先后要求时老老实实写配置文件更稳。这个细节很多人踩过本地跑的时候顺序碰巧是对的换个容器部署就乱了。6.1 编码过滤器要同时管请求和响应只设request.setCharacterEncoding(UTF-8)是不够的response的Content-Type也要带上字符集否则返回的中文照样是问号。完整的写法是两行都写而且setCharacterEncoding必须在读取任何参数之前调用一旦调用了getParameter()再设就无效了。public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); }顺带说一句 GET 请求的乱码。POST 的乱码上面这段就能解决但 GET 请求的参数编码跟请求体无关取决于容器对 URI 的解码方式。如果你的查询条件是通过 URL 拼参数传的分页链接基本都是那这段过滤器救不了你需要在容器配置里统一 URI 编码或者在代码里手动做一次转码。这是两套机制别混为一谈。6.2 登录拦截的放行名单要写全登录过滤器的逻辑很简单从 Session 里取 user 对象取到了就放行取不到就重定向到登录页。问题出在放行名单上。登录页本身、登录请求、静态资源CSS、JS、图片、错误页这些都必须放行否则会出现登录页自己被拦截重定向到登录页无限循环的经典死锁。String uri request.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.contains(/login) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png) || uri.endsWith(.jpg)) { chain.doFilter(request, response); return; }用endsWith和contains做匹配比较粗糙但在这种小项目里够用。真要做严谨一点应该把放行路径配置成白名单集合用前缀匹配避免出现/nolongin.jsp这种奇葩路径被误放行的情况。7. 真正跑起来才会遇到的那批问题文档里基本不写教程里的代码看起来天衣无缝你自己敲完一跑就各种报错。这不是你笨而是教程省略了大量环境相关的细节。下面这几个问题几乎每个做 smbms 的人都会遇到至少两个。7.1 中文乱码的三个来源要分开治第一个来源是数据库本身。建库建表时如果没有指定字符集默认可能是 latin1这时候无论代码怎么设 UTF-8 都白搭存进去就是乱码。检查方法是直接连数据库看表里存的是什么如果库里已经是乱码那问题不在 Java 层。建库语句必须明确写DEFAULT CHARACTER SET utf8mb4。第二个来源是 JDBC 连接串。URL 里要带上characterEncodingutf8否则驱动和服务器之间的传输编码可能对不上。这个参数经常被漏掉因为不带也能连上只是中文会出问题很难第一时间联想到连接串。第三个来源才是前面说的 Servlet 层的请求响应编码。三个来源的判断顺序是先看库里存的数据对不对再看驱动传输对不对最后才查代码里的编码设置。倒着查会浪费大量时间。7.2 连接不释放跑一会儿就卡死JDBC 直连最容易出的问题是连接没关。Connection、PreparedStatement、ResultSet三个对象都要关而且关闭顺序是反过来的。如果只在方法末尾关Connection不关ResultSet游标会一直占着资源反复调用几十次之后数据库连接池就满了页面开始转圈不动。用 try-with-resources 是现在最省心的写法把三个资源的声明都放进try的括号里出不出异常都会自动关。如果项目用的是老版本语法那就老老实实写finally块按ResultSet、PreparedStatement、Connection的顺序逐个判空关闭。这个模板虽然啰嗦但它是保命的。还有个隐蔽的坑BaseDao里如果每次方法调用都getConnection()而池子配置的最大连接数只有 10那嵌套调用Service 调两次 DAO就可能把池子占满。要么把连接数调大要么把事务的粒度设计得小一点避免一个请求里长时间持有连接。7.3 删除报外键错误页面直接白屏前面提过外键约束。如果你的表建了外键删除一个被引用的供应商会抛SQLIntegrityConstraintViolationException如果异常没被捕获用户看到的就是一个 500 错误页。处理方式是在 Service 里先做引用检查有引用就返回失败Servlet 把提示信息带回页面显示成一行红字。反过来如果表没建外键删除会静默成功但账单列表里的供应商名称变成空白这种看起来没报错但数据坏了的情况更难排查。所以我的建议永远是约束加上友好提示也加上两道防线都要有。7.4 JSP 路径写错导致整站 404JSP 里的相对路径是最让人头疼的东西。同一个页面从列表页点进去和在地址栏直接访问相对路径的基准是不一样的CSS 和图片可能一会儿加载得出来一会儿加载不出来。解决办法是统一用${pageContext.request.contextPath}拼接绝对路径或者在页面顶部用c:set定义一个ctx变量后面所有引用都带上它。报错现象根因处理方式页面上中文显示为问号库、连接串、代码编码三者之一按库、驱动、代码顺序逐层排查运行一段时间后请求无响应连接或结果集未关闭改用 try-with-resources删除按钮点击后 500外键约束被触发但异常未捕获Service 层做引用预检查并返回友好提示样式和图片加载不出来JSP 相对路径基准不一致统一用 contextPath 拼绝对路径表单刷新后数据重复增删改后用了 forward改为 redirect 回列表页8. 二次开发方向从能跑起来到能讲明白把项目跑通只是及格线真正拉开差距的是你有没有对它做过有意义的改造。我见过太多人的简历上写独立完成超市订单管理系统一问细节全是照着视频敲的这种项目在面试里基本没有加分。反过来哪怕你只做了两三个小改造只要能说清楚为什么改、改完解决了什么问题价值就完全不一样。8.1 低成本、高收益的增强清单第一个方向是密码安全。原版很多实现是明文存密码这个必须改。加一层摘要算法比如先做一次哈希再拼上用户唯一标识做二次哈希改动量很小但能让你的项目在安全性上说得过去。改造时要注意旧数据兼容简单做法是加一个标志位区分新旧密码登录时分别校验。第二个方向是操作日志。在 Service 层的关键方法上记录谁在什么时候做了什么落一张日志表。这个改造几乎不动原有逻辑但能让你讲清楚 AOP 的思想——虽然你是用最笨的方式手写的但思路是一样的。第三个方向是把某几个列表页改成 AJAX 异步加载。比如用户名校验原来要提交表单才知道重名改成输入框失焦时发一个请求实时提示。这个改造能让你把 XMLHttpRequest 或者 fetch 的用法讲透也能体现你对前后端数据交互的理解。第四个方向是补上单元测试。哪怕只给 DAO 层写几个测试用例验证查询条件拼接是否正确、分页参数是否生效也能说明你有质量意识。用 JUnit 加内存数据库跑起来很快。8.2 面试讲述时该抓的三个点第一讲数据模型而不是讲页面。说清楚五张表怎么关联、为什么账单要存供应商 ID 而不是名称、删除时的引用完整性怎么保证这比说我做了增删改查有信息量得多。第二讲一个你真正解决过的问题。比如中文乱码你查了多久、最后定位到是连接串少了参数比如分页最后一页数据不全你打了多少日志才发现是总页数算错了。这种细节是编不出来的面试官一听就知道你是真做过。第三讲清楚分层带来的好处。能说清楚为什么 Servlet 里不该写 SQL为什么事务边界要放在 Service 层说明你不是只会写代码而是理解了代码该往哪放。我自己在带新人做这个项目时从来不看他们功能做了几个只看两件事一是能不能把一次请求的完整链路画出来二是遇到报错时的排查顺序是不是从现象到根因有条理地推进。这两件事做到了这个项目的训练价值就吃到了做不到功能做得再全也只是复制粘贴。最后一个实用建议把项目跑通之后先别急着加功能花一个小时把web.xml、数据库建表脚本、BaseDao这三样东西从头到尾读一遍。这三处是整个项目的信息密度最高的地方读懂了它们剩下的代码基本都是同一套模式的重复。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →