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