ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM + easyUI 后台管理系统从搭建到权限落地实战解析

SSM + easyUI 后台管理系统从搭建到权限落地实战解析 “SSM easyUI 后台管理系统”大概是很多人学习路上的必经一站。这套组合从2015年前后开始流行一直到今天Spring Boot Vue 已经成为新项目的主流但大量已经上线的企业内部系统仍然跑在 SSM 这套架构上。哪怕你现在的项目已经切换到微服务SSM 时期积累的东西也一点没浪费Spring 的 IoC/DI、SpringMVC 的请求分发机制、MyBatis 的 ORM 思维这些底层逻辑换一层壳还在用。我最近刚好完整整理了一个基于 Spring SpringMVC MyBatis easyUI 的后台管理系统项目从环境搭建到权限设计到前端集成踩了不少坑也沉淀了一些很实用的经验。这篇文章把这些内容全部摊开讲适合正在做毕业设计、接手老旧系统维护、或者想通过一个完整项目理解框架协作机制的开发者。不需要你有三年经验只要你会 Java 基础语法、懂一点 SQL就能跟得上。先说一个很多人都纠结过的问题easyUI 都已经快没人提了我还有必要学吗我的答案是如果目标是理解后台管理系统的完整构建过程easyUI 反而是最省心的选择。它不像 Vue 那样需要 Node 环境、需要构建打包、需要跨域配置。引入几个 JS 和 CSS 文件写一个 table 标签数据表格就出来了。这个特性在当年被称为“拿过来就能用”在现在依然适合用来快速搭建内部工具和后台界面。更重要的是easyUI 的 DataGrid、Tree、Tabs 这些组件的交互逻辑和现在主流前端框架里的表格、树形控件在用户操作层面几乎是一样的。也就是说我们通过 easyUI 建立的“列表页 弹窗表单 树形菜单”的交互模型可以直接迁移到 Vue、React 项目上。这篇博文我会分五个部分来讲。第一章是整体设计与技术栈解析讲清楚为什么用这套组合而不是别的以及每层框架各自承担的职责。第二章是环境准备和项目骨架搭建包括数据库脚本、Maven 依赖、web.xml 配置这些让人头大的前置工作。第三章是核心配置与后端实现重点讲 Spring 整合 MyBatis 的细节、Mapper 代理机制的底层原理、以及一个通用的 CRUD 是怎么写出来的。第四章是 easyUI 前端集成和权限控制落地这也是全篇实操性最强的一部分我会直接给出代码片段和页面效果描述。第五章是常见问题与性能优化把我在这个项目里实际踩过的坑全部列出来比如 Jackson 格式化 LocalDateTime 报错、layui 和 easyUI 混用导致的样式错乱、分页插件 PageHelper 的线程隔离问题等等。每一章都会有可直接参考的代码、配置和排查逻辑。这套项目的技术含量其实不在“功能”上而在“理解”上。当你能把这四个框架串联起来让一次完整的“浏览器点击 → Controller 接收 → Service 处理 → Mapper 查库 → 数据返回 → DataGrid 渲染”流程在脑海中闭环你对 Java Web 开发的理解会上一个台阶。这个能力到今天仍然是值钱的。1. 整体设计与技术栈解析1.1 为什么是这四件套先把技术选型的逻辑讲明白。任何一个后台管理系统本质上做的事情就是两种对数据的增删改查以及对操作的人做权限控制。围绕这两件事选择什么框架核心考虑的是开发效率、团队熟悉度和系统维护成本。Spring 负责解耦。它通过 IoC 容器统一管理对象的创建和依赖关系。在传统写法里你要用 UserService就得手动 new 一个。在 Spring 世界里你只需要声明一个引用容器会自动把实例化好的对象注入进来。这个机制直接决定了 Service 层和 DAO 层之间可以用接口加注解的方式松散耦合。ServiceImpl 做成什么样子数据库怎么换上层代码都可以不动。SpringMVC 负责接收请求和调度。它是经典 MVC 模式的落地实现。DispatcherServlet 是整个请求链路的核心调度员它把 URL 映射到 Controller 方法把 Controller 返回的数据渲染成视图或者 JSON。之所以用 SpringMVC 而不是 Struts2主要是因为它和 Spring 是无缝集成——本质上是同一个生态不需要做适配层而且方法级别的映射粒度更细一个 URL 对应一个普通 Java 方法比 Struts2 的 Action 类加配置文件的方式直观得多。MyBatis 负责数据库访问。它是一个半自动 ORM 框架SQL 由开发者自己写映射关系用 XML 或注解声明。相比 Hibernate 的全自动映射MyBatis 给了你完全的 SQL 控制权复杂查询、多表联查、动态 SQL 都很好处理。后台管理系统里最多的就是各种报表和查询页面SQL 的可控性是刚需这也正是 MyBatis 在互联网公司内部系统里占有率极高的原因。easyUI 负责页面交互。它是基于 jQuery 的一套 UI 组件库提供表格、树、下拉框、弹窗、布局等组件。最关键的一点不需要前端工程化。把静态资源文件放到项目的 static 目录下页面上用 script 标签引入就能直接出效果。对于以数据管理为主、交互复杂度不高的后台系统这种模式比前后端分离的开发速度更快。1.2 三层架构怎么划分这个项目的代码分层是标准的 Java Web 三层架构按包名划分一目了然com.example.shop ├── controller # 控制层接收请求、参数校验、返回结果 ├── service # 业务层接口定义 ├── service.impl # 业务实现层核心业务逻辑 ├── mapper # 数据访问层接口定义 ├── mapper.xml # 数据访问层SQL 绑定 ├── entity # 实体类对应数据库表 ├── common # 公共组件分页、工具、常量 ├── interceptor # 拦截器登录校验、权限控制 └── exception # 全局异常处理Controller 层要薄只做参数接收和数据返回不写业务逻辑。Service 层是核心所有的业务规则都放在这里。Mapper 层只做最简单的数据读写不掺杂业务判断。这个分层不是教条而是为了“变化隔离”数据库变了只改 Mapper业务规则变了只改 ServiceURL 变了只改 Controller。我在实际项目中见过把业务逻辑全写在 Controller 里的代码一个方法六七百行后来要复用根本没法下手只能重写。分层的价值要在项目变大后才会真正体会得到。1.3 权限模型的设计思路后台管理系统的权限模型最常用的是 RBAC也就是角色-权限控制模型。三个核心表用户表、角色表、中间关联表。用户被分配角色角色绑定权限集合一个用户可能有多个角色一个角色也能分配给多个用户典型的多对多关系。再往下做一个简化。实现层面用“菜单权限”来做也就是每个权限点对应一个菜单项或一个按钮后台系统里的权限判断就收敛成“用户能不能看到这个菜单”和“用户能不能执行这个操作”两个维度。菜单存表URL 也存表通过角色关联到用户登录时把该用户的菜单和权限加载进 Session。这种方式简单直接对于中后台的管理系统足够用。如果是涉及大金额操作或敏感数据、安全要求更高的系统建议引入更细粒度的数据权限并增加操作日志审计但基础骨架依然是 RBAC。2. 环境准备与项目骨架搭建2.1 开发环境与版本选择这一步踩过的坑最多先给出一份稳定的版本组合JDK 1.8不用更高避免和旧版 Tomcat 及 Spring 版本冲突Maven 3.6.3Tomcat 8.5MySQL 5.7IDEA 2020任何版本均可easyUI 1.9.x版本之间会打架这是我反复试出来的一个结论。Spring 4.x 配 JDK 8 就是比配 JDK 11 顺手MyBatis 3.4.x 配 MySQL 5.7 就是比配 MySQL 8.0 少遇到时区问题。不是说新版本不行而是这个项目属于经典技术栈的传承选一个各方都“磨合过”的组合能省下很多排查环境问题的功夫。如果你确实要用 MySQL 8.0记得在 JDBC 连接串后面加上serverTimezoneAsia/Shanghai否则时间字段的读写会报异常。2.2 Maven 依赖的完整配置在pom.xml里除了常规的 spring-webmvc、spring-jdbc、mybatis 之外有几个依赖特别容易漏漏了就会在运行时莫名其妙报错。我把完整依赖列出来注释也写上properties spring.version5.1.20.RELEASE/spring.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis及Spring整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency !-- 连接池Druid -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.23/version /dependency !-- 分页插件 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.2.13/version /dependency !-- Jackson JSON 处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.12.5/version /dependency /dependenciespagehelper 的依赖在这里加的是 starter 形式如果你在非 Spring Boot 环境使用应该改为pagehelper核心包然后手动配置HelperDialect和reasonable参数。避免依赖混乱。2.3 数据库设计与初始化脚本以一个订单管理模块来贯穿这个项目那么数据库至少需要这几张核心表sys_user用户表字段包括用户ID、用户名、密码MD5加密存储、真实姓名、手机号、状态sys_role角色表角色ID、角色名称、角色编码、描述sys_user_role用户角色关联表sys_menu菜单表菜单ID、父菜单ID、菜单名称、菜单URL、权限标识、图标sys_role_menu角色菜单关联表biz_order业务订单表订单ID、订单编号、用户ID、商品名称、商品数量、订单金额、订单状态、创建时间用户、角色、菜单之间的关联表设计是这套权限系统的核心。我建议把关联表当作独立实体来对待而不是“顺手加个字段”。加字段的写法看起来简单但角色和菜单的关系一旦复杂起来一个角色有多个菜单一个菜单被多个角色持有字段法就完全失控了。关联表可以后续加索引、加额外属性比如创建时间而不用动主表结构。初始化 SQL 不需要全部贴出来但必须要强调一点字符集统一设置成utf8mb4排序规则用utf8mb4_general_ci否则存储中文和表情符号时会出现乱码。我见过一个系统上线半年后用户反馈昵称显示成问号就是建表时偷懒用了默认字符集导致的。2.4 web.xml 与 Spring 配置文件加载顺序这是一个新手必踩的坑区。web.xml 的配置顺序决定了 Spring 容器和 SpringMVC 容器的初始化顺序也就决定了 Bean 能不能被正确创建。下面是正确的配置逻辑context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/applicationContext-*.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mapping这里有个关键的父子容器概念需要理解清楚。ContextLoaderListener 负责创建父容器加载的是 Spring 配置包括 Service、Mapper、DataSource 这些 Bean。DispatcherServlet 创建的是子容器加载的是 spring-mvc.xml包括 Controller、视图解析器、处理器映射这些 Web 层组件。子容器可以访问父容器的 Bean反过来不行。实际操作中很多人容易把url-pattern配成*.do或者/api/*然后发现静态资源全部 404。我的建议是配成/让 DispatcherServlet 接管所有请求然后再用mvc:resources来放行静态资源。这样 URL 更干净Controller 的 RequestMapping 不用带后缀。2.5 log4j2 日志配置日志这事很多人不重视等出了问题才追悔莫及。mybatis 的 SQL 输出依赖日志框架如果你没配日志那么 mapper 里的 SQL 一句都看不懂排查问题就等于盲人摸象。我自己比较推荐 log4j2性能比亚军 logback 好而且是真异步。?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console /Appenders Loggers Logger namecom.example.shop.mapper levelDEBUG/ Root levelINFO AppenderRef refConsole/ /Root /Loggers /Configuration重点在最后一行com.example.shop.mapper这个包名要设置成 DEBUG 级别。这样 MyBatis 会在控制台直接打印出每个 Mapper 方法的执行 SQL 和参数。查数据与实际不匹配的问题扫一眼控制台就能定位到是哪条 SQL、哪个参数的问题。3. 核心配置与后端实现3.1 Spring 整合 MyBatis 的两种方式Spring 整合 MyBatis业界有两种方式一种是使用MapperFactoryBean手动配置另一种是使用MapperScannerConfigurer自动扫描。手动配置只适合 Mapper 接口特别少的项目一旦接口多起来每加一个接口都要改一次配置文件烦死人。于是主流方案是扫描。bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.shop.mapper/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /bean这个扫描器做的事情是把你指定包下的所有接口在 Spring 容器启动时动态生成代理对象并注册为 Bean。用到的时候直接注入不需要你手动写实现类。这就是 MyBatis 最核心的 Mapper 代理机制——你写一个接口配一段 XML框架帮你生成实现这也是“半自动”的含义所在。3.2 事务管理配置如果项目里有下单、支付这类涉及多张表更新的操作事务就是必须要开的。Spring 声明式事务最简单的方式就是注解驱动。bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/然后在需要事务的方法上加上Transactional注解。比如一个订单创建方法会执行“插入订单表 扣减库存 写操作日志”三个操作只要其中一个失败了全部回滚。这就是Transactional的意义。有一个很容易被忽视的细节Transactional的默认回滚条件是 RuntimeException 和 Error。如果你的代码抛的是 checked exception比如Exception事务不会回滚。这是不少项目里数据不一致的直接原因。踩过坑之后我现在习惯在注解上明确写一段Transactional(rollbackFor Exception.class)这是肖复式的最佳实践值得写进团队规范。3.3 通用 CRUD 接口设计与实现后端设计不能只盯着“写出来”还要考虑“可扩展”。我这个项目的 CRUD 接口按照一个通用模型来设计很多地方可以复用。先定义统一返回结果public class Result { private int code; private String msg; private long count; private Object data; public static Result success(Object data, long count) { Result r new Result(); r.setCode(200); r.setMsg(success); r.setCount(count); r.setData(data); return r; } // getter/setter 略 }count字段特意提供给 easyUI 的 DataGrid 使用它要求分页数据里带总数。像一个典型的分页查询接口Controller 是这样写的Controller RequestMapping(/order) public class OrderController { Resource private OrderService orderService; RequestMapping(/page) ResponseBody public Result page(OrderQuery query) { PageHelper.startPage(query.getPage(), query.getRows()); ListOrder orders orderService.queryOrderPage(query); PageInfoOrder pageInfo new PageInfo(orders); return Result.success(pageInfo.getList(), pageInfo.getTotal()); } }这里用到了 PageHelper 分页插件。它的原理很巧在你调用PageHelper.startPage()之后执行下一条 SQL 时插件会自动拦截生成带limit的查询语句并额外执行一条select count(*)来拿总数。这一套被封装得极其优雅也让分页代码异常简洁。有一个实战中最常见的坑在后面第四章里会专门讲。3.4 SpringMVC 请求链路与参数绑定很多人会用 SpringMVC但不清楚一次请求具体发生了什么。在这里展开讲一下因为理解了链路后面调 bug 会快很多。浏览器发送一个请求/order/page?page1rows10后首先进入 DispatcherServlet。DispatcherServlet 通过 HandlerMapping 找到匹配的 Controller 方法。然后通过 HandlerAdapter 调用方法方法执行完返回 Result 对象由于方法上有ResponseBody会交给 Jackson 的 HttpMessageConverter把对象序列化成 JSON 字符串返回给浏览器。参数绑定是 SpringMVC 的看家本领。请求里的page和rows会自动绑定到 OrderQuery 对象上不需要手动 getParameter。如果你遇到参数绑不上大部分原因是参数名不一致或者缺少无参构造方法。还有个常见坑如果前端传的是时间字符串后端实体类里是Date类型不处理的话会报 400 错误。解决办法是加一个全局格式化配置。在 spring-mvc.xml 中注册mvc:annotation-driven conversion-serviceconversionService/ bean idconversionService classorg.springframework.format.support.FormattingConversionServiceFactoryBean property nameformatters list bean classorg.springframework.format.datetime.DateFormatter property namepattern valueyyyy-MM-dd HH:mm:ss/ /bean /list /property /bean这个配置可以直接解决前后端时间格式不匹配的问题。这个问题在我经历过的项目里几乎每个新人都必踩。3.5 拦截器实现登录与权限校验登录拦截是这个系统的安全底线。用 SpringMVC 拦截器来实现比在每个 Controller 里重复写判断代码优雅得多。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { // 是异步请求还是页面跳转需要区分处理 String header request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(header)) { response.setContentType(application/json;charsetutf-8); response.getWriter().print({\code\:401,\msg\:\not login\}); } else { response.sendRedirect(request.getContextPath() /login); } return false; } return true; } }注册拦截器时要注意放行登录接口和静态资源mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ bean classcom.example.shop.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors为什么异步请求要单独判断因为直接跳转页面会导致前端收到 HTML 而不是 JSONDataGrid 组件解析时报错。这是拦截器实现中很容易被忽视的细节。而真正的项目里通常不止这一层还需要再写一个 PermissionInterceptor 来校验菜单权限判断当前用户的角色是否有访问权限。4. easyUI 前端集成与权限控制落地4.1 easyUI 的环境引入easyUI 在技术层次上走了另一个路线但特别适合快速见效将官方压缩包里的资源拷贝到项目的 static 目录然后在 HTML 页面用 link 和 script 引入 css 和 JS 文件即可。link relstylesheet typetext/css href${pageContext.request.contextPath}/static/easyui/themes/default/easyui.css link relstylesheet typetext/css href${pageContext.request.contextPath}/static/easyui/themes/icon.css script typetext/javascript src${pageContext.request.contextPath}/static/easyui/jquery.min.js/script script typetext/javascript src${pageContext.request.contextPath}/static/easyui/jquery.easyui.min.js/script不做构建、不做打包整个初始化过程再简单不过。第一行用了${pageContext.request.contextPath}带上项目上下文路径这在所有 JSP 页面里都应该保持这种写法否则部署到带路径的服务器上就会发现资源全部 404。4.2 DataGrid 数据表格的完整用法DataGrid 是 easyUI 里使用频率最高的组件对应后台系统最核心的需求——数据列表展示。我用一个订单列表的例子来说明。table idorderTable thead tr th fieldorderNo width120订单编号/th th fieldproductName width150商品名称/th th fieldorderAmount width100订单金额/th th fieldorderStatus width80 formatterformatStatus状态/th th fieldcreateTime width150创建时间/th /tr /thead /table对应的初始化脚本$(#orderTable).datagrid({ url: /order/page, method: get, pagination: true, pageSize: 10, pageList: [10, 20, 50], fitColumns: true, singleSelect: true, onLoadError: function(data) { if (data data.status 401) { window.location.href /login; } } });url 指向后端的/order/page接口我把参数page和rows直接通过 query 传过去这样后端只要按照分页参数的标准命名来接收前端表格不用再单独写请求逻辑。DataGrid 有一组默认请求参数约定可以在queryParams里覆盖所以后端使用RequestParam接收时一定注意这个参数约定。formatter 是一个使用率极高的功能专门用来做列格式化。比如状态列的数字需要转成文字function formatStatus(value, row, index) { var statusMap {0: 待支付, 1: 已支付, 2: 已发货, 3: 已完成}; return statusMap[value] || 未知; }这里有个经验DataGrid 的列字段必须和后端 JSON 里的 key 一一对应任何一边拼错这一列就是空的。比如数据库字段叫order_no后端如果右是orderNo而实体类里忘了加JsonProperty前端永远是 undefined。这个 bug 极其隐蔽排查了大半天才发现。4.3 表单弹窗实现新增与编辑列表页永远搭配一个表单弹窗。easyUI 的 Dialog 组件配合 Form 组件function openAddDialog() { $(#orderDialog).dialog({ title: 新增订单, width: 500, height: 350, closed: false, modal: true, buttons: [{ text: 保存, iconCls: icon-ok, handler: function() { submitForm(); } },{ text: 取消, iconCls: icon-cancel, handler: function() { $(#orderDialog).dialog(close); } }] }); } function submitForm() { $(#orderForm).form(submit, { url: /order/save, onSubmit: function() { return $(this).form(validate); // 校验不通过时不提交 }, success: function(result) { var obj JSON.parse(result); if (obj.code 200) { $.messager.alert(提示, 保存成功, info); $(#orderDialog).dialog(close); $(#orderTable).datagrid(reload); } else { $.messager.alert(提示, obj.msg, error); } } }); }表单组件有一个“伪 ajax 提交”的行为——实际上是创建了一个隐藏的 iframe表单提交到 iframe 里。所以 success 回调里拿到的返回体是字符串要自己 JSON.parse 一下。这个机制很多人刚用时都会踩坑明明接口返回了 {code:200,msg:success}前端却拿不到那是因为没转换。这个细节是 data 里面最常见的坑之一。编辑场景复用弹窗时要注意打开弹窗前要先把要编辑的数据“回显”。function openEditDialog() { var row $(#orderTable).datagrid(getSelected); if (!row) { $.messager.alert(提示, 请先选择一条记录, warning); return; } $(#orderForm).form(load, row); $(#orderDialog).dialog({title: 编辑订单}); }form(load)会自动按 name 匹配表单字段把 row 里的数据填进去。前提条件表单里的输入框 name 属性要和字段名完全一致否则回显空白。4.4 权限控制的管理端配置权限控制的落地需要一套“菜单管理”界面。用 easyUI 的 Tree 组件来展示菜单树$(#menuTree).tree({ url: /menu/tree, method: get, checkbox: true, cascadeCheck: false });后端返回的数据格式是 easyUI 规定的嵌套结构[{ id: 1, text: 系统管理, children: [{ id: 11, text: 用户管理, url: /user/list }] }]注意字段必须是id、text、children不是数据库里的menuId、menuName、childMenus。你需要做一个 DTO 转换。这里是 easyUI 这类老式组件和前端工程化框架的最大区别——它定义了一套数据协议你必须去适配它。选中某些节点后点击保存var nodes $(#menuTree).tree(getChecked); var nodeIds nodes.map(function(node) { return node.id; }); $.post(/role/saveMenus, {roleId: roleId, menuIds: nodeIds.join(,)}, function(result) { ... });后端接收到 menuIds 字符串之后按逗号分割并插入sys_role_menu表。这里有个细节保存角色菜单权限时要先删掉该角色的所有旧关联再插入新的。这是权限管理里应该执行的“先删后插”逻辑否则角色删除某个菜单时会很混乱极其容易出 bug。4.5 easyUI 与主流前端框架的核心差异写完上面这些把 easyUI 和 Vue 的差异说清楚就能更明白为什么现在用 easyUI 写后台还有意义。它们的核心区别在于“数据驱动 vs 直接操作 DOM”。easyUI 是直接操作 DOM 的命令式写法先有一个table标签然后初始化成 datagrid再调方法去 reload、去 getSelected。Vue/Element UI 是声明式写法你只需要维护数据模型页面会自动根据数据变化刷新 DOM。这个差异带来的影响是easyUI 上手快、没有构建链路、直接就能在浏览器里看效果适合简单系统Vue 复杂度和灵活性都高适合大型项目。但我还是建议每个 Java 开发者都亲手写一遍 easyUI 项目因为它能让你理解页面和数据的互动细节。理解了 jQuery 式的 DOM 操作之后再去看 Vue 的响应式原理会有一种“原来框架帮我省了这么多事”的感觉。5. 常见问题与排查技巧实录5.1 PageHelper 分页失灵这是我遇到的最典型的问题。加了startPage之后查询结果还是全部数据。排查之后发现原因是对 PageHelper 的执行机制理解不够。PageHelper.startPage()之后发出的第一条 SQL 才会被作为物理分页查询处理。如果你在它之前执行了其他查询那么分页就会落在那次查询上。更隐蔽的问题是要注意 PageHelper 的线程隔离机制。它的实现原理是使用 ThreadLocal 保存分页参数。如果代码里开了多线程比如并行查询子线程里是拿不到分页参数的。这在普通项目里不常遇到但一旦遇到就很头疼。ALI 的规范也有明确提示使用 ThreadLocal 必须清理特别是在配合线程池、异步任务或 Tomcat 线程重用时分页参数可能被错误地带到下一次请求。所以切忌在开了异步任务的方法里顺手调用 startPage。5.2 实体类的 Date 字段渲染异常典型的报错如下Caused by: java.lang.IllegalArgumentException: Cannot format given Object as a Date原因是MyBatis 查出的 java.util.Date 在 Jackson 序列化时格式不对。配置一个全局的 ObjectMapper BeanBean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); }或者基于 Java 8 的 LocalDateTime 字段做时间序列化。核心原则实体类统一使用 java.util.Date 或 LocalDateTime不要混用全局的时间格式需要统一配置。5.3 easyUI 和 layui 混用引发的样式错乱有段时间我想在项目里既用 easyUI 又用 layui结果页面上一片混乱弹窗的位置、层级、样式全都对不上。原因是两者都重写了大量基础样式和弹层组件冲突非常严重。我的建议是别混用。选一个组件库就全部用它即使它的一些组件不如另一个好看。实在要引入第三方库来做某些特有功能比如富文本编辑器可以用 easyUI 的 dialog 把它纳入管理。核心思想组件层只能有一个主导框架其余必须是孤立插件。5.4 前端请求 404 排查清单遇到 404按顺序排查这几项检查 URL 是否带了项目上下文路径比如/order/page还是/shop/order/page检查 Controller 的 RequestMapping 是不是匹配上了完整路径检查静态目录是否被拦截器误拦截检查 web.xml 的 servlet mapping 是不是/把 HTML、JS、图片全部拦进去了这里有个项目现场经验如果静态资源请求被 DispatcherServlet 接管后仍然显示 404最可能是 spring-mvc.xml 里没配置mvc:default-servlet-handler/或者mvc:resources。推荐用mvc:resources mapping/static/** location/static//把静态资源统一放在 static 目录下然后用mvc:resources放行是最快的解决方案。5.5 MyBatis 动态 SQL 与 N1 查询后台管理系统最容易出现 N1 查询问题。比如查订单列表后还要关联查询每个订单的商品名称、用户名称、操作日志。如果在循环里逐条调用 Mapper 方法系统会重复查数据库很多次接口变慢。正确的解法是使用 MyBatis 的关联查询或结果映射。在 XML 里用collection标签一次性把嵌套查询带出来resultMap idOrderDetailMap typecom.example.shop.entity.Order id propertyorderId columnorder_id/ result propertyorderNo columnorder_no/ collection propertyitems ofTypecom.example.shop.entity.OrderItem id propertyitemId columnitem_id/ result propertyproductName columnproduct_name/ result propertyquantity columnquantity/ /collection /resultMap select idselectOrderDetail parameterTypelong resultMapOrderDetailMap SELECT o.order_id, o.order_no, oi.item_id, oi.product_name, oi.quantity FROM biz_order o LEFT JOIN biz_order_item oi ON o.order_id oi.order_id WHERE o.order_id #{orderId} /select这个写法保持 SQL 可控可调优也避免了 N1 查询。开发后台系统时这种“一次查出关联数据”的思路要时刻放在心上。5.6 静态资源缓存与浏览器刷新问题改完 JS 或 CSS 后浏览器里怎么刷新都是旧效果。这是前端开发里最烦人的问题之一。easyUI 页面没有构建工具没办法自动加版本号。我的做法给静态资源手动加版本参数。script src/static/js/order.js?v20250120/script每次改完文件同步把 v 参数改一下。临时性强但零成本而且不会误伤。6. 可扩展方向与维护建议6.1 从 SSM 向 Spring Boot 迁移的路径如果你在这个项目上练完手下一步就是把它改造成 Spring Boot 项目。本质上业务代码几乎不需要动Controller、Service、Mapper 接口和 XML 全都能复用。真正要处理的只有这么几个点配置文件合并为 application.ymlweb.xml 的内容替换为配置类或注解用 Spring Boot 的自动装配替换一系列塞满 Bean 的 XML依赖简化之后pom.xml 瘦身明显。以 PageHelper 为例引入pagehelper-spring-boot-starter后连分页插件的 bean 配置都省了只要在 application.yml 里写上pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: truereasonable这一项很有用它能让传入的页码参数自动做边界修正。页码小于 1 就查第 1 页超过总页数就查最后一页这个对于前端不传参数时的兜底逻辑很关键。6.2 引入隐私保护与操作审计后台系统涉及用户数据时光做登录拦截不够。实际操作中我强烈建议至少加三样东西操作日志、访问审计、密码加密。尤其是密码加密要坚决杜绝明文存储。用 BCrypt 加盐哈希比 MD5 加固定 salt 的方式安全得多。安全无小事。操作日志这一块可以用 Spring AOP 统一处理。写一个切面拦截Log注解标记的方法自动记录操作人、操作时间、请求参数。这样就不用每个方法手动写日志代码了这是这个项目可以立即落地的优化点。Aspect Component public class LogAspect { Around(annotation(log)) public Object around(ProceedingJoinPoint joinPoint, Log log) throws Throwable { long start System.currentTimeMillis(); Object result joinPoint.proceed(); long elapsed System.currentTimeMillis() - start; // 将操作人、IP、耗时、参数写入日志表 return result; } }6.3 不用刻意断言 “学会了”最后说点掏心窝子的体会。我见过不少同学把 Spring、SpringMVC、MyBatis、easyUI 背得滚瓜烂熟各种注解的作用倒背如流配置项说得头头是道但真给他一台服务器、一个空白数据库让他从头搭一套系统他就卡住了。原因在哪顺序错了。框架是用来解决实际问题的不是用来背的。你看这篇博文的时候别只是在看找个环境把这个项目完整跑起来。从数据库建表开始到写完第一个分页查询页面中间每一个报错都是宝贵的经验。那个“浏览器点一下数据从 MySQL 里查出来再渲染到表格里”的过程只有亲手跑通一次才算真正把这条链路刻在脑海里。这个项目做完了后面的路其实还很长。Spring Boot 帮你省掉了配置的繁琐MyBatis-Plus 让 CRUD 更加简洁Vue 让前端体验更好但这些都是在同一个后端思想里长出的一层又一层的新内容。根扎稳了上面长什么都顺。
返回列表