springcloud添加自定义的endpoint来实现平滑发布

在我之前的文章  springcloud如何实现服务的平滑发布 里介绍了基于pause的发布方案。

平滑发布的核心思想就是:所有服务的调用者不再调用该服务了就表示安全的将服务kill掉。

另外actuator提供了优雅停机方式的endpoint:shutdown,那我们就可以结合 pause + 等待服务感知下线 + shutdown到一个endpoint里来提供优雅的停机发布方案。

之前的方案有一个不完美的地方,那就是IP白名单的filter是要在应用的application里加

@ServletComponentScan

注解,这样对应用程序的将是不透明的(有侵入性)。

故有了下面这我认为是相对完美的方案:

先建一个common模块,其他项目引用该模块。

在src/main/resources下新建 META-INF文件夹,然后新建:spring.factories文件

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.longge.config.PublishEndpointAutoConfiguration
PublishEndpointAutoConfiguration.java
package com.longge.config;

import javax.servlet.Filter;

import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.longge.endpoint.PublishEndpoint;
import com.longge.filter.PublishFilter;
import com.longge.filter.PublishProperties;

@Configuration
@ConditionalOnClass(Endpoint.class)
@AutoConfigureAfter(EndpointAutoConfiguration.class)
public class PublishEndpointAutoConfiguration {

    @Bean
    public PublishEndpoint publishEndpoint() {
        return new PublishEndpoint();
    }

    @Bean
    @ConditionalOnProperty("publish.ip-white-list")
    public PublishProperties publishProperties() {
        return new PublishProperties();
    }

    @Bean
    @ConditionalOnProperty("publish.ip-white-list")
    @ConditionalOnClass(Filter.class)
    public FilterRegistrationBean testFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new PublishFilter(publishProperties()));
        registration.addUrlPatterns("/publish");
        registration.setName("publishFilter");
        registration.setOrder(1);
        return registration;
    }

}

PublishEndpoint.java

package com.longge.endpoint;

import java.util.Collections;
import java.util.Map;

import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;

import lombok.Getter;
import lombok.Setter;

@ConfigurationProperties(prefix = "endpoints.publish")
public class PublishEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationListener<ApplicationPreparedEvent> {
    
    private static final Map<String, Object> NO_CONTEXT_MESSAGE = Collections
            .unmodifiableMap(Collections.<String, String>singletonMap("message",
                    "No context to publish."));

    /**
     * 等待多时秒
     */
    @Getter
    @Setter
    private Integer waitSeconds;
    
    public PublishEndpoint() {
        super("publish", true, false);
    }
    
    private ConfigurableApplicationContext context;

    @Override
    public Map<String, Object> invoke() {
        if (this.context == null) {
            return NO_CONTEXT_MESSAGE;
        }
        try {
            if(null == waitSeconds) {
                waitSeconds = 10;
            }
            Map<String, Object> shutdownMessage = Collections
                    .unmodifiableMap(Collections.<String, Object>singletonMap("message", "Service will exit after "+waitSeconds+" seconds"));
            return shutdownMessage;
        }
        finally {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        PublishEndpoint.this.context.stop();
                        Thread.sleep(waitSeconds * 1000);
                    }
                    catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    }
                    PublishEndpoint.this.context.close();
                }
            });
            thread.setContextClassLoader(getClass().getClassLoader());
            thread.start();
        }
    }

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent input) {
        if (this.context == null) {
            this.context = input.getApplicationContext();
        }
    }
}

PublishFilter.java

package com.longge.filter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import lombok.extern.slf4j.Slf4j;

/**
 * shutdown和pause的管理端点的ip白名单过滤
 * @author yangzhilong
 *
 */
@Slf4j
public class PublishFilter implements Filter {
    private static final String UNKNOWN = "unknown";
    /**
     * 本机ip
     */
    private List<String> localIp = Arrays.asList("0:0:0:0:0:0:0:1","127.0.0.1");
    
    private PublishProperties properties;
    
    @Override
    public void destroy() {
    }
    
    public PublishFilter() {}

    public PublishFilter(PublishProperties properties) {
        super();
        this.properties = properties;
    }

    @Override
    public void doFilter(ServletRequest srequest, ServletResponse sresponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) srequest;

        String ip = this.getIpAddress(request);
        log.info("访问publish的机器的原始IP:{}", ip);

        if (!isMatchWhiteList(ip)) {
            sresponse.setContentType("application/json");
            sresponse.setCharacterEncoding("UTF-8");
            PrintWriter writer = sresponse.getWriter();
            writer.write("{\"code\":401}");
            writer.flush();
            writer.close();
            return;
        }

        filterChain.doFilter(srequest, sresponse);
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        log.info("shutdown filter is init.....");
    }
    
    /**
     * 匹配是否是白名单
     * @param ip
     * @return
     */
    private boolean isMatchWhiteList(String ip) {
        if(localIp.contains(ip)) {
            return true;
        }
        if(null == properties.getIpWhiteList() || 0 == properties.getIpWhiteList().length) {
            return false;
        }
        List<String> list = Arrays.asList(properties.getIpWhiteList());
        return list.stream().anyMatch(item -> ip.startsWith(item));
    }
    
    /**
     * 获取用户真实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;
    }
}
PublishProperties.java
package com.longge.filter;
import org.springframework.boot.context.properties.ConfigurationProperties;

import lombok.Getter;
import lombok.Setter;

/**
 * publish白名单配置
 * @author yangzl
 * @data 2019年4月26日
 *
 */
@Getter
@Setter
@ConfigurationProperties(prefix="publish")
public class PublishProperties {
    /**
     * 白名单
     */
    private String[] ipWhiteList;
}

properties里配置如下:

 

management.security.enabled = false
# 开启自定义的publish端点
endpoints.publish.enabled = true
# 警用密码校验
endpoints.publish.sensitive = false
# 服务暂停时间
endpoints.publish.waitSeconds = 7
# 发布endpoint白名单
publish.ip-white-list=172.17.,172.16.

# 2秒拉取最新的注册信息
eureka.client.registry-fetch-interval-seconds=2
# 2秒刷新ribbon中的缓存信息
ribbon.ServerListRefreshInterval=2000

 

然后GET访问 http://IP:端口/publish,服务将先成注册中心下线,然后等待waitSeconds秒,然后再shutdown

 

posted @ 2019-05-06 18:12  自行车上的程序员  阅读(2967)  评论(0编辑  收藏  举报