Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >关键数据变更监控

关键数据变更监控

原创
作者头像
3号攻城狮
修改于 2018-05-19 23:29:01
修改于 2018-05-19 23:29:01
2.8K5
举报

#故事的开始

某个深夜,小朱(产品经理)悄悄发来微信

代码语言:txt
AI代码解释
复制
对于关键信息的变更,我们能持久化变更日志么?
.......

省略N多场景描述,总结就是:

代码语言:txt
AI代码解释
复制
想知道,某一天,某,把某个数据,从某改成了某?

技术架构

拿到需求之后,自然难以入睡.分析了一下我们当前的应用结构.

代码语言:txt
AI代码解释
复制
1.采用SpringCloud框架,以微服务的形式架构应用,每个服务都有自己独立的数据库,涉及到跨数据库取数时,非主数据均采用远程服务调用.
2.底层持久化框架采用mybatis.

#解决方案分析

数据库触发器

第一方案就想到在数据库写触发器,但是第一个否认的也是该方案.

代码语言:txt
AI代码解释
复制
灵活性差,针对不同表,对于每一个字段都需要处理,毕竟我们不是想监控每一个字段.不能灵活的配置监控表,监控字段.另直接嵌入数据库,不利于控制

mybatis拦截器

在经过了对mybatis的一番检索之后,没有发现对该需求的解决方式.在认知范围内,想到了使用mabatis拦截器解决该问题。

1.简单构建一个持久化实体,表结构省略.

代码语言:txt
AI代码解释
复制
public class SqlLog {
    @Id
    @GeneratedValue()
    protected String id;
    private Integer result;
    private Date whencreated;
    private String sql;
    private String parameter;
}

2.添加接口类

代码语言:txt
AI代码解释
复制
public interface SqlLogMapper extends BaseMapper<SqlLog> {
    int insertSqlLog(SqlLog log);
}

3.添加对应mapper.xml

代码语言:txt
AI代码解释
复制
<?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.hand.hap.cloud.mybatis.mapper.SqlLogMapper">
    <insert id="insertSqlLog" parameterType="com.hand.hap.cloud.mybatis.domain.SqlLog">
        insert into sql_log(result, whencreated,sql,parameter) values(#{result}, #{whencreated},#{sql},#{parameter})
    </insert>
</mapper>

4.编写拦截器

代码语言:txt
AI代码解释
复制
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class UpdateInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = null;
        Object object = invocation.getTarget();
        Object[] args = invocation.getArgs();
        SqlLog log = new SqlLog();
        if (object instanceof Executor) {
            MappedStatement mappedStatement = (MappedStatement) args[0];
            if (args.length > 1) {
                Object domain = args[1];
                Configuration configuration = mappedStatement.getConfiguration();
                Object target = invocation.getTarget();
                StatementHandler handler = configuration.newStatementHandler((Executor) target, mappedStatement,
                        domain, RowBounds.DEFAULT, null, null);
                BoundSql boundSql = handler.getBoundSql();
                log.setParameter(boundSql.getParameterObject().toString());
                log.setSql(boundSql.getSql());
                //记录时间
                log.setWhencreated(new Date());
            }
            //执行真正的操作
            result = invocation.proceed();
            mappedStatement = mappedStatement.getConfiguration().getMappedStatement("insertSqlLog");

            log.setResult(Integer.valueOf(Integer.parseInt(result.toString())));
            args[0] = mappedStatement;
            //insertSqlLog 方法的参数为 log
            args[1] = log;
            //执行insertSqlLog方法
            invocation.proceed();
        }
        return result;
    }
    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

该解决方案的思路就是在执行完更新操作之后,保存本次操作的操作记录.对比上一次操作记录,就可以形成一个修改闭环.

该方案也被否定.对于每一个数据库都要有一张日志记录表,或者是对改表有操作权限.对于mybatis而言,我们想要做的是一个通用的持久化方案,不应该嵌入业务需求.当然这仅是在本人的认知范围的一些拙见.

Spring AOP

最终采用的方案是在应用层监控mybatis的底层更新方法.达到了如下目标:

代码语言:txt
AI代码解释
复制
1.通过注解,简单可配置
2.异步解析与应用结偶

但是目前也存在如下不足:

代码语言:txt
AI代码解释
复制
一定程度上于我们项目的编程风格绑定,切合了我们的代码分层结构,如果其他小伙伴要用,一定程度上需要改写部分逻辑

AOP 插件编写

新建插件项目

image.png
image.png

编写注解,适用于表审计,列审计

