集成健康探测以及服务优雅下线接口

背景

目前大部分应用都未提供健康探测以及服务优雅下线接口,这里针对spring boot服务提供简单的web 接口服务和集成说明

集成说明

这里只要在spring boot项目中添加controller类,即可支持健康探测及服务优雅下线功能

STEP1

将该controller类文件拷贝到项目的controller层(确保类被spring扫描并注册)

HealthProbe.java.zip

java 复制代码
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 集成健康探测以及服务优雅下线接口
 *
 */
@RestController
@RequestMapping("/")
@Slf4j
public class HealthProbe {
    
    private final List<String> ALLOW_HOST = Lists.newArrayList("localhost","127.0.0.1","0:0:0:0:0:0:0:1");
    private volatile long lastlyCheck = 0;
    // 单位为s
    @Value("${probe.interval:10}")
    private long interval;
    @Value("${application.version:1.0.0}")
    private String version;
    @Autowired(required = false)
    private List<CheckModule> checkModules;
    
    @Autowired(required = false)
    private List<DisposerModule> disposers;
    
    @GetMapping("/probe")
    public void probe(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (System.currentTimeMillis() - lastlyCheck < interval * 1000) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden for too frequent");
            return;
        }
        
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        if (checkModules == null || checkModules.isEmpty()) {
            HealthStatus status = HealthStatus.builder().healthz(HealthStatus.Status.UP.name()).version(version).build();
            response.getWriter().write(gson.toJson(Collections.singletonList(status)));
            return;
        }
        List<HealthStatus> statuses = new ArrayList<>();
        for (CheckModule module : checkModules) {
            HealthStatus status = module.check();
            if (status != null) {
                statuses.add(status);
            }
        }
        lastlyCheck = System.currentTimeMillis();
        response.getWriter().write(gson.toJson(statuses));
    }
    
    @PostMapping("/shutdown")
    public void shutdown(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (!checkLocal(request)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden Host");
            return;
        }
        
        for (DisposerModule disposer : disposers) {
            disposer.shutdown();
        }
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write("done");
    }
    
    private boolean checkLocal(HttpServletRequest request) {
        String remoteHost = request.getRemoteHost();
        String remoteAddr = "";
        try {
            remoteAddr = request.getHeader("X-FORWARDED-FOR");
            if (remoteAddr == null || remoteAddr.isEmpty()) {
                remoteAddr = request.getRemoteAddr();
            }
            InetAddress inetAddress = InetAddress.getByName(remoteAddr);
            if (inetAddress instanceof java.net.Inet4Address) {
                remoteAddr = inetAddress.getHostAddress();
            }
        } catch (UnknownHostException e) {
            return false;
        }
        return ALLOW_HOST.contains(remoteHost) && (remoteAddr != null && ALLOW_HOST.contains(remoteAddr));
    }
    
    public interface CheckModule {
        HealthStatus check();
    }
    
    public interface DisposerModule {
        void shutdown();
    }
    
    @Builder
    public static class HealthStatus {
        String healthz;
        String version;
        
        enum Status {
            UP, DOWN, UNKNOWN
        }
    }
    
    private final Gson gson = new GsonBuilder().create();
}

这样就可以访问web端口下的probe服务,如:

curl http://localhost:{port}/probe

curl http://localhost:{port}/shutdown

要求:本地访问该接口,否则将访问status code 403

其他说明

跳过鉴权或者自定义filter的拦截的配置方式

一、集成了spring security的应用,可通过以下配置

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

复制代码
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            // 其他URL都需要鉴权
            .anyRequest().authenticated()
            .and()
            // 下面配置的URL不会被鉴权
            .authorizeRequests()
            .antMatchers("/probe", "/shutdown").permitAll()
            .and()
    // 其他安全配置 ...
    ;
}

}

二、使用Filter的应用,可通过以下配置

@Configuration

public class FilterConfiguration {

@Bean

public FilterRegistrationBean filterRegistrationBean() {

FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();

// 这里的TargetFilter为项目中的自定义Filter

registrationBean.setFilter(new TargetFilter());

registrationBean.addUrlPatterns("/api/*");

// 在新版本中以及移除该API,需要对上面的url路径做处理来避免将 /probe和/shutdown 做filter

// registrationBean.setExcludeUrlPatterns()

return registrationBean;

}

}

三、使用Interceptor的应用,可通过以下配置

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration

public class CustomWebMvcConfigurer implements WebMvcConfigurer {

复制代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new TestHandlerInterceptor())
            .addPathPatterns("/*").excludePathPatterns("/probe", "/shutdown");
}

}

支持扩展

可继承接口:

CheckModule ------ 实现必要组件的健康探测

DisposerModule ------ 实现优雅下线的服务关闭

相关推荐
Sam_Deep_Thinking9 小时前
Spring Boot 的启动原理是什么?
java·spring boot·后端
屋外雨大,惊蛰出没10 小时前
深入浅出Spring Boot
java·spring boot·ioc·aop
协享科技10 小时前
Spring Boot 与 Go 双服务架构实践:从单体拆分到通信设计
java·人工智能·spring boot·后端·架构·golang·ai编程
小林敲代码778812 小时前
记录一下IDEA中很多变量变色的方案
java·开发语言·spring boot·idea
Flittly12 小时前
【AgentScope Java新手村系列】(3)工具系统
java·spring boot·spring
Flittly13 小时前
【AgentScope Java新手村系列】(2)第一个Agent-基础对话
java·spring boot·spring·ai
小二·14 小时前
Spring Boot 3 + Vue 3 全栈开发实战
vue.js·spring boot·后端
码农飞哥14 小时前
Spring Boot 多角色权限隔离实战:接口层+路由层+UI层三层防御,杜绝生产数据泄露
spring boot·状态模式·架构设计·系统设计·权限控制
SuperArc199914 小时前
SpringBoot+Slf4j+Log4j2+mybatis 日志整合
spring boot·mybatis·log4j2·slf4j·日志整合
lfwh15 小时前
探针程序技术解析:基于 Spring Boot 非 Web 模式的云服务监控告警系统
前端·spring boot·后端