背景

后台业务开发的过程中,往往会遇到这种场景:需要记录每条记录产生时间、修改时间、修改人及添加人,在查询时查询出来。

以往的做法通常是手动在每个业务逻辑里耦合上这么一块代码,也有更优雅一点的做法是写一个拦截器,然后在Mybatis拦截器中为实体对象中的公共参数进行赋值,但最终依然需要在业务SQL上手动添加上这几个参数,很多开源后台项目都有类似做法。

这种做法往往不够灵活,新增或修改字段时每处业务逻辑都需要同步修改,业务量大的话这么改非常麻烦。

最近在我自己的项目中写了一个Mybatis插件,这个插件能够实现不修改任何业务逻辑就能实现添加或修改时数据库公共字段的赋值,并能在查询时自动查询出来。

实现原理

Mybatis提供了一系列的拦截器,用于实现在Mybatis执行的各个阶段允许插入或修改自定义逻辑。

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

ParameterHandler (getParameterObject, setParameters)

ResultSetHandler (handleResultSets, handleOutputParameters)

StatementHandler (prepare, parameterize, batch, update, query)

我这里用的是Executor,它能做到在所有数据库操作前后执行一些逻辑,甚至可以修改Mybatis的上下文参数后继续执行。

在Mybaits的拦截器中,可以拿到MappedStatement对象,这里面包含了一次数据库操作的原始SQL以及实体对象与结果集的映射关系,为了实现公共参数自动携带,我们就需要在拦截器中修改原始SQL:

  1. Insert操作:自动为Insert语句添加公共字段并赋值
  2. Update操作:自动为Update语句添加公共字段并赋值
  3. Select操作:自动为Select语句的查询参数上添加上公共字段

以及修改实体对象与结果集的映射关系,做到自动修改查询语句添加公共字段后能够使Mybatis将查出的公共字段值赋给实体类。

简单来说就是修改MappedStatement中的SqlSource以及ResultMap

修改SqlSource

在SqlSource中,包含了原始待执行的SQL,需要将它修改为携带公共参数的SQL。

需要注意的是Mybatis的SqlSource、ResultMap中的属性仅允许初次构造SqlSource对象时进行赋值,后续如果需要修改只能通过反射或者新构造一个对象替换旧对象的方式进行内部参数修改。

直接贴出来代码,这里新构造了SqlSource对象,在里面实现了原始SQL的解析修改:

SQL的动态修改使用了JSQLParser将原始SQL解析为AST抽象语法树后做参数追加,之后重新解析为SQL,使用自定义SqlSource返回修改后的SQL实现SQL修改

