Arthas
是Alibaba开源的Java诊断工具,深受开发者喜爱。
Arthas 3.1.5版本带来下面全新的特性:
火焰图的威名相信大家都有所耳闻,但可能因为使用比较复杂,所以望而止步。
在新版本的Arthas里集成了async-profiler,使用profiler
命令就可以很方便地生成火焰图,并且可以在浏览器里直接查看。
profiler
命令基本运行结构是 profiler action [actionArg]
。 下面介绍如何使用。
1 | $ profiler start |
默认情况下,生成的是cpu的火焰图,即event为
cpu
。可以用--event
参数来指定。
1 | $ profiler getSamples |
1 | $ profiler status |
可以查看当前profiler在采样哪种event
和采样时间。
1 | $ profiler stop |
默认情况下,生成的结果保存到应用的工作目录
下的arthas-output
目录里。
默认情况下,arthas使用3658端口,则可以打开: http://localhost:3658/arthas-output/ 查看到arthas-output
目录下面的profiler结果:
点击可以查看具体的结果:
如果是chrome浏览器,可能需要多次刷新。
标准的linux grep命令支持丰富的选项,可以很方便地定位结果的上下文等。
新版本的grep
命令支持更多标准的选项,下面是一些例子:
1 | sysprop | grep java |
感谢社区里 @qxo 的贡献。
默认情况下,Arthas的Telnet端口是3658,HTTP端口是8563,这个常常让用户迷惑。在新版本里,在3658端口同时支持Telnet/HTTP协议。
在浏览器里访问 http://localhost:3658/ 也可以访问到Web Console了。
在后续的版本里,考虑默认只侦听 3658端口,减少用户的配置项。
以前Arthas被诟病比较多的一个问题是,monitor/tt/trace等命令时间统计误差大。因为以前只使用了一个int来保存时间,所以不精确。
在新版本里,改用一个高效的stack来保存数据,时间的准确度大大提升,欢迎大家反馈效果。
感谢社区里 @huangjIT 的贡献。
总之,3.1.5
版本的Arthas引入了开箱即用的Profiler/火焰图功能,欢迎大家使用反馈。
最近看到一个很流行的标题,《开源XX年,star XXX,我是如何坚持的》。
看到这样的标题,忽然发觉Arthas从2018年9月开源以来,刚好一年了,正好在这个秋高气爽的时节做下总结和回顾。
Arthas
是Alibaba开源的Java诊断工具,深受开发者喜爱。
回顾Arthas Star数的历史,一直保持快速增长,目前已经突破16K。
感谢用户的支持,既是压力也是动力。在过去开源的一年里,Arthas发布了7个Release版本,我们一直坚持三点:
Arthas一直把易用性放在第一位,在开源之后,我们做了下面的改进:
Up/Down
直达尽管我们在易用性下了很大的功夫,但是发现很多时候用户比较难入门,因此,我们参考了k8s的 Interactive Tutorial,推出了Arthas的在线教程:
通过基础教程,可以在交互终端里一步步入门,通过进阶教程可以深入理解Arthas排查问题的案例。
另外,为了方便用户大规模部署,我们实现了tunnel server和用户数据回报功能:
Arthas号称是Java应用诊断利器,那么我们自己要对得起这个口号。在开源之后,Arthas持续增加了10多个命令。
下面重点介绍两个功能。
以 Arthas在线教程 里的UserController
为例:
使用jad反编译代码
1 | jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java |
使用vim编译代码
当 user id 小于1时,也正常返回,不抛出异常:
1 | "/user/{id}") ( |
使用mc
命令编译修改后的UserController.java
1 | $ mc /tmp/UserController.java -d /tmp |
使用redefine
命令,因为可以热更新代码
1 | $ redefine /tmp/com/example/demo/arthas/user/UserController.class |
在网站压力大的时候(比如双11),有个缓解措施就是把应用的日志level修改为ERROR。 那么有两个问题:
通过logger
命令,可以查看应用里logger的详细配置信息,比如FileAppender
输出的文件,AsyncAppender
是否blocking
。
1 | [arthas@2062]$ logger |
也可以在线修改logger的level:
1 | [arthas@2062]$ logger --name ROOT --level debug |
Arthas开源以来,一共有67位 Contributors,感谢他们贡献的改进:
社区提交了一系列的改进,下面列出一些点(不完整):
arthas@pid
mbean
命令另外,有83个公司/组织登记了他们的使用信息,欢迎更多的用户来登记:
基于Arthas,还产生了一些洐生项目,下面是其中两个:
广大用户在使用Arthas排查问题过程中,分享了很多排查过程和心得,欢迎大家来分享。
Arthas本身使用了很多开源项目的代码,在开源过程中,我们给netty, ognl, cfr等都贡献了改进代码,回馈上游。
在做Arthas宣传小册子时,Arthas的宣传语是:
“赠人玫瑰之手,经久犹有余香”
希望Arthas未来能帮助到更多的用户解决问题,也希望广大的开发者对Arthas提出更多的改进和建议。
最后是抽奖环节,大家可以转发文章,在公众号后台留言自己和Arthas的故事,或者给Arthas提出建议,奖品是Arthas的卫衣一件:
欢迎关注横云断岭的专栏,专注Java,Spring Boot,Arthas,Dubbo。
]]>Arthas
是Alibaba开源的Java诊断工具,深受开发者喜爱。
Arthas 3.1.2版本持续增加新特性,下面重点介绍:
logger/heapdump/vmoption/stop
命令arthas@pid
形式,支持ctrl + k
清屏快捷键查看logger信息,更新logger level
以下面的logback.xml
为例:
1 |
|
使用logger
命令打印的结果是:
1 | [arthas@2062]$ logger |
从appenders
的信息里,可以看到
CONSOLE
logger的target是System.out
APPLICATION
logger是RollingFileAppender
,它的file是app.log
ASYNC
它的appenderRef
是APPLICATION
,即异步输出到文件里1 | [arthas@2062]$ logger -n org.springframework.web |
1 | [arthas@2062]$ logger --name ROOT --level debug |
dump java heap, 类似jmap命令的heap dump功能。
1 | [arthas@58205]$ heapdump /tmp/dump.hprof |
1 | [arthas@58205]$ heapdump --live /tmp/dump.hprof |
查看,更新VM诊断相关的参数
1 | [arthas@56963]$ vmoption |
1 | [arthas@56963]$ vmoption PrintGCDetails |
1 | [arthas@56963]$ vmoption PrintGCDetails true |
之前有用户吐槽,不小心退出Arthas console之后,shutdown
会关闭系统,因此增加了stop
命令来退出arthas,功能和shutdown
命令一致。
在新版本里,增加了arthas tunnel server的功能,用户可以通过tunnel server很方便连接不同网络里的arthas agent,适合做统一管控。
在启动arthas,可以传递--tunnel-server
参数,比如:
1 | as.sh --tunnel-server 'ws://47.75.156.201:7777/ws' |
目前
47.75.156.201
是一个测试服务器,用户可以自己搭建arthas tunnel server
--agent-id
参数里指定agentId。默认情况下,会生成随机ID。attach成功之后,会打印出agentId,比如:
1 | ,---. ,------. ,--------.,--. ,--. ,---. ,---. |
如果是启动时没有连接到 tunnel server,也可以在后续自动重连成功之后,通过 session命令来获取 agentId:
1 | [arthas@86183]$ session |
以上面的为例,在浏览器里访问 http://47.75.156.201:8080/ ,输入 agentId
,就可以连接到本机上的arthas了。
1 | browser <-> arthas tunnel server <-> arthas tunnel client <-> arthas agent |
https://github.com/alibaba/arthas/blob/master/tunnel-server/README.md
提示符修改为arthas@pid
形式,用户可以确定当前进程ID,避免多个进程时误操作
1 | [arthas@86183]$ help |
增加ctrl + k
清屏快捷键
总之,3.1.2
版本的Arthas新增加了logger/heapdump/vmoption/stop
命令,增加了tunnel server,方便统一管控。另外还有一些bug修复等,可以参考
最后,Arthas的在线教程考虑重新组织,欢迎大家参与,提出建议:
]]>本文介绍本人觉得最好用的bash配置技巧,一次配置,每天受益。
在bash里,最常见的搜索历史命令的办法是ctrl + r
,但是这个步骤太多,比较麻烦。
下面介绍一种非常快捷的补全方式。
执行:
1 | curl -L http://hengyunabc.github.io/bash_completion_install.sh | sh |
这样子,先输入部分命令,再按键盘的Up/Down
就可以自动补全出历史命令了。
实际上给~/.inputrc
文件添加了下面的内容:
1 | "\e[A": history-search-backward |
前面两行自然是绑定了快捷键,后面两行是什么意思呢?
show-all-if-ambiguous
是指tab补全时,按一次tab就会把最长匹配的自动补全。具体参考 https://stackoverflow.com/a/42193784
completion-ignore-case
是指tab补全时,忽略大小写,这点也非常方便。
注意,在修改完
~/.inputrc
文件,要显式执行bind -f ~/.inputrc
才会生效。
题外话,在arthas
里也支持了Up/Down
自动补全历史命令这个特性,所以在arthas
里自动补全历史命令非常的方便。
jad
命令的实现原理。jad
命令介绍jad即java decompiler,把JVM已加载类的字节码反编译成Java代码。比如反编译String
类:
1 | $ jad java.lang.String |
反编译有两部分工作:
那么怎么从运行的JVM里获取到字节码?
最常见的思路是,在classpaths
下面查找,比如 ClassLoader.getResource("java/lang/String.class")
,但是这样子查找到的字节码不一定对。比如可能有多个冲突的jar,或者有Java Agent修改了字节码。
从JDK 1.5起,有一套ClassFileTransformer
的机制,Java Agent通过Instrumentation
注册ClassFileTransformer
,那么在类加载或者retransform
时就可以回调修改字节码。
显然,在Arthas里,要增强的类是已经被加载的,所以它们的字节码都是在retransform
时被修改的。
通过显式调用Instrumentation.retransformClasses(Class<?>...)
可以触发回调。
Arthas里增强字节码的watch
/trace
/stack
/tt
等命令都是通过ClassFileTransformer
来实现的。
ClassFileTransformer
的接口如下:
1 | public interface ClassFileTransformer { |
看到这里,读者应该猜到jad
是怎么获取到字节码的了:
ClassFileTransformer
Instrumentation.retransformClasses
触发回调transform
函数里获取到字节码ClassFileTransformer
获取到字节码之后,怎样转换为Java代码呢?
以前大家使用比较多的反编译软件可能是jd-gui
,但是它不支持JDK8的lambda语法和一些新版本JDK的特性。
后面比较成熟的反编译软件是cfr
,它以前是不开源的。直到最近的0.145
版本,作者终于开源了,可喜可贺。地址是
在Arthas jad
命令里,通过调用cfr
来完成反编译。
jad
命令的缺陷99%的情况下,jad
命令dump下来的字节码是准确的,除了一些极端情况。
ClassFileTransformer
可能有多个,那么在JVM里运行的字节码里,可能是被多个ClassFileTransformer
处理过的。retransformClasses
之后,这些注册的ClassFileTransformer
会被依次回,上一个处理的字节码传递到下一个。ClassFileTransformer
第二次执行会返回同样的结果。ClassFileTransformer
会被删掉,触发retransformClasses
之后,之前的一些修改就会丢失掉。所以目前在Arthas里,如果开两个窗口,一个窗口执行watch
/tt
等命令,另一个窗口对这个类执行jad
,那么可以观察到watch
/tt
停止了输出,实际上是因为字节码在触发了retransformClasses
之后,watch
/tt
所做的修改丢失了。
如果想精确获取到JVM内运行的Java字节码,可以使用这个dumpclass
工具,它是通过sa-jdi.jar
来实现的,保证dump下来的字节码是JVM内所运行的。
总结jad
命令的工作原理:
ClassFileTransformer
,再触发retransformClasses
来获取字节码cfr
来反编译ClassFileTransformer
的方式来获取字节码有一定缺陷dumpclass
工具可以精确获取字节码jad
命令可以在线上快速检查运行时的代码,并且结合mc
/redefine
命令可以热更新代码:
最近排查一个JMX本地连接问题,记录一下。
我们的启动脚本在应用启动后,会通过JMX来动态检查应用状态,那么这里就需要动态启动JMX功能了。
management-agent
com.sun.management.jmxremote.localConnectorAddress
的Agent Property,可以获取到JMX URL1 | public MBeanServerConnection connect(String pid) throws IOException { |
在用上面的代码动态去连接目标进程时,抛出了下面的异常:
1 | java.rmi.ConnectException: Connection refused to host: 11.164.235.11; nested exception is: |
11.164.234.171
11.164.235.11
?通过调试,发现management-agent
加载成功了,localConnectorAddress
的值是:
1 | jmx:rmi://127.0.0.1/stub/rO0ABXN9AAAAAQAlamF2YXgubWFuYWdlbWVudC5yZW1vdGUucm1pLlJNSVNlcnZlcnhyABdqYXZhLmxhbmcucmVmbGVjdC5Qcm94eeEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNyAC1qYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN0SW52b2NhdGlvbkhhbmRsZXIAAAAAAAAAAgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc4AAtVbmljYXN0UmVmMgAADTExLjE2NC4yMzUuMTEAAIfoCEScYyGQodFlwEdFAAABawK/zE6AAQB4 |
为什么显示的是127.0.0.1
,但实际连接的是11.164.235.11
?是不是在连接时出的问题?
再仔细调试,发现
ObjectInputStream
解析readObject
得到RMIServer
对象来连接的。1 | //javax.management.remote.rmi.RMIConnector.findRMIServer(JMXServiceURL, Map<String, Object>) |
通过代码处理,发现
1 | rO0ABXN9AAAAAQAlamF2YXgubWFuYWdlbWVudC5yZW1vdGUucm1pLlJNSVNlcnZlcnhyABdqYXZhLmxhbmcucmVmbGVjdC5Qcm94eeEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNyAC1qYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN0SW52b2NhdGlvbkhhbmRsZXIAAAAAAAAAAgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc4AAtVbmljYXN0UmVmMgAADTExLjE2NC4yMzUuMTEAAIfoCEScYyGQodFlwEdFAAABawK/zE6AAQB4 |
转换为了:
1 | RMIServerImpl_Stub[UnicastRef2 [liveRef: [endpoint:[11.164.235.11:26449](remote),objID:[-5ddae53d:16b0887d710:-7fff, 7209064096623493021]]]] |
可见RMI Server的IP的确是11.164.235.11
。
那么现在问题变成了:
management-agent
,得到的JMX URL是指向外部IP的?但是调试management-agent
的加载过程可能会比较痛苦,于是考虑从别的地方入手。
从上面的调查里,发现management-agent
启动之后,11.164.235.11
这个外部IP就会出现在JVM内存里,那么考虑用heap dump的方式来定位。
通过执行heap dump,再用jvisualvm
来分析。
用OQL来搜索所有包含11.164.235.11
的String:
1 | select s from java.lang.String s where s.toString().equals("11.164.235.11") |
可以发现有好几个结果:
再依次点开,查看引用,发现其中一个引用的字段名是localHost
:
因此可以猜测:是不是localHost域名解析有问题?
执行hostname命令,得到机器名,再ping一下:
1 | $hostname |
发现本机被解析到11.164.235.11
了,但是本机的IP是11.164.234.171
:
1 | $ifconfig |
到这里,大概猜到原因了,检查下 /etc/hosts
文件,果然发现有配置:
1 | 11.164.235.11 web-app201641.we42 |
把这个错误的host配置去掉之后,再执行jmx连接终于成功了。
为什么会有错误的hosts配置呢?据说是机器迁移时遗留的。
动态JMX连接的工作原理:
VirtualMachine
动态加载management-agent
com.sun.management.jmxremote.localConnectorAddress
stub
的字符串,实际上是base64转换为byte[],再用ObjectInputStream
转换为RMIServer
排查问题的关键:
Arthas是阿里巴巴开源的Java诊断利器,深受开发者喜爱。
之前分享了Arthas怎样排查 404/401 的问题: http://hengyunabc.github.io/arthas-spring-boot-404-401/
我们可以快速定位一个请求是被哪些Filter
拦截的,或者请求最终是由哪些Servlet
处理的。
但有时,我们想知道一个请求是被哪个Spring MVC Controller处理的。如果翻代码的话,会比较难找,并且不一定准确。
通过Arthas可以精确定位是哪个Controller
处理请求。
还是以这个demo为例: https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-404-401
启动之后,访问: http://localhost:8080/user/1 ,会返回一个user对象。那么这个请求是被哪个Controller
处理的呢?
我们先试下跟踪Servlet
:
1 | trace javax.servlet.Servlet * |
从trace的结果可以看出来,请求最终是被DispatcherServlet#doDispatch()
处理了,但是没有办法知道是哪个Controller
处理。
1 | `---[27.453122ms] org.springframework.web.servlet.DispatcherServlet:doDispatch() |
trace结果里把调用的行号打印出来了,我们可以直接在IDE里查看代码(也可以用jad命令反编译):
1 | // org.springframework.web.servlet.DispatcherServlet |
mappedHandler = getHandler(processedRequest);
得到了处理请求的handler下面用watch
命令来获取getHandler
函数的返回结果。
watch
之后,再次访问 http://localhost:8080/user/1
1 | $ watch org.springframework.web.servlet.DispatcherServlet getHandler returnObj |
可以看到处理请求的handler是 com.example.demo.arthas.user.UserController.findUserById
。
DispatcherServlet
分发,查找到对应的mappedHandler
来处理Arthas
在阿里巴巴内部起源于2015年,当时微服务方兴未艾,我们团队一方面专注Spring Boot落地,提高开发效率,另外一方面,希望可以提高线上排查问题的能力和效率。当时我们经过选型讨论,选择基于Greys
来开发,提供更好的应用诊断体验。
我们在用户体验上做了大量的改进:彩色UI,Web Console,内网一键诊断等。下面是内网一键在线诊断的截图,很多同事线上诊断问题的必备工具:
尽管Arthas
在阿里内部广受好评,但它只是内部工具,很多离职同事都在一些文章里提到。做为Java开发的一员,我们使用到了很多开源的代码和工具,我们也希望可以提升广大开发人员的诊断效率,因此在2018年9月底,我们正式开源了Arthas。
在开源之后,Arthas
多次登顶Github Trending,还被@Java
官方twitter转发:
在开源之后,Arthas
发布了3个release版本,做了大量的改进:
目前,Arthas
Github star数10000+,月下载量7000+。在开源中国2018开源软件排行榜里,Arthas
获得国产新秀榜第一名。
这是对我们过去工作的支持和肯定,也是我们持续改进Arthas
的动力。
Arthas
在开源以来,不断收获到国内外贡献者的支持,目前已有40+贡献者,非常感谢他们的工作:
特别感谢@Hearen
贡献了大部分的英文翻译,@wetsion
重构了新版本的Web Console。
参与贡献: https://github.com/alibaba/arthas/blob/master/CONTRIBUTING.md
做为Arthas的用户,我们在实践中积累了很多经验,总结为一系列的文章,希望对大家线上排查问题有帮助:
bytekit
详细链接: https://github.com/alibaba/arthas/issues/536 ,同时希望大家可以提出建议和参与。
]]>尽管在生产环境热更新代码,并不是很好的行为,很可能导致:热更不规范,同事两行泪。
但很多时候我们的确希望能热更新代码,比如:
线上排查问题,找到修复思路了,但应用重启之后,环境现场就变了,难以复现。怎么验证修复方案?
又比如:
本地开发时,发现某个开源组件有bug,希望修改验证。如果是自己编译开源组件再发布,流程非常的长,还不一定能编译成功。有没有办法快速测试?
Arthas是阿里巴巴开源的Java应用诊断利器,深受开发者喜爱。
下面介绍利用Arthas 3.1.0版本的 jad
/mc
/redefine
一条龙来热更新代码。
下面通过Arthas在线教程演示热更新代码的过程。
在例子里,访问 curl http://localhost/user/0
,会返回500错误:
1 | { |
下面通过热更新代码,修改这个逻辑。
反编译UserController
,保存到 /tmp/UserController.java
文件里。
1 | jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java |
用文本编辑器修改/tmp/UserController.java
,把抛出异常改为正常返回:
1 | "/user/{id}"}) (value={ |
1 | $ sc -d *UserController | grep classLoaderHash |
可以发现是spring boot的 LaunchedURLClassLoader@1be6f5c3
加载的。
保存好/tmp/UserController.java
之后,使用mc(Memory Compiler)命令来编译,并且通过-c
参数指定ClassLoader
:
1 | $ mc -c 1be6f5c3 /tmp/UserController.java -d /tmp |
再使用redefine命令重新加载新编译好的UserController.class
:
1 | $ redefine /tmp/com/example/demo/arthas/user/UserController.class |
再次访问 curl http://localhost/user/0
,会正常返回:
1 | { |
Arthas里 jad
/mc
/redefine
一条龙来线上热更新代码,非常强大,但也很危险,需要做好权限管理。
比如,线上应用启动帐号是 admin,当用户可以切换到admin,那么
所以:
最后,Arthas提醒您: 诊断千万条,规范第一条,热更不规范,同事两行泪。
Arthas
是Alibaba开源的Java诊断工具,深受开发者喜爱。
从Arthas上个版本发布,已经过去两个多月了,Arthas 3.1.0版本不仅带来大家投票出来的新LOGO,还带来强大的新功能和更好的易用性,下面一一介绍。
在新版本Arthas里,增加了在线教程,用户可以在线运行Demo,一步步学习Arthas的各种用法,推荐新手尝试:
非常欢迎大家来完善这些教程。
3.1.0
版本里新增命令mc
,不是方块游戏mc,而是Memory Compiler。
在之前版本里,增加了redefine
命令,可以热更新字节码。但是有个不方便的地方:需要把.class
文件上传到服务器上。
在3.1.0
版本里,结合jad
/mc
/redefine
可以完美实现热更新代码。
以 Arthas在线教程 里的UserController
为例:
使用jad反编译代码
1 | jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java |
使用vim编译代码
当 user id 小于1时,也正常返回,不抛出异常:
1 | "/user/{id}") ( |
使用mc
命令编译修改后的UserController.java
1 | $ mc /tmp/UserController.java -d /tmp |
使用redefine
命令,因为可以热更新代码
1 | $ redefine /tmp/com/example/demo/arthas/user/UserController.class |
在新版本里,改进了很多命令的自动补全,比如 watch/trace/tt/monitor/stack
等。
下面是watch命令的第一个Tab
补全结果,用户可以很方便的一步步补全类名,函数名:
1 | $ watch |
另外,新增加了 jad/sc/sm/redefine
等命令的自动补全支持,多按Tab
有惊喜。
新版本的Web Console切换到了xtermd.js
,更好地支持现代浏览器。
Ctrl + C
复制Arthas支持Docker镜像了
参考: https://alibaba.github.io/arthas/docker.html
之前的版本里,Arthas的重定向是会放到一个~/logs/arthas-cache/
目录里,违反直觉。
在新版本里,重定向和Linux下面的一致,>
/>>
的行为也和Linux下一致。
并且,增加了 cat
/pwd
命令,可以配置使用。
总之,3.1.0
版本的Arthas带了非常多的新功能,改进了很多的用户体验,欢迎大家使用反馈。
Release Note: https://github.com/alibaba/arthas/releases/tag/3.1.0
]]>Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。
Arthas提供了非常丰富的关于调用拦截的命令,比如 trace/watch/monitor/tt 。但是很多时候我们在排查问题时,需要更多的线索,并不只是函数的参数和返回值。
比如在一个spring应用里,想获取到spring context里的其它bean。如果能随意获取到spring bean,那就可以“为所欲为”了。
下面介绍如何利用Arthas获取到spring context。
Demo: https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-arthas-spring-boot
Arthas快速开始:https://alibaba.github.io/arthas/quick-start.html
Demo是一个spring mvc应用,请求会经过一系列的spring bean处理,那么我们可以在spring mvc的类里拦截到一些请求。
启动Demo: mvn spring-boot:run
使用Arthas Attach成功之后,执行tt
命令来记录RequestMappingHandlerAdapter#invokeHandlerMethod
的请求
1 | tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod |
然后访问一个网页: http://localhost:8080/
可以看到Arthas会拦截到这个调用,index是1000,并且打印出:
1 | $ watch com.example.demo.Test * 'params[0]@sss' |
那么怎样获取到spring context?
可以用tt
命令的-i
参数来指定index,并且用-w
参数来执行ognl表达式来获取spring context:
1 | $ tt -i 1000 -w 'target.getApplicationContext()' |
获取到spring context之后,就可以获取到任意的bean了,比如获取到helloWorldService
,并调用getHelloMessage()
函数:
1 | $ tt -i 1000 -w 'target.getApplicationContext().getBean("helloWorldService").getHelloMessage()' |
在很多代码里都有static函数或者static holder类,顺滕摸瓜,可以获取很多其它的对象。比如在Dubbo里通过SpringExtensionFactory
获取spring context:
1 | $ ognl '#context=@com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory@contexts.iterator.next, |
在Java Web/Spring Boot开发时,很常见的问题是:
碰到这种问题时,通常很头痛,特别是在线上环境时。
本文介绍使用Alibaba开源的Java诊断利器Arthas,来快速定位这类Web请求404/401问题。
在进入正题之前,先温习下知识。一个普通的Java Web请求处理流程大概是这样子的:
1 | Request -> Filter1 -> Filter2 ... -> Servlet |
本文的介绍基于一个很简单的Demo:https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-404-401
Demo启动后,访问:http://localhost:8080/a.txt ,返回404:
1 | $ curl http://localhost:8080/a.txt |
我们知道一个HTTP Request,大部分情况下都是由一个Servlet处理的,那么到底是哪个Servlet返回了404?
我们使用Arthas的trace
命令来定位:
1 | $ trace javax.servlet.Servlet * |
然后访问 http://localhost:8080/a.txt ,Arthas会打印出整个请求树,完整的输出太长,这里只截取关键的一输出:
1 | +---[13.087083ms] org.springframework.web.servlet.DispatcherServlet:resolveViewName() |
可以看出请求经过Spring MVC的DispatcherServlet
处理,最终由ViewResolver
分派给FreeMarkerView$GenericServletAdapter
处理。所以我们可以知道这个请求最终是被FreeMarker
处理的。
后面再排查FreeMarker
的配置就可以了。
这个神奇的trace javax.servlet.Servlet *
到底是怎样工作的呢?
实际上Arthas会匹配到JVM里所有实现了javax.servlet.Servlet
的类,然后trace
它们的所有函数,所以HTTP请求会被打印出来。
这里留一个小问题,为什么只访问了
http://localhost:8080/a.txt
,但Arthas的trace
命令打印出了两个请求树?
在Demo里,访问 http://localhost:8080/admin ,会返回401,即没有权限。那么是哪个Filter拦截了请求?
1 | $ curl http://localhost:8080/admin |
我们还是使用Arthas的trace
命令来定位,不过这次trace
的是javax.servlet.Filter
:
1 | $ trace javax.servlet.Filter * |
再次访问admin,在Arthas里,把整个请求经过哪些Filter处理,都打印为树。这里截取关键部分:
1 | +---[0.704625ms] org.springframework.web.filter.OncePerRequestFilter:doFilterInternal() |
可以看到HTTP Request最终是被com.example.demo404401.AdminFilterConfig$AdminFilter
处理的。
打个广告,Arthas正在征集使用者,您的使用是对我们最大的支持。
如果Arthas对您排查问题有帮助,请到这个Issue登记下,并在30分钟内成为Arthas Contributor:
https://book.douban.com/subject/27204181/
赋能,这个词经常听到,但不知道是啥意思。看了这本书,算是理解到了。
这本书讲的是在美国特种部队,在伊拉克应对各种“不确定性”事件,不得不把信息公开透明化,加强互动,从而提高应对速度和能力,打造应对不确定性的敏捷团队的故事。
在公司里,很多时候都是各种小团体,软件开发也是各种小群讨论。同一个大部门里,不同的组件都要互相打架。
感觉大部分人并没有意识到事情要公开,信息要流动,事情才能做好。也许是KPI导向的结果,也许是缺少共同目标,每个人都管自己的一亩三分地。
我个人感受是,尽量获取不同部门,不同组件的信息,把解决问题的过程记录下来,并公开化。
https://book.douban.com/subject/1870268/
这本书很早就知道了,但没有动机去看。真正起了兴趣去看,是因为看到一位美国留学生的Blog,里面提到了各种街区。所以去看了这本书。
感觉中国的城市设计者们受这本书的影响很深,城市基本都是混合功能的,并没有像美国那样子整齐的街区划分。之前还有传言说要把小区的墙都拆掉。
这本书是1961年出版的,所以我很好奇这本书到底在美国产生了怎样的影响。后面在《规模》里找到了一些答案。
这本书只看了三分之二,因为翻译真的不好。
也许中国人不能真正理解美国的街区,正如美国人不能理解中国人的户口。
https://book.douban.com/subject/30244461/
推荐。这本书是同事推荐的,里面谈的都是事物上了规模之后的一些统计学规律。看了可能有点沮丧,人逃脱不了这些规律。
不过看了这本书,明白了一个事情:城市化是不可逆转的,超大规模的城市群会越来越多。
书里提到很多《美国大城市的死与生》给美国人的影响。读书有乐趣的一点就是,两本书的互相印证。
https://book.douban.com/subject/6436046/
推荐。这本书说的是新教怎么影响资本主义的。翻译的版本里有一些很不错的配图,导致我一开始以为是80/90年代的书,但看到中间时,感觉不太对,去查了下,发现是1905年出版的,十分惊讶。
无论这本书是否是给资本主义正名,但里面提到的一点比较有意思:新教的禁欲主义使得新教徒很勤奋,所以他们资本财富不断增加。但财富增长到一定程度之后,这种宗教的精神慢慢泯灭了。这点和现在的中国很像,70/80还比较能体会到艰苦奋斗,90/00对艰苦奋斗这种概念已经很淡薄了。日本也曾经如此。
还有一点,现代人可能不太能理解为什么当时的人对资本主义深恶痛绝。在《众病之王:癌症传》里提到有一种消失了的癌症:阴囊癌。18世纪的很多童工因为身体接触到这些煤烟而患上阴囊癌。一开始是立法禁止8岁以下儿童,后面才逐步完全禁止。
有关联的一本书:《非理性的人》,在看中,推荐。
https://book.douban.com/subject/27167992/
推荐。这本书里学到了一个观点:让用户充值不重要,重要的是要让用户花出去。
https://book.douban.com/subject/26365250/
一般,武侠小说,只看了不到一半,太啰嗦了。
https://book.douban.com/subject/2159124/
很有时代特征,可以看到很多细节。但是都没有写到位。军队的腐败也只能带一下。现在的人对越战有认知的很少,希望未来可以有对越战争的电影。
https://book.douban.com/subject/10434287/
对小白来说,比较有意思。学到了点通货膨胀,钱币贬值的知识,不过现在已忘记了,哈哈。
https://book.douban.com/subject/26295448/
因为《降临》电影不知所云,找的原著来看。书是由多个小故事组成的,只看了两个故事:《你一生的故事》和《巴比伦塔》。太多宗教色彩了,提不起兴趣再看。
https://book.douban.com/subject/24749465/
囤积物品是人进化的天性,丢掉旧东西,也许是现代人的一种解脱方式。今年搬了一次家,把一些两三年都没用过的东西丢了,感觉有点爽。
还有一个关联的日剧《我的家里空无一物》,看了一两集,没追。
https://book.douban.com/subject/1054685/
王小波的杂文集。王小波的小说一直没有看进去,偶然看到这本杂文还挺有意思的。
https://book.douban.com/subject/26301856/
因为同名日剧去看的,日剧本身没看,直接看的小说,还行。
https://book.douban.com/subject/26954748/
了解一些“知识分子”的历史,书里都是当事人的访谈。“知识分子”这个词,对于年青人是比较陌生的,没有经历过的人,可能真的没办法理解。
https://book.douban.com/subject/25728591/
日本人写的,怎样读马克思的书。感觉有点神奇。通过两个作者的书信来往,想讨论马克思是怎样思考的,马克思的方法论。
金庸去世,把《笑傲江湖》和《倚天屠龙记》重新看了一遍。 为什么这两本呢?因为小学的时候,从角落里翻到《笑傲江湖》的第二本,《倚天屠龙记》的第一本。当时看完了又找不到后续,只能翻来覆去的看了好几遍。直到上高中,从图书馆借书才看了完本。
《笑傲江湖》有个点很有意思,岳不群和左冷禅争五岳盟主时,岳不群说:“只须方针一变,天下同道协力以赴,期之以五十年、一百年,决无不成之理。”金庸在后记里专门说明小说是早年写的,并非影射。不管真相是怎样,看到还是觉得挻搞笑的。
2018年把今日头条卸载了,因为推的内容越来越单一,越来越八卦。发现这些软件都没有重置的功能,应该允许用户重新选择感兴趣的内容,重新开始推荐。大数据时代,每个人都只能看到自己圈子里的,越来越难看到另外领域的信息了。
五星推荐:
Arthas是Alibaba开源的应用诊断利器,9月份开源以来,Github Star数三个月超过6000。
当Dubbo遇上Arthas,会碰撞出什么样的火花呢?下面来分享Arthas排查Dubbo问题的一些经验。
下面的排查分享基于这个dubbo-arthas-demo
,非常简单的一个应用,浏览器请求从Spring MVC到Dubbo Client,再发送到Dubbo Server。
Demo里有两个spring boot应用,可以先启动server-demo
,再启动client-demo
。
1 | /user/{id} -> UserService -> UserServiceImpl |
Client端:
1 |
|
Server端:
1 | "1.0.0") (version = |
1 | $ wget https://alibaba.github.io/arthas/arthas-boot.jar |
启动后,会列出所有的java进程,选择1,然后回车,就会连接上ServerDemoApplication
1 | $ java -jar arthas-boot.jar |
当线上服务抛出异常时,最着急的是什么参数导致了抛异常?
在demo里,访问 http://localhost:8080/user/0 ,UserServiceImpl
就会抛出一个异常,因为user id不合法。
在Arthas里执行 watch com.example.UserService * -e -x 2 '{params,throwExp}'
,然后再次访问,就可以看到watch命令把参数和异常都打印出来了。
1 | $ watch com.example.UserService * -e -x 2 '{params,throwExp}' |
在本地开发时,可能会用到热部署工具,直接改代码,不需要重启应用。但是在线上环境,有没有办法直接动态调试代码?比如增加日志。
在Arthas里,可以通过redefine
命令来达到线上不重启,动态更新代码的效果。
比如我们修改下UserServiceImpl
,用System.out
打印出具体的User
对象来:
1 | public User findUser(int id) { |
本地编绎后,把server-demo/target/classes/com/example/UserServiceImpl.class
传到线上服务器,然后用redefine
命令来更新代码:
1 | $ redefine -p /tmp/UserServiceImpl.class |
这样子更新成功之后,访问 http://localhost:8080/user/1 ,在ServerDemoApplication
的控制台里就可以看到打印出了user信息。
在排查问题时,需要查看到更多的信息,如果可以把logger级别修改为DEBUG
,就非常有帮助。
ognl
是apache开源的一个轻量级表达式引擎。下面通过Arthas里的ognl
命令来动态修改logger级别。
首先获取Dubbo里TraceFilter
的一个logger对象,看下它的实现类,可以发现是log4j。
1 | $ ognl '@com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter@logger.logger' |
再用sc
命令来查看具体从哪个jar包里加载的:
1 | $ sc -d org.apache.log4j.Logger |
可以看到log4j是通过slf4j代理的。
那么通过org.slf4j.LoggerFactory
获取root
logger,再修改它的level:
1 | $ ognl '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)' |
可以看到修改之后,root
logger的level变为DEBUG
。
在平时开发时,可能需要测试小姐姐发请求过来联调,但是我们在debug时,可能不小心直接跳过去了。这样子就尴尬了,需要测试小姐姐再发请求过来。
Arthas里提供了tt
命令,可以减少这种麻烦,可以直接重放请求。
1 | $ tt -t com.example.UserServiceImpl findUser |
上面的tt -t
命令捕获到了3个请求。然后通过tt --play
可以重放请求:
1 | $ tt --play -i 1000 |
Dubbo运行时会加载很多的Filter,那么一个请求会经过哪些Filter处理,Filter里的耗时又是多少呢?
通过Arthas的trace
命令,可以很方便地知道Filter的信息,可以看到详细的调用栈和耗时。
1 | $ trace com.alibaba.dubbo.rpc.Filter * |
通过Arthas的jad
命令,可以看到Dubbo通过javaassist动态生成的Wrappr类的代码:
1 | $ jad com.alibaba.dubbo.common.bytecode.Wrapper1 |
除了上面介绍的一些排查技巧,下面分享一个获取Spring Context,然后“为所欲为”的例子。
在Dubbo里有一个扩展com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory
,把Spring Context保存到了里面。
因此,我们可以通过ognl
命令获取到。
1 | $ ognl '#context=@com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory@contexts.iterator.next, #context.getBean("userServiceImpl").findUser(1)' |
获取到
SpringExtensionFactory`里保存的spring context对象#context.getBean("userServiceImpl").findUser(1)
获取到userServiceImpl
再执行一次调用只要充分发挥想像力,组合Arthas里的各种命令,可以发挥出神奇的效果。
本篇文章来自杭州Dubbo Meetup的分享《当DUBBO遇上Arthas - 排查问题的实践》,希望对大家线上排查Dubbo问题有帮助。
分享的PDF可以在 https://github.com/alibaba/arthas/issues/327 里下载。
]]>在Arthas 3.0.5版本里,我们在用户体验方面做了很多改进,下面逐一介绍。
arthas-boot
是新增加的支持全平台的启动器,Windows/Mac/Linux下使用体验一致。下载后,直接java -jar
命令启动:
1 | wget https://alibaba.github.io/arthas/arthas-boot.jar |
arthas-boot
的功能比以前的as.sh
更强大。
比如下载速度比较慢,可以指定阿里云的镜像。
1 | java -jar arthas-boot.jar --repo-mirror aliyun --use-http |
比如可以通过session-timeout
指定超时时间:
1 | java -jar arthas-boot.jar --session-timeout 3600 |
更多的功能可以通过java -jar arthas-boot.jar -h
查看
arthas-boot
在attach成功之后,会启动一个java telent client去连接Arthas Server,用户没有安装telnet的情况下也可以正常使用。
在新版本里,默认会从arthas-boot.jar
和as.sh
所在的目录下查找arthas home,这样子用户全量安装之后,不需要再从远程下载Arthas。
在之前的版本里,用户困扰最多的是,明明选择了进程A,但是实际连接到的却是进程B。
原因是之前attach了进程B,没有执行shutdown
,下次再执行时,还是连接到进程B。
在新版本里,做了改进:
1 | $ java -jar arthas-boot.jar |
新版本对键盘Up/Down
有了更好的匹配机制,试用有惊喜:)
比如执行了多次trace,但是在命令行输入 trace后,想不起来之前trace的具体类名,那么按Up
,可以很轻松地匹配到之前的历史命令,不需要辛苦翻页。
新版本增加了history
命令
改进对Windows的字体支持
之前Windows下面使用的是非等宽字体,看起来很难受。新版本里统一为等宽字体。
sysenv命令和sysprop类似,可以打印JVM的环境变量。
ognl命令提供了单独执行ognl脚本的功能。可以很方便调用各种代码。
比如执行多行表达式,赋值给临时变量,返回一个List:
1 | $ ognl '#value1=@System@getProperty("java.home"), #value2=@System@getProperty("java.runtime.name"), {#value1, #value2}' |
之前watch命令只支持打印入参返回值等,新版本同时打印出调用耗时,可以很方便定位性能瓶颈。
1 | $ watch demo.MathGame primeFactors 'params[0]' |
之前的版本里,在搜索lambda类时,或者反编绎lambda类有可能会失败。新版本做了修复。比如
1 | $ jad Test$$Lambda$1/1406718218 |
改进了很多命令的tab
自动补全功能,有停顿时,可以多按tab
尝试下。
详细的Release Note:https://github.com/alibaba/arthas/releases/tag/arthas-all-3.0.5
]]>随着应用越来越复杂,依赖越来越多,日志系统越来越混乱,有时会出现一些奇怪的日志,比如:
1 | [] [] [] No credential found |
那么怎样排查这些奇怪的日志从哪里打印出来的呢?因为搞不清楚是什么logger打印出来的,所以想定位就比较头疼。
下面介绍用Alibaba开源的应用诊断利器Arthas的redefine命令快速定位奇怪日志来源。
首先在java代码里,字符串拼接基本都是通过StringBuilder
来实现的。比如下面的代码:
1 | public static String hello(String world) { |
实际上生成的字节码也是用StringBuilder
来拼接的:
1 | public static java.lang.String hello(java.lang.String); |
在java的logger系统里,输出日志时通常也是StringBuilder
来实现的,最终会调用StringBuilder.toString()
,那么我们可以修改StringBuilder
的代码来检测到日志来源。
StringBuilder.toString()
的原生实现是:
1 |
|
修改为:
1 |
|
增加的逻辑是:当String里包含No credential found
时打印出当前栈,这样子就可以定位日志输出来源了。
其实很简单,在IDE里把StringBuilder
的代码复制一份,然后贴到任意一个工程里,然后编绎即可。
也可以直接用javac来编绎:
1 | javac StringBuilder.java |
启动应用后,在奇怪日志输出之前,先使用arthas attach应用,再redefine StringBuilder:
1 | $ redefine -p /tmp/StringBuilder.class |
当执行到输出[] [] [] No credential found
的logger代码时,会打印当前栈。实际运行结果是:
1 | [] [] [] No credential found |
可以看到是spas.sdk
打印出了[] [] [] No credential found
的日志。
在应用的 service_stdout.log
里一直输出下面的日志,直接把磁盘打满了:
1 | 23:07:34.441 [TAIRCLIENT-1-thread-1] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 14 times in a row. |
service_stdout.log
是进程标准输出的重定向,可以初步判定是tair插件把日志输出到了stdout里。
尽管有了初步的判断,但是具体logger为什么会打到stdout里,还需要进一步排查,常见的方法可能是本地debug。
下面介绍利用arthas直接在线上定位问题的过程,主要使用sc
和getstatic
命令。
日志是io.netty.channel.nio.NioEventLoop
输出的,到netty的代码里查看,发现是DEBUG级别的输出:
然后用arthas的sc
命令来查看具体的io.netty.channel.nio.NioEventLoop
是从哪里加载的。
1 | class-info io.netty.channel.nio.NioEventLoop |
可见,的确是从tair插件里加载的。
查看NioEventLoop的代码,可以发现它有一个logger
的field:
1 | public final class NioEventLoop extends SingleThreadEventLoop { |
使用arthas的getstatic
命令来查看这个logger
具体实现类是什么(使用-c
参数指定classloader):
1 | $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'getClass().getName()' |
可以发现是Slf4JLogger
。
再查看io.netty.util.internal.logging.Slf4JLogger的实现,发现它内部有一个logger的field:
1 | package io.netty.util.internal.logging; |
那么使用arthas的getstatic
命令来查看这个logger
属性的值:
1 | $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger' |
可见,logger的最本质实现类是:ch.qos.logback.classic.Logger
。
再次用getstatic
命令来确定jar包的location:
1 | $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger.getClass().getProtectionDomain().getCodeSource().getLocation()' |
可见这个ch.qos.logback.classic.Logger
的确是tair插件里加载的。
上面已经定位logger的实现类是ch.qos.logback.classic.Logger
,但是为什么它会输出DEBUG
level的日志?
其实在上面的getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger'
输出里,已经打印出它的level是null了。如果对logger有所了解的话,可以知道当child logger的level为null时,它的level取决于parent logger的level。
我们再来看下ch.qos.logback.classic.Logger
的代码,它有一个parent logger的属性:
1 | public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable { |
所以,我们可以通过getstatic
来获取到这个parent属性的内容。然后通过多个parent操作,可以发现level都是null,最终发现ROOT level是DEBUG 。
1 | $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger.parent.parent.parent.parent.parent' |
所以 io.netty.channel.nio.NioEventLoop
的logger的level取的是ROOT logger的配置,即默认值DEBUG
。
具体实现可以查看ch.qos.logback.classic.LoggerContext
1 | public LoggerContext() { |
上面我们得到结论
那么我们可以猜测:
那么实现上tair里是使用了logger-api
,它通过LoggerFactory.getLogger
函数获取到了自己package的logger,然后设置level为INFO
,并设置了appender。
换而言之,tair插件里的logback没有设置ROOT logger,所以它的默认level是DEBUG,并且默认的appender会输出到stdout里。
所以tair插件可以增加设置ROOT logger level为INFO
来修复这个问题。
1 | private static com.taobao.middleware.logger.Logger logger |
DEBUG
,输出是stdoutsc
命令定位具体的类getstatic
获取static filed的值有时spring boot应用会遇到java.lang.NoSuchMethodError
的问题,下面以具体的demo来说明怎样利用arthas来排查。
Demo: https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-NoSuchMethodError
很多时候当应用抛出异常后,进程退出了,就比较难排查问题。可以先改下main函数,把异常catch住:
1 | public static void main(String[] args) throws IOException { |
Demo启动之后,抛出的异常是:
1 | java.lang.NoSuchMethodError: org.springframework.core.annotation.AnnotationAwareOrderComparator.sort(Ljava/util/List;)V |
显然,异常的意思是AnnotationAwareOrderComparator
缺少sort(Ljava/util/List;)V
这个函数。
参考:https://alibaba.github.io/arthas/install-detail.html
应用需要抛出了异常,但是进程还没有退出,我们用arthas来attach上去。比如在mac下面:
1 | ./as.sh |
然后选择com.example.demoNoSuchMethodError.DemoNoSuchMethodErrorApplication
进程。
再执行sc命令来查找类:
1 | $ sc -d org.springframework.core.annotation.AnnotationAwareOrderComparator |
可以看到AnnotationAwareOrderComparator
是从spring-2.5.6.SEC03.jar
里加载的。
下面使用jad命令来查看AnnotationAwareOrderComparator
的源代码
1 | $ jad org.springframework.core.annotation.AnnotationAwareOrderComparator |
可见,AnnotationAwareOrderComparator
的确没有sort(Ljava/util/List;)V
函数。
从上面的排查里,可以确定
AnnotationAwareOrderComparator
来自spring-2.5.6.SEC03.jar
,的确没有sort(Ljava/util/List;)V
函数。所以,可以检查maven依赖,把spring 2的jar包排掉,这样子就可以解决问题了。
NoSuchMethodError
的异常信息,了解是什么类缺少了什么函数当用户在自己的spring boot main class上面显式使用了@EnableWebMvc
,发现原来的放在 src/main/resources/static
目录下面的静态资源访问不到了。
1 |
|
比如在用户代码目录src/main/resources
里有一个hello.txt
的资源。访问 http://localhost:8080/hello.txt
返回的结果是404:
1 | Whitelabel Error Page |
@EnableWebMvc
的实现那么为什么用户显式配置了@EnableWebMvc
,spring boot访问静态资源会失败?
我们先来看下@EnableWebMvc
的实现:
1 | .class) (DelegatingWebMvcConfiguration |
1 | /** |
可以看到@EnableWebMvc
引入了 WebMvcConfigurationSupport
,是spring mvc 3.1里引入的一个自动初始化配置的@Configuration
类。
再来看下spring boot里是怎么实现对src/main/resources/static
这些目录的支持。
主要是通过org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
来实现的。
1 |
|
可以看到 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
,这个条件导致spring boot的WebMvcAutoConfiguration
不生效。
总结下具体的原因:
@EnableWebMvc
@Import
,把这些@Import
引入的bean信息都缓存起来@EnableWebMvc
时,通过@Import
加入了 DelegatingWebMvcConfiguration
,也就是WebMvcConfigurationSupport
@Conditional
相关的注解,判断发现已有WebMvcConfigurationSupport
,就跳过了spring bootr的WebMvcAutoConfiguration
所以spring boot自己的静态资源配置不生效。
其实在spring boot的文档里也有提到这点: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration
在spring boot里静态资源目录的配置是在ResourceProperties
里。
1 | "spring.resources", ignoreUnknownFields = false) (prefix = |
然后在 WebMvcAutoConfigurationAdapter
里会初始始化相关的ResourceHandler。
1 | //org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter |
用户可以自己修改这个默认的静态资源目录,但是不建议,因为很容易引出奇怪的404问题。
]]>