·设为首页收藏本站📧邮箱修改🎁免费下载专区💎积分✅卡密📒收藏夹👽聊天室
DZ插件网 门户 网站安全 查看内容

工作六年,看到这样的代码,内心五味杂陈...

2024-8-18 17:14| 发布者: 左右不逢缘| 查看: 53922| 评论: 0

摘要: mall学习教程官网:macrozheng.com作者:uzong来源:juejin.cn/post/7294844864020430902在掘金看到一篇文章,让我产生了想要分享的欲望。讲述的是面对同一个需求,一个工作经验不到两年的小鲜肉和一个工作六年的老 ...

工作六年,看到这样的代码,内心五味杂陈...1586 作者: 来源: 发布时间:2024-8-18 17:14

mall学习教程官网:macrozheng.com
作者:uzong来源:juejin.cn/post/7294844864020430902

在掘金看到一篇文章,让我产生了想要分享的欲望。

讲述的是面对同一个需求,一个工作经验不到两年的小鲜肉和一个工作六年的老司机给出的两个不同技术方案的实现落地。

先是小鲜肉写了一版实现,然后老司机在审查代码的时候觉得应该有更优雅的落地解决方案,于是又按照自己的思路重构了一版。

一、历史背景


那天下午,看到了令我终生难忘的代码,那一刻破防了......

故事还得从半年前数据隔离的那个事情说起......

1.1 数据隔离


预发,灰度,线上环境共用一个数据库。

每一张表有一个 env 字段,环境不同值不同。

特别说明:env 字段即环境字段。

如下图所示:

工作六年,看到这样的代码,内心五味杂陈...2193 作者: 来源: 发布时间:2024-8-18 17:14

这或许是一个对你有用的开源项目,mall项目是一套基于 SpringBoot + Vue + uni-app 实现的电商系统(Github标星60K),采用Docker容器化部署,后端支持多模块和微服务架构。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
  • Boot项目:https://github.com/macrozheng/mall
  • Cloud项目:https://github.com/macrozheng/mall-swarm
  • 视频教程:https://www.macrozheng.com/video/

项目演示:
工作六年,看到这样的代码,内心五味杂陈...4997 作者: 来源: 发布时间:2024-8-18 17:14

1.2 隔离之前


插曲:一开始只有 1 个核心表有 env 字段,其他表均无该字段;有一天预发环境的操作影响到客户线上的数据。为了彻底隔离,剩余的二十几个表均要添加上环境隔离字段。

当时二十几张表已经大量生产数据,隔离需要做好兼容过渡,保障数据安全。

1.3 隔离改造


其他表历史数据很难做区分,于是新增加的字段 env 初始化 all ,表示预发线上都能访问。以此达到历史数据的兼容。

每一个环境都有一个自己独立标志;从 application.properties 中读该字段;最终到数据库执行的语句如下:
SELECT XXX FROM tableName WHERE env = ${环境字段值} and ${condition}

1.4 隔离方案


最拉胯的做法:每一张表涉及到的 DO、Mapper、XML等挨个添加 env 字段。但我指定不能这么干!!!

工作六年,看到这样的代码,内心五味杂陈...2111 作者: 来源: 发布时间:2024-8-18 17:14

具体方案:自定义 mybatis 拦截器进行统一处理。

通过这个方案可以解决以下几个问题:
  • 业务代码不用修改,包括 DO、Mapper、XML等。只修改 mybatis 拦截的逻辑。
  • 挨个添加补充字段,工程量很多,出错概率极高
  • 后续扩展容易

1.5 最终落地


在 mybatis 拦截器中, 通过改写 SQL。新增时填充环境字段值,查询时添加环境字段条件,真正实现改一处即可。

考虑历史数据过渡,将 env = {当前环境},'all')
SELECT xxx FROM ${tableName} WHERE env in (${当前环境},'all') AND ${其他条件}

具体实现逻辑如下图所示:

工作六年,看到这样的代码,内心五味杂陈...1041 作者: 来源: 发布时间:2024-8-18 17:14

1.其中 env 字段是从 application.properties 配置获取,全局唯一,只要环境不同,env 值不同

借助 JSqlParser 开源工具,改写 sql 语句,修改重新填充、查询拼接条件即可。

https://github.com/JSQLParser/JSqlParser

思路:自定义拦截器,填充环境参数,修改 sql 语句,下面是部分代码示例:
@Intercepts(
        {@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)
@Component
public class EnvIsolationInterceptor implements Interceptor {
    ......
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ......
        if (SqlCommandType.INSERT == sqlCommandType) {
            try {
                // 重写 sql 执行语句,填充环境参数等
                insertMethodProcess(invocation, boundSql);
            } catch (Exception exception) {
                log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), exception);
                throw exception;
            }
        }

        return invocation.proceed();
    }
}

