2024年2月

之前写过一篇关于 IDA 在无 dmp 文件下如何定位到崩溃点的文章,由于其中涉及到公司项目,就仅限自己看了

正好今天看到一篇关于
火绒软件误杀 explorer
的文章,并且有相关的复现过程

文章已经概述的很清楚,不过缺少软件的调试步骤,借此机会补充一下

该调试过程一共使用两个软件,
IDA

010 Editor
,以及调试文件,explorer.exe 和 pdb 文件,这两个文件可以从上面链接中的高赞回答中获取

010 Editor 是个调试器,里面有块功能是 EXE 的 PE 信息查看,需要另外安装 EXE.bt

那么开始吧,

打开 IDA,加载 exe 文件

选择 Portable executable for AMD64,会提示加载一些 dll,跳过就行

IDA 比较智能会提示是否连接微软官方 symbol server 并下载符号文件,并且也会提示输入 explorer.exe 的 pdb 文件路径,按照指示加载已下载的 pdb 文件就行

点击 Jump->Jump to function,弹出对话框,再点击 Search

输入要寻找的 api,即 IsHijackingProcessRunning

双击找到的 IsHijackingProcessRunning,会以 Graph View 显示汇编代码

在汇编代码的空白处鼠标右键选择 Text View,即可切换到文件阅读,方便查看

接着我们需要 IDA 给我们生成伪代码,鼠标在汇编代码处,按快捷键 F5 即可生成伪代码

里面有很多变量是 v 开头的,因为这些看似正确的代码其实并不准确,只是方便我们看清楚逻辑,v 开头的变量也是 IDA 自动生成的变量名,仅供参考

对于其他 api 也可以使用该方法去查看

顺便摘录下引用文章的小结=》explorer.exe 中 IsHijackingProcessRunning 的行为就是检测某些程序是否在运行,如果在运行则关闭 ShellFeedsCampaign 功能。

那么如何查看 exe 的 PE 时间戳,无论是 exe 还是 dll,两者的 PE 结构其实是类似的

安装好 010 Editor 后,打开它并加载 explorer.exe,软件会弹出提示框安装 exe.bt,安装就行,然后我们就可以看到 exe 的 PE 二进制数据

PE 时间戳可以在地址上直接查看,或者在下面的显示框中

好了,完毕,有问题在评论区中交流,谢谢观看

分享是最有效的学习方式。

博客:
https://blog.ktdaddy.com/

从最近一个经历说起

周五了,轻松点儿,今天破例不写纯技术类的干货文了,聊聊最近一个比较郁闷的经历,这事儿发生在老猫自己身上,不是“总是遇到事故深陷于系统重构泥潭的倒霉小猫”,也不是苦苦面试找工作的“张小帅”(如果想要知道小猫和张小帅的故事,欢迎订阅专栏)。

春节假期即将结束,去了趟老丈人家拜了年,准备坐高铁回来的路上悲剧斜跨背包丢了,现金、银行卡、驾照、身份证、最爱的文玩串串、耳机等等都没了。细节就不多聊了,越想越心痛。证件现金等等都可以不论,尤其是盘了多年的文玩串串,之前一天刚在朋友圈嘚瑟,结果第二天就给丢了,真的是后悔懊恼万分。
嘚瑟

当意识到事情发生之后,大脑一片空白,口干舌燥,着急万分,因为距离高铁发车还有40分钟的时间,在这个时间段内要找到,简直就是痴人说梦。于是草率下定决心,要是监控调不出来就不走了,一定要找到,当时冲动之后也就只有这一个想法......后来媳妇的一句话让我冷静了下来,“事情都发生了,着急有什么用,最多损失一个包,人都还在慌什么?”。是啊,遇到突发状况究竟在慌什么呢?做好最坏的打算去争取解决问题就行了。于是就有了下面这样的流程。

流程

事儿已经发生了,现在也已过去多日,老猫觉得上述的决策可能不是最好,但是大家都安全返程了,并且没有耽误上学和工作,这是最好的。当然这是老猫遇到的“生活Bug”。

这件事情算是翻篇了,虽然最近还是在挂失办理各种证件,但是这个事情发生之后,老猫还是以程序员的角度去复盘了这么一个事情,如果我们日常工作中遇到突发困难,对应的我们又该是如何去做呢?

一点小小的经验总结,和大家分享一下。

面对问题的基本认知

看着自己的麻烦,清楚地知道,是你,就是你自己,而非他人所致。这真是件痛苦的事情————索福克勒斯《埃阿斯》

当我们接手一个系统之后,很多时候往往也会躺枪,就像小猫那样,【
前开发在代码里下毒
】。

我们很多时候会去抱怨,就像老猫丢包发生之后就在抱怨,如果不吃那个丈母娘煮的鸡蛋就不会肚子疼,不会肚子疼就不会想上厕所也就不会丢包了,所以归根到底还是得怪丈母娘煮的那个鸡蛋。

相信我们大部分人在发现是别人的Bug导致事故的时候,也同样会费时费力地把责任推到罪魁祸首身上。在一些工作场所,这是文化的一部分,而且可能有助于宣泄。
但是在技术领域,我们还是得把精力集中到解决问题上,而不是归咎于他人。

Bug是我们的错还是别人的错无关紧要,无论谁的错,问题还是得有当前系统负责人的我们来解决。

不要恐慌

