设为首页 收藏本站
查看: 1226|回复: 0

[经验分享] 分享一个完整的Mybatis分页解决方案

[复制链接]

尚未签到

发表于 2016-11-28 06:42:24 | 显示全部楼层 |阅读模式
  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" >首页</button>
<button class="btn btn-default btn-sm" >前页</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" >${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" >后页</button>
<button class="btn btn-default btn-sm" >末页</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,完成分页自动化。  【效果图】
DSC0000.jpg


【总结】  现在回过头来看下最开始提出的几个问题:
  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.iyunv.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(
...
  同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-306285-1-1.html 上篇帖子: MyBatis缓存加载机制/自定义二级缓存 下篇帖子: Mybatis源代码分析之parsing包【转】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表