一气呵成,完美上线。

二、发展演变

2.1 业务需求


随着业务发展,出现了以下需求:

1.上下游合作,我们的 PRC 接口在匹配环境上与他们有差异,需要改造
SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?,'all')

2.有一些环境的数据相互相共享,比如预发和灰度等

3.开发人员的部分后面,希望在预发能纠正线上数据等

2.2 初步沟通


这个需求的落地交给了来了快两年的小鲜肉。

在开始做之前,他也问我该怎么做。我简单说了一些想法,比如可以跳过环境字段检查,不拼接条件;或者拼接所有条件,这样都能查询;亦或者看一下能不能注解来标志特定方法,你想一想如何实现......

工作六年,看到这样的代码,内心五味杂陈...1490 作者: 来源: 发布时间:2024-8-18 17:14

年纪大了需要给年轻人机会。

2.3 勤劳能干


小鲜肉,没多久就实现了。

不过有一天下午他遇到了麻烦。他填充的环境字段取出来为 null,看来很久没找到原因,让我帮他看看。

但是不久前也还教过他 Arthas 如何使用呢,这种问题应该不在话下吧?

2.4 具体实现


大致逻辑:在需要跳过环境条件判断的方法前后做硬编码处理,同环切面逻辑, 一加一删。

填充颜色部分为小鲜肉的改造逻辑。

工作六年,看到这样的代码,内心五味杂陈...8735 作者: 来源: 发布时间:2024-8-18 17:14

大概逻辑就是:将 env 字段填充所有环境。条件过滤的忽略的目的。
SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${其他条件}

2.5 错误原因


经过排查是因为 API 里面有多处对 threadLoal 进行处理的逻辑,方法之间存在调用。

简化举例:A 和 B 方法都是独立的方法, A 在调用 B 的过程,B 结束时把上下文环境字段删除, A 在获取时得到 null。

具体如下:

工作六年,看到这样的代码,内心五味杂陈...2575 作者: 来源: 发布时间:2024-8-18 17:14

2.6 五味杂陈


当我看到代码的一瞬间,彻底破防了......

工作六年,看到这样的代码,内心五味杂陈...3047 作者: 来源: 发布时间:2024-8-18 17:14

queryProject 方法里面调用 findProjectWithOutEnv, 在两个方法中,都有填充处理 env 的代码。

2.7 遍地开花


然而,这三行代码,随处可见,在业务代码中遍地开花.......
// 1. 变量保存 oriFilterEnv
String oriFilterEnv = UserHolder.getUser().getFilterEnv();

// 2. 设置值到应用上下文
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());

//....... 业务代码 ....

// 3. 结束复原
UserHolder.getUser().setFilterEnv(oriFilterEnv);

工作六年,看到这样的代码,内心五味杂陈...8640 作者: 来源: 发布时间:2024-8-18 17:14

工作六年,看到这样的代码,内心五味杂陈...6114 作者: 来源: 发布时间:2024-8-18 17:14

改了个遍,很勤劳......

2.8 灵魂开问


工作六年,看到这样的代码,内心五味杂陈...7515 作者: 来源: 发布时间:2024-8-18 17:14

难道真的就只能这么做吗,当然还有......
  • 开闭原则符合了吗
  • 改漏了应该办呢
  • 其他人遇到跳过的检查的场景也加这样的代码吗
  • 业务代码和功能代码分离了吗
  • 填充到应用上下文对象 user 合适吗
  • .......

大量魔法值,单行字符超 500,方法长度拖几个屏幕也都睁一眼闭一只眼了,但整这一出,还是破防......

内心涌动,我觉得要重构一下。

三、重构一下

3.1 困难之处


在 mybatis intercept 中不能直接精准地获取到 service 层的接口调用。只能通过栈帧查询到调用链。

3.2 问题列表

  • 尽量不要修改已有方法,保证不影响原有逻辑;
  • 尽量不要在业务方法中修改功能代码;关注点分离;
  • 尽量最小改动,修改一处即可实现逻辑;
  • 改造后复用能力,而不是依葫芦画瓢地添加这种代码

3.3 实现分析

  • 用独立的 ThreadLocal,不与当前用户信息上下文混合使用
  • 注解+AOP,通过注解参数解析,达到目标功能
  • 对于方法之间的调用或者循环调用,要考虑优化

同一份代码,在多个环境运行,不管如何,一定要考虑线上数据安全性。

3.4 使用案例


采用了自定义注解的方式:@InvokeChainSkipEnvRule

其使用案例如下:

@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})

案例说明:project 表在预发环境校验跳过。

使用的方式就是在调用入口处添加该注解:
@SneakyThrows
@GetMapping("/importSignedUserData")
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void importSignedUserData(
    ......
    HttpServletRequest request,
    HttpServletResponse response) {
    ......
}