人们很容易陷入恐慌,比如刚意识到丢包的老猫也是一样。在工作中,或者在问题发生之后,或者是最后期限逼近,老板和客户站在背后“死亡凝视”的时候,我们其实不能把注意力过度转移到老板和客户身上。亦或者说,在此期间我们不能把注意力过度放到结果所造成的影响上,因为事情已经发生了。如果注意力转移了,那么期间,我们花费解决问题的时间可能就会更久,或者直接就搞不定,因为心态已经崩了。

不应病急乱投医,错上加错

老猫试想了如果当时一时冲动,选择留在高铁站查一天监控,最终也还是一无所获,不仅耽误了几天上班的时间,而且还耽误证件补办的时间,如果错过最好的挂失时间,可能还会被盗刷,简直是“赔了夫人又折兵”。

写到这里,让老猫又想到多年前经历的一次支付事故。由于疏忽将第三方支付工具的回调状态&符号写成了||,造成了资损。当时心里想着的是尽快找到问题,并且解决掉,不能再造成更大的资损了。然而匆匆忙忙中找到的问题大部分是错误的,在二次订正想要修复问题的时候,被领导给制止了。

事后才发现,领导是对的,我匆匆赶工修复的方案也是存在一定的问题。事情已经发生了。在万无一失的方案还没有出来之前就不要去碰现状系统了。因为慌慌张张地改造修复往往只会给现有的错误系统带来更多的“熵”。

所以当事情发生之后,冷静地去分析去思考,去衡量解决方案的利弊和得失,能够从最坏的角度去分析并且制定出有效挽回损失的方案才是最明智的选择。

复盘从而避免二次踩坑

经历了这事儿以后,老猫也是被深刻地教育了,“今后出门就别带钱包了,顶多带个身份证就够了,互联网的世界,手机在到处都能搞定。也别背那么多串串了,选一串能套手上戴着的就行”。看,这就是事故复盘的魅力。

老猫在写这篇文章的过程中其实就是在复盘,在这里老猫的收货可能是锻炼了自己的分析问题,定位问题的能力,并制定有效的解决方案,培养透过现象看本质的思维方式。

日常工作中也是一样,出了事故不可怕,可怕的是同样的事故,出了多次。所以复盘的核心价值就是让犯过的错误不再重犯,不在同一个地方反复跌倒。对于软件系统体系而言复盘故障能够帮助深挖问题的根因,找到目前稳定性的薄弱环节,并进行系统化的分析,从而变成未来提升优化动作的抓手。

总结

以上就是老猫由一次“生活事故”引发的一系列的对于日常工作的思考,如果有其他看法的小伙伴也欢迎留言。

开心一刻

减肥是有技巧的,比如我朋友

早上一杯水,中午一个鸡蛋,晚上一碗稀粥

每天 10 公里,500 个跳绳,200 个蛙跳

他以前 180 斤,现在连人带盒才 5 斤

就问你们,这减肥效果是不是杠杠的?

MyBatis 替换成 MyBatis-Plus

背景介绍

一个老项目,数据库用的是
MySQL
5.7
.
36


ORM
框架用的
MyBatis
3.5
.
0


mysql
-
connector
-
java

版本是
5.1
.
26

新来了一个干练的小伙,精力充沛,看着就是一个喜欢折腾的主

他就觉得
MyBatis
使用起来不够简单,要写的代码还比较多,觉得有必要替换成
MyBatis
-
Plus

Mybatis-Plus 替换 Mybatis

先准备一张表
tbl_order
,然后初始化 2 条数据


