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引入依赖
    
      org.springframework.boot
      spring-boot-starter-actuator
    
  • 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。

        
        
            io.micrometer
            micrometer-core
            1.2.0
        
        
        
            io.micrometer
            micrometer-registry-prometheus
            1.2.0
        

以下是访问结果


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 list = Arrays.asList(ipWhitelist);
        return list.stream().anyMatch(ip::startsWith);
    }

    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
     * 

* 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100 *

* 用户真实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时的最佳实践。如有问题还请指出,欢迎留言~

你可能感兴趣的:(JAVA框架-1:结合实际说Springboot2.X-actuator:Production-ready features)