Mybatis自定義攔截器和插件開(kāi)發(fā)詳解
前言
在Spring中我們經(jīng)常會(huì)使用到攔截器,在登錄驗(yàn)證、日志記錄、性能監(jiān)控等場(chǎng)景中,通過(guò)使用攔截器允許我們?cè)诓桓膭?dòng)業(yè)務(wù)代碼的情況下,執(zhí)行攔截器的方法來(lái)增強(qiáng)現(xiàn)有的邏輯。在mybatis中,同樣也有這樣的業(yè)務(wù)場(chǎng)景,有時(shí)候需要我們?cè)诓磺秩朐袠I(yè)務(wù)代碼的情況下攔截sql,執(zhí)行特定的某些邏輯。那么這個(gè)過(guò)程應(yīng)該怎么實(shí)現(xiàn)呢,同樣,在mybatis中也為開(kāi)發(fā)者預(yù)留了攔截器接口,通過(guò)實(shí)現(xiàn)自定義攔截器這一功能,可以實(shí)現(xiàn)我們自己的插件,允許用戶在不改動(dòng)mybatis的原有邏輯的條件下,實(shí)現(xiàn)自己的邏輯擴(kuò)展。
本文將按下面的結(jié)構(gòu)進(jìn)行mybatis攔截器學(xué)習(xí):
本文結(jié)構(gòu)
1、攔截器核心對(duì)象
2、工作流程
3、攔截器能實(shí)現(xiàn)什么
4、插件定義與注冊(cè)
5、攔截器使用示例
6、總結(jié)
攔截器核心對(duì)象
在實(shí)現(xiàn)攔截器之前,我們首先看一下攔截器的攔截目標(biāo)對(duì)象是什么,以及攔截器的工作流程是怎樣的。mybatis攔截器可以對(duì)下面4種對(duì)象進(jìn)行攔截:
1、Executor:mybatis的內(nèi)部執(zhí)行器,作為調(diào)度核心負(fù)責(zé)調(diào)用StatementHandler操作數(shù)據(jù)庫(kù),并把結(jié)果集通過(guò)ResultSetHandler進(jìn)行自動(dòng)映射
2、StatementHandler: 封裝了JDBC Statement操作,是sql語(yǔ)法的構(gòu)建器,負(fù)責(zé)和數(shù)據(jù)庫(kù)進(jìn)行交互執(zhí)行sql語(yǔ)句
3、ParameterHandler:作為處理sql參數(shù)設(shè)置的對(duì)象,主要實(shí)現(xiàn)讀取參數(shù)和對(duì)PreparedStatement的參數(shù)進(jìn)行賦值
4、ResultSetHandler:處理Statement執(zhí)行完成后返回結(jié)果集的接口對(duì)象,mybatis通過(guò)它把ResultSet集合映射成實(shí)體對(duì)象
工作流程
在mybatis中提供了一個(gè)Interceptor接口,通過(guò)實(shí)現(xiàn)該接口就能夠自定義攔截器,接口中定義了3個(gè)方法:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
intercept:在攔截目標(biāo)對(duì)象的方法時(shí),實(shí)際執(zhí)行的增強(qiáng)邏輯,我們一般在該方法中實(shí)現(xiàn)自定義邏輯
plugin:用于返回原生目標(biāo)對(duì)象或它的代理對(duì)象,當(dāng)返回的是代理對(duì)象的時(shí)候,會(huì)調(diào)用intercept方法
setProperties:可以用于讀取配置文件中通過(guò)property標(biāo)簽配置的一些屬性,設(shè)置一些屬性變量
看一下plugin方法中的wrap方法源碼:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
可以看到,在wrap方法中,通過(guò)使用jdk動(dòng)態(tài)代理的方式,生成了目標(biāo)對(duì)象的代理對(duì)象,在執(zhí)行實(shí)際方法前,先執(zhí)行代理對(duì)象中的邏輯,來(lái)實(shí)現(xiàn)的邏輯增強(qiáng)。以攔截Executor的query方法為例,在實(shí)際執(zhí)行前會(huì)執(zhí)行攔截器中的intercept方法:

