背景

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

以往的做法通常是手动在每个业务逻辑里耦合上这么一块代码,也有更优雅一点的做法是写一个拦截器,然后在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. 推荐一个react上拉加载更多插件:react-infinite-scroller

    在开发网页和移动应用时,经常需要处理大量数据的展示和加载.如果数据量非常大,一次性全部加载可能会导致页面卡顿或崩溃.为了解决这个问题,我们可以使用无限滚动(Infinite Scroll)的技术.Re ...

  2. 记一次 .NET 某电力系统 内存暴涨分析

    一:背景 1. 讲故事 前些天有位朋友找到我,说他生产上的程序有内存暴涨情况,让我帮忙看下怎么回事,最简单粗暴的方法就是让朋友在内存暴涨的时候抓一个dump下来,看一看大概就知道咋回事了. 二:Win ...

  3. Building-Mobile-Apps-with-Ionic-2中文翻译工作

    最近没啥工作量, 然后学完了这本书, 接着又茫然找不到该干啥, 所以想着何不翻译这个书呢. 这本书首先给我们普及了Ionic 2的基础知识, Ionic 2和Ionic 1有本质上的区别, Ionic ...

  4. 运行在容器中Postgres数据库数据损坏后如何恢复?

    前言 在使用 K8S 部署 RSS 全套自托管解决方案- RssHub + Tiny Tiny Rss, 我介绍了将 RssHub + Tiny Tiny RSS 部署到 K8s 集群中的方案. 其中 ...

  5. std::copy与std::back_inserter引发的惨案

    #include <iostream> #include <vector> #include <numeric> #include <sstream> ...

  6. DevOps|研发效能解决的是企业效率问题

    研发效能并不能解决企业效益问题 它不是利润中心,不能给你带来直接收入(研发效能相关工具厂商做咨询.出方案.卖工具除外).想要解决企业效益问题,依赖于企业战略.业务/产品.组织.运营.创新等其他方面. ...

  7. Bridge 桥接模式简介与 C# 示例【结构型2】【设计模式来了_7】

    〇.简介 1.什么是桥接模式? 一句话解释:   通过一个类的抽象,与另一个类的抽象关联起来,当做桥.此后不管两个抽象类的实现有多少种,均可以通过这个桥来将两个对象联系起来. 桥接,顾名思义就是用桥来 ...

  8. PHP-basename

    basename 定义: basename() 函数返回路径中的文件名部分. 语法: basename(path,suffix) 参数 描述 path 必需.规定要检查的路径 suffix 可选.规定 ...

  9. 15. 从零开始编写一个类nginx工具, 如果将nginx.conf转成yaml,toml,json会怎么样

    wmproxy wmproxy将用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,后续将实现websocket代理, 内外网穿透等, 会将实现过程分享出来, 感 ...

  10. Nodejs环境打包前端项目

    Node.js 在Linux下安装和环境搭建/编译项目 安装nodejs:1.下载nodejs源码包 wget https://nodejs.org/dist/v14.16.0/node-v14.16 ...