题图: 北京海洋馆里的水母
在以前的文章里,曾经介绍过一种在嵌入式的 Tomcat 里部署其他应用的方式(如何给Spring Boot 的嵌入式 Tomcat 部署多个应用?)。通过这种方式,我们可以把 Psi-Probe 这一类管理、监控应用部署到 Spring Boot 应用的嵌入容器中,这样可以更清晰的了解容器的内部资源使用、应用的请求处理等情况。当然也可以将 Spring Boot 应用打包成 WAR 文件的形式,和传统应用一样,在一个容器内部署多类应用来进行监控。
也写过一篇通过 Actuator 来了解 应用内Bean 的引用关系、各类Mapping信息的文章(一览Spring Bean 定义和映射全貌的神器)。
但是,第一种形式在 Spring Boot 的 「Starter」大行其道的今天,使用方式上并不一致。
这次我们介绍一款工具,结合了上面两部分的内容,也不需要像自己添加嵌入容器的部署那样,单独进行开发,而是在 maven 里添加 对应的依赖,在 Spring Boot 应用的 application 配置文件中增加部分配置就可以实现。使用习惯上和 Spring Boot的一贯形式一样,开箱之后,稍加配置就可以使用。
这就是我们今天要介绍的主角:Spring Boot Admin, 官方文档里又简称为 SBA。
上手也比较简单,在我们的 Spring Boot 应用中,增加 SBA 的依赖。
说明:这里有一点需要注意的是, SBA的版本一定要和 Spring Boot 的版本匹配。比如 Spring Boot 2.0.x 对应的 SBA 也是 2.0.x 如果是2.1.x,那大家也都用2.1.x。
由于 Spring Boot Admin 又分为 Server 和 Client 两个部分,所以配置这一部分我们分开来说。
Server 会做为响应 Client 信息注册以及最终各类应用信息服务信息查看的应用入口,和一个独立Web服务一样。配置的时候 增加依赖如下:
de.codecentric
spring-boot-admin-starter-server
2.1.3
之后在 Spring Boot 的应用主类中增加注解@EnableAdminServer
,然后正常启动。之后在浏览器里请求就能看到Admin的管理界面。
这个时候没有对应的节点,所以信息都是空的。
接着配置一个 Client ,注册过来就能看到信息。
增加依赖
de.codecentric
spring-boot-admin-starter-client
2.1.3
配置文件里,需要指定要注册到的 Server是谁,同时为了不同应用之间的区分,起个名字。最后再把actuator的信息开关打开。
spring.application.name=Spring Boot Client
spring.boot.admin.client.url=http://localhost:8000
management.endpoints.web.exposure.include=*
如果要同时查看Actuator
提供的信息,把 actuator 的依赖也一并加上。 启动之。
然后我们就能看到 SBA Server 里注册的实例信息。
点进去可以查看对应实例的信息。
一般看到一个有意思的开源项目,我会看看其中部分特性的实现。 这次关于 SBA 我们先来说下 Server 的启动原理。
首先做为一个Starter,和其他的都类似,也是从一个AutoConfiguration
做为入口的 然后,会加载其他的配置文件。
这是 Server 的 AutoConfiguration,内容如下:
@Configuration
@ConditionalOnBean(AdminServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties(AdminServerProperties.class)
@Import({AdminServerWebConfiguration.class})
这里有一个AdminServerWebConfiguration。 我们看看里面定义了什么。
@Bean
@ConditionalOnMissingBean
public InstancesController instancesController(InstanceRegistry instanceRegistry, InstanceEventStore eventStore) {
return new InstancesController(instanceRegistry, eventStore);
}
@Bean
@ConditionalOnMissingBean
public ApplicationsController applicationsController(InstanceRegistry instanceRegistry,
InstanceEventPublisher eventPublisher) {
return new ApplicationsController(instanceRegistry, eventPublisher);
}
我们看到,这里声明了两个Controller 的Bean。不过别急,这里作用虽和 SpringMVC 的类似,不过声明和创建略有不同,我们在开发项目时,也可以参考学习一下。
接着在该类向下看,你会发现有个HandlerMapping的处理Bean
@Bean
public org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping adminHandlerMapping(
ContentNegotiationManager contentNegotiationManager) {
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping mapping = new de.codecentric.boot.admin.server.web.servlet.AdminControllerHandlerMapping(
adminServerProperties.getContextPath());
mapping.setOrder(0);
mapping.setContentNegotiationManager(contentNegotiationManager);
return mapping;
}
具体内容在 AdminControllerHandlerMapping
类里。比较关键的一句话在这
@Override
protected boolean isHandler(Class> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, AdminController.class);
}
也就是包含 AdminController
这个注解的,会被识别为 Handler。 我们再看上面注册的那个ApplicationsController
,果然也是。
@AdminController
@ResponseBody
public class ApplicationsController {
...
@GetMapping(path = "/applications", produces = MediaType.APPLICATION_JSON_VALUE)
public Flux applications() {
return registry.getInstances()
.filter(Instance::isRegistered)
.groupBy(instance -> instance.getRegistration().getName())
.flatMap(grouped -> toApplication(grouped.key(), grouped));
}
}
我们一般是通过注解@Controller 或者 @RestController 来声明一个 Controller 的,这里的「@AdminController」并不是 Spring 内置的注解,但也依然能做为 Handler生效,上面就是一个原因。 另一个是识别出是个 Handler 之后,把对应方法注册到 Handler 列表里。
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
super.registerHandlerMethod(handler, method, withPrefix(mapping));
}
完整点的识别注册路径如下:
/**
* Determine the type of the specified candidate bean and call
* {@link #detectHandlerMethods} if identified as a handler type.
* This implementation avoids bean creation through checking
* {@link org.springframework.beans.factory.BeanFactory#getType}
* and calling {@link #detectHandlerMethods} with the bean name.
* @param beanName the name of the candidate bean
* @since 5.1
* @see #isHandler
* @see #detectHandlerMethods
*/
protected void processCandidateBean(String beanName) {
Class> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
这是AbstractHandlerMethodMapping里的一段,用于判断是否要注册成HandlerMethod,找到之后就注册成HandlerMethod
/**
* Look for handler methods in the specified handler bean.
* @param handler either a bean name or an actual handler instance
* @see #getMappingForMethod
*/
protected void detectHandlerMethods(Object handler) {
Class> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class> userType = ClassUtils.getUserClass(handlerType);
Map methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
对应的,在AdminControllerHandlerMapping里,isHandler的处理,也简单直接,只要看看有没有AdminController的注解
@Override
protected boolean isHandler(Class> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, AdminController.class);
}
之后就和其他的 Spring MVC 过程类似,添加到Registry里,在请求时,会从所有的HandlerMapping里找, 这里对应的请求会找到 AdminControllerHandlerMapping,从中再找出对应的执行方法。
所以具体到我们请求 Server 里对应的 实例列表时,就直接调用到 Controller 里的方法了。
比如对于应用的列表
@GetMapping(path = "/applications", produces = MediaType.APPLICATION_JSON_VALUE)
public Flux applications() {
return registry.getInstances()
.filter(Instance::isRegistered)
.groupBy(instance -> instance.getRegistration().getName())
.flatMap(grouped -> toApplication(grouped.key(), grouped));
}
应用列表就直接从 registry
这个注册的地方来获取,所以可以想像 Client 在启动之后,应该是直接向这里注册了信息。 我们看到这里使用了 Spring 5 的 WebFlux, 还包含一些 Reactive Stream 的内容。
下一篇文章,我们再分析下这些内容。
更多常见问题,请关注公众号,在菜单「常见问题」中查看。
源码|实战|成长|职场
这里是「Tomcat那些事儿」
请留下你的足迹
我们一起「终身成长」
识别二维码,关注我