static class ModifiedSqlSourceV2 implements SqlSource {
private final MappedStatement mappedStatement;
private final Configuration configuration; public ModifiedSqlSourceV2(MappedStatement mappedStatement, Configuration configuration) {
this.mappedStatement = mappedStatement;
this.configuration = configuration;
} @Override
public BoundSql getBoundSql(Object parameterObject) {
// 获取原始的 BoundSql 对象
BoundSql originalBoundSql = mappedStatement.getSqlSource().getBoundSql(parameterObject); // 获取原始的 SQL 字符串
String originalSql = originalBoundSql.getSql();
log.debug("公共参数添加 - 修改前SQL:{}", originalSql); // 创建新的 BoundSql 对象
String modifiedSql;
try {
modifiedSql = buildSql(originalSql);
log.debug("公共参数添加 - 修改后SQL:{}", modifiedSql);
} catch (JSQLParserException e) {
log.error("JSQLParser解析修改SQL添加公共参数失败, 继续使用原始SQL执行" , e);
modifiedSql = originalSql;
}
BoundSql modifiedBoundSql = new BoundSql(configuration, modifiedSql,
originalBoundSql.getParameterMappings(), parameterObject);
// 复制其他属性
originalBoundSql.getAdditionalParameters().forEach(modifiedBoundSql::setAdditionalParameter);
modifiedBoundSql.setAdditionalParameter("_parameter", parameterObject); return modifiedBoundSql;
} private String buildSql(String originalSql) throws JSQLParserException {
Statement statement = CCJSqlParserUtil.parse(originalSql); switch(mappedStatement.getSqlCommandType()) {
case INSERT -> {
if(statement instanceof Insert insert) {
insert.addColumns(new Column(CREATE_BY_COLUMN), new Column(CREATE_TIME_COLUMN));
ExpressionList expressionList = insert.getItemsList(ExpressionList.class);
Timestamp currentTimeStamp = new Timestamp(System.currentTimeMillis()); if (!expressionList.getExpressions().isEmpty()) {
// 多行插入 行构造器解析
if (expressionList.getExpressions().get(0) instanceof RowConstructor) {
expressionList.getExpressions().forEach((expression -> {
if (expression instanceof RowConstructor rowConstructor) {
rowConstructor.getExprList().getExpressions().add(new StringValue(getCurrentUser()));
rowConstructor.getExprList().getExpressions().add(new TimestampValue().withValue(currentTimeStamp));
}
}));
} else {
// 其余默认单行插入
expressionList.addExpressions(new StringValue(getCurrentUser()), new TimestampValue().withValue(currentTimeStamp));
}
} return insert.toString();
}
}
case UPDATE -> {
if(statement instanceof Update update) {
List<UpdateSet> updateSetList = update.getUpdateSets();
UpdateSet updateBy = new UpdateSet(new Column(UPDATE_BY_COLUMN), new StringValue(getCurrentUser()));
Timestamp currentTimeStamp = new Timestamp(System.currentTimeMillis());
UpdateSet updateTime = new UpdateSet(new Column(UPDATE_TIME_COLUMN), new TimestampValue().withValue(currentTimeStamp));
updateSetList.add(updateBy);
updateSetList.add(updateTime); return update.toString();
}
}
case SELECT -> {
if(statement instanceof Select select) {
SelectBody selectBody = select.getSelectBody();
if(selectBody instanceof PlainSelect plainSelect) {
TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
List<String> tableNames = tablesNamesFinder.getTableList(select); List<SelectItem> selectItems = plainSelect.getSelectItems();
tableNames.forEach((tableName) -> {
String lowerCaseTableName = tableName.toLowerCase();
selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), CREATE_BY_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + CREATE_BY_COLUMN)));
selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), CREATE_TIME_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + CREATE_TIME_COLUMN)));
selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), UPDATE_BY_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + UPDATE_BY_COLUMN)));
selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), UPDATE_TIME_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + UPDATE_TIME_COLUMN)));
}); return select.toString();
}
}
}
default -> {
return originalSql;
}
}
return originalSql;
}
}

修改ResultMap

ResultMap中存放了结果列与映射实体类属性的对应关系,这里为了自动生成公共属性的结果映射,直接根据当前ResultMap中存储的结果映射实体类的名称作为表名,自动建立与结果列的映射关系。

就是说数据库表对应的实体类的名字需要与数据库表保持一致(但是实体类名可以是数据库表的名字的驼峰命名,如表user_role的实体类需要命名为UserRole),只要遵守这个命名规则即可实现查询结果中自动携带公共参数值

如下为添加公共参数结果映射的代码