在mybatis中,不同類型的攔截器按照下面的順序執(zhí)行:
Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler
以執(zhí)行query 方法為例對(duì)流程進(jìn)行梳理,整體流程如下:
1、Executor執(zhí)行query()方法,創(chuàng)建一個(gè)StatementHandler對(duì)象
2、StatementHandler 調(diào)用ParameterHandler對(duì)象的setParameters()方法
3、StatementHandler 調(diào)用 Statement對(duì)象的execute()方法
4、StatementHandler 調(diào)用ResultSetHandler對(duì)象的handleResultSets()方法,返回最終結(jié)果
攔截器能實(shí)現(xiàn)什么
在對(duì)mybatis攔截器有了初步的認(rèn)識(shí)后,來(lái)看一下攔截器被普遍應(yīng)用在哪些方面:
- sql 語(yǔ)句執(zhí)行監(jiān)控
可以攔截執(zhí)行的sql方法,可以打印執(zhí)行的sql語(yǔ)句、參數(shù)等信息,并且還能夠記錄執(zhí)行的總耗時(shí),可供后期的sql分析時(shí)使用
- sql 分頁(yè)查詢
mybatis中使用的RowBounds使用的內(nèi)存分頁(yè),在分頁(yè)前會(huì)查詢所有符合條件的數(shù)據(jù),在數(shù)據(jù)量大的情況下性能較差。通過(guò)攔截器,可以做到在查詢前修改sql語(yǔ)句,提前加上需要的分頁(yè)參數(shù)
- 公共字段的賦值
在數(shù)據(jù)庫(kù)中通常會(huì)有createTime,updateTime等公共字段,這類字段可以通過(guò)攔截統(tǒng)一對(duì)參數(shù)進(jìn)行的賦值,從而省去手工通過(guò)set方法賦值的繁瑣過(guò)程
- 數(shù)據(jù)權(quán)限過(guò)濾
在很多系統(tǒng)中,不同的用戶可能擁有不同的數(shù)據(jù)訪問(wèn)權(quán)限,例如在多租戶的系統(tǒng)中,要做到租戶間的數(shù)據(jù)隔離,每個(gè)租戶只能訪問(wèn)到自己的數(shù)據(jù),通過(guò)攔截器改寫sql語(yǔ)句及參數(shù),能夠?qū)崿F(xiàn)對(duì)數(shù)據(jù)的自動(dòng)過(guò)濾
除此之外,攔截器通過(guò)對(duì)上述的4個(gè)階段的介入,結(jié)合我們的實(shí)際業(yè)務(wù)場(chǎng)景,還能夠?qū)崿F(xiàn)很多其他功能。
插件定義與注冊(cè)
在我們自定義的攔截器類實(shí)現(xiàn)了Interceptor接口后,還需要在類上添加@Intercepts 注解,標(biāo)識(shí)該類是一個(gè)攔截器類。注解中的內(nèi)容是一個(gè)@Signature對(duì)象的數(shù)組,指明自定義攔截器要攔截哪一個(gè)類型的哪一個(gè)具體方法。其中type指明攔截對(duì)象的類型,method是攔截的方法,args是method執(zhí)行的參數(shù)。通過(guò)這里可以了解到 mybatis 攔截器的作用目標(biāo)是在方法級(jí)別上進(jìn)行攔截,例如要攔截Executor的query方法,就在類上添加:
@Intercepts({
@Signature(type = Executor.class,method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class })
})
如果要攔截多個(gè)方法,可以繼續(xù)以數(shù)組的形式往后追加。這里通過(guò)添加參數(shù)可以確定唯一的攔截方法,例如在Executor中存在兩個(gè)query方法,通過(guò)上面的參數(shù)可以確定要攔截的是下面的第2個(gè)方法:
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql); <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler);
當(dāng)編寫完成我們自己的插件后,需要向mybatis中注冊(cè)插件,有兩種方式可以使用,第一種直接在SqlSessionFactory中配置:
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setPlugins(new Interceptor[]{new ExecutorPlugin()});
return sqlSessionFactoryBean.getObject();
}
第2種是在mybatis-config.xml中對(duì)自定義插件進(jìn)行注冊(cè):
<configuration>
<plugins>
<plugin interceptor="com.cn.plugin.interceptor.MyPlugin">
<property name="text" value="hello"/>
</plugin>
<plugin interceptor="com.cn.plugin.interceptor.MyPlugin2"></plugin>
<plugin interceptor="com.cn.plugin.interceptor.MyPlugin3"></plugin>
</plugins>
</configuration>
在前面我們了解了不同類型攔截器執(zhí)行的固定順序,那么對(duì)于同樣類型的多個(gè)自定義攔截器,它們的執(zhí)行順序是怎樣的呢?分別在plugin方法和intercept中添加輸出語(yǔ)句,運(yùn)行結(jié)果如下:

