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

背景

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

相关推荐
qq_12498707532 小时前
基于微信小程序的付费自习室系统的设计与实现(源码+论文+部署+安装)
spring boot·微信小程序·小程序·毕业设计·计算机毕业设计·毕设源码
故渊ZY2 小时前
SpringBoot与Redis实战:企业级缓存进阶指南
java·spring boot
老华带你飞2 小时前
农产品销售管理|基于springboot农产品销售管理系统(源码+数据库+文档)
数据库·vue.js·spring boot
czlczl200209253 小时前
SpringBoot实践:从验证码到业务接口的完整交互生命周期
java·spring boot·redis·后端·mysql·spring
Han_coding12083 小时前
从原理到实战:基于游标分页解决深分页问题(附源码方案)
java·服务器·数据库·spring boot·spring cloud·oracle
qq_12498707533 小时前
校园失物招领微信小程序设计与实现(源码+论文+部署+安装)
spring boot·微信小程序·小程序·毕业设计·毕设
小小8程序员3 小时前
springboot + vue
vue.js·spring boot·后端
酸菜谭丶4 小时前
SpringBoot工程如何发布第三方Jar
spring boot·后端·jar
武昌库里写JAVA4 小时前
java设计模式 - 工厂方法模式
vue.js·spring boot·sql·layui·课程设计