private static List<ResultMapping> addResultMappingProperty(Configuration configuration, List<ResultMapping> resultMappingList, Class<?> mappedType) {
// resultMappingList为不可修改对象
List<ResultMapping> modifiableResultMappingList = new ArrayList<>(resultMappingList); String []checkList = {CREATE_BY_PROPERTY, CREATE_TIME_PROPERTY, UPDATE_BY_PROPERTY, UPDATE_TIME_PROPERTY};
boolean hasAnyTargetProperty = Arrays.stream(checkList).anyMatch((property) -> ReflectionUtils.findField(mappedType, property) != null); // 用于防止映射目标为基本类型却被添加映射 导致列名规则 表名_列名 无法与映射的列名的添加规则 映射类型名_列名 相照应
// 从而导致映射类型为基本类型时会生成出类似与string_column1的映射名 而产生找不到映射列名与实际结果列相照应的列名导致mybatis产生错误
// 规则: 仅映射类型中包含如上四个字段其一时才会添加映射
if(hasAnyTargetProperty) {
// 支持类型使用驼峰命名
String currentTable = upperCamelToLowerUnderscore(mappedType.getSimpleName()); // 映射方式 表名_公共字段名 在实体中 表名与实体名相同 则可完成映射
modifiableResultMappingList.add(new ResultMapping.Builder(configuration, CREATE_BY_PROPERTY, currentTable + "_" + CREATE_BY_COLUMN, String.class).build());
modifiableResultMappingList.add(new ResultMapping.Builder(configuration, CREATE_TIME_PROPERTY, currentTable + "_" + CREATE_TIME_COLUMN, Timestamp.class).build());
modifiableResultMappingList.add(new ResultMapping.Builder(configuration, UPDATE_BY_PROPERTY, currentTable + "_" + UPDATE_BY_COLUMN, String.class).build());
modifiableResultMappingList.add(new ResultMapping.Builder(configuration, UPDATE_TIME_PROPERTY, currentTable + "_" + UPDATE_TIME_COLUMN, Timestamp.class).build());
} return modifiableResultMappingList;
}

构建MappedStatement

原本的由Mybatis创建的MappedStatement无法直接修改,因此这里手动通过ResultMap.Builder()构造一个新的MappedStatement,同时保持其余参数不变,只替换SqlSource、ResultMap为先前重新创建的对象。

public MappedStatement buildMappedStatement(Configuration newModifiedConfiguration, MappedStatement mappedStatement) {
SqlSource modifiedSqlSource = new ModifiedSqlSourceV2(mappedStatement, newModifiedConfiguration); List<ResultMap> modifiedResultMaps = mappedStatement.getResultMaps().stream().map((resultMap) -> {
List<ResultMapping> resultMappingList = resultMap.getResultMappings();
// 为每个resultMap中的resultMappingList添加公共参数映射
List<ResultMapping> modifiedResultMappingList = addResultMappingProperty(newModifiedConfiguration, resultMappingList, resultMap.getType()); return new ResultMap.Builder(newModifiedConfiguration, resultMap.getId(), resultMap.getType(), modifiedResultMappingList, resultMap.getAutoMapping()).build();
}).toList(); // 构造新MappedStatement 替换SqlSource、ResultMap、Configuration
MappedStatement.Builder newMappedStatementBuilder = new MappedStatement.Builder(newModifiedConfiguration, mappedStatement.getId(), modifiedSqlSource, mappedStatement.getSqlCommandType())
.cache(mappedStatement.getCache()).databaseId(mappedStatement.getDatabaseId()).dirtySelect(mappedStatement.isDirtySelect()).fetchSize(mappedStatement.getFetchSize())
.flushCacheRequired(mappedStatement.isFlushCacheRequired())
.keyGenerator(mappedStatement.getKeyGenerator())
.lang(mappedStatement.getLang()).parameterMap(mappedStatement.getParameterMap()).resource(mappedStatement.getResource()).resultMaps(modifiedResultMaps)
.resultOrdered(mappedStatement.isResultOrdered())
.resultSetType(mappedStatement.getResultSetType()).statementType(mappedStatement.getStatementType()).timeout(mappedStatement.getTimeout()).useCache(mappedStatement.isUseCache());
if(mappedStatement.getKeyColumns() != null) {
newMappedStatementBuilder.keyColumn(StringUtils.collectionToDelimitedString(Arrays.asList(mappedStatement.getKeyColumns()), ","));
}
if(mappedStatement.getKeyProperties() != null) {
newMappedStatementBuilder.keyProperty(StringUtils.collectionToDelimitedString(Arrays.asList(mappedStatement.getKeyProperties()), ","));
}
if(mappedStatement.getResultSets() != null) {
newMappedStatementBuilder.resultSets(StringUtils.collectionToDelimitedString(Arrays.asList(mappedStatement.getResultSets()), ","));
}
return newMappedStatementBuilder.build();
}

