基于Spring Aop+Mybatis实现修改记录

一、前言

工作时有个需求,记录修改记录。但是好几张表关联在一起,如果一张表一张表写,造成大量的业务代码,还浪费时间。
在此基础上想到一个办法,利用AOP实现修改记录。

基于环境:springboot springaop mybatis

二、注意点及实现方案

1. 表结构设计

CREATE TABLE `biz_update_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `type` char(2) DEFAULT NULL COMMENT '标识',
  `type_id` varchar(51) DEFAULT NULL COMMENT '标识ID',
  `field` varchar(255) DEFAULT NULL COMMENT '字段',
  `field_remark` varchar(255) DEFAULT NULL COMMENT '字段描述',
  `old_text` varchar(255) DEFAULT NULL COMMENT '旧值',
  `new_text` varchar(255) DEFAULT NULL COMMENT '新值',
  `action` varchar(255) DEFAULT NULL COMMENT '操作唯一标识',
  `remark` varchar(500) DEFAULT NULL COMMENT '描述',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=918 DEFAULT CHARSET=utf8mb4;

2. 思路实现

自定义注解 解决主表与关联表的关系和描述,使用AOP完成拦截器的功能。
根据不同的service 调用不同的selectXXByXXId完成旧数据的查询。

2.1 Update注解

public @interface Update {
    ......
    /**
     * 插入的ID
     */
    String id() default "id";

    /**
     * update查询的主键
     */
    String primaryKey() default "id";

    /**
     * 说明内容
     */
    String remark() default "";

    /**
     * 读取内容转表达式 (如: 0=男|1=女|2=未知)
     */
    String readConverterExp() default "";
    ......
}

主要几个属性:

  • id:当前表的ID
  • primaryKey:主表的ID
  • remark:字段的描述
  • readConverterExp:支持内容表达式(必须按照规定结构)

2.2 主要实现流程

  • 1 aop 拦截注解
  • 2 根据不同的操作类型,执行不同handler方法。(以Update为例)
  • 3 update方法中形参的Class对象,转成默认的mapper.selectXXByXXId方法查询旧数据。
  • 4 旧数据与新数据对比后,执行插入操作。

三、注意点及实现方案

1. 获取成员属性

    /**
     * 获取类成员的名称和注解
     */
    private List<UpdateField> getClassField(Class clazz, Object obj) {
        List<UpdateField> list = new ArrayList<>();
        // 占位
        list.add(0, null);
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(Update.class)) {
                Update update = field.getAnnotation(Update.class);

                Object value = typeFormatter(ReflectUtils.invokeGetter(obj, field.getName()));
                // 解析内容表达式
                value = StringUtils.isNotEmpty(update.readConverterExp()) ? reverseByExp(value, update.readConverterExp()) : value;

                if (StringUtils.isNotEmpty(update.remark())) {
                    UpdateField updateField = new UpdateField();
                    updateField.setUpdate(field.getAnnotation(Update.class));
                    updateField.setField(field.getName());
                    updateField.setProperty(value);
                    list.add(updateField);
                }

                // pojo 只允许一个field属性 代表当前数据的标识
                if ((update.field())) {
                    list.set(0, new UpdateField(field.getName(), update, value));
                }
            }
        }
        return list;
    }

2. 形参转默认mapper查询方法

    /**
     *
     * 根据当前class 类名 获取默认mapper名称以及默认ById方法
     */
    private String getName(Class clazz, String id) {
        String methodName = clazz.getName().replaceAll(DEFAULT_PACKAGE_MODEL_NAME, DEFAULT_PACKAGE_MAPPER_NAME);
        methodName = methodName.replaceAll(clazz.getSimpleName(), clazz.getSimpleName() + "Mapper");
        methodName += ".select" + StringUtils.capitalize(clazz.getSimpleName()) + "By" + StringUtils.capitalize(id);
        return methodName;
    }

3. 解析表达式

 /**
     *
     * 反向解析值
     */
    private static Object reverseByExp(Object propertyValue, String converterExp) {
        for (String item : converterExp.split("\\|")) {
            String[] itemArray = item.split("=");
            if (itemArray[0].equals(propertyValue)) {
                return itemArray[1];
            }
        }
        return propertyValue;
    }
  1. 执行handler( 以UPDATE操作为例)
private void handleUpdateRecord(Update update, Object newObj) {
        try {
            Class clazz = newObj.getClass();

            //根据默认方法名称 查询 update之前数据
            Object oldObj = sqlSession.selectOne(getName(clazz, update.primaryKey()), ReflectUtils.invokeGetter(newObj, update.primaryKey()));
            Object id = ReflectUtils.invokeGetter(oldObj, update.id());

            List<UpdateField> newList = getClassField(clazz, newObj);
            List<UpdateField> oldList = getClassField(clazz, oldObj);

            // 删除field
            oldList.remove(0);
            UpdateField fieldUpdateType = newList.remove(0);
            for (int i = 0; i < newList.size(); i++) {
                UpdateField oldUpdateField = oldList.get(i);
                UpdateField newUpdateField = newList.get(i);

                UpdateRecord updateRecord = new UpdateRecord();
                updateRecord.setTypeId(String.valueOf(id));
                updateRecord.setType(update.updateRecordEnum().getType());

                Object oldProperty = oldUpdateField.getProperty();
                Object newProperty = newUpdateField.getProperty();

                if (oldProperty == null) {
                    if (newProperty == null || (StringUtils.isEmpty(newProperty))) {
                        break;
                    }
                } else if (oldProperty.getClass() == String.class && StringUtils.isEmpty(oldProperty.toString())) {
                    if (newProperty == null || (StringUtils.isEmpty(newProperty))) {
                        break;
                    }
                } else if (!oldUpdateField.getProperty().equals(newUpdateField.getProperty())) {
                    updateRecord.setOldText(oldProperty.toString());
                }

                AsyncManager.me().execute(AsyncFactory.recordUpdate(setUpdateRecord(updateRecord, newUpdateField, fieldUpdateType)));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

四、使用说明

  • field = true 描述的成员是不能修改的
  • 由于水平有限,代码只供参考。这代码质量用到项目里怕是有点难。。。
  • 项目里比较乱,东拼西凑的代码,请见谅。

五、未解决问题

如果拦截的方法抛了异常。回滚的问题目前还在考虑!!!

六、总结

总体上来说功能算是实现了,但是细节上得打磨一下。我估摸着如果公司项目里不出BUG,我是不会改了,能跑就行要求不高,水平有限。
项目地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 对于java中的思考的方向,1必须要看前端的页面,对于前端的页面基本的逻辑,如果能理解最好,不理解也要知道几点。 ...
    神尤鲁道夫阅读 797评论 0 0
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,422评论 0 4
  • mybatis中的sqlSession是线程安全的吗? 链接:https://blog.csdn.net/qq_3...
    刘小刀tina阅读 2,050评论 0 3
  • 这部分主要是开源Java EE框架方面的内容,包括Hibernate、MyBatis、Spring、Spring ...
    杂货铺老板阅读 1,336评论 0 2
  • 想我孙女,又失眠了。十二点,一点,两点,快三点了,怎么也睡不着。我宝宝现在长多高了,会说哪些话了,喜欢吃些什么东西...
    寒江雪810阅读 198评论 0 0