代码语言:txt
AI代码解释
复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableAudit {
    //默认针对字段单个字段审计 配合ColumnAudit使用
    boolean all() default false;
}
代码语言:txt
AI代码解释
复制
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ColumnAudit {
}

编写切面

代码语言:txt
AI代码解释
复制
@Aspect
@Component
public class TableUpdateAop {
    private static Logger LOGGER = LoggerFactory.getLogger(TableUpdateAop.class);
    @Autowired
    private RabbitMqSender rabbitMqSender;

    private static final String OPERATOR_ID ="operatorId";
    private static final String TABLE_NAME ="tableName";
    private static final String LAST_NAME ="lastName";
    private static final String ID ="id";
    private static final String MAPPER = "Mapper";

    /*我们使用的是mybatis源码,然后下载下来,自己做过一定的加工处理
    *如果小伙伴在采用改方式处理table日志变更记录时只需要把切面对接到mybatis的核心修改接口就可以
    * UpdateByPrimaryKeySelectiveMapper
    * UpdateByPrimaryKeyMapper
    * */
    @Pointcut("execution(public * com.hand.hap.cloud.mybatis.common.base.update.*.update*(..))")
    public void audit(){}
 
    @Around("audit()")
    public Object doBefore(ProceedingJoinPoint pjp) throws  Throwable{

        Map<String,Object> originValues = null;
        Map<String,Object> newValues = null;
        Map<String,Object> baseValues = null;

        boolean sendAudit = false;

        try {
            Object[] args = pjp.getArgs();
            Object parameter = args[0];
            Class clazz =parameter.getClass();

            TableAudit tableAudit = (TableAudit)clazz.getAnnotation(TableAudit.class);
            if( tableAudit!=null){

                StringBuilder clazzName = new StringBuilder();

                String mapperName = clazz.getName().replace("domain","mapper");
                clazzName.append(mapperName)
                        .append(MAPPER);
                //此处和我们的项目代码结构有关,如果采用这种方式需要作出一些调整
                BaseMapper baseMapper = (BaseMapper) SpringContextUtil.getBean(Class.forName(clazzName.toString()));
                Object dbObj =baseMapper.selectByPrimaryKey(parameter);
                //收集当前信息
                originValues = collectValue(dbObj,tableAudit.all());
                baseValues = new HashMap<>();

                //收集表名
                String tableName = StringUtil.camel2Underline(clazz.getSimpleName());
                baseValues.put(TABLE_NAME,tableName);
                //收集用户信息 需要根根自己项目做一些调整
                baseValues.put(OPERATOR_ID,RequestContext.getUserId());
                baseValues.put(LAST_NAME, RequestContext.getLastName());
                //数据主键
                baseValues.putAll(getFieldValueByName(clazz.getDeclaredField(ID),dbObj));

                newValues = collectValue(parameter,tableAudit.all());
                sendAudit = true;
            }
        }catch (Exception e){
            LOGGER.warn("Table update data collect failed, error info {}",e.getMessage());
        }

        Object result = pjp.proceed();

        try {
            if(sendAudit && Integer.valueOf(Integer.parseInt(result.toString()))>0){
                sender(baseValues,originValues,newValues);
            }
        }catch (Exception e){
            LOGGER.warn("Table update data send failed, error info {}",e.getMessage());
        }

        return result;
    }


    public  Map<String,Object> collectValue(Object obj,boolean scope) throws Exception{

        Map<String,Object> values = new HashMap();

        List<Field> list = Arrays.asList(obj.getClass().getDeclaredFields());
        for(int i=0;i<list.size();i++){
            Field field = list.get(i);
            if(scope){
                getFieldValueByName(field,obj);
            }else{
                if(field.isAnnotationPresent(ColumnAudit.class)){
                    values.putAll(getFieldValueByName(field,obj));
                }
            }
        }
        return values;
    }

