JAVA框架-1:结合实际说Springboot2.X-actuator:Production-ready features

前言

Spring Boot中有四个非常好用的模块,堪称神奇,auto-configuration、starters、cli、actuator,其中actuator很少被人提到,但是却不能掩盖actuator强大的功能。下面就让我们来一起看一下,Actuator在实际应用中的最佳实践。本文要点:安全性、自定义端点,基于Spring Boot版本-2.0.6。

一、依赖引入

springboot项目的依赖版本通常由Spring Boot的版本统一约束,这里也是一个需要注意的地方。当spring-boot-starter-parent,或spring-boot-dependencies版本固定的时候,就不要指定Spring Boot其他组件版本了。因为Spring Boot其他组件都是基于Spring Boot的开发的,如果版本不一致,将引起冲突。

  • maven引入依赖
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  • Gradle引入依赖
dependencies {
compile("org.springframework.boot:spring-boot-starter-actuator")
}
二、Actuator能做什么

先说一下endpoint的概念,个人理解,在spring MVC中,每一个接口都可以称为一个端点,在Log中,所有包、类、组成的树状结构的每一个节点,也可以认为是一个端点。以下是Actuator中默认提供的端点,其中有一些端点是默认不开启的,如shutdown。

ID Description Enabled by default
auditevents Exposes audit events information for the current application. Yes
beans Displays a complete list of all the Spring beans in your application. Yes
conditions Shows the conditions that were evaluated onconfiguration and auto-configuration classes and the reasons why they did or did not match. Yes
configprops Displays a collated list of all @ConfigurationProperties. Yes
env Exposes properties from Spring’s ConfigurableEnvironment. Yes
flyway Shows any Flyway database migrations that have been applied. Yes
health Shows application health information. Yes
httptrace Displays HTTP trace information (by default, the last 100 HTTP request-response exchanges). Yes
info Displays arbitrary application info. Yes
metrics Shows ‘metrics’ information for the current application. Yes
mappings Displays a collated list of all @RequestMapping paths. Yes
scheduledtasks Displays the scheduled tasks in your application. Yes
sessions "Allows retrieval and deletion of user sessions from aSpring Session-backed session store. Not available when using Spring Session’s support for reactive web applications." Yes
shutdown Lets the application be gracefully shutdown. No
threaddump Performs a thread dump. Yes
loggers Shows and modifies the configuration of loggers in the application. Yes
liquibase Shows any Liquibase database migrations that have been applied. Yes

另外当应用程序是WEB应用时,还会启用以下端点

ID Description Enabled by default
heapdump Returns a GZip compressed hprof heap dump file. Yes
jolokia Exposes JMX beans over HTTP (when Jolokia is on the classpath, not available for WebFlux). Yes
logfile Returns the contents of the logfile (if logging.file or logging.path properties have been set). Supports the use of the HTTP Range header to retrieve part of the log file’s content. Yes
prometheus Exposes metrics in a format that can be scraped by a Prometheus server Yes
三、向用户暴露端点

端点启用之后,用户如何访问呢?Actuator提供了两种方式向用户暴露端点:

  • 基于JMX
    JMX是借助于一些工具,如jdk自带的jvisualvm 等
  • 基于HTTP
    HTTP访问只支持GET、POST、DELETE三种,当然这三种已经很多了:)
    HTTP请求肯定要有路径呀,那么Actuator的访问路径具体是什么样的呢?请看
# Actuator 默认URL是IP:[WEB项目配置的端口号]+/actuator,非WEB项目需要指定端口号
#WEB项目中,为Actuator指定其他端口号后,Actuator会和应用监听不同的端口号
#修改Actuator服务端口号还可以与应用服务端口隔离,防止非法请求——划重点
management.server.port=9190
# 还可以自定义Actuator的默认路径,或者叫discovery page,因为可以输出所有的端点。
# 自定义discovery page 访问地址变成ip:port/custom
management.endpoints.web.base-path=/custom
# 自定义根路径 访问地址变成ip:port/router/custom
management.server.servlet.context-path=/router
# 修改默认的端点启用规则,默认情况(true)下除shutdown外,全部启用,false即禁止默认启用
management.endpoints.enabled-by-default=false
# 以下配置可以指定端点启用,因上面禁用了默认启用,所以需要手动指定端点
# 语法是management.endpoint.[Endpoint ID].enabled=ture
# 启用health
management.endpoint.health.enabled=true
# 启用info
management.endpoint.info.enabled=true
# 启用prometheus
management.endpoint.prometheus.enabled=true
# 启用了端点需要制定暴露方式,JMX默认暴露所有端点,WEB(HTTP方式默认暴露health、info端点)
# 通过web方式暴露的端点,默认 health, info
management.endpoints.web.exposure.include=health,info,prometheus