從結(jié)果可以看到,攔截順序是按照注冊(cè)順序執(zhí)行的,但代理邏輯的執(zhí)行順序正好相反,最后注冊(cè)的會(huì)被最先執(zhí)行。這是因?yàn)樵趍ybatis中有一個(gè)類InterceptorChain,在它的pluginAll()方法中,會(huì)對(duì)原生對(duì)象target進(jìn)行代理,如果有多個(gè)攔截器的話,會(huì)對(duì)代理類再次進(jìn)行代理,最終實(shí)現(xiàn)一層層的增強(qiáng)target對(duì)象,因此靠后被注冊(cè)的攔截器的增強(qiáng)邏輯會(huì)被優(yōu)先執(zhí)行。從下面的圖中可以直觀的看出代理的嵌套關(guān)系:

在xml中注冊(cè)完成后,在application.yml中啟用配置文件,這樣插件就可以正常運(yùn)行了:
mybatis: config-location: classpath:mybatis-config.xml
在了解了插件的基礎(chǔ)概念與運(yùn)行流程之后,通過(guò)代碼看一下應(yīng)用不同的攔截器能夠?qū)崿F(xiàn)什么功能。
攔截器使用示例
Executor
通過(guò)攔截Executor的query和update方法實(shí)現(xiàn)對(duì)sql的監(jiān)控,在攔截方法中,打印sql語(yǔ)句、執(zhí)行參數(shù)、實(shí)際執(zhí)行時(shí)間:
@Intercepts({
@Signature(type = Executor.class,method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class,method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class })})
public class ExecutorPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Executor Plugin 攔截 :"+invocation.getMethod());
Object[] queryArgs = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) queryArgs[0];
//獲取 ParamMap
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) queryArgs[1];
// 獲取SQL
BoundSql boundSql = mappedStatement.getBoundSql(paramMap);
String sql = boundSql.getSql();
log.info("==> ORIGIN SQL: "+sql);
long startTime = System.currentTimeMillis();
Configuration configuration = mappedStatement.getConfiguration();
String sqlId = mappedStatement.getId();
Object proceed = invocation.proceed();
long endTime=System.currentTimeMillis();
long time = endTime - startTime;
printSqlLog(configuration,boundSql,sqlId,time);
return proceed;
}
public static void printSqlLog(Configuration configuration, BoundSql boundSql, String sqlId, long time){
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql= boundSql.getSql().replaceAll("[\\s]+", " ");
StringBuffer sb=new StringBuffer("==> PARAM:");
if (parameterMappings.size()>0 && parameterObject!=null){
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", parameterObject.toString());
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
String parameterValue = obj.toString();
sql = sql.replaceFirst("\\?", parameterValue);
sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
String parameterValue = obj.toString();
sql = sql.replaceFirst("\\?", parameterValue);
sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
}
}
}
sb.deleteCharAt(sb.length()-1);
}
log.info("==> SQL:"+sql);
log.info(sb.toString());
log.info("==> SQL TIME:"+time+" ms");
}
}
執(zhí)行代碼,日志輸出如下:

在上面的代碼中,通過(guò)Executor攔截器獲取到了BoundSql對(duì)象,進(jìn)一步獲取到sql的執(zhí)行參數(shù),從而實(shí)現(xiàn)了對(duì)sql執(zhí)行的監(jiān)控與統(tǒng)計(jì)。
StatementHandler
下面的例子中,通過(guò)改變StatementHandler對(duì)象的屬性,動(dòng)態(tài)修改sql語(yǔ)句的分頁(yè):
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class StatementPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
metaObject.setValue("delegate.rowBounds.offset", 0);
metaObject.setValue("delegate.rowBounds.limit", 2);
return invocation.proceed();
}
}
MetaObject是mybatis提供的一個(gè)用于方便、優(yōu)雅訪問(wèn)對(duì)象屬性的對(duì)象,通過(guò)將實(shí)例對(duì)象作為參數(shù)傳遞給它,就可以通過(guò)屬性名稱獲取對(duì)應(yīng)的屬性值。雖然說(shuō)我們也可以通過(guò)反射拿到屬性的值,但是反射過(guò)程中需要對(duì)各種異常做出處理,會(huì)使代碼中堆滿難看的try/catch,通過(guò)MetaObject可以在很大程度上簡(jiǎn)化我們的代碼,并且它支持對(duì)Bean、Collection、Map三種類型對(duì)象的操作。
對(duì)比執(zhí)行前后:

