同事写的代码,太垃圾!破防了......内心五味杂陈

点击关注公众号:互联网架构师,后台回复 2T获取2TB学习资源!

上一篇:2T架构师学习资料干货分享

大家好,我是互联网架构师!


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

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

ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起......


一、历史背景


1.1 数据隔离

预发,灰度,线上环境共用一个数据库。每一张表有一个 env 字段,环境不同值不同。特别说明:env 字段即环境字段。如下图所示:

755afa665794d438730b43363d2b8463.jpeg


1.2 隔离之前

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

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

1.3 隔离改造

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

每一个环境都有一个自己独立标志;从 application.properties 中读该字段;最终到数据库执行的语句如下:

SELECT XXX FROM tableName WHERE env = ${环境字段值} and ${condition}

1.4 隔离方案

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

4ff3ad35a9dc14b0863971c82c526b75.jpeg

具体方案:自定义 mybatis 拦截器进行统一处理。通过这个方案可以解决以下几个问题:

  • 业务代码不用修改,包括 DO、Mapper、XML等。只修改 mybatis 拦截的逻辑。

  • 挨个添加补充字段,工程量很多,出错概率极高

  • 后续扩展容易


1.5 最终落地

在 mybatis 拦截器中, 通过改写 SQL。新增时填充环境字段值,查询时添加环境字段条件。真正实现改一处即可。考虑历史数据过渡,将 env = ${当前环境}修改成 env in (${当前环境},'all')

 
 
SELECT xxx FROM ${tableName} WHERE env in (${当前环境},'all') AND ${其他条件}

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

738d176c22006b9cc79d7df0295cbc59.jpeg

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

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

思路:自定义拦截器,填充环境参数,修改 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 业务需求

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

上下游合作,我们的 PRC 接口在匹配环境上与他们有差异,需要改造

SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?,'all')

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

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


2.2 初步沟通

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

1838483e2d06ecd15e1429f8caf9915b.jpeg

(●ˇ∀ˇ●)年纪大了需要给年轻人机会。


2.3 勤劳能干

小鲜肉,没多久就实现了。不过有一天下午他遇到了麻烦。他填充的环境字段取出来为 null,看来很久没找到原因,让我帮他看看。(不久前也还教过他 Arthas 如何使用呢,这种问题应该不在话下吧🤔)

2.4 具体实现

大致逻辑:在需要跳过环境条件判断的方法前后做硬编码处理,同环切面逻辑, 一加一删。填充颜色部分为小鲜肉的改造逻辑。

ef91b88e69d35f4a540ce27a9a4926f3.jpeg

大概逻辑就是:将 env 字段填充所有环境。条件过滤的忽略的目的。

SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${其他条件}

2.5 错误原因

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

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

a363f4fa436c090aeb94622a7bd38669.jpeg


2.6 五味杂陈

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

54a0db7680d1911e5fd8ae7edf48bee1.jpeg

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

2.7 遍地开花

然而,这三行代码,随处可见,在业务代码中遍地开花.......

344d73eddaf2bcfc93db4dddd5320cd5.jpeg

 
 
// 1. 变量保存 oriFilterEnvString oriFilterEnv = UserHolder.getUser().getFilterEnv();// 2. 设置值到应用上下文UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());//....... 业务代码 ....// 3. 结束复原UserHolder.getUser().setFilterEnv(oriFilterEnv);

dd3f5536338982e3ebb2b5c9e622ac39.jpeg

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


2.8 灵魂开问

f79f08fe31bf256e1e6c037ba3fdab95.jpeg

难道真的就只能这么做吗,当然还有......

  • 开闭原则符合了吗

  • 改漏了应该办呢

  • 其他人遇到跳过的检查的场景也加这样的代码吗

  • 业务代码和功能代码分离了吗

  • 填充到应用上下文对象 user 合适吗

  • .......

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

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

三、重构一下


3.1 困难之处

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

3.2 问题列表

  • 尽量不要修改已有方法,保证不影响原有逻辑;

  • 尽量不要在业务方法中修改功能代码;关注点分离;

  • 尽量最小改动,修改一处即可实现逻辑;

  • 改造后复用能力,而不是依葫芦画瓢地添加这种代码


3.3 实现分析

  1. 用独立的 ThreadLocal,不与当前用户信息上下文混合使用

  2. 注解+APO,通过注解参数解析,达到目标功能

  3. 对于方法之间的调用或者循环调用,要考虑优化

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


3.4 使用案例

改造后的使用案例如下,案例说明:project 表在预发环境校验跳过。

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

 
 
@SneakyThrows
@GetMapping("/importSignedUserData")
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void importSignedUserData(
    ......
    HttpServletRequest request,
    HttpServletResponse response) {
    ......
}

在使用的调用入口处添加注解。


3.5 具体实现

  1. 方法上标记注解, 注解参数定义规则

  2. 切面读取方法上面的注解规则,并传递到应用上下文

  3. 拦截器从应用上下文读取规则进行规则判断

341c1cf3f158d70475c199d4d455fe8a.jpeg

注解代码

 
 
@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 的场景

场景

详细描述

分布式锁

通过添加自定义注解,让调用方法实现分布式锁

合规参数校验

结合 ognl 表达式,对特定的合规性入参校验校验

接口数据权限

对不同的接口,做不一样的权限校验,以及不同的人员身份有不同的校验逻辑

路由策略

通过不同的注解,转发到不同的 handler

......


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


4.4 反思总结

  • 如果一开始就做好技术方案或者直接使用不同的数据库

  • 是否可以拒绝那个所谓的需求

  • 先有设计再有编码,别瞎搞


4.5 最后感想

在这个只讲业务结果,不讲技术氛围的环境里,突然有一些伤感;身体已经开始吃不消了,好像也过了那个对技术较真死抠的年纪;突然一想,这么做的意义又有多大呢?

作者:uzong
链接:https://juejin.cn/post/7294844864020430902

最后,关注公众号互联网架构师,在后台回复:2T,可以获取我整理的 Java 系列面试题和答案,非常齐全。

正文结束

推荐阅读 ↓↓↓

1.JetBrains 如何看待自己的软件在中国被频繁破解?

2.无意中发现了一位清华妹子的资料库!

3.程序员一般可以从什么平台接私活?

4.40岁,刚被裁,想说点啥。

5.为什么国内 996 干不过国外的 955呢?

6.中国的铁路订票系统在世界上属于什么水平?                        

7.15张图看懂瞎忙和高效的区别!

355e09573ff67ee01bdf1e2dfc888aa2.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值