月度记录-2025-7月

81 阅读9分钟

1. qa测过的代码别改,非得改的话,改之前就要通知qa

晚了半小时下班,因为优化了一下那该死的xx代码,忘记通知qa了。以后养成习惯:

修改前评估影响范围(是否涉及已测功能点)。

主动同步 QA:

     告知原因(性能优化/bug修复)

     明确告知需要回归的功能点

     争取 QA 的时间窗口

QA 明确回测过,方可提交上线

2. 自测接口,缺失数据的情况

接口调用时,不应假设“所有依赖数据都一定存在”,应主动设计好 “缺数据” 时的容错逻辑 和 明确信息提示。

比如做数据统计,实际数据要和业务指标做比较。那就要考虑,如果人家没有业务指标咋办?这个没有很正常,某个公司可能就是还没来得及定指标。开发阶段最好就考虑到,然后测试阶段,告知qa,让qa也覆盖到。

3. 数据库一定要记得加唯一索引

不要在业务层单靠 Redis 锁解决这种问题,那是“弱防御”。即使业务层有校验,也必须靠数据库兜底。

Redis 锁不能防“非业务流程”写入。

4. 写完代码,还要考虑怎么验证数据

不是代码写完就结束了,代码写完只算一阶段结束,真正结束是测试通过、验收通过。

写代码的时候,最好就给出测试方案,比如搞个sql用来删数据,不要让测试测着测着就测不下去来找了。要运筹帷幄,掌控全局。

5. Redisson 看门狗机制。续期核心实现

private void renewExpiration() {
    ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
    if (ee != null) {
        Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
                if (ent != null) {
                    Long threadId = ent.getFirstThreadId();
                    if (threadId != null) {
                        CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
                        future.whenComplete((res, e) -> {
                            if (e != null) {
                                RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
                                RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
                            } else {
                                if (res) {
                                    RedissonBaseLock.this.renewExpiration();
                                } else {
                                    RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
                                }
                            }
                        });
                    }
                }
            }
        }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
        ee.setTimeout(task);
    }
}

RedissonBaseLock.this.renewExpiration();并不是在调用函数本体,而是“安排一个10秒后执行的定时任务”。

此递归不是立即执行递归体,而是“安排”在未来执行,所以不会爆栈,也不会瞬间多次执行。

用的是Netty提供的Timing Wheel实现。

6. Netty HashedWheelTimer

它是 Netty 提供的高效时间轮定时器实现,适合大批量定时任务的场景;

优点:内存占用低,任务调度时间复杂度近似 O(1),非常适合像 Redisson 这种需要大量定时续期任务的场景。

HashedWheelTimer实现:通过一个环形数组(时间轮),将未来的任务分散到不同的槽里,每个槽维护一个任务链表。时间轮指针每次跳动处理当前槽的任务,任务到期就执行。

7. Timing Wheel

时间轮(Timing Wheel):类似机械钟表的指针,时间轮把时间划分成一个个“槽(bucket)”,每个槽对应一个时间间隔。当时间指针转动到某个槽时,执行该槽中所有的定时任务。

用于实现延时功能,性能很高。

8. TMD MapStruct 不是 Service,不写业务逻辑

业务逻辑不要写到mapstruct那个破转换里面。mapstruct功能是做字段映射,目的是提高开发效率,减少模板代码。不是让你写业务逻辑的。

mapstruct只是处理两个对象的转换的,除了转换别做那么多事情。为了方便,可以做一些枚举的映射、数据类型转换或数据展示形式转换。但是,业务逻辑不应该在这里面写,尤其是一看就属于领域层的业务逻辑。

你判断个能否编辑,判断逻辑竟然在mapstruct里面,咋想的???这明显应该放到领域里面。

9. 某个领域逻辑(比如 canChangeXXX())应该放到 Domain Entity 还是 Domain Service?

如果逻辑只依赖对象自身的数据,应该放在 Domain Entity;

如果逻辑依赖多个对象或外部上下文,放到 Domain Service。

10. mq命名规范

Topic: {业务域}{实体} {动作},表达业务动作。

Tag:{子类型} or {状态},精细过滤,控制消费。

Consumer Group:{系统缩写}{功能模块}{动作}_group ,表示具体消费职责。

类型

名称

含义

Topic

resume_update_event

简历修改事件

Tag

education, work_exp, basic_info

更新了哪一块信息

Consumer Group

es_resume_sync_group

消费并同步数据到 Elasticsearch

类型

名称

含义

Topic

order_status_change

订单状态变更

Tag

paid, canceled, refunded, completed

对应具体变更类型

Consumer Group

notify_order_status_group

用于推送用户通知的消费者组

Topic / Tag 是“语义本身”,不需要后缀;Consumer Group 是“角色身份”,加 _group 更清晰也更实用。

11. 怎么 让 AI 成为真正的“项目协作成员”而不是代码补全工具?

tmd我怎么知道。

12. 有逻辑删除的表 left join的时候deleted = 0 应该写在 ON 后面

select * from A left join B ON A.id = B.xxx AND deleted = 0 千万不要写到 where 条件里面去了,会破坏left join

13. 为什么需要Lambda表达式

本质:为了解决 **函数无法作为一等公民**的问题。都这么说,什么TM的叫一等公民?

其实就是java8之前,Java 的函数不能独立存在,必须嵌在 类(interface)的方法中。这就导致一个核心问题:如果我只是想把一个逻辑(行为)传递给另一个方法,我居然得造一个接口,写一堆实现代码。

// 我想让线程执行一个行为,但只能传对象
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("do something");
    }
}).start();

// 有了 Lambda 之后,语义直接多了
 new Thread(() -> System.out.println("do something")).start();

