Mybatis 的物理分页是应用中的一个难点,特别是配合检索和排序功能叠加时更是如此。
我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,新年第一天,特此发文回馈网站。
特别鸣谢 paginator项目 (https://github.com/miemiedev/mybatis-paginator ) ,阅读源码帮助很大。
【背景】
项目框架是 SpringMVC+Mybatis, 需求是想采用自定义的分页标签,同时,要尽量少的影响业务程序开发的。
如果你已经使用了JS框架( 如:Ext,EasyUi等)自带的分页机能,是属于前端分页,不在本文讨论范围。
【关于问题】
大多数分页器会使用在查询页面,要考虑以下问题:
1)分页时是要随时带有最近一次查询条件
2)不能影响现有的sql,类似aop的效果
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
4)尽量少的影响现有service等接口
【关于依赖库】
Google Guava 作为基础工具包
Commons JXPath 用于对象查询 (1/23日版改善后,不再需要)
Jackson 向前台传送Json格式数据转换用
【关于适用数据库】
现在只适用mysql
(如果需要用在其他数据库可参考 paginator的Dialect部分,改动都不大)
首先是Page类,比较简单,保存分页相关的所有信息,涉及到分页算法。虽然“其貌不扬”,但很重要。后面会看到这个page类对象会以“信使”的身份出现在全部与分页相关的地方。
/** * 封装分页数据 */ import java.util.List; import java.util.Map; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; public class Page { private static final Logger logger = LoggerFactory.getLogger(Page.class); private static ObjectMapper mapper = new ObjectMapper(); public static String DEFAULT_PAGESIZE = "10"; private int pageNo; //当前页码 private int pageSize; //每页行数 private int totalRecord; //总记录数 private int totalPage; //总页数 private Map<String, String> params; //查询条件 private Map<String, List<String>> paramLists; //数组查询条件 private String searchUrl; //Url地址 private String pageNoDisp; //可以显示的页号(分隔符"|",总页数变更时更新) private Page() { pageNo = 1; pageSize = Integer.valueOf(DEFAULT_PAGESIZE); totalRecord = 0; totalPage = 0; params = Maps.newHashMap(); paramLists = Maps.newHashMap(); searchUrl = ""; pageNoDisp = ""; } public static Page newBuilder(int pageNo, int pageSize, String url){ Page page = new Page(); page.setPageNo(pageNo); page.setPageSize(pageSize); page.setSearchUrl(url); return page; } /** * 查询条件转JSON */ public String getParaJson(){ Map<String, Object> map = Maps.newHashMap(); for (String key : params.keySet()){ if ( params.get(key) != null ){ map.put(key, params.get(key)); } } String json=""; try { json = mapper.writeValueAsString(map); } catch (Exception e) { logger.error("转换JSON失败", params, e); } return json; } /** * 数组查询条件转JSON */ public String getParaListJson(){ Map<String, Object> map = Maps.newHashMap(); for (String key : paramLists.keySet()){ List<String> lists = paramLists.get(key); if ( lists != null && lists.size()>0 ){ map.put(key, lists); } } String json=""; try { json = mapper.writeValueAsString(map); } catch (Exception e) { logger.error("转换JSON失败", params, e); } return json; } /** * 总件数变化时,更新总页数并计算显示样式 */ private void refreshPage(){ //总页数计算 totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : (totalRecord/pageSize + 1); //防止超出最末页(浏览途中数据被删除的情况) if ( pageNo > totalPage && totalPage!=0){ pageNo = totalPage; } pageNoDisp = computeDisplayStyleAndPage(); } /** * 计算页号显示样式 * 这里实现以下的分页样式("[]"代表当前页号),可根据项目需求调整 * [1],2,3,4,5,6,7,8..12,13 * 1,2..5,6,[7],8,9..12,13 * 1,2..6,7,8,9,10,11,12,[13] */ private String computeDisplayStyleAndPage(){ List<Integer> pageDisplays = Lists.newArrayList(); if ( totalPage <= 11 ){ for (int i=1; i<=totalPage; i++){ pageDisplays.add(i); } }else if ( pageNo < 7 ){ for (int i=1; i<=8; i++){ pageDisplays.add(i); } pageDisplays.add(0);// 0 表示 省略部分(下同) pageDisplays.add(totalPage-1); pageDisplays.add(totalPage); }else if ( pageNo> totalPage-6 ){ pageDisplays.add(1); pageDisplays.add(2); pageDisplays.add(0); for (int i=totalPage-7; i<=totalPage; i++){ pageDisplays.add(i); } }else{ pageDisplays.add(1); pageDisplays.add(2); pageDisplays.add(0); for (int i=pageNo-2; i<=pageNo+2; i++){ pageDisplays.add(i); } pageDisplays.add(0); pageDisplays.add(totalPage-1); pageDisplays.add(totalPage); } return Joiner.on("|").join(pageDisplays.toArray()); } public int getPageNo() { return pageNo; } public void setPageNo(int pageNo) { this.pageNo = pageNo; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getTotalRecord() { return totalRecord; } public void setTotalRecord(int totalRecord) { this.totalRecord = totalRecord; refreshPage(); } public int getTotalPage() { return totalPage; } public void setTotalPage(int totalPage) { this.totalPage = totalPage; } public Map<String, String> getParams() { return params; } public void setParams(Map<String, String> params) { this.params = params; } public Map<String, List<String>> getParamLists() { return paramLists; } public void setParamLists(Map<String, List<String>> paramLists) { this.paramLists = paramLists; } public String getSearchUrl() { return searchUrl; } public void setSearchUrl(String searchUrl) { this.searchUrl = searchUrl; } public String getPageNoDisp() { return pageNoDisp; } public void setPageNoDisp(String pageNoDisp) { this.pageNoDisp = pageNoDisp; } }
然后是最核心的拦截器了。涉及到了mybatis的核心功能,期间阅读大量mybatis源码几经修改重构,辛苦自不必说。
核心思想是将拦截到的select语句,改装成select count(*)语句,执行之得到,总数据数。再根据page中的当前页号算出limit值,拼接到select语句后。
为简化代码使用了Commons JXPath 包,做对象查询。
/** * 分页用拦截器 */ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.Properties; import org.apache.commons.jxpath.JXPathContext; import org.apache.commons.jxpath.JXPathNotFoundException; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.parameter.DefaultParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.MappedStatement.Builder; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; @Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })}) public class PageInterceptor implements Interceptor{ public Object intercept(Invocation invocation) throws Throwable { //当前环境 MappedStatement,BoundSql,及sql取得 MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; BoundSql boundSql = mappedStatement.getBoundSql(parameter); String originalSql = boundSql.getSql().trim(); Object parameterObject = boundSql.getParameterObject(); //Page对象获取,“信使”到达拦截器! Page page = searchPageWithXpath(boundSql.getParameterObject(),".","page","*/page"); if(page!=null ){ //Page对象存在的场合,开始分页处理 String countSql = getCountSql(originalSql); Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection() ; PreparedStatement countStmt = connection.prepareStatement(countSql); BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql); DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS); parameterHandler.setParameters(countStmt); ResultSet rs = countStmt.executeQuery(); int totpage=0; if (rs.next()) { totpage = rs.getInt(1); } rs.close(); countStmt.close(); connection.close(); //分页计算 page.setTotalRecord(totpage); //对原始Sql追加limit int offset = (page.getPageNo() - 1) * page.getPageSize(); StringBuffer sb = new StringBuffer(); sb.append(originalSql).append(" limit ").append(offset).append(",").append(page.getPageSize()); BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, sb.toString()); MappedStatement newMs = copyFromMappedStatement(mappedStatement,new BoundSqlSqlSource(newBoundSql)); invocation.getArgs()[0]= newMs; } return invocation.proceed(); } /** * 根据给定的xpath查询Page对象 */ private Page searchPageWithXpath(Object o,String... xpaths) { JXPathContext context = JXPathContext.newContext(o); Object result; for(String xpath : xpaths){ try { result = context.selectSingleNode(xpath); } catch (JXPathNotFoundException e) { continue; } if ( result instanceof Page ){ return (Page)result; } } return null; } /** * 复制MappedStatement对象 */ private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) { Builder builder = new Builder(ms.getConfiguration(),ms.getId(),newSqlSource,ms.getSqlCommandType()); builder.resource(ms.getResource()); builder.fetchSize(ms.getFetchSize()); builder.statementType(ms.getStatementType()); builder.keyGenerator(ms.getKeyGenerator()); builder.keyProperty(ms.getKeyProperty()); builder.timeout(ms.getTimeout()); builder.parameterMap(ms.getParameterMap()); builder.resultMaps(ms.getResultMaps()); builder.resultSetType(ms.getResultSetType()); builder.cache(ms.getCache()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache()); return builder.build(); } /** * 复制BoundSql对象 */ private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) { BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); for (ParameterMapping mapping : boundSql.getParameterMappings()) { String prop = mapping.getProperty(); if (boundSql.hasAdditionalParameter(prop)) { newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop)); } } return newBoundSql; } /** * 根据原Sql语句获取对应的查询总记录数的Sql语句 */ private String getCountSql(String sql) { return "SELECT COUNT(*) FROM (" + sql + ") aliasForPage"; } public class BoundSqlSqlSource implements SqlSource { BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) { this.boundSql = boundSql; } public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } public Object plugin(Object arg0) { return Plugin.wrap(arg0, this); } public void setProperties(Properties arg0) { } }
到展示层终于可以轻松些了,使用了文件标签来简化前台开发。
采用临时表单提交,CSS使用了Bootstrap。
<%@tag pageEncoding="UTF-8"%> <%@ attribute name="page" type="cn.com.intasect.ots.common.utils.Page" required="true"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <% int current = page.getPageNo(); int begin = 1; int end = page.getTotalPage(); request.setAttribute("current", current); request.setAttribute("begin", begin); request.setAttribute("end", end); request.setAttribute("pList", page.getPageNoDisp()); %> <script type="text/javascript"> var paras = '<%=page.getParaJson()%>'; var paraJson = eval('(' + paras + ')'); //将提交参数转换为JSON var paraLists = '<%=page.getParaListJson()%>'; var paraListJson = eval('(' + paraLists + ')'); function pageClick( pNo ){ paraJson["pageNo"] = pNo; paraJson["pageSize"] = "<%=page.getPageSize()%>"; var jsPost = function(action, values, valueLists) { var id = Math.random(); document.write('<form id="post' + id + '" name="post'+ id +'" action="' + action + '" method="post">'); for (var key in values) { document.write('<input type="hidden" name="' + key + '" value="' + values[key] + '" />'); } for (var key2 in valueLists) { for (var index in valueLists[key2]) { document.write('<input type="hidden" name="' + key2 + '" value="' + valueLists[key2][index] + '" />'); } } document.write('</form>'); document.getElementById('post' + id).submit(); } //发送POST jsPost("<%=page.getSearchUrl()%>", paraJson, paraListJson); } </script> <div class="page-pull-right"> <% if (current!=1 && end!=0){%> <button class="btn btn-default btn-sm" onclick="pageClick(1)">首页</button> <button class="btn btn-default btn-sm" onclick="pageClick(${current-1})">前页</button> <%}else{%> <button class="btn btn-default btn-sm" >首页</button> <button class="btn btn-default btn-sm" >前页</button> <%} %> <c:forTokens items="${pList}" delims="|" var="pNo"> <c:choose> <c:when test="${pNo == 0}"> <label style="font-size: 10px; width: 20px; text-align: center;">•••</label> </c:when> <c:when test="${pNo != current}"> <button class="btn btn-default btn-sm" onclick="pageClick(${pNo})">${pNo}</button> </c:when> <c:otherwise> <button class="btn btn-primary btn-sm" style="font-weight:bold;">${pNo}</button> </c:otherwise> </c:choose> </c:forTokens> <% if (current<end && end!=0){%> <button class="btn btn-default btn-sm" onclick="pageClick(${current+1})">后页</button> <button class="btn btn-default btn-sm" onclick="pageClick(${end})">末页</button> <%}else{%> <button class="btn btn-default btn-sm">后页</button> <button class="btn btn-default btn-sm">末页</button> <%} %> </div>
注意“信使”在这里使出了浑身解数,7个主要的get方法全部用上了。
page.getPageNo() //当前页号 page.getTotalPage() //总页数 page.getPageNoDisp() //可以显示的页号 page.getParaJson() //查询条件 page.getParaListJson() //数组查询条件 page.getPageSize() //每页行数 page.getSearchUrl() //Url地址(作为action名称)
到这里三个核心模块完成了。然后是拦截器的注册。
【拦截器的注册】
需要在mybatis-config.xml 中加入拦截器的配置
<plugins> <plugin interceptor="cn.com.dingding.common.utils.PageInterceptor"> </plugin> </plugins>
【相关代码修改】
首先是后台代码的修改,Controller层由于涉及到查询条件,需要修改的内容较多。
1)入参需增加 pageNo,pageSize 两个参数
2)根据pageNo,pageSize 及你的相对url构造page对象。(
3)最重要的是将你的其他入参(查询条件)保存到page中
4)Service层的方法需要带着page这个对象(最终目的是传递到sql执行的入参,让拦截器识别出该sql需要分页,同时传递页号)
5)将page对象传回Mode中
修改前
@RequestMapping(value = "/user/users") public String list( @ModelAttribute("name") String name, @ModelAttribute("levelId") String levelId, @ModelAttribute("subjectId") String subjectId, Model model) { model.addAttribute("users",userService.selectByNameLevelSubject( name, levelId, subjectId)); return USER_LIST_JSP; }
修改后
@RequestMapping(value = "/user/users") public String list( @RequestParam(required = false, defaultValue = "1") int pageNo, @RequestParam(required = false, defaultValue = "5") int pageSize, @ModelAttribute("name") String name, @ModelAttribute("levelId") String levelId, @ModelAttribute("subjectId") String subjectId, Model model) { // 这里是“信使”诞生之地,一出生就加载了很多重要信息! Page page = Page.newBuilder(pageNo, pageSize, "users"); page.getParams().put("name", name); //这里再保存查询条件 page.getParams().put("levelId", levelId); page.getParams().put("subjectId", subjectId); model.addAttribute("users",userService.selectByNameLevelSubject( name, levelId, subjectId, page)); model.addAttribute("page", page); //这里将page返回前台 return USER_LIST_JSP; }
注意pageSize的缺省值决定该分页的每页数据行数 ,实际项目更通用的方式是使用配置文件指定。
Service层
拦截器可以自动识别在Map或Bean中的Page对象。
如果使用Bean需要在里面增加一个page项目,Map则比较简单,以下是例子。
@Override public List<UserDTO> selectByNameLevelSubject(String name, String levelId, String subjectId, Page page) { Map<String, Object> map = Maps.newHashMap(); levelId = DEFAULT_SELECTED.equals(levelId)?null: levelId; subjectId = DEFAULT_SELECTED.equals(subjectId)?null: subjectId; if (name != null && name.isEmpty()){ name = null; } map.put("name", name); map.put("levelId", levelId); map.put("subjectId", subjectId); map.put("page", page); //MAP的话加这一句就OK return userMapper.selectByNameLevelSubject(map); }
前台页面方面,由于使用了标签,在适当的位置加一句就够了。
<tags:page page="${page}"/>
“信使”page在这里进入标签,让分页按钮最终展现。
至此,无需修改一句sql,完成分页自动化。
【效果图】
【总结】
现在回过头来看下最开始提出的几个问题:
1)分页时是要随时带有最近一次查询条件
回答:在改造Controller层时,通过将提交参数设置到 Page对象的 Map<String, String> params(单个基本型参数) 和 Map<String, List<String>> paramLists(数组基本型)解决。
顺便提一下,例子中没有涉及参数是Bean的情况,实际应用中应该比较常见。简单的方法是将Bean转换层Map后加入到params。
2)不能影响现有的sql,类似aop的效果
回答:利用Mybatis提供了 Interceptor 接口,拦截后改头换面去的件数并计算limit值,自然能神不知鬼不觉。
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
回答:@Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) 只拦截查询语句,其他增删改查不会影响。
4)尽量少的影响现有service等接口
回答:这个自认为本方案做的还不够好,主要是Controller层改造上,感觉代码量还比较大。如果有有识者知道更好的方案还请多指教。
【遗留问题】
1)一个“明显”的性能问题,是每次检索前都要去 select count(*)一次。在很多时候(数据变化不是特别敏感的场景)是不必要的。调整也不难,先Controller参数增加一个 totalRecord 总记录数 ,在稍加修改一下Page相关代码即可。
2)要排序怎么办?本文并未讨论排序,但是方法是类似的。以上面代码为基础,可以较容易地实现一个通用的排序标签。
===================================== 分割线 (1/8)=======================================
对于Controller层需要将入参传入Page对象的问题已经进行了改善,思路是自动从HttpServletRequest 类中提取入残,减低了分页代码的侵入性,详细参看文章 http://duanhengbin.iteye.com/blog/2001142
===================================== 分割线 (1/23)=======================================
再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。
===================================== 分割线 (2/21)=======================================
今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:
一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):
<pager url="/user/users" pagesize="10" />
二是增加一个过滤器,并将剩下的位于Control类中 唯一侵入性的分页相关代码移入过滤器。发现当前的 Url 在配置文件中有匹配是就构造Page对象,并加入到Response中。
使用最终版后,对于开发者需要分页时,只要在配置文件中加一行,并在前端页面上加一个分页标签即可,其他代码,SQL等都不需要任何改动,可以说简化到了极限。
【技术列表】
总结下最终方案用到的技术:
- Mybatis 提供的拦截器接口实现(实现分页sql自动 select count 及limit 拼接)
- Servlet过滤器+ThreadLocal (生成线程共享的Page对象)
- 标签文件 (实现前端共通的分页效果)
- 临时表单提交 (减少页面体积)
【其他分页方案比较】
时下比较成熟的 JPA 的分页方案,(主要应用在 Hibernate + Spring Data 的场合),主要切入点在DAO层,而Controller等各层接口依然需要带着pageNumber,pageSize 这些的参数,另外框架开发者还要掌握一些必须的辅助类,如:
org.springframework.data.repository.PagingAndSortingRepository 可分页DAO基类
org.springframework.data.domain.Page 抽取结果封装类
org.springframework.data.domain.Pageable 分页信息类
比较来看 本方案 做到了分页与业务逻辑的完全解耦,开发者无需关注分页,全部通过配置实现。通过这个例子也可以反映出Mybatis在底层开发上有其独特的优势。
【备选方案】
最后再闲扯下,上面的最终案是基于 Url 配置的,其实也可以基于方法加自定义注解来做。这样配置文件省了,但是要增加一个注解解析类。注解中参数 为初始的每页行数。估计注解fans会喜欢,如下面的样子:
@RequestMapping(value = "/user/users") @Pagination(size=10) public String list( ...
同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。
相关推荐
这个源码分享主要针对MySQL和Oracle两大主流数据库,为开发者提供了高效且易于使用的分页解决方案。下面我们将深入探讨MyBatis分页插件的相关知识点。 1. **MyBatis框架基础** MyBatis是一个优秀的持久层框架,它...
在"项目中真实的分页,拿来就能用的示例"中,你可以找到一个完整的分页解决方案,包括但不限于以下关键知识点: 1. **MyBatis分页插件**:MyBatis自身并不直接支持分页,但可以通过使用第三方分页插件,如...
MyBatis逆向工程是一个强大且灵活的持久层框架,它可以根据数据库表结构自动生成Java实体类、Mapper接口和XML映射文件。MyBatis逆向工程可以大大减少开发时间和工作量,让开发者更专注于业务逻辑的实现。 在Spring ...
总的来说,Ibatis在分页查询上提供了多种解决方案,开发者可以根据项目的具体需求选择合适的方法。无论是直接在SQL中处理,还是借助插件或自定义实现,都能实现高效且易于维护的分页功能。理解并掌握这些知识点,将...
通过对以上知识点的理解和应用,我们可以构建一个高效、灵活的通用分页解决方案,满足不同项目的需要。在实践中,结合具体的开发环境和工具,如JSP、Servlet、Spring Boot、MyBatis等,可以进一步细化和优化分页实现...
在大型应用程序中,由于数据量庞大,一次性加载所有数据不仅效率低下,也会消耗大量内存,因此分页成为了一个必要的解决方案。Java提供了多种实现分页的方式,其中之一就是通过自定义标签来实现,这种方式更加灵活且...
融合JPA功能和最佳的sql编写及查询模式、独创的缓存翻译、最优化的分页、并提供无限层级分组汇总、同比环比、行列转换、树形排序汇总、sql自适配不同数据库、分库分表、多租户、数据加解密、脱敏以及面向复杂业务和...
Mybatis是一个优秀的Java ORM(对象关系映射)框架,它允许开发者将数据库操作与应用程序逻辑分离,提高了代码的可读性和可维护性。这本书主要面向Java开发者,特别是对Mybatis有一定基础但渴望深入了解其工作原理和...
MyBatis-PageHelper是一个MyBatis的分页插件,它可以自动处理SQL的分页问题,使得开发者无需手动编写分页代码,提高了开发效率。 5. fastadmin / fastadmin FastAdmin是一款基于ThinkPHP5.1+Vue.js的后台管理系统...
Ninja提供了一个完整的栈,包括模板引擎、ORM、身份验证等功能,能够快速搭建Web应用。 6. pagehelper / PageHelper PageHelper是MyBatis的分页插件,它简化了数据库分页查询的操作,使得在后台管理系统中实现分页...
5. **实战应用**:在真实的项目中使用 MyBatis,遇到问题及时寻求解决方案,不断积累实践经验。 通过上述介绍,我们可以看出 MyBatis 不仅功能强大,而且非常灵活易用。无论是初学者还是有经验的开发者,都能够从中...
标题中的“开源的Java开发脚手架,工作经验总结”表明这是一个关于Java开发工具和实践的资源集合,可能包含了作者在实际工作中的经验分享。提到的“springboot”和“springcloud”是Java生态中非常重要的两个框架,...
综上所述,这个项目提供了从后端服务到前端展示的完整解决方案,涵盖了Java Web开发的多个层面,适合学习者深入理解SSM与Vue.js的结合使用,以及如何构建一个实际的社交网络应用。同时,对于想要从事Web开发的学生或...
针对每个技术点,面试者应该准备相关场景下的解决方案,了解其背后的设计哲学和实现原理。比如,在面对Mybatis分页问题时,要熟悉Mybatis的分页插件原理;在讨论ZooKeeper时,要清楚其如何保证分布式环境下的一致性...
压缩包内的文件“springboot美食分享平台论文报告.docx”应该是对整个项目的详细文档,涵盖了项目的目标、技术架构、实现方法、遇到的问题及解决方案等内容,对于理解项目有重要作用。而“lifeshare.sql”文件,根据...
整合Swagger、Spring Boot和MyBatis,可以实现从数据库操作到API设计的无缝连接,提供了一站式的解决方案,让开发者能够高效地开发、测试和文档化RESTful服务。通过这种方式,不仅提高了开发效率,还增强了API的可...
【描述】: 这个资源是一个完整的毕业设计项目,它涉及到一个使用Java技术栈开发的微信小程序,专注于展示和播放原创音乐。核心技术是Spring Boot、Spring MVC和MyBatis(统称为SSM框架),这是一个广泛应用于企业级...
- **问答社区**:如Stack Overflow上的相关问题和答案,提供了遇到问题时的解决方案。 通过阅读源码、笔记和相关资料,开发者不仅可以了解通用Mapper的工作原理,还能掌握如何在实际项目中有效利用这个框架。对于...
该系统不仅满足了旅游爱好者了解新疆旅游景点及分享旅行经验的需求,同时也为旅游业提供了信息化解决方案。 #### 技术选型与架构 1. **后端技术栈**: - **Spring框架**:负责管理Bean的生命周期和依赖注入,简化...
【标题】: "基于微信小程序的短视频系统" 涉及到的是一...通过这个项目,开发者可以提升前后端协同开发的能力,加深对微信小程序和Java Web开发的理解,并且能够实际操作一个完整的项目流程,从而提高自己的职业技能。