可以看到這里通過(guò)改變了分頁(yè)對(duì)象RowBounds的屬性,動(dòng)態(tài)的修改了分頁(yè)參數(shù)。
ResultSetHandler
ResultSetHandler 會(huì)負(fù)責(zé)映射sql語(yǔ)句查詢得到的結(jié)果集,如果在生產(chǎn)環(huán)境中存在一些保密數(shù)據(jù),不想在外部系統(tǒng)中展示,那么可能就需要在查詢到結(jié)果后做一下數(shù)據(jù)的脫敏處理,這時(shí)候就可以使用ResultSetHandler對(duì)結(jié)果集進(jìn)行改寫。
@Intercepts({
@Signature(type= ResultSetHandler.class,method = "handleResultSets",args = {Statement.class})})
public class ResultSetPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Result Plugin 攔截 :"+invocation.getMethod());
Object result = invocation.proceed();
if (result instanceof Collection) {
Collection<Object> objList= (Collection) result;
List<Object> resultList=new ArrayList<>();
for (Object obj : objList) {
resultList.add(desensitize(obj));
}
return resultList;
}else {
return desensitize(result);
}
}
//脫敏方法,將加密字段變?yōu)樾翘?hào)
private Object desensitize(Object object) throws InvocationTargetException, IllegalAccessException {
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
Confidential confidential = field.getAnnotation(Confidential.class);
if (confidential==null){
continue;
}
PropertyDescriptor ps = BeanUtils.getPropertyDescriptor(object.getClass(), field.getName());
if (ps.getReadMethod() == null || ps.getWriteMethod() == null) {
continue;
}
Object value = ps.getReadMethod().invoke(object);
if (value != null) {
ps.getWriteMethod().invoke(object, "***");
}
}
return object;
}
}
運(yùn)行上面的代碼,查看執(zhí)行結(jié)果:
{"id":1358041517788299266,"orderNumber":"***","money":122.0,"status":3,"tenantId":2}
在上面的例子中,在執(zhí)行完sql語(yǔ)句得到結(jié)果對(duì)象后,通過(guò)反射掃描結(jié)果對(duì)象中的屬性,如果實(shí)體的屬性上帶有自定義的@Confidential注解,那么在脫敏方法中將它轉(zhuǎn)化為星號(hào)再返回結(jié)果,從而實(shí)現(xiàn)了數(shù)據(jù)的脫敏處理。
ParameterHandler
mybatis可以攔截ParameterHandler注入?yún)?shù),下面的例子中我們將結(jié)合前面介紹的其他種類的對(duì)象,通過(guò)組合攔截器的方式,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的多租戶攔截器插件,實(shí)現(xiàn)多租戶下的查詢邏輯。
@Intercepts({
@Signature(type = Executor.class,method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class }),
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class TenantPlugin implements Interceptor {
private static final String TENANT_ID = "tenantId";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
String methodName = invocation.getMethod().getName();
if (target instanceof Executor && methodName.equals("query") && invocation.getArgs().length==4) {
return doQuery(invocation);
}
if (target instanceof StatementHandler){
return changeBoundSql(invocation);
}
if (target instanceof ParameterHandler){
return doSetParameter(invocation);
}
return null;
}
private Object doQuery(Invocation invocation) throws Exception{
Executor executor = (Executor) invocation.getTarget();
MappedStatement ms= (MappedStatement) invocation.getArgs()[0];
Object paramObj = invocation.getArgs()[1];
RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
if (paramObj instanceof Map){
MapperMethod.ParamMap paramMap= (MapperMethod.ParamMap) paramObj;
if (!paramMap.containsKey(TENANT_ID)){
Long tenantId=1L;
paramMap.put("param"+(paramMap.size()/2+1),tenantId);
paramMap.put(TENANT_ID,tenantId);
paramObj=paramMap;
}
}
//直接執(zhí)行query,不用proceed()方法
return executor.query(ms, paramObj,rowBounds,null);
}
private Object changeBoundSql(Invocation invocation) throws Exception {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
PreparedStatementHandler preparedStatementHandler = (PreparedStatementHandler) metaObject.getValue("delegate");
String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
metaObject.setValue("delegate.boundSql.sql",originalSql+ " and tenant_id=?");
return invocation.proceed();
}
private Object doSetParameter(Invocation invocation) throws Exception {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
MetaObject metaObject = SystemMetaObject.forObject(parameterHandler);
BoundSql boundSql= (BoundSql) metaObject.getValue("boundSql");
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
boolean hasTenantId=false;
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getProperty().equals(TENANT_ID)) {
hasTenantId=true;
}
}
//添加參數(shù)
if (!hasTenantId){
Configuration conf= (Configuration) metaObject.getValue("configuration");
ParameterMapping parameterMapping= new ParameterMapping.Builder(conf,TENANT_ID,Long.class).build();
parameterMappings.add(parameterMapping);
}
parameterHandler.setParameters(ps);
return null;
}
}
在上面的過(guò)程中,攔截了sql執(zhí)行的三個(gè)階段,來(lái)實(shí)現(xiàn)多租戶的邏輯,邏輯分工如下:
- 攔截Executor的query方法,在查詢的參數(shù)Map中添加租戶的屬性值,這里只是簡(jiǎn)單的對(duì)Map的情況作了判斷,沒(méi)有對(duì)Bean的情況進(jìn)行設(shè)置
- 攔截StatementHandler的prepare方法,改寫sql語(yǔ)句對(duì)象BoundSql,在sql語(yǔ)句中拼接租戶字段的查詢條件
- 攔截ParameterHandler的setParameters方法,動(dòng)態(tài)設(shè)置參數(shù),將租戶id添加到要設(shè)置到參數(shù)列表中
最終通過(guò)攔截不同執(zhí)行階段的組合,實(shí)現(xiàn)了基于租戶的條件攔截。
總結(jié)
總的來(lái)說(shuō),mybatis攔截器通過(guò)對(duì)Executor、StatementHandler、ParameterHandler、ResultSetHandler 這4種接口中的方法進(jìn)行攔截,并生成代理對(duì)象,在執(zhí)行方法前先執(zhí)行代理對(duì)象的邏輯,來(lái)實(shí)現(xiàn)我們自定義的邏輯增強(qiáng)。從上面的例子中,可以看到通過(guò)靈活使用mybatis攔截器開(kāi)發(fā)插件能夠幫助我們解決很多問(wèn)題,但是同樣它也是一把雙刃劍,在實(shí)際工作中也不要濫用插件、定義過(guò)多的攔截器,因?yàn)橥ㄟ^(guò)學(xué)習(xí)我們知道m(xù)ybatis插件在執(zhí)行中使用到了代理模式和責(zé)任鏈模式,在執(zhí)行sql語(yǔ)句前會(huì)經(jīng)過(guò)層層代理,如果代理次數(shù)過(guò)多將會(huì)消耗額外的性能,并增加響應(yīng)時(shí)間。
到此這篇關(guān)于Mybatis自定義攔截器和插件開(kāi)發(fā)的文章就介紹到這了,更多相關(guān)Mybatis自定義攔截器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- mybatisplus 的SQL攔截器實(shí)現(xiàn)關(guān)聯(lián)查詢功能
- mybatis 自定義實(shí)現(xiàn)攔截器插件Interceptor示例
- mybatis 通過(guò)攔截器打印完整的sql語(yǔ)句以及執(zhí)行結(jié)果操作
- Mybatis Plugin攔截器開(kāi)發(fā)過(guò)程詳解
- 簡(jiǎn)單了解mybatis攔截器實(shí)現(xiàn)原理及實(shí)例
- mybatis攔截器實(shí)現(xiàn)通用權(quán)限字段添加的方法
- Mybatis中攔截器的簡(jiǎn)單實(shí)現(xiàn)方法
- mybatis攔截器與分頁(yè)插件實(shí)例教程
- Mybatis Interceptor 攔截器的實(shí)現(xiàn)
- MyBatis攔截器實(shí)現(xiàn)分頁(yè)功能的實(shí)現(xiàn)方法
- MyBatis攔截器的原理與使用
相關(guān)文章
解決MyBatis中為類配置別名,列名與屬性名不對(duì)應(yīng)的問(wèn)題
這篇文章主要介紹了解決MyBatis中為類配置別名,列名與屬性名不對(duì)應(yīng)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11
華為鴻蒙系統(tǒng)應(yīng)用開(kāi)發(fā)工具 DevEco Studio的安裝和使用圖文教程
HUAWEI DevEco Studio 是華為消費(fèi)者業(yè)務(wù)為開(kāi)發(fā)者提供的集成開(kāi)發(fā)環(huán)境(IDE),旨在幫助開(kāi)發(fā)者快捷、方便、高效地使用華為EMUI開(kāi)放能力。這篇文章主要介紹了華為鴻蒙系統(tǒng)應(yīng)用開(kāi)發(fā)工具 DevEco Studio的安裝和使用圖文教程,需要的朋友可以參考下2021-04-04
Spark SQL關(guān)于性能調(diào)優(yōu)選項(xiàng)詳解
這篇文章將為大家詳細(xì)講解有關(guān)Spark SQL性能調(diào)優(yōu)選項(xiàng),小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲2023-02-02
Java Socket編程筆記_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Socket對(duì)于我們來(lái)說(shuō)就非常實(shí)用了。下面是本次學(xué)習(xí)的筆記。主要分異常類型、交互原理、Socket、ServerSocket、多線程這幾個(gè)方面闡述2017-05-05
Java數(shù)據(jù)結(jié)構(gòu)之樹(shù)和二叉樹(shù)的相關(guān)資料
這篇文章主要介紹了Java?數(shù)據(jù)結(jié)構(gòu)之樹(shù)和二叉樹(shù)相關(guān)資料,文中通過(guò)示例代碼和一些相關(guān)題目來(lái)做介紹,非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下!2023-01-01
Sentinel實(shí)現(xiàn)動(dòng)態(tài)配置的集群流控的方法
這篇文章主要介紹了Sentinel實(shí)現(xiàn)動(dòng)態(tài)配置的集群流控,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
SpringBoot分頁(yè)查詢功能的實(shí)現(xiàn)方法
在實(shí)際的項(xiàng)目開(kāi)發(fā)過(guò)程中,分頁(yè)顯示是很常見(jiàn)的頁(yè)面布局,所以學(xué)習(xí)如何實(shí)現(xiàn)分頁(yè)也是必要的,下面這篇文章主要給大家介紹了關(guān)于SpringBoot分頁(yè)查詢功能的實(shí)現(xiàn)方法,需要的朋友可以參考下2022-06-06
mybatis?返回Map類型key默認(rèn)為大寫問(wèn)題
這篇文章主要介紹了mybatis?返回Map類型key默認(rèn)為大寫問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11