Lambda 让 Java 不再只是面向对象的语言,而是逐步拥有了函数式编程的能力,最关键的一点就是:行为可以当成值来传递了。

14. lambda 表达式原理

匿名内部类的实现方式是:编译时生成 class 文件。

Lambda 表达式实现方式是: lambda 表达式在编译期不会生成具体实现类,而是在运行期借助 invokedynamic + LambdaMetafactory 动态生成实例。且只会生成 Main.class,不会生成额外的 .class 文件。

编译期:lambda 发生了什么?

当你写下:Runnable r = () -> System.out.println("Hello");

编译器并不会为 lambda 生成一个 .class 文件(不像匿名内部类那样),而是转为 invokedynamic 指令,相当于埋了个“钩子”,告诉 JVM:“等你运行到这里时,请通过 LambdaMetafactory 来决定我该怎么执行”。

运行期:lambda 是怎么被实例化的?

当 JVM 执行到 lambda 表达式时,invokedynamic 被触发,会调用:java.lang.invoke.LambdaMetafactory.metafactory(...)

它做了几件事:

这个“动态生成类并创建实例”的过程只有一次,之后会缓存起来,提高性能。

关键点总结:

问题

结论

lambda 是什么时候生成的?

运行时,通过 LambdaMetafactory

编译期干了什么?

把 lambda 转换成 invokedynamic指令

lambda 有没有生成 class 文件?

没有显式生成,是运行时生成匿名类并加载到内存中的

是否每次都会重新生成?

不会,有缓存机制,一次生成,多次复用

15. BizContent 入参

“BizContent” 是在接入**第三方平台接口(尤其是支付、认证、平台网关类)**中常见的一种入参封装方式,全称可理解为 Business Content(业务内容)。这种设计通常出现在支付宝、微信、钉钉、众腾这类平台接口里。

通常指:将业务请求参数整体封装为一个 JSON 对象,统一放在一个字段里(例如 bizContent)传输给后端接口或三方系统。

其中:

规范统一,便于签名计算、扩展性强、对接统一、降低接口爆炸风险。

{
  "method": "apply.withdraw",
  "timestamp": "2025-07-29 15:00:00",
  "appId": "your-app-id",
  "sign": "xxxxx",
  "bizContent": {
    "userId": 12345,
    "amount": 500,
    "bankCard": "6214***********"
  }
}

16. Spring AOP 和 AspectJ

Spring AOP 是基于“代理机制”的运行时 AOP,必须被spring管理。

AspectJ 是基于“编译期字节码增强”的静态 AOP。支持在任何类、任何方法上织入增强,即使不是 Spring 管理的对象。

对比项

Spring AOP

AspectJ

实现方式

运行时代理(JDK / CGLIB)

编译期 / 加载期字节码增强

支持对象

仅容器内 Bean

所有 Java 类

切点能力

方法级别

方法、字段、构造函数等全方位

性能

低频切面开销不大

高性能(无代理),适合高频调用

易用性

✅ 极易上手,Spring Boot 无缝支持

❌ 配置复杂,需要特殊编译器或加载器

调试维护

✅ 容易断点调试

❌ 增强不可见,调试复杂

实际使用率

✅ 占主流,90%+ 项目足够

❌ 少数场景才用(如性能优化、字节码探针)

17. Spring AOP 支持 5 种类型的通知

通常顺序是:@Around → @Before → [目标方法] → @AfterReturning / @AfterThrowing → @After

通知类型

注解

执行时机

前置通知

@Before

方法执行前

返回通知

@AfterReturning

方法正常返回后

异常通知

@AfterThrowing

方法抛出异常后

后置通知

@After

无论是否异常都会执行

环绕通知

@Around

最强大的,可包裹整个方法执行过程

18. Spring BeanPostProcessor 解耦扩展逻辑

BeanPostProcessor 是 Spring 容器提供的一个 扩展接口,允许你在 Spring 完成对 Bean 的实例化、依赖注入后,但在 Bean 被正式使用前,对 Bean 进行额外加工和处理。

简单说,BeanPostProcessor 就像一个“Bean 的加工厂”或“拦截器链”,Spring 会在 Bean 初始化的前后阶段调用它的方法,让你对 Bean 做增强、包装、修改、替换。

Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

postProcessBeforeInitialization:在 Bean 的初始化方法(比如 @PostConstruct、InitializingBean.afterPropertiesSet()、init-method)调用之前执行。作用是:给你机会在初始化前对 Bean 进行加工,比如修改属性、替换 Bean、做某些预处理。

postProcessAfterInitialization:在初始化方法执行完毕后调用,Spring AOP 就是利用这个时机,给目标 Bean 包装代理对象,返回代理替代原始 Bean。

为何@PostConstruct 里面用不了aop?

答:他一个在before里搞的,怎么用在after里面搞的AOP嘛。

19. 加速maven打包

maven默认会执行测试代码,以下命令可以不编译不执行测试代码。加快速度。

.\mvnw clean package "-Dmaven.test.skip=true" -T 1C -s D:\mavenrepository\setting\xxx\settings.xml

20. 表名一般用名词还是用动词?

数据库表名应使用“名词”或“名词性短语”表示“实体集合”,不要用动词。

因为表是“数据的集合”,不是“行为的动作”。动词表示行为,行为应出现在代码逻辑或服务层中,而不是存储结构中。

动词命名

问题

create_user

像是某个 API 或函数

update_status

看起来像操作,不像存储

insure_config

"insure" 是动词,看起来像“去配置”而不是“配置项”

正确命名风格应该是“名词 + 修饰语”:

表名

说明

user

用户表

user_profile

用户档案

user_insurance_strategy

用户的保险策略配置项(策略是名词)

insurance_policy_record

保单记录(全是名词)

position_user_assignment

职位-用户关系绑定表