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

背景

目前大部分应用都未提供健康探测以及服务优雅下线接口,这里针对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 ------ 实现优雅下线的服务关闭

相关推荐
Yyyyyy~10 小时前
【Java】Day4Web基础知识
java·spring boot
逆回14 小时前
升级 Boot 4 后,我的 mock 全红了
spring boot
学长毕设18 小时前
基于SpringBoot的野生动物公益保护平台设计与实现-附源码
java·spring boot·后端
她的男孩19 小时前
三人会签只批了一个就往下走:Forge Admin 把 all / any / ratio 写成 completionCondition
spring boot·低代码·工作流引擎
vipxieliang20 小时前
ValidX vs 手写验证代码:开发效率对比实测
java·spring boot·后端
vipxieliang20 小时前
虚拟线程与 GraalVM:Spring Boot 3.x 云原生性能跃迁
java·spring boot·后端
头发还在的女程序员21 小时前
基于 Spring Boot + Vue 3 + uni-app 的灵活用工系统:四端架构与业务流程设计
vue.js·spring boot·uni-app·灵活用工·灵工系统
CoderYanger1 天前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第二弹)
java·数据库·spring boot·spring·面试·java-ee·mybatis
计算机毕设定制辅导-无忧学长2 天前
《基于微信小程序的居家医疗陪护预约系统设计与实现》
java·spring boot·微信小程序·uniapp·居家医疗陪护预约系统
小郎君6582 天前
注解式脱敏:用 Jackson 的 ContextualSerializer 做字段级策略(一)
spring boot·spring cloud