springcloud添加自定義的endpoint來實現平滑發佈 springcloud如何實現服務的平滑發佈

在我以前的文章  springcloud如何實現服務的平滑發佈 裏介紹了基於pause的發佈方案。html

平滑發佈的核心思想就是:全部服務的調用者再也不調用該服務了就表示安全的將服務kill掉。java

另外actuator提供了優雅停機方式的endpoint:shutdown,那咱們就能夠結合 pause + 等待服務感知下線 + shutdown到一個endpoint裏來提供優雅的停機發布方案。web

以前的方案有一個不完美的地方,那就是IP白名單的filter是要在應用的application里加spring

@ServletComponentScan

註解,這樣對應用程序的將是不透明的(有侵入性)。json

故有了下面這我認爲是相對完美的方案:緩存

先建一個common模塊,其餘項目引用該模塊。安全

在src/main/resources下新建 META-INF文件夾,而後新建:spring.factories文件app

# 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.javaide

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.javapost

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
相關文章
相關標籤/搜索