到这里为止,已经完全实现了修改原始SQL、修改结果映射的工作了,将修改后的MappedStatement对象往下传入到invoke()即可但是还能改进。

改进

在Mybatis拦截器中可以通过MappedStatement.getConfiguration()拿到整个Mybatis的上下文,在这个里面可以拿到所有Mybatis的所有SQL操作的映射结果以及SQL,可以一次性修改完后,将Configuration作为一个缓存使用,每次有请求进入拦截器后就从Configuration获取被修改的MappedStatement后直接invoke,效率会提升不少。

经给改进后,除了应用启动后执行的第一个SQL请求由于需要构建Configuration会慢一些,之后的请求几乎没有产生性能方面的影响。

现在唯一的性能消耗是每次执行请求前Mybatis会调用我们自己重新定义的SqlSource.getBoundSql()将原始SQL解析为AST后重新构建生成新SQL的过程了,这点开销几乎可忽略不计。如果想更进一步的优化,可以考虑将原始SQL做key,使用Caffeine、Guava缓存工具等方式将重新构建后的查询SQL缓存起来(Update/Insert由于追加有时间参数的原因,不能被缓存),避免多次重复构建SQL带来的开销

完整实现

经过优化后,整个插件已经比较完善了,能够满足日常使用,无论是单表查询,还是多表联查,嵌套查询都能够实现无侵入的参数追加,目前仅实现了创建人、创建时间、修改人、修改时间的参数追加&映射绑定,如有需要的可以自行修改。

我把它放到了GitHub上,并附带有示例项目:https://github.com/Random-pro/ExtParamInterctptor

觉得好用的欢迎点点Star

使用的人多的话,后续会将追加哪些参数做成动态可配置的,等你们反馈

插件使用示例

