Java 业务开发常见谬误 100 例(代码篇 -2)
11 丨空值解决:分不清楚的 null 和宜人的空指针
-
业务代码中 5 种最容易呈现空指针异样的写法
- 参数值是 Integer 等包装类型,应用时因为主动拆箱呈现了空指针异样;
- 字符串比拟呈现空指针异样;
- 诸如 ConcurrentHashMap 这样的容器不反对 Key 和 Value 为 null,强行 put null 的 Key 或 Value 会呈现空指针异样;
- A 对象蕴含了 B,在通过 A 对象的字段取得 B 之后,没有对字段判空就级联调用 B 的办法呈现空指针异样;
- 办法或近程服务返回的 List 不是空而是 null,没有进行判空就间接调用 List 的办法呈现空指针异样;
- 通过 Optional 配合 Stream 能够防止大多数简短的 if-else 判空逻辑,实现一行代码优雅判空。另外,要定位和修复空指针异样,除了能够通过减少日志进行排查外,在生产上应用 Arthas 来查看办法的调用栈和入参会更快捷。
- POJO 中字段的 null 定位,从服务端的角度往往很难分分明,到底是客户端心愿疏忽这个字段还是无意传了 null,因而咱们尝试用 Optional 类来辨别 null 的定位。同时,为防止把空值更新到数据库中,能够实现动静 SQL,只更新必要的字段。
- 数据库字段容许保留 null,会进一步减少出错的可能性和复杂度。会有 NULL、空字符串和字符串 null 三种状态
-
MySQL sum 函数、count 函数,以及 NULL 值条件可能踩的坑
- sum 函数没统计到任何记录时,会返回 null 而不是 0,能够应用 IFNULL 函数把 null 转换为 0;
- count 字段不统计 null 值,COUNT(*) 才是统计所有记录数量的正确形式。
- =NULL 并不是判断条件而是赋值,对 NULL 进行判断只能应用 IS NULL 或者 IS NOT NULL。
12 丨异样解决:别让本人在出问题的时候变为瞎子
大多数业务利用都采纳的三层架构:
业务性质上异样可能分为 业务异样 和零碎异样 两大类:
对于自定义的业务异样 :以 Warn 级别的日志记录异样以及以后 URL、执行办法等信息后,提取异样中的错误码和音讯等信息,转换为适合的 API 包装体返回给 API 调用方;
对于无奈解决的零碎异样:以 Error 级别的日志记录异样和上下文信息(比方 URL、参数、用户 ID)后,转换为普适的“服务器忙,请稍后再试”异样信息,同样以 API 包装体返回给调用方。
- Repository 层出现异常或者能够疏忽,或者能够降级,或者须要转化为一个敌对的异样。如果一律捕捉异样仅记录日志,很可能业务逻辑曾经出错,而用户和程序自身齐全感知不到。
- Service 层往往波及数据库事务,出现异常同样不适宜捕捉,否则事务无奈主动回滚。此外 Service 层波及业务逻辑,有些业务逻辑执行中遇到业务异样,可能须要在异样后转入分支业务流程。如果业务异样都被框架捕捉了,业务性能就会不失常。
- 如果上层异样回升到 Controller 层还是无奈解决的话,Controller 层往往会给予用户敌对提醒,或是依据每一个 API 的异样表返回指定的异样类型,同样无奈对所有异样厚此薄彼。
-
谬误打 log 姿态:
- 捕捉了异样后间接生吞
-
没有生吞,然而抛弃异样的原始信息
@GetMapping("wrong1") public void wrong1(){ try {readFile(); } catch (IOException e) { //wrong1: 原始异样信息失落 //throw new RuntimeException("零碎忙请稍后再试"); //wrong2: 只保留了异样音讯,栈没有记录 //log.error("文件读取谬误, {}", e.getMessage()); //throw new RuntimeException("零碎忙请稍后再试"); //correct1: log.error("文件读取谬误", e); throw new RuntimeException("零碎忙请稍后再试"); //correct2: throw new RuntimeException("零碎忙请稍后再试", e); } }
-
抛出异样时不指定任何音讯
throw new RuntimeException();
-
如果你捕捉了异样打算解决的话,除了通过日志正确记录异样原始信息外,通常还有
三种解决模式:- 转换,即转换新的异样抛出。对于新抛出的异样,最好具备特定的分类和明确的异样音讯,而不是轻易抛一个无关或没有任何信息的异样,并最好通过 cause 关联老异样。
- 重试,即重试之前的操作。比方近程调用服务端过载超时的状况,自觉重试会让问题更
重大,须要思考当前情况是否适宜重试。 - 复原,即尝试进行降级解决,或应用默认值来代替原始数据。
-
小心 finally 中的异样
-
尽管 try 中的逻辑呈现了异样,但却被 finally
中的异样笼罩了@GetMapping("wrong") public void wrong() { try {log.info("try"); // 异样失落 throw new RuntimeException("try"); } finally { // wrong //log.info("finally"); //throw new RuntimeException("finally"); //correct log.info("finally"); try {throw new RuntimeException("finally"); } catch (Exception ex) {log.error("finally", ex); } } }
-
能够改为 try-with-resources 模式
@GetMapping("useresourceright") public void useresourceright() throws Exception {try (TestResource testResource = new TestResource()){testResource.read(); } }
-
- 千万别把异样定义为动态变量, 务必确保异样是每次 new 进去的。否则可能会引起栈信息的错乱。
-
确保正确处理了线程池中工作的异样:
- 如果工作通过 execute 提交,那么出现异常会导致线程退出,大量的异样会导致线程反复创立引起性能问题,咱们应该尽可能确保工作不出异样,同时设置默认的未捕捉异样处理程序来兜底;
- 如果工作通过 submit 提交意味着咱们关怀工作的执行后果,应该通过拿到的 Future 调用其 get 办法来取得工作运行后果和可能呈现的异样,否则异样可能就被生吞了。
13 丨日志:日志记录真没你设想的那么简略
记录日志引起的坑,容易出错次要在于三个方面:
- 日志框架的兼容问题
- 日志文件配置简单且容易出错
-
日志记录自身就有些误区,比方没思考到日志内容获取的代价、胡乱应用日志级别等
Java 体系的日志框架,的确十分多,而不同的类库,还可能抉择应用不同的日志框架。这样一来,日志的对立治理就变得十分艰难。为了解决这个问题,就有了 SLF4J((SimpleLogging Facade For Java))。而个别咱们也都是应用 SLF4J 去治理:
SLF4J 实现了三种性能:
- 提供了对立的日志门面 API,即图中紫色局部,实现了中立的日志记录 API。
- 提供桥接性能,即图中蓝色局部,用来把各种日志框架的 API(图中绿色局部)桥接到 SLF4J API。这样一来,即使你的程序中应用了各种日志 API 记录日志,最终都能够桥接到 SLF4J 门面 API。
- 提供适配性能,即图中红色局部,能够实现 SLF4J API 和理论日志框架(图中灰色局部)的绑定。SLF4J 只是日志规范,咱们还是须要一个理论的日志框架。日志框架自身没有实现 SLF4J API,所以须要有一个前置转换。Logback 就是依照 SLF4J API 规范实现的,因而不须要绑定模块做转换。
常见问题:
- 如果程序启动时呈现 SLF4J 的谬误提醒,那很可能是配置呈现了问题,能够应用 Maven 的 dependency:tree 命令梳理依赖关系
- Logback 是 Java 最罕用的日志框架,其配置比较复杂,你能够参考官网文档中对于 Appender、Layout、Filter 的配置,切记不要随便从其余中央复制他人的配置,避免出现谬误或与以后需要不符。
- 应用异步日志解决性能问题,是用空间换工夫。但空间毕竟无限,当空间满了之后,咱们要思考是阻塞期待,还是抛弃日志。如果更心愿不抛弃重要日志,那么抉择阻塞期待;如果更心愿程序不要因为日志记录而阻塞,那么就须要抛弃日志。
- 应用日志占位符,而不是 字符串拼接
14 丨文件 IO:实现高效正确的文件读写并非易事
-
如果须要读写字符流,那么须要确保文件中字符的字符集和字符流的字符集是统一
的,否则可能产生乱码。-
不指定 Charset(程序会主动以以后机器的默认字符集来读取文件的)
char[] chars = new char[10]; String content = ""; try (FileReader fileReader = new FileReader("hello.txt")) { int count; while ((count = fileReader.read(chars)) != -1) {content += new String(chars, 0, count); } } log.info("result:{}", content);
-
指定 Charset
Files.write(Paths.get("hello2.txt"), "你好 hi".getBytes(Charsets.UTF_8)); byte[] content = Files.readAllBytes(Paths.get("hello2.txt")); log.info("bytes:{}",Hex.encodeHexString(content));
-
-
应用 Files 类的一些流式解决操作,留神应用 try-with-resources 包装 Stream,确保底层文件资源能够开释,防止产生 too many open files 的问题。
-
不应用 try-with-resources(后盾不会敞开过程,而是会始终新开一个过程,直到无奈再开新的线程)
LongAdder longAdder = new LongAdder(); IntStream.rangeClosed(1, 1000000).forEach(i -> { try {Files.lines(Paths.get("demo.txt")).forEach(line -> longAdder.increment} catch (IOException e) {e.printStackTrace(); } }); log.info("total : {}", longAdder.longValue());
-
应用 try-with-resources
LongAdder longAdder = new LongAdder(); IntStream.rangeClosed(1, 1000000).forEach(i -> {try (Stream<String> lines = Files.lines(Paths.get("demo.txt"))) {lines.forEach(line -> longAdder.increment()); } catch (IOException e) {e.printStackTrace(); } }); log.info("total : {}", longAdder.longValue());
-
-
进行文件字节流操作的时候,个别状况下不思考进行逐字节操作,应用缓冲区进行批量读写缩小 IO 次数,性能会好很多。个别能够思考间接应用缓冲输入输出流 BufferedXXXStream,谋求极限性能的话能够思考应用 FileChannel 进行流转发。
-
不对数据进行解决,间接把原文件数据写入指标文件;
private static void perByteOperation() throws IOException {try (FileInputStream fileInputStream = new FileInputStream("src.txt"); FileOutputStream fileOutputStream = new FileOutputStream("dest.txt")) int i; while ((i = fileInputStream.read()) != -1) {fileOutputStream.write(i); } } }
复制一个 35MB 的文件,耗时 190 秒
-
改进后,应用 100 字节作为缓冲区
private static void bufferOperationWith100Buffer() throws IOException {try (FileInputStream fileInputStream = new FileInputStream("src.txt"); FileOutputStream fileOutputStream = new FileOutputStream("dest.txt")) byte[] buffer = new byte[100]; int len = 0; while ((len = fileInputStream.read(buffer)) != -1) {fileOutputStream.write(buffer, 0, len); } } }
复制一个 35MB 的文件,耗时 26 秒
(能够看到,在进行文件 IO 解决的时候,应用适合的缓冲区能够明显提高性能) -
应用 BufferedXXXStream,其外部实现了一个默认 8KB 大小的缓冲区(然而,在应用 BufferedInputStream 和 BufferedOutputStream 时,还是倡议大家再应用一个缓冲进行读写,不要因为它们实现了外部缓冲就进行逐字节的操作)。
这里我间接贴出三种形式,具体代码 放在 code repository 里,可自行翻阅:
Java 业务开发常见谬误 100 例- 间接应用 BufferedInputStream 和 BufferedOutputStream;
- 额定应用一个 8KB 缓冲,应用 BufferedInputStream 和 BufferedOutputStream;
- 间接应用 FileInputStream 和 FileOutputStream,再应用一个 8KB 的缓冲。
最初,三者的性能别离是 1.4、110 毫秒 和 110 毫秒 - 应用 FileChannel,速度最快,可达 50 毫秒,比最原始的 190 秒,快了足足 数千倍
-
最初要强调一点的是,文件操作因为波及操作系统和文件系统的实现,JDK 并不能确保所有 IO API 在所有平台的逻辑一致性,代码迁徙到新的操作系统(比方上到测试场或者生产场)或文件系统时,要从新进行功能测试和性能测试。
15 丨序列化:一来一回你还是原来的你吗?
基于 Redis 和 Web API 的入参和出参两个场景,介绍 序列化和反序列化时须要避开的几个坑
- 要确保序列化和反序列化算法的一致性。因为,不同序列化算法输入必然不同,要正确处理序列化后的数据就要应用雷同的反序列化算法。
- Jackson 有大量的序列化和反序列化个性,能够用来微调序列化和反序列化的细节。须要留神的是,如果自定义 ObjectMapper 的 Bean,小心不要和 Spring Boot 主动配置的 Bean 抵触。
- 在调试序列化反序列化问题时,咱们肯定要捋分明三点:是哪个组件在做序列化反序列化、整个过程有几次序列化反序列化,以及目前到底是序列化还是反序列化
- 对于反序列化默认状况下,框架调用的是 无参构造方法,如果要调用自定义的有参构造方法,那么须要告知框架如何调用。更正当的形式是,对于须要序列化的 POJO 思考尽量不要自定义构造方法。
- 枚举不倡议定义在 DTO 中跨服务传输,因为会有版本问题,并且波及序列化反序列化时会很简单,容易出错。因而,只倡议在程序外部应用枚举。
16 | 用好 Java 8 的日期工夫类,少踩一些“老三样”的坑
Java Date 系列已成为遗留产品,新的 Java8 中的工夫新个性,曾经能够全面替换旧的了,旧的不仅可读性差、易用性差、应用起来冗余繁琐,还有线程平安问题,所以也强烈建议大家应用 JDK8 的。除了好用之外,二者有区别的中央还在于:
java.util.Date 类是因为应用 UTC 示意,所以没有时区概念,实质是工夫戳;
而 LocalDateTime,严格上能够认为是一个日期工夫的示意,而不是一时间点。
-
初始化工夫:(例子:2019 年 12 月 31 日 11 点 12 分 13 秒)
-
jdk8 之前:
Date date = new Date(2019 - 1900 , 11, 31, 11, 12, 13);
有国际化需要,须要应用到 Calendar 类jdk8 之前: 年应该是和 1900 的差值,月应该是从 0 到 11 而不是从 1 到 12。
- jdk8 后:
LocalDateTime date = LocalDateTime.of(2019, 12, 31, 11, 12, 13);
-
-
时区问题:解决好工夫和时区问题首先就是要正确保留日期工夫。这里有两种保留形式:
- 以 UTC 保留,保留的工夫没有时区属性,是不波及时区时间差问题的世界对立
工夫,后面咱们说过,Date 类就是存得是 UTC 的工夫戳, - 以字面量保留,比方年 / 月 / 日 时: 分: 秒,肯定要同时保留时区信息。
时区因素会带来两个问题:
-
Date 存得是 UTC 工夫戳,不同时区服务器读出的工夫是不一样的,例如:拿 2020-01-02 22:00:00,这个工夫作为例子,别离依照默认程序时区,和指定 NewYork 时区,输入 解析后的工夫
String stringDate = "2020-01-02 22:00:00"; SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date1 = inputFormat.parse(stringDate); System.out.println(date1 + ":" + date1.getTime()); inputFormat.setTimeZone(TimeZone.getTimeZone("America/New_York")); Date date2 = inputFormat.parse(stringDate); System.out.println(date2 + ":" + date2.getTime());
输入:(发现相差 13 个小时,所以说,如果你的公司服务器有跨时区的,)
Thu Jan 02 22:00:00 CST 2020:1577973600000
Fri Jan 03 11:00:00 CST 2020:1578020400000解决方案:务必指定 存和读的 时区是统一的。存的时候,须要应用正确的以后时区来保留,这样 UTC 工夫才会正确;读的时候,也只有正确设置本地时区,能力把 UTC 工夫转换为正确的当地工夫。
-
更好的计划:Java 8 推出了新的工夫日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime
和 DateTimeFormatter,解决时区问题更简略清晰。LocalDateTime 不带有时区属性,所以命名为本地时区的日期工夫;
而 ZonedDateTime=LocalDateTime+ZoneId,具备时区属性。
因而,LocalDateTime 只能认为是一个工夫示意,ZonedDateTime 才是一个无效的工夫咱们拿上海、纽约和东京,举个例子:仍旧是 2020-01-02 22:00:00这个 time
-
代码:
String stringDate = "2020-01-02 22:00:00"; ZoneId timeZoneSH = ZoneId.of("Asia/Shanghai"); ZoneId timeZoneNY = ZoneId.of("America/New_York"); ZoneId timeZoneJST = ZoneOffset.ofHours(9); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(stringDate, dateTimeFormatter), timeZoneJST); DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z"); System.out.println(timeZoneSH.getId() + outputFormat.withZone(timeZoneSH).format(date)); System.out.println(timeZoneNY.getId() + outputFormat.withZone(timeZoneNY).format(date)); System.out.println(timeZoneJST.getId() + outputFormat.withZone(timeZoneJST).format(date));
-
输入:
Asia/Shanghai2020-01-02 21:00:00 +0800
America/New_York2020-01-02 08:00:00 -0500
+09:002020-01-02 22:00:00 +0900 - 论断:要正确处理国际化工夫问题,举荐应用 Java 8 的日期工夫类,即应用 ZonedDateTime 保留工夫,而后应用设置了 ZoneId 的 DateTimeFormatter 配合 ZonedDateTime 进行工夫格式化失去本地工夫示意。这样的划分非常清晰、细化,也不容易出错。
-
- 以 UTC 保留,保留的工夫没有时区属性,是不波及时区时间差问题的世界对立
-
日期工夫格式化和解析
-
Date –”YYYY-MM-dd 驰名 Bug“– 提前跨年,”这明明是一个 2019 年的日期,怎么应用 SimpleDateFormat 格式化后就提前跨年了“。
- 例如:初始化一个 Calendar,设置日期工夫为 2019 年 12 月 29 日,应用大写的 YYYY 来初始化 SimpleDateFormat。但最初输入的,却是 2020 年 12 月 29 日,好家伙,间接多了一年!
- 其起因在于:开发人员混同了 SimpleDateFormat 的各种格式化模式。JDK 的文档中有阐明:小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年。
- 而依照以后 zh_CN 区域来说,2020 年第一周的条件是,从周日开始的残缺 7 天,2020 年蕴含 1 天即可。显然,2019 年 12 月 29 日周日到 2020 年 1 月 4 日周六是 2020 年第一周,得出的 weekyear 就是 2020 年。然而如果你把时区换成 France,就不会有问题。
- Date – 定义的 static 的 SimpleDateFormat 可能会呈现线程平安问题
-
Date – 当须要解析的字符串和格局不匹配的时候,SimpleDateFormat 体现得很宽容,例如:
应用 yyyyMM 来解析 20160901,它竟然不报错,然而后果是:
2091 年 1 月 1 日,起因在于:把 0901 当成了月份,相当于 75 年,无语子。。。 - 相比旧的 Date,新的 JDK8 Date 就没有这些问题,也不必管是 YYYY 还是 yyyy,DateTimeFormatter 要是线程平安的。
-
-
日期工夫的计算:
-
日期工夫的计算,一个很多开发常踩的坑。有人间接应用工夫戳进行工夫计算,比方心愿失去以后工夫之后 30 天的工夫,会这么写代码:间接把 newDate().getTime 办法失去的工夫戳加 30 天对应的毫秒数,也就是 30 天 1000 毫秒 3600 秒 *24 小时。然而会发现 后果基本不对
其起因在于 int 产生溢出,修复形式就是把 30 改为 30L
但还是很繁琐,且容易出错,所以 jdk8 之前,更举荐应用 Calendar -
jdk8 后,日期工夫类型,能够间接进行各种计算,更加简洁、不便和弱小。
但 计算两个日期差时可能会踩坑,
Period.between 失去了两个 LocalDate 的差,返回的是两个日期差几年零几月零几天。如果心愿得悉两个日期之间差几天,间接调用 Period 的 getDays() 办法失去的只是最初的“零几天”,而不是算总的距离天数。
-
17 丨别以为“自动挡”就不可能呈现 OOM
通常而言,Java 程序的 OOM 有如下几种可能:
- 程序的确须要超出 JVM 配置的内存下限的内存。不论是程序实现的不合理,还是因为各种框架对数据的反复解决、加工和转换,雷同的数据在内存中不肯定只占用一份空间。针对内存量应用超大的业务逻辑,比方缓存逻辑、文件上传下载和导出逻辑,咱们在做容量评估时,可能还须要理论做一下 Dump,而不是进行简略的假如。
- 呈现内存泄露,其实就是咱们认为没有用的对象最终会被 GC,但却没有。GC 并不会回收强援用对象,咱们可能常常在程序中定义一些容器作为缓存,但如果容器中的数据有限增长,要特地小心最终会导致 OOM。应用 WeakHashMap 是解决这个问题的好方法,但值得注意的是,如果强援用的 Value 有援用 Key,也无奈回收 Entry。
- 不合理的资源需要配置,在业务量小的时候可能不会呈现问题,但业务量一大可能很快就会撑爆内存。比方,随便配置 Tomcat 的 max-http-header-size 参数,会导致一个申请应用过多的内存,申请量大的时候呈现 OOM。在进行参数配置的时候,咱们要意识到,很多限度类参数限度的是背地资源的应用,资源始终是无限的,须要依据理论需要来正当设置参数。
最初想说的是,在呈现 OOM 之后,也不必过于缓和。咱们能够依据谬误日志中的异样信息,再联合 jstat 等命令行工具察看内存应用状况,以及程序的 GC 日志,来大抵定位呈现 OOM 的内存区块和类型。其实,咱们遇到的 90% 的 OOM 都是堆 OOM,对 JVM 过程进行堆内存 Dump,或应用 jmap 命令剖析对象内存占用排行,个别都能够很容易定位到问题。
18 丨当反射、注解和泛型遇到 OOP 时,会有哪些坑?
尽管咱们日常业务我的项目中简直都是增删改查,用到反射、注解和泛型这些高级个性的机会少之又少,没啥好学的。然而,只有学好、用好这些高级个性,能力开发出更简洁易读的代码,而且简直所有的框架都应用了这三大高级个性。比方,要缩小反复代码,就得用到反射和注解。
- 反射调用办法并不是通过调用时的传参确定办法重载,而是在获取办法的时候通过办法名和参数类型来确定的。遇到办法有包装类型和根本类型重载的时候,所以须要特地留神这一点。
- 反射获取类成员,须要留神 getXXX 和 getDeclaredXXX 办法的区别,其中 XXX 包含 Methods、Fields、Constructors、Annotations。
- 泛型因为类型擦除会导致泛型办法 T 占位符被替换为 Object,子类如果应用具体类型笼罩父类实现,编译器会生成桥接办法。这样既满足子类办法重写父类办法的定义,又满足子类实现的办法有具体的类型。应用反射来获取办法清单时,所以须要特地留神这一点。
- 自定义注解能够通过标记元注解 @Inherited 实现注解的继承,不过这只实用于类。如果要继承定义在接口或办法上的注解,能够应用 Spring 的工具类 AnnotatedElementUtils,并留神各种 getXXX 办法和 findXXX 办法的区别。
19 丨 Spring 框架:IoC 和 AOP 是扩大的外围
- 让 Spring 容器治理对象,要思考对象默认的 Scope 单例是否适宜,对于有状态的类型,单例可能产生内存泄露问题。
- 如果要为单例的 Bean 注入 Prototype 的 Bean,绝不是仅仅批改 Scope 属性这么简略。因为单例的 Bean 在容器启动时就会实现一次性初始化。最简略的解决方案是,把 Prototype 的 Bean 设置为通过代理注入,也就是设置 proxyMode 属性为 TARGET_CLASS。
- 如果一组雷同类型的 Bean 是有程序的,须要明确应用 @Order 注解来设置程序。能够再回顾下,两个不同优先级切面中 @Before、@After 和 @Around 三种加强的执行程序,是什么样的。
20 丨 Spring 框架:框架帮咱们做了很多工作也带来了复杂度
- Spring Cloud 会应用 Spring Boot 的个性,依据以后引入包的状况做各种主动拆卸。如果咱们要扩大 Spring 的组件,那么只有清晰理解 Spring 主动拆卸的运作形式,能力甄别运行时对象在 Spring 容器中的状况,不能想当然认为代码中能看到的所有 Spring 的类都是 Bean。
- 对于配置优先级的案例,剖析配置源优先级时,如果咱们认为看到 PropertySourcesPropertyResolver 就看到了假相,后续进行扩大开发时就可能会踩坑。咱们肯定要留神,剖析 Spring 源码时,你看到的表象不肯定是理论运行时的状况,还须要借助日志或调试工具来理清整个过程。如果没有调试工具,能够借助 Arthas,来剖析代码调用门路。