查看原文
其他

一览Spring Bean 定义和映射全貌的神器

侯树成 Tomcat那些事儿 2021-03-14


用 Spring 进行开发的时候,你有没有操心过这些事儿:

  1. 声明的 Bean 到底有没有被 IOC 容器识别出来并管理起来?

  2. 信心满满的加了个 @Componentscan,发现包下的 Bean 并没有被扫描出来。 

  3. 写了一堆的 @RequestMapping,在Web 容器启动后却死活请求不成功,各种404 和 whitelabel error page 。


这种问题,一定是配置的原因。你我咬牙切齿的心想。此时,除了 Debug 进 Spring 的源码,看看各种 Bean的创建,HandlerMapping的声明外来确认你自己的想法外,还有啥办法?


但跟进Spring 的源码中毕竟也是比较耗时费力的。如果能有一个工具,看看现在加载了哪些Bean,哪些RequestMapping都和什么请求路径做了 mapping,这样起码我们也能心中有数,排除是请求错误等问题。



你还别说,真有这样一个工具。在 Spring Boot 里使用更是方便,直接添加个 Starter 就行。

当然,对于 Spring Boot 1.x 和 Spring Boot 2.x 多少有些区别, 在2.x里需要再加个小配置,把这些内容的开关打开。


它是谁?


这个工具就是 Spring Boot 中的监控诊断工具 Actuator

通过 Actuator,我们可以监控我们的应用,获取环境信息,对数据库、Redis等应用中使用到的项进行健康检查。除此之外就是我们前面提到的 Bean 定义, Mapping 以及 Metrics、Dump 等等。


而使用Actuator ,也很简单,直接添加其依赖 starter即可。 对外可以暴露HTTP 或 JMX 的接口,通过这些我们来进行交互。


不过,随着 Spring Boot 的升级, 在2.x的时候,默认是看不到 Bean 和 Mapping这些信息的,需要增加配置打开。


配置


注意:如果在生产环境中使用,请注意额外增加访问权限的校验,以免信息泄漏。


下面的配置以 Spring Boot 2.x为例。 在 pom.xml 中增加依赖


<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-actuator</artifactId>

</dependency>


默认的输出以下内容:

{"_links":{"self":{"href":"http://localhost:8090/actuator","templated":false},"health":{"href":"http://localhost:8090/actuator/health","templated":false},"health-component":{"href":"http://localhost:8090/actuator/health/{component}","templated":true},"health-component-instance":{"href":"http://localhost:8090/actuator/health/{component}/{instance}","templated":true},"info":{"href":"http://localhost:8090/actuator/info","templated":false}}}


在项目的application.properties 或yaml文件中增加配置


management.endpoints.web.exposure.include=*


启动的时候 ,控制台会有这样一句输出:

Exposing 15 endpoint(s) beneath base path '/actuator'


Beans


此时在应用启动后,就可以在浏览器里直接观察了。我看现在可以接收的请求多了不少

其中包含咱们感兴趣的beans和mapping。

所以,想查看 bean 信息,发这样的请求就可以:

http://localhost:8090/actuator/beans

 

此时浏览器里会有大量的Bean信息的输出,看冰山的一角:


我们一般关心自己写的代码,这个时候就直接关键词搜索就OK。

我们这里假设有一个名为 HelloController 的 Controller 类,AutoWired了一个名为 TestService的类。

此时在输出的信息中,可以查到这样的内容:


"helloController": {

                    "aliases": [],

                    "scope": "singleton",

                    "type": "com.example.hello.HelloController",

     "resource": "file [C:\\Users\\Test\\com\\example\\hello\\HelloController.class]",

                    "dependencies": [

                        "testService"

                    ]

                }

                "testService": {

                    "aliases": [],

                    "scope": "singleton",

                    "type": "com.example.hello.TestService",

                    "resource": "file [C:\\Users\\Test\\com\\example\\\\hello\\TestService.class]",

                    "dependencies": []

                }


我们看到一个 Bean 依赖了哪些,对应加载的资源路径都一清二楚的。


Mappings


再来看 mappings,请求路径是将上面的beans 换成 mappings。

同样也有大量的输出,我们来看下上面提到的HelloController,看看这里的输出是什么样的

{

  "handler": "public java.lang.String com.example.hello.HelloController.hello()",

                            "predicate": "{ /hello}",

                            "details": {

                                "handlerMethod": {

                                    "className": "com.example.hello.HelloController",

                                    "name": "hello",

                                    "descriptor": "()Ljava/lang/String;"

                                },

                                "requestMappingConditions": {

                                    "consumes": [],

                                    "headers": [],

                                    "methods": [],

                                    "params": [],

                                    "patterns": [

                                        "/hello"

                                    ],

                                    "produces": []

                                }

                            }

  }


我们看到,整个 Controller 的 path 映射到具体哪个方法上,方法签名是啥,返回什么,也很明显。