    public  void sender(Map<String,Object> baseValues ,Map<String,Object> originValues , Map<String,Object> newValues){

        List<TableUpdateLog> logs = collectValue(baseValues,originValues,newValues);
        //此处我们采用的是rabbitMq 理论上此处只要通过异步方式处理即可
        rabbitMqSender.handleTableAuditData(logs);
    }
    public static List<TableUpdateLog> collectValue( Map<String,Object> baseValues ,Map<String,Object> originValues , Map<String,Object> newValues ){

        List<TableUpdateLog> logs = new ArrayList<>();

        if(!CollectionUtils.isEmpty(baseValues) && !CollectionUtils.isEmpty(originValues) && !CollectionUtils.isEmpty(newValues) ){
            for(Map.Entry entry : originValues.entrySet()){
                TableUpdateLog log = new TableUpdateLog();
                String columnName = entry.getKey().toString();
                log.setColumnName(columnName);
                if(entry.getValue()!=null){
                    log.setOriginValue(entry.getValue().toString());
                }
                if(newValues.get(columnName)!=null){
                    log.setNewValue(newValues.get(columnName).toString());
                }
                if(baseValues.get(OPERATOR_ID)!=null){
                    log.setOperatorId(baseValues.get(OPERATOR_ID).toString());
                }

                log.setCreationDate(DateUtil.now().getTime());
                if(baseValues.get(LAST_NAME)!=null){
                    log.setLastName(baseValues.get(LAST_NAME).toString());
                }
                if(baseValues.get(ID)!=null){
                    log.setPrimaryKey(baseValues.get(ID).toString());
                }
                if(baseValues.get(TABLE_NAME)!=null){
                    log.setTableName(baseValues.get(TABLE_NAME).toString());
                }
                logs.add(log);
            }
        }
        return logs;
    }


    public  Map<String,Object> getFieldValueByName(Field field,Object obj) throws Exception{

        Map<String,Object>  kv = new HashMap<>();

        String name = field.getName();
        // 将属性的首字符大写,方便构造get,set方法
        name = name.substring(0, 1).toUpperCase() + name.substring(1);
        // 获取属性的类型
        //String type = field.getGenericType().toString();
        // 如果type是类类型,则前面包含"class ",后面跟类名
        Method m = obj.getClass().getMethod("get" + name);
        // 调用getter方法获取属性值
        Object value =  m.invoke(obj);
        String underlineFieldName = StringUtil.camel2Underline(name);
        kv.put(underlineFieldName,value);
        return kv;
    }
}

工具方法

代码语言:txt
AI代码解释
复制
@Service
public class SpringContextUtil  implements ApplicationListener<ContextRefreshedEvent> {
    private static ApplicationContext applicationContext = null;
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if(applicationContext == null){
            applicationContext = event.getApplicationContext();
        }
    }
    /*ApplicationContext context= ContextLoader.getCurrentWebApplicationContext();//尝试下这个方法*/
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static Object getBean(Class clazz) {
        return applicationContext.getBean(clazz);
    }
}

MQ 处理工具

代码语言:txt
AI代码解释
复制
@Configuration
public class RabbitMqConfig {
public static final String QUEUE\_TABLE\_AUDIT = "queue.table.audit";
@Bean
public Queue queueTableAudit() {
return new Queue(QUEUE_TABLE_AUDIT);
}
}
代码语言:txt
AI代码解释
复制
@Component
public class RabbitMqSender {
@Autowired
private AmqpTemplate rabbitTemplate;
public void handleTableAuditData(List<TableUpdateLog> logs) {
    String data = new JSONWriter().write(logs);
    this.rabbitTemplate.convertAndSend(RabbitMqConfig.QUEUE_TABLE_AUDIT, data.getBytes());
}
}

日志审计bean

代码语言:txt
AI代码解释
复制
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TableUpdateLog  {
@Id
@GeneratedValue()
private Integer id;
private String tableName;
private String primaryKey;
private String batchNumber;
private String columnName;
private String originValue;
private String newValue;
private String operatorId;
private String lastName;
private Long   creationDate;
}

基础配置

代码语言:txt
AI代码解释
复制
spring:
  rabbitmq:
    host: 192.168.11.210
    port: 5672

整个插件我们已经完成,接下来我们使用刚刚搭建的插件

插件使用

导入插件

代码语言:txt
AI代码解释
复制
 <dependency>
            <groupId>com.hscf.cloud</groupId>
            <artifactId>hscf-table-monitor-starter</artifactId>
            <version>${hcloud.version}</version>
</dependency>

对应domain上配置注解

代码语言:txt
AI代码解释
复制
@ModifyAudit
@VersionAudit
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableAudit
public class SysResourceGroup extends AuditDomain {
    @Id
    @GeneratedValue
    @ColumnAudit
    private Long id;
    @ColumnAudit
    private String groupCode;
    @ColumnAudit
    @NotNull
    private String groupName;
    @ColumnAudit
    private String description;
}

使用就这么简单,当对这个domain进行修改操作时,就会监控其变更数据.

接下来我们对收集到的信息进行处理.

信息收集

新建服务

image.png
image.png

处理MQ队列消息

代码语言:txt
AI代码解释
复制
@Component
public class RabbitMqReceivers {

    private Logger logger = LoggerFactory.getLogger(RabbitMqReceivers.class);
    @Autowired
    private TableUpdateLogDao tableUpdateLogDao;