经过以上配置,开启了health、info、prometheus端点,同时也暴露了他们。
ps prometheus需要引入相关依赖,此处使用了1.2.0的prometheus。

        <!-- https://mvnrepository.com/artifact/io.micrometer/micrometer-core -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-core</artifactId>
            <version>1.2.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.micrometer/micrometer-registry-prometheus -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.2.0</version>
        </dependency>

以下是访问结果


prometheus访问结果

so easy~~~

四、自定义端点

默认的端点就说到这里,接下来说一下如何自定义端点,以及为何要自定义端点。

  • 首先为什么要通过Actuator自定义端点呢?
    我的考虑是,应用向用户提供服务时,并不是说有的接口都要向用户暴露,有些控制功能的接口,只能掌握在管理员手中,当我需要将用户访问和管理服务端点分离开时,就可以使用Actuator来自定义端点。
  • 如何自定义端点?
    非常简单,给Bean打上端点的注解,那么拥有@ReadOperation,@WriteOperation,@DeleteOperation注解的方法就会通过JMX向外暴露,如果是Web应用也会通过HTTP方式向外暴露。
    端点的种类有很多种,通过注解实现。如@Endpoint、@WebEndpoint、@JmxEndpoint、@ServletEndpoint、@ControllerEndpoint、@RestControllerEndpoint各自有不同的偏向,但官方推荐@Endpoint,因为其他的都会牺牲可移植性。
  • Operation注解和HTTP method的对应关系,如下
Operation HTTP method 暴露方式个数限制
@ReadOperation GET 无限制,根据参数个数匹配
@WriteOperation POST 1
@DeleteOperation DELTEE 无限制,根据参数个数匹配

标准情况下,ReadOperation和DeleteOperation两种Operation将会根据传输参数个数的不同,匹配到不同的方法。但WriteOperation只能存在一个,否则会在运行时抛出异常。

  • 说了HTTP的请求类型就不得不说Content-type,三种注解,在返回不同类型的对象时,Content-type也不一样。
Operation Return Content-type(Consumes) Content-type(Produces)
@ReadOperation returns void
returns org.springframework.core.io.Resource application/octet-stream
returns other application/vnd.springboot.actuator.v2+json, application/json.
@WriteOperation returns void application/vnd.spring-boot.actuator.v2+json, application/json
returns org.springframework.core.io.Resource application/vnd.spring-boot.actuator.v2+json, application/json application/octet-stream
returns other application/vnd.spring-boot.actuator.v2+json, application/json application/vnd.springboot.actuator.v2+json, application/json.
@DeleteOperation returns void
returns org.springframework.core.io.Resource application/octet-stream
  • 最重要的来了,如何进行参数传递

@Selector 注解可以取出路径中的参数
注意只能在方法的形参中使用简单类型(包括包装类型),因为endpoint和技术无关。即意味着,如果使用request body传递json格式参数,如
{"name": "test","counter": 42}
那么方法形参应该是(String name,String counter),可以使用@org.springframework.lang.Nullable注解使参数非必传

下面给出几个样例

    /**
     * get
     *
     * @param name
     * @return
     */
    @ReadOperation
    public String get(@Selector  String name) {
        return name;
    }
    @ReadOperation
    public String get2(@Selector String param1, @Selector String param2) {
        return param1 + param2;
    }
   /**
     * post
     *
     * @param name
     * @param counter
     * @return
     */
    @WriteOperation
    public String postRequest(String name, @Nullable Integer counter) {
        return name + String.valueOf(counter);
    }

    /**
     * delete
     * 
     * @author wxtang
     * @Date: 2019/08/30 11:08
     * @param: param1
     */
    @DeleteOperation
    public String del1(@Selector String param1) {
        return "del1";
    }

    @DeleteOperation
    public String del2(@Selector String param1, @Selector String param2) {
        return "del2";
    }