所有的新增操作均会被自动添加创建人、创建时间。更新操作则会被自动添加更新人、更新时间。正常使用Mybatis操作即可,与原先无任何差别就不在这里给出示例了,如果需要示例请前往我在GitHub上的示例项目。

  1. 单表查询

    // 实体类Child(类名对应具体的表名 使用驼峰命名法,如表名为user_role,则类名应写为UserRole)
    @Data
    public class Child extends BaseDomain {
    private int childId;
    private int parentId;
    private String childName;
    private String path;
    } // 公共字段
    @Data
    public class BaseDomain {
    private String createBy;
    private Date createTime;
    private String updateBy;
    private Date updateTime;
    } // Mapper接口
    @Mapper
    public interface TestMapper {
    @Select("SELECT id as childId, name as childName, parent_id as parentId, path FROM child")
    List<Child> getChildList();
    } // Controller
    @RestController
    @RequestMapping("user")
    public record UserController(TestMapper testMapper) {
    @GetMapping("getChildList")
    public List<Child> getChildList() {
    return testMapper.getChildList();
    }
    }

    访问user/getChildList获取结果:

    [
    {
    "createBy": "sun11",
    "createTime": "2023-12-18T07:58:58.000+00:00",
    "updateBy": "random",
    "updateTime": "2023-12-18T07:59:19.000+00:00",
    "childId": 1,
    "parentId": 1,
    "childName": "childName1_1",
    "path": "childPath1_1"
    },
    {
    "createBy": "sun12",
    "createTime": "2023-12-18T07:58:59.000+00:00",
    "updateBy": "RANDOM",
    "updateTime": "2023-12-18T07:59:20.000+00:00",
    "childId": 2,
    "parentId": 1,
    "childName": "childName1_2",
    "path": "childPath1_2"
    },
    {
    "createBy": "sun21",
    "createTime": "2023-12-18T07:59:00.000+00:00",
    "updateBy": "randompro",
    "updateTime": "2023-12-18T07:59:21.000+00:00",
    "childId": 3,
    "parentId": 2,
    "childName": "childName2_1",
    "path": "childPath2_2"
    }
    ]
  2. 多表查询

    // 实体类Base(类名对应具体的表名 使用驼峰命名法,如表名为user_role,则类名应写为UserRole) 注意:当关联多个表时,需要取哪个表里的公共字段(创建人、创建时间等字段)则将映射实体类名命名为该表的表名
    @Data
    public class Base extends BaseDomain {
    private int id;
    private String baseName;
    private String basePath;
    private List<Child> pathChildList;
    } @Data
    public class Child extends BaseDomain {
    private int childId;
    private int parentId;
    private String childName;
    private String path;
    } // 公共字段
    @Data
    public class BaseDomain {
    private String createBy;
    private Date createTime;
    private String updateBy;
    private Date updateTime;
    } // Mapper接口
    @Mapper
    public interface TestMapper {
    @Select("SELECT BASE.ID as id , BASE.BASE_NAME as baseName, CHILD.PATH as basePath FROM BASE, CHILD WHERE BASE.ID = CHILD.PARENT_ID")
    List<Base> getBaseAndChildPath();
    } // Controller
    @RestController
    @RequestMapping("user")
    public record UserController(TestMapper testMapper) {
    @GetMapping("getBaseAndChildPath")
    public List<Base> getBaseAndChildPath() {
    return testMapper.getBaseAndChildPath();
    }
    }

    访问user/getBaseAndChildPath获取结果:

    [
    {
    "createBy": "sun_base",
    "createTime": "2023-12-18T07:59:29.000+00:00",
    "updateBy": "random_base",
    "updateTime": "2023-12-18T08:00:09.000+00:00",
    "id": 1,
    "baseName": "baseName1",
    "basePath": "childPath1_1",
    "pathChildList": null
    },
    {
    "createBy": "sun_base",
    "createTime": "2023-12-18T07:59:29.000+00:00",
    "updateBy": "random_base",
    "updateTime": "2023-12-18T08:00:09.000+00:00",
    "id": 1,
    "baseName": "baseName1",
    "basePath": "childPath1_2",
    "pathChildList": null
    },
    {
    "createBy": "sun2_base",
    "createTime": "2023-12-18T07:59:30.000+00:00",
    "updateBy": "randompro_base",
    "updateTime": "2023-12-18T08:00:09.000+00:00",
    "id": 2,
    "baseName": "baseName2",
    "basePath": "childPath2_2",
    "pathChildList": null
    }
    ]
  3. 多表嵌套查询

    // 实体类Base(类名对应具体的表名 使用驼峰命名法,如表名为user_role,则类名应写为UserRole) 嵌套查询中使用到的多个实体若均可映射到对应表中的如上四个字段的值(只要该实体通过继承、直接添加的方式获取到了以上声明的四个实体属性的getter/setter方法即可)
    @Data
    public class Base extends BaseDomain {
    private int id;
    private String baseName;
    private String basePath;
    private List<Child> pathChildList;
    } @Data
    public class Child extends BaseDomain {
    private int childId;
    private int parentId;
    private String childName;
    private String path;
    } // 公共字段
    @Data
    public class BaseDomain {
    private String createBy;
    private Date createTime;
    private String updateBy;
    private Date updateTime;
    } // Mapper接口
    @Mapper
    public interface TestMapper {
    List<Base> getPathList();
    } // Controller
    @RestController
    @RequestMapping("user")
    public record UserController(TestMapper testMapper) {
    @GetMapping("getPathList")
    public List<Base> getPathList() {
    return testMapper.getPathList();
    }
    }

    Mapper.xml:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.live.mapper.TestMapper"> <resultMap type="com.live.domian.Base" id="PathDomainMap">
    <result property="id" column="id" />
    <result property="baseName" column="base_name"/>
    <result property="basePath" column="base_path"/> <collection property="pathChildList" ofType="com.live.domian.Child">
    <id property="childId" column="child_id"/>
    <result property="parentId" column="parent_id"/>
    <result property="childName" column="child_name"/>
    <result property="path" column="path"/>
    </collection>
    </resultMap> <select id="getPathList" resultMap="PathDomainMap">
    SELECT base.id, base.base_name, base.base_path, child.id AS child_id, child.name AS child_name,
    child.path, child.parent_id FROM base LEFT JOIN child ON base.id = child.parent_id
    </select>
    </mapper>

    访问user/getPathList获取结果,可见嵌套查询中每个层次都取到了公共字段createBy、createTime、updateBy、updateTime的值:

    [
    {
    "createBy": "sun_base",
    "createTime": "2023-12-18T07:59:29.000+00:00",
    "updateBy": "random_base",
    "updateTime": "2023-12-18T08:00:09.000+00:00",
    "id": 1,
    "baseName": "baseName1",
    "basePath": "basePath1",
    "pathChildList": [
    {
    "createBy": "sun12",
    "createTime": "2023-12-18T07:58:59.000+00:00",
    "updateBy": "RANDOM",
    "updateTime": "2023-12-18T07:59:20.000+00:00",
    "childId": 2,
    "parentId": 1,
    "childName": "childName1_2",
    "path": "childPath1_2"
    },
    {
    "createBy": "sun11",
    "createTime": "2023-12-18T07:58:58.000+00:00",
    "updateBy": "random",
    "updateTime": "2023-12-18T07:59:19.000+00:00",
    "childId": 1,
    "parentId": 1,
    "childName": "childName1_1",
    "path": "childPath1_1"
    }
    ]
    },
    {
    "createBy": "sun2_base",
    "createTime": "2023-12-18T07:59:30.000+00:00",
    "updateBy": "randompro_base",
    "updateTime": "2023-12-18T08:00:09.000+00:00",
    "id": 2,
    "baseName": "baseName2",
    "basePath": "basePath2",
    "pathChildList": [
    {
    "createBy": "sun21",
    "createTime": "2023-12-18T07:59:00.000+00:00",
    "updateBy": "randompro",
    "updateTime": "2023-12-18T07:59:21.000+00:00",
    "childId": 3,
    "parentId": 2,
    "childName": "childName2_1",
    "path": "childPath2_2"
    }
    ]
    }
    ]

    嵌套查询中,如果只希望获取到特定的表的那四个公共属性,则把不希望获取公共属性的表对应的实体类中的四个映射属性去掉(若使用BaseDomain继承来的四个属性的的话去掉继承BaseDomain)即可