    @RabbitListener(queues = "queue.table.audit")
    @RabbitHandler
    public void handleApiMonitor(byte[] data, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        JSONArray arrays = null;
        try {
            arrays = JSON.parseArray(new String(data));

            if(arrays!=null && !arrays.isEmpty()){

                String batchNumber = UUID.randomUUID().toString();

                for(Object logJson :arrays ){
                    TableUpdateLog log = (TableUpdateLog)BeanParser.parse((JSONObject)logJson, TableUpdateLog.class);
                    log.setBatchNumber(batchNumber);
                    tableUpdateLogDao.save(log);
                }
            }
        } catch (Exception e) {
            logger.error("Handle table admin data failed, data:{}", arrays);
        } finally {
            channel.basicAck(deliveryTag, false);
        }
    }
}

效果截图

image.png
image.png

总结

以上方案还有很多不足,编写也比较匆忙,小伙伴们多多原谅.

总的说来,这是目前想到的一个比较实用的解决方式.

如果小伙伴们有好的解决方式,QAQ,评论区走起留言.

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
5 条评论
热度
最新
专程注册了帐号来点赞,这篇文章完美地解决了我的问题,感谢!
专程注册了帐号来点赞,这篇文章完美地解决了我的问题,感谢!
回复回复点赞举报
考虑到了事务的情况吗?如果一个service里面有多个Dao调用,最后此service回滚了,按照这个代码,也会当做成功记录下来,是么?
考虑到了事务的情况吗?如果一个service里面有多个Dao调用,最后此service回滚了,按照这个代码,也会当做成功记录下来,是么?
回复回复点赞举报
其实关键数据变更的记录挺重要的,尤其是变更的日志或变更权限等一定要设置好~
其实关键数据变更的记录挺重要的,尤其是变更的日志或变更权限等一定要设置好~
回复回复点赞举报
需求不是看得很明白,想知道,某一天,某,把某个数据,从某改成了某?,git好像就可以
需求不是看得很明白,想知道,某一天,某,把某个数据,从某改成了某?,git好像就可以
11点赞举报
git如果没有理解错的话,是对代码或文件的一个变更追踪,本文解决的是针对数据(数据库数据)变更的一个日志记录方案。
git如果没有理解错的话,是对代码或文件的一个变更追踪,本文解决的是针对数据(数据库数据)变更的一个日志记录方案。
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
mybatis拦截器详解_短信拦截器
  拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。