五、安全性

开启了端点,就不得不考虑一下安全性,自我提问一下:我想让谁访问?什么人可以访问?什么样的请求是合法的?
Springboot官方使用Spring Security组件进行鉴权演示,但是实际应用中,通常会对多个启用Actuator的应用做集中管理,这时Spring Security组件就不是那么好用了。而在Spring Boot的官方文档中,有这么一句话:

If you deploy applications behind a firewall, you may prefer that all your actuator
endpoints can be accessed without requiring authentication.
如果您在防火墙后部署应用程序,您可能更喜欢所有执行器可以在不需要身份验证的情况下访问端点。

在微服务盛行的时代,我们的应用通常处于API网关之后的位置,在经过网关的路由分发,鉴权,访问控制之后,用户很难直接访问到Actuator的端点。对于网关没有做路径约束的情况,还可以通过端口隔离的方式对Actuator端点进行保护,参看第三部分。
可能有人说了,日防夜防,家贼难防,万一有人通过内部网络跳过网关访问直接访问应用,岂不是钻了空子?take it easy ~ 我们还有另一个办法:基于IP过滤,只有在白名单中的ip才能访问。具体实现如下

  1. 基于IP过滤需要将Actuator的端口号与服务本身的端口号隔离开,以免产生影响(当然可以不分开,只是IP白名单就要多加一些地址了)
  2. 写Filter,实现对IP的过滤,代码如下:

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;

/**
 * @author wxtang
 * @version 1.0.0
 * @ClassName: FilterConfig
 * @Description: 注册filter
 * @Date: 2019/08/20 15:04
 */
public class FilterConfig {
    @Bean
    public FilterRegistrationBean actuatorFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new IpFilter());
        registration.addUrlPatterns("/custom/test-endpoint-json/*");
        registration.setName("ipfilter");
        registration.setOrder(1);
        return registration;
    }
}


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;

/**
 * @author wxtang
 * @version 1.0.0
 * @ClassName: IpFilter
 * @Description: ip过滤器
 * @Date: 2019/08/20 13:36
 */
public class IpFilter implements Filter {
    private final static Logger log = LoggerFactory.getLogger(IpFilter.class);
    //    @Value("${shutdown.whitelist:0:0:0:0:0:0:0:1}")
    private String[] ipWhitelist = {"0:0:0:0:0:0:0:1", "127.0.0.1", "172.22.42.132"};

    @Override
    public void destroy() {
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("ipFilter is init!");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        String ip = this.getIpAddress(httpServletRequest);
        log.info("访问的机器的原始IP:{}", ip);

        if (!isMatchWhiteList(ip)) {
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            PrintWriter writer = response.getWriter();
            writer.write("{\"code\":401}");
            writer.flush();
            writer.close();
            return;
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 匹配是否是白名单
     *
     * @param ip
     * @return
     */
    private boolean isMatchWhiteList(String ip) {
        List<String> list = Arrays.asList(ipWhitelist);
        return list.stream().anyMatch(ip::startsWith);
    }

    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
     * <p>
     * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100
     * <p>
     * 用户真实IP为: 192.168.1.110
     *
     * @param request
     * @return
     */
    private String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

注意,获取ip时应该获取源ip,避免使用代理伪造ip
写到这里并没有向容器中注入filter,为什么呢?因为,如果Actuator使用独立端口号,那么将启用新的servletContext,也就是说,如果使用默认的@Configuration注解向容器中注入filter,过滤器就不会再正确的位置生效。需要使用其他方法标注filter注入到Actuator建立的servletContext中,如下:
resource目录下建立META-INF文件夹,其下新建文件spring.factory 内容:

org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=\
[FilterConfig.reference]
#例如org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=
#\com.tir.na.nog.actuatortest.config.FilterConfig

以上就是个人在使用Actuator时的最佳实践。如有问题还请指出,欢迎留言~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容