用 Actuator 的好处是,我们只添加了依赖,并没做什么事情,就可以对应用进行监控、分析,来了解内部情况。 比如将上面的mappings再换成 env,换成 metrics,甚至还能dump heap 和 thread。


原理


了解 Spring 的朋友应该都知道,所谓的容器,是在启动的时候将 Bean的定义「注册」到 Container 或者说叫 Factory 中。这里显示 mappings,还是 beans,都再从容器里拿。这一部分我们不多看。

我在看 Actuator 的时候,很好奇这个HTTP 的接口是如何实现的。因为我们单独的一个应用,它不可能也写一个Controller和我们打包时整合到一起。难道是单独写了一个应用吗?


带着这个疑问,看了看这一部分的代码。下面的内容主要来梳理一下暴露HTTP 服务这一部分内容。


既然在 Spring 的生态里,又涉及到 HTTP,我们就从 Spring MVC入手 看看 Actuator怎样实现的。


一般入口的 DispatcherServlet,会判断当前请求对应的 Handler来进行处理。

这里查找 Handler的过程,是从各个 HandlerMapping里查找

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {

if (this.handlerMappings != null) {

for (HandlerMapping mapping : this.handlerMappings) {

HandlerExecutionChain handler = mapping.getHandler(request);

if (handler != null) {

return handler;

}

}

}

return null;

}


这里的handlerMappings里包含一个了一个名为WebMvcEndpointHandlerMapping的类,其中注册了一系列启动时扫描到的「Endpoint」。


这是BeansEndpoint的声明:


启动时的扫描也是基于这个@Endpoint注解进行的。

这个HandlerMapping 的 Bean 在创建Bean的阶段,会执行一个 初始化HandlerMethod的方法


protected void initHandlerMethods() {

for (ExposableWebEndpoint endpoint : this.endpoints) {

for (WebOperation operation : endpoint.getOperations()) {

registerMappingForOperation(endpoint, operation);

}

}

if (StringUtils.hasText(this.endpointMapping.getPath())) {

registerLinksMapping();

}

}


这里再向下其实注册的都是一堆ServletWebOperation, 这些Operation对应的处理方法,都是在代理类OperationHandler里调用operation完成的。

接着再把这个 HandlerMethod注册一下。统一都注册到一个mappingRegistry里。


那在后面请求到达的时候,getHandler的阶段,还是从这个MethodMapping 对应的 mappingRegistry里去查,就找到了对应的 Handler了。


Handler最终就这样真正的mapping到我们前面提到的 Endpoint上


public Object handle(HttpServletRequest request,

@RequestBody(required = false) Map<String, String> body) {

Map<String, Object> arguments = getArguments(request, body);

try {

return handleResult(

this.operation.invoke(new InvocationContext(

new ServletSecurityContext(request), arguments)),

HttpMethod.valueOf(request.getMethod()));

}

catch (InvalidEndpointRequestException ex) {

throw new BadOperationRequestException(ex.getReason());

}

}


再之后就是反射调用 Endpoint里对应的方法,像下面这个Beans对应的方法。

@ReadOperation

public ApplicationBeans beans() {

Map<String, ContextBeans> contexts = new HashMap<>();

ConfigurableApplicationContext context = this.context;

while (context != null) {

contexts.put(context.getId(), ContextBeans.describing(context));

context = getConfigurableParent(context);

}

return new ApplicationBeans(contexts);

}



总结一下,为什么 Actuator 这些 endpoint 可以在我们应用之外也接收HTTP请求,我就相当于我们自己写了一个 Servlet,在里面判断各种不同的请求 path, 根据不同的 path,执行不同的方法罢了。至于输出beans, mappings 这一类的信息,就相当于我们你到一个统计信息「注册处」的地方拿一个列表,人家那里本来就记着,所以能直接告诉你。


你有什么好用的开发小工具或小技艺吗?欢迎留言分享。



相关阅读(点击下方图片查看)

                    (SpringBoot DevTools 是怎样完成应用热部署的)

(你真的会高效的在GitHub搜索开源项目吗?)

(怎样才能Java Champion)


(如何开发自己的Spring Boot Starter)

(Tomcat是如何处理SpringBoot应用的?)


如果你喜欢本文

请长按二维码关注

转发朋友圈,是对我最大的支持。


更多精彩内容:

一台机器上安装多个Tomcat 的原理(回复001)

监控Tomcat中的各种数据 (回复002)

启动Tomcat的安全机制(回复003)

乱码问题的原理及解决方式(回复007)

Tomcat 日志工作原理及配置(回复011)

web.xml 解析实现(回复 012)

线程池的原理( 回复 014)

Tomcat 的集群搭建原理与实现 (回复 015)

类加载器的原理 (回复 016)

类找不到等问题 (回复 017)

代码的热替换实现(回复 018)

Tomcat 进程自动退出问题 (回复 019)

为什么总是返回404? (回复 020)

...


PS: 对于一些 Tomcat常见问题,在公众号的【常见问题】菜单中,有需要的朋友欢迎关注查看

                                                                  如有帮助请点好看

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存