全栈程序员站长
2022/09/30
1.7K0
MyBatis插件编写
Mybatis是一个操作数据库的工具,在一些场景下应用有些自定义的需求,在数据库整个执行流程上需有一些插入点可以接入自己的逻辑,如针对数据库敏感字段加密,分页等,因此MyBatis在设计的时候就采取发插件化的设计,可以让应用加入自己的逻辑。
心平气和
2021/01/29
6440
SpringBoot集成Mybatis
开始之前,我们一起学习一下,社区其他小伙伴的文章:中小银行咨询服务的实战案例分享:技术咨询第二阶段的“术”—选型原则构筑技术高地, 作者:爱艺江河
后台技术汇
2024/11/16
1190
SpringBoot集成Mybatis
Mybatis分页拦截器
5.注意,参数都要封装到对象里,不能用string,int等基本类型,因为在拦截器中获取参数时用的是getter,基本类型数据没有getter和setter
IT云清
2019/01/22
2.2K0
mybatis 拦截器 添加参数_mybatis传递多个参数
在mybatis的mapper.xml文件中,我们可以使用#{}或${}的方式获取到参数,这些参数都需要提前我们在mapper.java接口文件中通过参数的方式传入参数才能取到
全栈程序员站长
2022/10/03
1.9K0
mybatis 拦截器 添加参数_mybatis传递多个参数
基于Mybatis手撸一个分表插件
事情是酱紫的,阿星的上级leader负责记录信息的业务,每日预估数据量是15万左右,所以引入sharding-jdbc做分表。
Java小咖秀
2021/06/08
1.5K0
基于Mybatis手撸一个分表插件
mybatis清空一级缓存_jvm缓存
本次封装需要所掌握的知识面要求比叫高,涉及到 反射、注解取值、启动类、mybatis缓存原理、mybatis拦截器、Redis操作、数据结构、sqlSessionFactory 掌握、lambda表达式等一系列的内容,代码层面可以多研究研究。
全栈程序员站长
2022/09/27
1.7K0
mybatis清空一级缓存_jvm缓存
Mybatis plus 动态表名插件开发
因为 mybatis plus 重写了一些类,只要将相关的插件放到 Spring 当中就会自动加载插件。所以这里注册一下 Bean 就行。
啵啵肠
2023/11/20
3500
Rpamis-security-原理解析
rpamis-security (opens new window)1.0.1主要通过Mybatis-Plugin及AOP切面实现安全功能,其主要组件如下图所示
benym
2023/12/14
2540
Rpamis-security-原理解析
利用Mybatis拦截器,全局处理入库字段
需要对某张表的个别字段删除全部空格、替换半角括号,但是项目里入口比较多,不止有前端录入,还有接口接收的数据。即使现在全部入口处理了,后续新增入口也不能保证。所以需要统一处理,一劳永逸。
Yuyy
2022/09/21
6790
Mybatis-Plus 支持分库分表了?-官方神器发布!
今天介绍一个 MyBatis - Plus 官方发布的神器:mybatis-mate 为 mp 企业级模块,支持分库分表,数据审计、数据敏感词过滤(AC算法),字段加密,字典回写(数据绑定),数据权限,表结构自动生成 SQL 维护等,旨在更敏捷优雅处理数据。
JAVA葵花宝典
2021/11/15
2.1K0
你还不会搞数据脱敏?MyBatis 插件 + 注解轻松实现数据脱敏,So easy~!
根据不同的要求,我们只需要对ParameterHandler和ResultSetHandler进行切入。定义特定注解,在切入时需要检查字段中是否包含注解来是否加解密。
Java技术栈
2023/02/27
1.9K0
你还不会搞数据脱敏?MyBatis 插件 + 注解轻松实现数据脱敏,So easy~!
官方发布 | Mybatis-Plus 支持分库分表
MyBatis - Plus 官方发布的神器:mybatis-mate 为 mp 企业级模块,支持分库分表,数据审计、数据敏感词过滤(AC算法),字段加密,字典回写(数据绑定),数据权限,表结构自动生成 SQL 维护等,旨在更敏捷优雅处理数据。
码农架构
2021/12/01
2.1K0
官方发布 | Mybatis-Plus 支持分库分表
springboot实战之ORM整合(mybatis篇)
本文会介绍一下springboot与mybatis、mybatisplus如何进行整合,文章篇幅会有点长
lyb-geek
2019/09/25
1.4K0
面试官:聊聊你读过的开源代码中用到的设计模式
我:大家都知道,经典的设计模式中有创建型、结构型、行为型3大类,共23种设计模式。但是我们日常开发中经常使用到的非常少,不到10种,比如代理模式、装饰器模式、策略模式、模板模式、职责链模式...
jinjunzhu
2020/08/20
6450
用晋升加薪,讲解DDD领域模型中的对象设计 —— 聚合、实体、值对象
此外本文也通过关于雇员薪酬调整的案例,渗透讲解 DDD 模型中的聚合对象、实体对象和值对象在领域模型中的实践。
小傅哥
2023/09/06
1.2K0
用晋升加薪,讲解DDD领域模型中的对象设计 —— 聚合、实体、值对象
MyBatisPlus又在搞事了!一个依赖轻松搞定权限问题!堪称神器
◆前言: 今天介绍一个 MyBatis - Plus 官方发布的神器:mybatis-mate 为 mp 企业级模块,支持分库分表,数据审计、数据敏感词过滤(AC算法),字段加密,字典回写(数据绑定),数据权限,表结构自动生成 SQL 维护等,旨在更敏捷优雅处理数据。 ◆1. 主要功能 字典绑定 字段加密 数据脱敏 表结构动态维护 数据审计记录 数据范围(数据权限) 数据库分库分表、动态数据源、读写分离、数- - 据库健康检查自动切换。 ◆2.使用 ◆2.1 依赖导入 Spring Boot 引入自动依赖注
IT大咖说
2022/09/15
1.8K0
MyBatisPlus又在搞事了!一个依赖轻松搞定权限问题!堪称神器
分库分表路由组件构建方案V1
获取多个数据源我们肯定需要在yaml或者properties中进行配置。所以首先需要获取到配置信息; 定义配置文件中的库和表:
xbhog
2022/10/31
3890
Mybatis-Plus3.0默认主键策略导致自动生成19位长度主键id的坑
某天检查一位离职同事写的代码,发现其对应表虽然设置了AUTO_INCREMENT自增,但页面新增功能生成的数据主键很诡异,长度达到了19位,且并非是从1开始递增的——
朱季谦
2021/12/10
6.5K0
Mybatis-Plus3.0默认主键策略导致自动生成19位长度主键id的坑
MyBatis 源码分析 - 插件机制
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
田小波
2018/09/20
5460
MyBatis 源码分析 - 插件机制
相关推荐
mybatis拦截器详解_短信拦截器
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档