DROP TABLE IF EXISTS`tbl_order`;CREATE TABLE`tbl_order`  (
`id`
bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`order_no`
varchar(50) NOT NULL COMMENT '订单号',
`pay_time`
datetime(3) DEFAULT NULL COMMENT '付款时间',
`created_at`
datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updated_at`
datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间',PRIMARY KEY(`id`) USING BTREE
) ENGINE
= InnoDB COMMENT = '订单';INSERT INTO `tbl_order` VALUES (1, '123456', '2024-02-21 18:38:32.000', '2024-02-21 18:37:34.000', '2024-02-21 18:40:01.720');INSERT INTO `tbl_order` VALUES (2, '654321', '2024-02-21 19:33:32.000','2024-02-21 19:32:12.020', '2024-02-21 19:34:03.727');

View Code

为了简化演示,我就直接用
Mybatis-Plus
搭建一个示例
demo
,以此来模拟下
"小伙"
替换的过程

只是用
MyBatis
-
Plus

替换
MyBatis
,其他组件的版本暂不动

Mybatis
-
Plus

版本就用
"小伙"
引用的版本:
3.1
.
1


mysql
-
connector
-
java

版本保持不变还是
5.1
.
26

示例代码:
play_it_safe

此时运行
com.qsl.OrderTest#orderListAllTest
,会报错,异常信息如下


org.springframework.dao.TransientDataAccessResourceException: Error attempting to get column 'pay_time' from result set.  Cause: java.sql.SQLException: Conversion not supported fortype java.time.LocalDateTime
; Conversion not supported
for type java.time.LocalDateTime; nested exception is java.sql.SQLException: Conversion not supported fortype java.time.LocalDateTime

at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:
110)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:
72)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:
81)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:
81)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:
73)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:
446)
at com.sun.proxy.$Proxy53.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:
230)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:
158)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:
76)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:
62)
at com.sun.proxy.$Proxy59.selectList(Unknown Source)
at com.qsl.OrderTest.orderListAllTest(OrderTest.java:
28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:
62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:
43)
at java.lang.reflect.Method.invoke(Method.java:
498)
at org.junit.runners.model.FrameworkMethod$
1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:
12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:
47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:
17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:
74)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:
84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:
75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:
86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:
84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:
325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:
251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:
97)
at org.junit.runners.ParentRunner$
3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$
1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:
288)
at org.junit.runners.ParentRunner.access$
000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$
2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:
61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:
70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:
363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:
190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:
137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:
69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$
1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:
11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:
35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:
232)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:
55)
Caused by: java.sql.SQLException: Conversion not supported
fortype java.time.LocalDateTime
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:
1078)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:
989)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:
975)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:
920)
at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:
5126)
at com.mysql.jdbc.JDBC4ResultSet.getObject(JDBC4ResultSet.java:
547)
at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:
5133)
at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:
62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:
43)
at java.lang.reflect.Method.invoke(Method.java:
498)
at org.apache.ibatis.logging.jdbc.ResultSetLogger.invoke(ResultSetLogger.java:
69)
at com.sun.proxy.$Proxy71.getObject(Unknown Source)
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:
38)
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:
28)
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:
81)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:
521)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:
402)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:
354)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:
328)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:
301)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:
194)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:
65)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:
79)
at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:
67)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:
324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:
156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:
109)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:
83)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:
147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:
140)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:
62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:
43)
at java.lang.reflect.Method.invoke(Method.java:
498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:
433)
...
39 more

View Code

注意看
Caused by

不支持的转换类型:
java.time.LocalDateTime

谁不支持?
mysql-connector-java
不支持!


mysql-connector-java
哪个版本支持了,答案是:
5.1.37

升级mysql-connector-java


mysql-connector-java
升级到
5.1.37
,再执行下
com.qsl.OrderTest#orderListAllTest

不再报异常,查询结果也正确

MyBatis-Plus
替换
Mybatis
似乎就完成了

顺的让人有点怀疑

Conversion not supported for type java.time.LocalDateTime

我们再回过头去看看前面说到的异常:
Conversion not supported
for
type java.time.LocalDateTime

Mybatis-Plus
替换
MyBatis
之前没这个异常,替换之后就有了这个异常,这不是
Mybatis-Plus
的问题?

如何找这个异常的根因了?

很简单,直接从异常堆栈入手

点了之后,你会发现方法很简单

这么简单的代码能有什么问题?

大家注意看图中左上角
MyBatis
的版本,是
3.5.1
,并不是最初的 3.5.0

有小伙伴可能会问了:不是用
MyBatis-Plus
替换了
MyBatis
吗,怎么还有
Mybatis

这个问题问的真的好,我只想给你个大嘴巴子

你看下
MyBatis-Plus

官方说明

既然基于
Mybatis 3.5.0
没有抛异常,而基于
3.5.1
抛了异常,
LocalDateTimeTypeHandler

3.5.1
肯定做了调整

我们来看下调整了什么?

看出什么了?

MyBatis 3.5.0
会处理
LocalDateTime
类型的转换(将
java.sql.Timestamp
转换成
java.time.LocalDateTime

然而,注意了,然而来了!!!

然而从
MyBatis 3.5.1
开始,不再处理
LocalDateTime
(还包括:
LocalDate

LocalTime
)类型的转换

而是交由
JDBC
组件,也就是
mysql-connector-java
来实现

而巧的是,
mysql-connector-java 5.1.26
不支持类型
LocalDateTime

那它支持哪些类型了?

我们同样从异常堆栈入手

点了之后,可以看到下图

往上滑动鼠标,就可以看到支持的类型了


public <T> T getObject(int columnIndex, Class<T> type) throwsSQLException {if (type == null) {throw SQLError.createSQLException("Type parameter can not be null", 
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
}
if (type.equals(String.class)) {return(T) getString(columnIndex);
}
else if (type.equals(BigDecimal.class)) {return(T) getBigDecimal(columnIndex);
}
else if (type.equals(Boolean.class) ||type.equals(Boolean.TYPE)) {return(T) Boolean.valueOf(getBoolean(columnIndex));
}
else if (type.equals(Integer.class) ||type.equals(Integer.TYPE)) {return(T) Integer.valueOf(getInt(columnIndex));
}
else if (type.equals(Long.class) ||type.equals(Long.TYPE)) {return(T) Long.valueOf(getLong(columnIndex));
}
else if (type.equals(Float.class) ||type.equals(Float.TYPE)) {return(T) Float.valueOf(getFloat(columnIndex));
}
else if (type.equals(Double.class) ||type.equals(Double.TYPE)) {return(T) Double.valueOf(getDouble(columnIndex));
}
else if (type.equals(byte[].class)) {return(T) getBytes(columnIndex);
}
else if (type.equals(java.sql.Date.class)) {return(T) getDate(columnIndex);
}
else if (type.equals(Time.class)) {return(T) getTime(columnIndex);
}
else if (type.equals(Timestamp.class)) {return(T) getTimestamp(columnIndex);
}
else if (type.equals(Clob.class)) {return(T) getClob(columnIndex);
}
else if (type.equals(Blob.class)) {return(T) getBlob(columnIndex);
}
else if (type.equals(Array.class)) {return(T) getArray(columnIndex);
}
else if (type.equals(Ref.class)) {return(T) getRef(columnIndex);
}
else if (type.equals(URL.class)) {return(T) getURL(columnIndex);//} else if (type.equals(Struct.class)) {// //}//} else if (type.equals(RowId.class)) {// //} else if (type.equals(NClob.class)) {// //} else if (type.equals(SQLXML.class)) { }else{if (this.connection.getAutoDeserialize()) {try{return(T) getObject(columnIndex);
}
catch(ClassCastException cce) {
SQLException sqlEx
= SQLError.createSQLException("Conversion not supported for type " +type.getName(),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
sqlEx.initCause(cce);
throwsqlEx;
}
}
throw SQLError.createSQLException("Conversion not supported for type " +type.getName(),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
}
}

View Code

确实没有
LocalDateTime

LocalDate

LocalTime

mysql-connector-java 5.1.37
开始支持
LocalDateTime

LocalDate

LocalTime
,前面已经介绍过了,不再过多赘述

总结下异常根因:
MyBatis 3.5.1
开始不再处理
LocalDateTime

LocalDate

LocalTime
的转换,而
mysql-connector-java 5.1.37
之前都不支持这些类型

弄清楚这个异常的来龙去脉之后,顺的是不是又理所当然一些了?

暴风雨的来临

版本上线没 2 天,该来的终究还是来了

我们往表
tbl_order
中插入一条记录:
INSERT INTO `tbl_order` VALUES (3, 'asdfgh', NULL, '2024-02-21 20:01:31.666666', '2024-02-21 20:02:56.764');

再执行
com.qsl.OrderTest#orderListAllTest

此刻我就想问
"小伙"
:刺不刺激?

碰到了异常,那就找原因

同样从异常堆栈入手

看出什么了?

如果
getTimestamp(columnIndex)
得到的是
NULL
,不就
NullPointerException
? 严谨性了?

修复问题要紧,我们先看哪个版本进行修复了?


mysql-connector-java
升级到
5.1.42

问题得以修复

经此一役,
"小伙"
似乎成长了很多,但眼里的光却暗淡了不少

mybatis-plus-issues-6666664

无意中看到了这个
issue-6666664
,跟我们前面分析的
Conversion not supported
for
type java.time.LocalDateTime

是不是同一个问题?

只是我们用到的数据库连接池是默认的
HikariCP
而非
Druid

结合
druid/issues/3302
来看,如果使用
Druid
作为数据库连接池,出现的异常可能跟我们前面分析的确实不一样

所以大家需要根据自己的实际情况来分析,但针对异常的分析方法是通用的

修了“不该修的Bug”

这是我亲身经历的一次事故,到现在都觉得这锅背的有点冤

背景介绍

文件分为主文件和附属文件,主文件生成之后再生成附属文件

附属文件生成的时候,会校验其依赖的主文件是否都生成了,如果有任意一个主文件未生成,依赖文件不能生成并抛出异常

这个业务还是比较简单吧

但在附属文件校验的优化上,我背上了生产事故

优化前的校验

listFileGenerateLog
作用是根据参数查询文件生成记录,具体实现不用关注

这个校验逻辑是什么?只要有任意一个主文件生成,校验就算通过了,与业务要求(主文件全部生成,才算校验通过)不匹配呀

这不是妥妥的
Bug

优化后的校验

碰到
Bug
你能忍?我是忍不了一点,反手就是一个优化

这是不是就符合业务要求了?

生产异常

中午升级之后,稳定运行了一段时间,期间文件正常生成,没出现任何问题

晚上 19 点,有个附属文件生成失败,异常提示:
依赖的资源[abc_{yyyyMMdd}.txt]未生成

当时看到这个异常的第一眼,觉得既熟悉又陌生,熟悉的是这个异常信息的结构,陌生的是
abc_{yyyyMMdd}.txt
,这不是文件名吗?

正常来讲应该是
fileId
,是一个自增的正整数呀,怎么会是文件名了?

脑中瞬间闪过一个念头:数据库数据有问题?

一查吓一跳,这个附属文件关联主文件的字段值是:
4356,abc_{yyyyMMdd}.txt
,看最终修改时间是:
2021-08-21 15:22:12.652

4356
文件的文件名就是
abc_{yyyyMMdd}.txt
,正常来讲,这个关联字段的值应该是:
4356

敢情这个
校验Bug
完美的兼容了这个
脏数据
,所以几年了,一直没出现异常

是不是有这味了?

这可倒好,我把
Bug
修好,还出现问题了,你说我是不是手贱?

经此一役,我眼里的光又暗淡了些许

总结

关于对组件的升级,或者对旧代码的调整,都有可能牵一发动全身,影响甚大

我的观点是:能不动就不要动,改好没绩效,改出问题要背锅,吃力不讨好,又不是不能跑

如果到了不得不改的地步了,那就需要全面的测试

知乎上有个提问的回答百花齐放,很有意思,推荐给大家:
为什么程序员会有代码能跑就不要动的观点?

不是 MySQL 用不起,而是 SQLite 更有性价比,绝大多数的 Web 应用 SQLite 都可以满足。

SQLite
是一个用 C 语言编写的开源、轻量级、快速、独立且高可靠性的 SQL 数据库引擎,它提供了功能齐全的数据库解决方案。SQLite 几乎可以在所有的手机和计算机上运行,它被嵌入到无数人每天都在使用的众多应用程序中。

此外,SQLite 还具有稳定的文件格式、跨平台能力和向后兼容性等特点。SQLite 的开发者承诺,至少在 2050 年之前保持该文件格式不变。

本文将介绍 SQLite 的基础知识和使用方法。

SQLite 安装


SQLite 官方页面
(
https://sqlite.org/download.html
) 下载适合你目标系统的压缩包。

下载并解压后,无论是在 Windows、Linux 还是 Mac OS 系统上,你都可以得到一个
sqlite3
命令行工具。

以下是在 Mac OS 上解压后得到的命令行工具示例:

➜  sqlite-tools-osx-x64-3450100 ls -l
total 14952
-rwxr-xr-x@ 1 darcy  staff  1907136  1 31 00:27 sqldiff
-rwxr-xr-x@ 1 darcy  staff  2263792  1 31 00:25 sqlite3
-rwxr-xr-x@ 1 darcy  staff  3478872  1 31 00:27 sqlite3_analyzer

SQLite 使用场景

SQLite 与客户端/服务器类型的 SQL 数据库引擎(例如 MySQL、Oracle、PostgreSQL 或 SQL Server)不同,它们解决的问题也不同。

服务器端的 SQL 数据库引擎旨在实现企业级数据的共享存储,它们强调的是
可扩展性、并发性、集中化和控制性
。相比之下,SQLite 通常用于为个人应用程序和设备提供本地数据存储,它强调的是
经济、高效、可靠、独立和简单

SQLite 的使用场景:

  1. 嵌入式设备和物联网

    SQLite 不需要额外的管理或服务启动,非常适合用在手机、电视、机顶盒、游戏机、相机、手表等智能设备上。

  2. 网站

    多数低流量网站可以使用 SQLite 作为数据库。根据官方网站的介绍,通常每天访问量少于 10 万次的网站都可以很好地运行 SQLite。SQLite 的官方网站(
    https://www.sqlite.org/
    )自身就使用 SQLite 作为数据库引擎,每天处理大约 50 万 HTTP 请求,其中约 15-20% 的请求涉及数据库查询

  3. 数据分析

    SQLite3 命令行工具能方便地与 CSV 和 Excel 文件进行交互操作,适合分析大数据集。同时,许多语言(如 Python)都内置了 SQLite 支持,可以轻松编写脚本进行数据操作。

  4. 缓存

    SQLite 可以作为应用服务的缓存,减轻对中心数据库的压力。

  5. 内存或者临时数据库

    得益于 SQLite 的简单快速,非常使用程序演示或者日常测试。

SQLite 不适合的场景包括

  1. 需要通过网络访问数据库
    的情况。SQLite 是一个本地文件数据库,没有提供远程访问功能。
  2. 要求高可用性和可扩展性
    的场合。SQLite 简单易用,但不可扩展。
  3. 数据量极大
    时。尽管 SQLite 数据库大小的限制高达 281 TB,但所有数据都必须存储在单个磁盘上。
  4. 写入操作高并发
    时。SQLite 在任何时刻只允许一个写入操作执行,其他写入操作需要排队。

SQLite3 命令操作

SQLite 提供了
sqlite3
(在windows 为 sqlite3.exe)命令行工具,通过该工具可以执行 SQLite 数据库操作和 SQL 语句。

直接在命令提示符下执行
./sqlite3
启动 sqlite3 程序,然后可以通过输入
.help
查看帮助指南,或者输入
.help 关键词
获取特定关键词的帮助信息。

部分命令列表如下:

sqlite> .help
.databases               List names and files of attached databases
.dbconfig ?op? ?val?     List or change sqlite3_db_config() options
.dbinfo ?DB?             Show status information about the database
.excel                   Display the output of next command in spreadsheet
.exit ?CODE?             Exit this program with return-code CODE
.expert                  EXPERIMENTAL. Suggest indexes for queries
.explain ?on|off|auto?   Change the EXPLAIN formatting mode.  Default: auto
.help ?-all? ?PATTERN?   Show help text for PATTERN
.hex-rekey OLD NEW NEW   Change the encryption key using hexadecimal
.indexes ?TABLE?         Show names of indexes
.mode MODE ?OPTIONS?     Set output mode
.open ?OPTIONS? ?FILE?   Close existing database and reopen FILE
.output ?FILE?           Send output to FILE or stdout if FILE is omitted
.quit                    Exit this program
.read FILE               Read input from FILE or command output
.schema ?PATTERN?        Show the CREATE statements matching PATTERN
.show                    Show the current values for various settings
.tables ?TABLE?          List names of tables matching LIKE pattern TABLE
.......

sqlite3 只是读取输入行信息,然后传递给 SQLite 库来执行,SQL 语句都要以分号
;
结尾才会开始执行,因此你可以自由的分行输入。

在 sqlite3 中,SQL 语句需以分号
;
结尾才会执行,允许跨行输入。特殊的点命令(如
.help

.tables
)以小数点
.
开头,
不需要分号

SQLite 新建数据库

直接执行
sqlite3 filename
打开或创建一个 SQLite 数据库。如果文件不存在,SQLite 会自动创建它。

示例:打开或创建名为
my_sqlite.db
的 SQLite 数据库文件。

$ sqlite3 my_sqlite.db
SQLite version 3.39.5 2022-10-14 20:58:05
Enter ".help" for usage hints.
sqlite>

也可以首先创建一个空白文件,然后使用
sqlite3
命令打开它。接下来使用
CREATE TABLE
命令创建一个名为
user
的表,用
.tables
命令查看现有表格,使用
.exit
退出 sqlite3 工具。

$ touch test.db
$ sqlite3 test.db
SQLite version 3.39.5 2022-10-14 20:58:05
Enter ".help" for usage hints.
sqlite> create table user(name text,age int);
sqlite> .tables
user
sqlite>

SQLite 查看当前数据库

使用点命令
.databases
查看当前打开的数据库。

sqlite> .databases
main: /Users/darcy/develop/sqlite-tools-osx-x86-3420000/my_sqlite.db r/w
sqlite>

SQLite 增删改查

SQLite 几乎完全兼容常见的 SQL 语句规范,因此可以直接编写和执行标准的 SQL 语句。

创建表:

sqlite> create table user(name text,age int);
sqlite>

插入数据:

sqlite> insert into user values('aLang',20);
sqlite> insert into user values('Darcy',30);
sqlite> insert into user values('XiaoMing',40);

查询数据:

sqlite> select * from user;
aLang|20
Darcy|30
XiaoMing|40

添加索引,为 user 表的 name 创建名为 user_name 的索引:

sqlite> create index user_name on user(name);

SQLite 更改输出格式

在查询数据时,SQLite 默认使用
|
分割每列数据,这可能不便于阅读。实际上,sqlite3 工具支持多种输出格式,默认为
list
模式。

以下是可用的输出格式:ascii、
box
、csv、
column
、html、insert、
json
、line、list、markdown、quote、
table

可以使用
.mode
命令更改输出格式。

Box 格式:

sqlite> .mode box
sqlite> select * from user;
┌──────────┬─────┐
│   name   │ age │
├──────────┼─────┤
│ aLang    │ 20  │
│ Darcy    │ 30  │
│ XiaoMing │ 40  │
└──────────┴─────┘

json 格式:

sqlite> .mode json
sqlite> select * from user;
[{"name":"aLang","age":20},
{"name":"Darcy","age":30},
{"name":"XiaoMing","age":40}]

column 格式:

sqlite> .mode column
sqlite> select * from user;
name      age
--------  ---
aLang     20
Darcy     30
XiaoMing  40

table 格式:

sqlite> .mode table
sqlite> select * from user;
+----------+-----+
|   name   | age |
+----------+-----+
| aLang    | 20  |
| Darcy    | 30  |
| XiaoMing | 40  |
+----------+-----+
sqlite>

查询 Schema

sqlite3 工具提供了几个方便的命令,可用于查看数据库的 schema ,这些命令纯粹作为快捷方式提供。

例如,
.table
查看数据库中的所有表:

sqlite> .table
user

点命令
.table
相当于下面的查询语句。

sqlite> SELECT name FROM sqlite_schema
   ...> WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%'
   ...> ;
user

sqlite_master
是 SQLite 中的一个特殊表,其中包含了数据库的 schema 信息。你可以查询这个表以获取表的创建语句和索引信息。

sqlite> .mode table
sqlite> select * from sqlite_schema;
+-------+-----------+----------+----------+--------------------------------------+
| type  |   name    | tbl_name | rootpage |                 sql                  |
+-------+-----------+----------+----------+--------------------------------------+
| table | user      | user     | 2        | CREATE TABLE user(name text,age int) |
| index | user_name | user     | 3        | CREATE INDEX user_name on user(name) |
+-------+-----------+----------+----------+--------------------------------------+

使用
.indexes
查看索引,使用
.schema
查看 schema 详情。

sqlite> .indexes
user_name

sqlite> .schema
CREATE TABLE user(name text,age int);
CREATE INDEX user_name on user(name);

结果写出到文件

使用
.output filename
命令将查询结果写入指定文件。

下面是一个示例,先使用
.mode json
更改输出为 JSON 格式,然后在查询表写出到
sql_result.json

sqlite> .output sql_result.json
sqlite> .mode json
sqlite> select * from user;
sqlite> .exit

$ cat sql_result.json
[{"name":"aLang","age":20},
{"name":"Darcy","age":30},
{"name":"XiaoMing","age":40}]

**写出并打开 EXCEL **

使用
.excel
会让下一个查询语句输出到 Excel 中。

sqlite> .excel
sqlite> select * from sqlite_schema;

结果写出到文件

sqlite> .output sql_result.txt
sqlite> select * from sqlite_schema;
sqlite> select * from user;

读取运行 SQL 脚本

使用
.read
可以读取指定文件中的 SQL 语句并运行,这在需要批量执行 SQL 脚本的场景中非常有用。

创建SQL文件:

$ echo "select * from user" > sql_query.sql
$ cat sql_query.sql
select * from user

$ ./sqlite3 my_sqlite.db
SQLite version 3.42.0 2023-05-16 12:36:15
Enter ".help" for usage hints.

sqlite> .mode table
sqlite> .read sql_query.sql
+----------+-----+
|   name   | age |
+----------+-----+
| aLang    | 20  |
| Darcy    | 30  |
| XiaoMing | 40  |
+----------+-----+
sqlite>

SQLite 备份与恢复

在涉及数据库操作时,备份和恢复是至关重要的步骤,它们用于防止数据丢失并确保数据的持续性。SQLite 提供了简单的方法来备份和恢复你的数据库。

在 SQLite 中可以通过导出整个数据库为一个 SQL 脚本来备份数据库。此功能使用
.dump
命令实现。

$ ./sqlite3 my_sqlite.db
SQLite version 3.42.0 2023-05-16 12:36:15
Enter ".help" for usage hints.
sqlite> .output backup.sql
sqlite> .dump
sqlite> .exit

$ cat backup.sql
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE user(name text,age int);
INSERT INTO user VALUES('aLang',20);
INSERT INTO user VALUES('Darcy',30);
INSERT INTO user VALUES('XiaoMing',40);
CREATE INDEX user_name on user(name);
COMMIT;

这将导出整个
my_sqlite.db
数据库到
backup.sql
文件中。此 SQL 文件包含了重建数据库所需的所有 SQL 语句。要恢复数据库,只需在
sqlite3
中运行这个脚本。

示例:恢复数据到库
my_sqlite_2
中。

$ ./sqlite3 my_sqlite_2.db
SQLite version 3.42.0 2023-05-16 12:36:15
Enter ".help" for usage hints.
sqlite> .read backup.sql
sqlite> select * from user;
aLang|20
Darcy|30
XiaoMing|40

这将执行
backup.sql
文件中的所有 SQL 语句,重建数据库。通过以上的备份与恢复方法,你可以确保你的 SQLite 数据库资料得到可靠的保护,且在需要时能够迅速恢复。

SQLite 可视化工具

命令行操作总归不太直观,如果你喜欢可视化操作,可以下载 SQLite Database Browser 进行操作。

下载页面:
https://sqlitebrowser.org/dl/

附录

SQLite 常用函数列表,见名知意不写注释了。

Function 1 Function 2 Function 3 Function 4
abs(X) changes() char(X1,X2,...,XN) coalesce(X,Y,...)
concat(X,...) concat_ws(SEP,X,...) format(FORMAT,...) glob(X,Y)
hex(X) ifnull(X,Y) iif(X,Y,Z) instr(X,Y)
last_insert_rowid() length(X) like(X,Y) like(X,Y,Z)
likelihood(X,Y) likely(X) load_extension(X) load_extension(X,Y)
lower(X) ltrim(X) ltrim(X,Y) max(X,Y,...)
min(X,Y,...) nullif(X,Y) octet_length(X) printf(FORMAT,...)
quote(X) random() randomblob(N) replace(X,Y,Z)
round(X) round(X,Y) rtrim(X) rtrim(X,Y)
sign(X) soundex(X) sqlite_compileoption_get(N) sqlite_compileoption_used(X)
sqlite_offset(X) sqlite_source_id() sqlite_version() substr(X,Y)
substr(X,Y,Z) substring(X,Y) substring(X,Y,Z) total_changes()
trim(X) trim(X,Y)

参考

  1. SQLite 开源代码:
    https://www.sqlite.org/cgi/src/dir?ci=trunk
  2. SQLite 文件格式介绍:
    https://sqlite.org/fileformat2.html
  3. SQLite 可视化工具:
    https://sqlitebrowser.org/dl/
  4. SQL 函数文档:
    https://www.sqlite.org/lang_corefunc.html

一如既往,文章中代码存放在
Github.com/niumoo/javaNotes
.

本文
Github.com/niumoo/JavaNotes
仓库已经收录。
本文原发于网站:
SQLite 入门教程
本文原发于公众号:
SQLite 入门教程

引言

在软件开发中,进行本地单元测试是一项常规且必要的任务。然而,在进行单元测试时,有时需要启动一些中间件服务,如Kafka、Elasticjob等。举例来说,我曾经遇到过一个问题:项目中使用了
Redisson
锁,但由于
Redisson
版本较低,在Mac环境下偶尔会报错
# RedisConnectionException: Unable to init enough connections amount
。鉴于升级版本带来的风险,以及问题仅在本地启动时出现,我决定在本地环境中排除
Redisson
的Starter,从而避免影响其他环境的配置。那么,我们应该如何做呢?

我们以上篇介绍如何自定义Starter中的文章中示例
CoderAcademyStarter
为例。我们引入了这个starter。

Starter自动配置类的排除

在《SpringBoot如何自定义Starter》中,我们介绍了如何在
META-INF/spring.factories
文件中使用
org.springframework.boot.autoconfigure.EnableAutoConfiguration
来指定Starter的自动配置类。Spring Boot启动时会扫描所有已引入jar包中的
spring.factories
文件,并根据
EnableAutoConfiguration
键下的类来加载和执行相应的自动配置逻辑。当我们不希望应用启动时使用该Starter的功能时,就需要排除自动配置类。

我们可以通过
spring.autoconfigure.exclude
属性排除
CoderAcademyStarter
的自动配置类:

spring.autoconfigure.exclude=com.springboot.starter.coderacademy.config.CoderAcademyAutoConfig

spring.autoconfigure.exclude
是Spring Boot中的一个属性,用于指定在自动配置过程中要排除的自动配置类。通过设置该属性,我们可以明确告知Spring Boot不要自动配置指定的类,即使它们满足自动配置的条件。当需要禁用特定的自动配置类时,可以在
application.properties

application.yml
中设置
spring.autoconfigure.exclude
属性,并提供要排除的自动配置类的完全限定类名。这样,Spring Boot在自动配置过程中将不会考虑这些类。

此时,如果我们在使用
CoderAcademyService
时会出现错误:

image.png

根据不同环境排除Starter自动配置类

在日常开发中,我们通常需要针对不同的环境指定不同的配置。我们可以通过
spring.actice.profiles
属性来指定不同环境的配置文件的加载。例如,我们可以在本地指定
spring.actice.profiles=local
,然后创建一个
application-local.properties
的配置文件,在其中指定
spring.autoconfigure.exclude

另外,我们还可以实现
ApplicationListener<ApplicationContextInitializedEvent>
接口,通过监听上下文初始化事件来根据环境变量的标识排除Starter的自动配置类。当Spring应用程序的
ApplicationContext
被初始化时,将触发
ApplicationContextInitializedEvent
事件。通常,在应用程序的上下文初始化过程中会先加载bean定义、执行后处理器等操作。因此,通过监听
ApplicationContextInitializedEvent
事件,我们可以在Spring容器初始化的早期阶段执行一些定制化的逻辑。

我们可以通过实现
ApplicationListener<ApplicationContextInitializedEvent>
接口,根据一些环境变量的标识排除Starter的自动配置类。例如,我们可以定义一个
coderacademy.enable
的标识来决定是否扫描Starter。以下是一个示例:

import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;

@Configuration
public class EnvironmentHandler implements ApplicationListener<ApplicationContextInitializedEvent> {

    @Override
    public void onApplicationEvent(ApplicationContextInitializedEvent event) {
        ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
        if (environment.getProperty("coderacademy.enable") != null && "false".equals(environment.getProperty("coderacademy.enable"))) {
            System.setProperty("spring.autoconfigure.exclude", "com.springboot.starter.coderacademy.config.CoderAcademyAutoConfig");
        }
    }
}

然后,在启动应用时,通过
-Dcoderacademy.enable=false
指定排除Starter的自动配置类。这种方式特别适用于本地启动应用时排除Starter或其他Bean的初始化。

当然本地启动也可以直接通过
-Dspring.autoconfigure.exclude=com.springboot.starter.coderacademy.config.CoderAcademyAutoConfig
也可以满足排除Starter的配置。

自定义Starter Bean排除

在《SpringBoot如何自定义Starter》文中,我们还提到了一种调用方使用Starter的方式,我们可以不是用自动配置类的Starter,可以自定义配置的信息,在手动创建Starter对应的服务的Bean。例如:

@Data
@Configuration
public class StarterConfig {

    @Value("${springboot.coderacademy.name}")
    private String staterMsg;

    @Bean
    public CoderAcademyService coderAcademyService(){
        CoderAcademyConfig coderAcademyConfig = new CoderAcademyConfig();
        coderAcademyConfig.setUrl(staterMsg);
        return new CoderAcademyService(coderAcademyConfig);
    }
}

对于这种情况,我们可以使用Spring Boot的注解
@ConditionalOnProperty
来控制是否创建Bean。
@ConditionalOnProperty
是一个条件注解,根据配置属性的值来决定是否应该创建一个Bean或应用某个配置。具体来说,
@ConditionalOnProperty

name
属性表示配置属性的名称,
havingValue
属性表示配置属性的期望值,默认为
true

matchIfMissing
属性表示当配置属性不存在时是否匹配条件,默认为
false

因此,我们可以给
CoderAcademyService
的Bean添加
@ConditionalOnProperty
注解:

@Data
@Configuration
public class StarterConfig {

    @Value("${springboot.coderacademy.name}")
    private String staterMsg;

    @ConditionalOnProperty(name = "coderacademy.enable", havingValue = "true", matchIfMissing = true)
    @Bean
    public CoderAcademyService coderAcademyService(){
        CoderAcademyConfig coderAcademyConfig = new CoderAcademyConfig();
        coderAcademyConfig.setUrl(staterMsg);
        return new CoderAcademyService(coderAcademyConfig);
    }
}

这样,我们在启动应用时可以通过
-Dcoderacademy.enable=false
变量来控制是否创建
CoderAcademyService
,而设置了
matchIfMissing=true
,即使其他环境没有该环境变量也不受影响。

除了
@ConditionalOnProperty
之外,Spring Boot还提供了其他一些条件注解,用于根据不同的条件来决定是否应该创建Bean或者是否应该应用某个配置。一些常见的条件注解包括:

  1. @ConditionalOnClass
    :当类路径中存在指定的类时,才会创建Bean或应用配置。
  2. @ConditionalOnMissingClass
    :当类路径中不存在指定的类时,才会创建Bean或应用配置。
  3. @ConditionalOnBean
    :当容器中存在指定的Bean时,才会创建Bean或应用配置。
  4. @ConditionalOnMissingBean
    :当容器中不存在指定的Bean时,才会创建Bean或应用配置。
  5. @ConditionalOnExpression
    :当满足SpEL表达式定义的条件时,才会创建Bean或应用配置。
  6. @ConditionalOnJava
    :当JVM运行的Java版本符合指定条件时,才会创建Bean或应用配置。
  7. @ConditionalOnWebApplication
    :当运行的环境是Web应用程序时,才会创建Bean或应用配置。
  8. @ConditionalOnNotWebApplication
    :当运行的环境不是Web应用程序时,才会创建Bean或应用配置。

总结

本文介绍了在Spring Boot项目中如何排除Starter自动配置类,以及根据不同环境动态排除配置的方法。通过
spring.autoconfigure.exclude
属性和条件注解如
@ConditionalOnProperty
,我们可以灵活控制Bean的创建和配置的应用,从而更好地适应不同的部署环境和需求。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等