使用Mybatis自定义插件实现不侵入业务的公共参数自动追加的更多相关文章

  1. Mybatis框架(9)---Mybatis自定义插件生成雪花ID做为表主键项目

    Mybatis自定义插件生成雪花ID做为主键项目 先附上项目项目GitHub地址 spring-boot-mybatis-interceptor 有关Mybatis雪花ID主键插件前面写了两篇博客作为 ...

  2. mybatis 自定义插件的使用

    今天看了别人的mybatis的教学视频,自己手写了一个简单的自定义的插件,有些细节记录一下. 先看下mybatis的插件的一些说明: MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用. ...

  3. mybatis自定义插件(拦截器)开发详解

    mybatis插件(准确的说应该是around拦截器,因为接口名是interceptor,而且invocation.proceed要自己调用,配置中叫插件)功能非常强大,可以让我们无侵入式的对SQL的 ...

  4. mybatis自定义插件动态修改sql语句

    step1:定义Interceptor实现org.apache.ibatis.plugin.Interceptor import org.apache.commons.logging.Log; imp ...

  5. 浅析MyBatis(三):聊一聊MyBatis的实用插件与自定义插件

    在前面的文章中,笔者详细介绍了 MyBatis 框架的底层框架与运行流程,并且在理解运行流程的基础上手写了一个自己的 MyBatis 框架.看完前两篇文章后,相信读者对 MyBatis 的偏底层原理和 ...

  6. mybatis自定义打印执行时间并格式化sql插件

    打印SQL的执行时间,我们可以实现mybatis官方我们提供的接口org.apache.ibatis.plugin.Interceptor,我们可以拦截的类有多个Executor,StatementH ...

  7. Mybatis自定义拦截器与插件开发

    在Spring中我们经常会使用到拦截器,在登录验证.日志记录.性能监控等场景中,通过使用拦截器允许我们在不改动业务代码的情况下,执行拦截器的方法来增强现有的逻辑.在mybatis中,同样也有这样的业务 ...

  8. SSM 使用 mybatis 分页插件 pagehepler 实现分页

    使用分页插件的原因,简化了sql代码的写法,实现较好的物理分页,比写一段完整的分页sql代码,也能减少了误差性. Mybatis分页插件 demo 项目地址:https://gitee.com/fre ...

  9. mybaits源码分析--自定义插件(七)

    一.MyBatis插件 插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或者改变原有的功能,MyBatis中也提供的有插件,虽然叫插件,但是实际上是通过拦截器(Int ...

  10. Cordova与现有框架的结合,Cordova插件使用教程,Cordova自定义插件,框架集成Cordova,将Cordova集成到现有框架中

    一.框架集成cordova 将cordova集成到现有框架中 一般cordova工程是通过CMD命令来创建一个工程并添加Android.ios等平台,这样的创建方式可以完整的下载开发过程中所需要的的插 ...

随机推荐

  1. LeetCode297:hard级别中最简单的存在,java版,用时击败98%,内存击败百分之九十九

    本篇概览 因为欣宸个人水平有限,在刷题时一直不敢面对hard级别的题目,生怕出现一杯茶一包烟,一道hard做一天的窘境 这种恐惧心理一直在,直到遇见了它:LeetCode297,建议不敢做hard题的 ...

  2. 如何创建集成 LSP 支持多语言的 Web 代码编辑器

    对于一个云开发平台来说,一个好的 Web IDE 能很大程度地提高用户的编码体验,而一个 Web IDE 的一个重要组成部分就是代码编辑器. 目前有着多款 web 上的代码编辑器可供选择,比如 Ace ...

  3. 「atcoder - agc054c」Roughly Sorted

    link. 高妙题,我只会到构造下界那一步-- 构造下界比较容易,只需要注意到交换一次最多让序列向合法迫近一步即可.则答案下界为 \(\sum_i \max\{\left(\sum_{j < i ...

  4. Note -「SOS DP」高维前缀和

    本文差不多算是翻译了一遍 CF blog?id=45223 就是抄了一遍,看不懂可以去原文. 当然我的翻译并不是完全遵从原文的. Part. 1 Introduction 平时我们怎么求高维前缀和?容 ...

  5. k8s 自动扩缩容HPA原理及adapter配置详解👑

    大家好,我是蓝胖子,都知道,k8s拥有自动扩缩容机制HPA,我们能够通过配置针对不同的扩缩容场景进行自动扩缩容,往往初学者在面对其中繁多配置的时候会学了又忘记,今天我将会以一种不同的视角,结合api ...

  6. vue~封装一个文本框添加与删除的组件

    标签组件的效果如下 组件作用 这是一个div,包含了两个文本框,后面是添加和删除按钮 添加按钮复制出新的div,除了文本框没有内容,其它都上面一样 删除按钮将当前行div删除 组件实现 <tem ...

  7. Quantitative Relationship Induction

    数量关系是指事物之间的数值或数量之间的相互关系(+.-.*./). 数量关系描述各种量的变化和相互关系.数量关系可以包括数值的比较.增减.比例.百分比.平均值等方面. 在数学中,数量关系可以通过代数方 ...

  8. jmeter生成随机英文的几种方法

    第一种:用BeanShell后置处理程序 1.写脚本 import java.util.Random; String random(int s_length) {       strings= &qu ...

  9. Speex详解(2019年09月25日更新)

    Speex详解 整理者:赤勇玄心行天道 QQ号:280604597 微信号:qq280604597 QQ群:511046632 博客:www.cnblogs.com/gaoyaguo 大家有什么不明白 ...

  10. tunm, 一种对标JSON的二进制数据协议

    Tunm simple binary proto 一种对标JSON的二进制数据协议 支持的数据类型 基本支持的类型 "u8", "i8", "u16& ...