3.5 具体实现

  • 1.方法上标记注解, 注解参数定义规则
  • 2.切面读取方法上面的注解规则,并传递到应用上下文
  • 3.拦截器从应用上下文读取规则进行规则判断

工作六年,看到这样的代码,内心五味杂陈...881 作者: 来源: 发布时间:2024-8-18 17:14
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {

    /**
     * 是否跳过环境。 默认 true,不推荐设置 false
     *
     * @return
     */
    boolean isKip() default true;

    /**
     * 赋值则判断规则,否则不判断
     *
     * @return
     */
    String[] skipEnvList() default {};

    /**
     * 赋值则判断规则,否则不判断
     *
     * @return
     */
    String[] skipTableList() default {};
}

3.6 不足之处

  • 1.整个链路上的这个表操作都会跳过,颗粒度还是比较粗
  • 2.注解只能在入口处使用,公共方法调用尽量避免

那还要不要完善一下,还有什么没有考虑到的点呢?

拿起手机看到快 12 点的那一刻,我还是选择先回家了......

四、总结思考

4.1 隔离总结


这是一个很好参考案例:在应用中既做了数据隔离,也做了数据共享。通过自定义拦截器做数据隔离,通过自定注解切面实现数据共享。

4.2 编码总结


同样的代码写两次就应该考虑重构了
  • 尽量修改一个地方,不要写这种边边角角的代码
  • 善用自定义注解,解决这种通用逻辑
  • 可以妥协,但是要有底线
  • ......

4.3 场景总结


简单梳理,自定义注解 + AOP 的场景

工作六年,看到这样的代码,内心五味杂陈...6202 作者: 来源: 发布时间:2024-8-18 17:14

自定义注解很灵活,应用场景广泛,可以多多挖掘。

4.4 反思总结

  • 如果一开始就做好技术方案或者直接使用不同的数据库
  • 是否可以拒绝那个所谓的需求
  • 先有设计再有编码,别瞎搞

4.5 最后感想


工作六年,看到这样的代码,内心五味杂陈...4550 作者: 来源: 发布时间:2024-8-18 17:14

在这个只讲业务结果,不讲技术氛围的环境里,突然有一些伤感。身体已经开始吃不消了,好像也过了那个对技术较真死抠的年纪。

突然一想,这么做的意义又有多大呢?


Github上标星60K的电商实战项目mall,全套 视频教程(2023最新版) 已更新完毕!全套教程约40小时,共113期,通过这套教程你可以拥有一个涵盖主流Java技术栈的完整项目经验,同时提高自己独立开发一个项目的能力,下面是项目的整体架构图,感兴趣的小伙伴可以点击链接 mall视频教程 加入学习。

工作六年,看到这样的代码,内心五味杂陈...2726 作者: 来源: 发布时间:2024-8-18 17:14

整套 视频教程 的内容还是非常完善的,涵盖了mall项目最佳学习路线、整体框架搭建、业务与技术实现全方位解析、线上Docker环境部署、微服务项目学习等内容,具体大纲可以参考下图,你也可以点击链接 mall视频教程 了解更多内容。

工作六年,看到这样的代码,内心五味杂陈...7119 作者: 来源: 发布时间:2024-8-18 17:14

推荐阅读

  • 69K Star!这是我见过最强的开源电商系统 !!
  • Github标星60K!一套完整的项目实战教程来了,主流Java技术一网打尽!
  • 看了我项目中购物车、订单、支付一整套设计,同事也开始悄悄模仿了...
  • 订单系统就该这么设计,稳的一批!
  • 支付系统就该这么设计,稳的一批!
  • 权限系统就该这么设计,稳的一批!







上一篇:【前端】成都知道创宇信息技术有限公司
下一篇:GitHub上最励志的计算机自学教程:8个月,从中年Web前端到亚马逊百万年薪软件工程师 | 中文版

鲜花

握手

雷人

路过

鸡蛋

评论

您需要登录后才可以发表言论 登录立即注册
创宇盾启航版免费网站防御网站加速服务
投诉/建议联系

discuzaddons@vip.qq.com

未经授权禁止转载,复制和建立镜像,
如有违反,按照公告处理!!!
  • 联系QQ客服
  • 添加微信客服

联系DZ插件网微信客服|最近更新|Archiver|手机版|小黑屋|DZ插件网! ( 鄂ICP备20010621号-1 )|网站地图 知道创宇云防御

您的IP:18.218.108.8,GMT+8, 2024-12-26 20:02 , Processed in 0.211005 second(s), 43 queries , Gzip On, Redis On.

Powered by Discuz! X5.0 Licensed

© 2001-2024 Discuz! Team.

关灯
扫一扫添加微信客服
QQ客服返回顶部
返回顶部